diff --git a/application.go b/application.go index 26fa2c00..cb41237e 100644 --- a/application.go +++ b/application.go @@ -12,7 +12,6 @@ import ( "github.com/go-flutter-desktop/go-flutter/embedder" "github.com/go-flutter-desktop/go-flutter/internal/execpath" - "github.com/go-flutter-desktop/go-flutter/internal/tasker" ) // Run executes a flutter application with the provided options. @@ -100,7 +99,12 @@ func (a *Application) Run() error { return errors.Errorf("invalid window mode %T", a.config.windowMode) } - if a.config.windowInitialLocations.xpos != 0 { + glfw.WindowHint(glfw.ContextVersionMajor, 4) + glfw.WindowHint(glfw.ContextVersionMinor, 1) + glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) + glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) + + if a.config.windowInitialLocation.xpos != 0 { // To create the window at a specific position, make it initially invisible // using the Visible window hint, set its position and then show it. glfw.WindowHint(glfw.Visible, glfw.False) @@ -110,12 +114,11 @@ func (a *Application) Run() error { if err != nil { return errors.Wrap(err, "creating glfw window") } - glfw.DefaultWindowHints() defer a.window.Destroy() + glfw.DefaultWindowHints() - if a.config.windowInitialLocations.xpos != 0 { - a.window.SetPos(a.config.windowInitialLocations.xpos, - a.config.windowInitialLocations.ypos) + if a.config.windowInitialLocation.xpos != 0 { + a.window.SetPos(a.config.windowInitialLocation.xpos, a.config.windowInitialLocation.ypos) a.window.Show() } @@ -152,22 +155,18 @@ func (a *Application) Run() error { a.engine = embedder.NewFlutterEngine() + // Create a messenger and init plugins messenger := newMessenger(a.engine) - for _, p := range a.config.plugins { - err = p.InitPlugin(messenger) - if err != nil { - return errors.Wrap(err, "failed to initialize plugin "+fmt.Sprintf("%T", p)) - } + // Create a TextureRegistry + texturer := newTextureRegistry(a.engine, a.window) - // Extra init call for plugins that satisfy the PluginGLFW interface. - if glfwPlugin, ok := p.(PluginGLFW); ok { - err = glfwPlugin.InitPluginGLFW(a.window) - if err != nil { - return errors.Wrap(err, "failed to initialize glfw plugin"+fmt.Sprintf("%T", p)) - } - } - } + // Create a new eventloop + eventLoop := newEventLoop( + glfw.PostEmptyEvent, // Wakeup GLFW + a.engine.RunTask, // Flush tasks + ) + // Set configuration values to engine, with fallbacks to sane defaults. if a.config.flutterAssetsPath != "" { a.engine.AssetsPath = a.config.flutterAssetsPath } else { @@ -177,7 +176,6 @@ func (a *Application) Run() error { } a.engine.AssetsPath = filepath.Join(filepath.Dir(execPath), "flutter_assets") } - if a.config.icuDataPath != "" { a.engine.IcuDataPath = a.config.icuDataPath } else { @@ -188,7 +186,7 @@ func (a *Application) Run() error { a.engine.IcuDataPath = filepath.Join(filepath.Dir(execPath), "icudtl.dat") } - // Render callbacks + // Attach GL callback functions onto the engine a.engine.GLMakeCurrent = func() bool { a.window.MakeContextCurrent() return true @@ -214,19 +212,29 @@ func (a *Application) Run() error { a.engine.GLProcResolver = func(procName string) unsafe.Pointer { return glfw.GetProcAddress(procName) } + a.engine.GLExternalTextureFrameCallback = texturer.handleExternalTexture + + // Attach TaskRunner callback functions onto the engine + a.engine.TaskRunnerRunOnCurrentThread = eventLoop.RunOnCurrentThread + a.engine.TaskRunnerPostTask = eventLoop.PostTask + // Attach PlatformMessage callback functions onto the engine a.engine.PlatfromMessage = messenger.handlePlatformMessage // Not very nice, but we can only really fix this when there's a pluggable // renderer. defaultTextinputPlugin.keyboardLayout = a.config.keyboardLayout + // Set the glfw window user pointer to point to the FlutterEngine so that + // callback functions may obtain the FlutterEngine from the glfw window + // user pointer. flutterEnginePointer := uintptr(unsafe.Pointer(a.engine)) defer func() { runtime.KeepAlive(flutterEnginePointer) }() a.window.SetUserPointer(unsafe.Pointer(&flutterEnginePointer)) + // Start the engine result := a.engine.Run(unsafe.Pointer(&flutterEnginePointer), a.config.vmArguments) if result != embedder.ResultSuccess { switch result { @@ -240,20 +248,40 @@ func (a *Application) Run() error { os.Exit(1) } - defaultPlatformPlugin.glfwTasker = tasker.New() + // Register plugins + for _, p := range a.config.plugins { + err = p.InitPlugin(messenger) + if err != nil { + return errors.Wrap(err, "failed to initialize plugin "+fmt.Sprintf("%T", p)) + } - m := newWindowManager() - m.forcedPixelRatio = a.config.forcePixelRatio + // Extra init call for plugins that satisfy the PluginGLFW interface. + if glfwPlugin, ok := p.(PluginGLFW); ok { + err = glfwPlugin.InitPluginGLFW(a.window) + if err != nil { + return errors.Wrap(err, "failed to initialize glfw plugin"+fmt.Sprintf("%T", p)) + } + } - m.glfwRefreshCallback(a.window) - a.window.SetRefreshCallback(m.glfwRefreshCallback) - a.window.SetPosCallback(m.glfwPosCallback) + // Extra init call for plugins that satisfy the PluginTexture interface. + if texturePlugin, ok := p.(PluginTexture); ok { + err = texturePlugin.InitPluginTexture(texturer) + if err != nil { + return errors.Wrap(err, "failed to initialize texture plugin"+fmt.Sprintf("%T", p)) + } + } + } - // flutter's PlatformMessage handler is registered through the dart:ui.Window - // interface. ui.Window must have at least paint one frame, before any - // platfrom message can be corectly handled by ui.Window.onPlatformMessage. - glfw.WaitEvents() + // Setup a new windowManager to handle windows pixel ratio's and pointer + // devices. + windowManager := newWindowManager(a.config.forcePixelRatio) + // force first refresh + windowManager.glfwRefreshCallback(a.window) + // Attach glfw window callbacks for refresh and position changes + a.window.SetRefreshCallback(windowManager.glfwRefreshCallback) + a.window.SetPosCallback(windowManager.glfwPosCallback) + // Attach glfw window callbacks for text input a.window.SetKeyCallback( func(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { defaultTextinputPlugin.glfwKeyCallback(window, key, scancode, action, mods) @@ -261,17 +289,24 @@ func (a *Application) Run() error { }) a.window.SetCharCallback(defaultTextinputPlugin.glfwCharCallback) + // Attach glfw window callback for iconification a.window.SetIconifyCallback(defaultLifecyclePlugin.glfwIconifyCallback) - a.window.SetCursorEnterCallback(m.glfwCursorEnterCallback) - a.window.SetCursorPosCallback(m.glfwCursorPosCallback) - a.window.SetMouseButtonCallback(m.glfwMouseButtonCallback) - a.window.SetScrollCallback(m.glfwScrollCallback) + // Attach glfw window callbacks for mouse input + a.window.SetCursorEnterCallback(windowManager.glfwCursorEnterCallback) + a.window.SetCursorPosCallback(windowManager.glfwCursorPosCallback) + a.window.SetMouseButtonCallback(windowManager.glfwMouseButtonCallback) + a.window.SetScrollCallback(windowManager.glfwScrollCallback) + + // Shutdown the engine if we return from this function (on purpose or panic) defer a.engine.Shutdown() + // Handle events until the window indicates we should stop. An event may tell the window to stop, in which case + // we'll exit on next iteration. for !a.window.ShouldClose() { - glfw.WaitEventsTimeout(0.016) // timeout to get 60fps-ish iterations - embedder.FlutterEngineFlushPendingTasksNow() + eventLoop.WaitForEvents(func(duration float64) { + glfw.WaitEventsTimeout(duration) + }) defaultPlatformPlugin.glfwTasker.ExecuteTasks() messenger.engineTasker.ExecuteTasks() } diff --git a/embedder/embedder.go b/embedder/embedder.go index 95045ce8..4223b5fa 100644 --- a/embedder/embedder.go +++ b/embedder/embedder.go @@ -3,13 +3,18 @@ package embedder // #include "embedder.h" // FlutterEngineResult runFlutter(void *user_data, FlutterEngine *engine, FlutterProjectArgs * Args, // const char *const * vmArgs, int nVmAgrs); +// FlutterEngineResult +// createMessageResponseHandle(FlutterEngine engine, void *user_data, +// FlutterPlatformMessageResponseHandle **reply); // char** makeCharArray(int size); // void setArrayString(char **a, char *s, int n); // const int32_t kFlutterSemanticsNodeIdBatchEnd = -1; // const int32_t kFlutterSemanticsCustomActionIdBatchEnd = -1; import "C" import ( + "errors" "fmt" + "runtime" "runtime/debug" "sync" "unsafe" @@ -53,6 +58,19 @@ const ( ResultEngineNotRunning Result = -1 ) +// FlutterOpenGLTexture corresponds to the C.FlutterOpenGLTexture struct. +type FlutterOpenGLTexture struct { + // Target texture of the active texture unit (example GL_TEXTURE_2D) + Target uint32 + // The name of the texture + Name uint32 + // The texture format (example GL_RGBA8) + Format uint32 +} + +// FlutterTask is a type alias to C.FlutterTask +type FlutterTask = C.FlutterTask + // FlutterEngine corresponds to the C.FlutterEngine with his associated callback's method. type FlutterEngine struct { // Flutter Engine. @@ -65,12 +83,17 @@ type FlutterEngine struct { index int // GL callback functions - GLMakeCurrent func() bool - GLClearCurrent func() bool - GLPresent func() bool - GLFboCallback func() int32 - GLMakeResourceCurrent func() bool - GLProcResolver func(procName string) unsafe.Pointer + GLMakeCurrent func() bool + GLClearCurrent func() bool + GLPresent func() bool + GLFboCallback func() int32 + GLMakeResourceCurrent func() bool + GLProcResolver func(procName string) unsafe.Pointer + GLExternalTextureFrameCallback func(textureID int64, width int, height int) *FlutterOpenGLTexture + + // task runner interop + TaskRunnerRunOnCurrentThread func() bool + TaskRunnerPostTask func(trask FlutterTask, targetTimeNanos uint64) // platform message callback function PlatfromMessage func(message *PlatformMessage) @@ -238,8 +261,11 @@ type PlatformMessage struct { Channel string Message []byte - // ResponseHandle is only set when receiving a platform message. - // https://github.com/flutter/flutter/issues/18852 + // ResponseHandle is set on some recieved platform message. All + // PlatformMessage recieved with this attribute must send a response with + // `SendPlatformMessageResponse`. + // ResponseHandle can also be created from the embedder side when a + // platform(golang) message needs native callback. ResponseHandle PlatformMessageResponseHandle } @@ -299,10 +325,82 @@ func (flu *FlutterEngine) SendPlatformMessageResponse( return (Result)(res) } -// FlutterEngineFlushPendingTasksNow flush tasks on a message loop not -// controlled by the Flutter engine. -// -// deprecated soon. -func FlutterEngineFlushPendingTasksNow() { - C.__FlutterEngineFlushPendingTasksNow() +// RunTask inform the engine to run the specified task. +func (flu *FlutterEngine) RunTask(task *FlutterTask) Result { + res := C.FlutterEngineRunTask(flu.Engine, task) + return (Result)(res) +} + +// RegisterExternalTexture registers an external texture with a unique identifier. +func (flu *FlutterEngine) RegisterExternalTexture(textureID int64) Result { + flu.sync.Lock() + defer flu.sync.Unlock() + if flu.closed { + return ResultEngineNotRunning + } + res := C.FlutterEngineRegisterExternalTexture(flu.Engine, C.int64_t(textureID)) + return (Result)(res) +} + +// UnregisterExternalTexture unregisters a previous texture registration. +func (flu *FlutterEngine) UnregisterExternalTexture(textureID int64) Result { + flu.sync.Lock() + defer flu.sync.Unlock() + if flu.closed { + return ResultEngineNotRunning + } + res := C.FlutterEngineUnregisterExternalTexture(flu.Engine, C.int64_t(textureID)) + return (Result)(res) +} + +// MarkExternalTextureFrameAvailable marks that a new texture frame is +// available for a given texture identifier. +func (flu *FlutterEngine) MarkExternalTextureFrameAvailable(textureID int64) Result { + flu.sync.Lock() + defer flu.sync.Unlock() + if flu.closed { + return ResultEngineNotRunning + } + res := C.FlutterEngineMarkExternalTextureFrameAvailable(flu.Engine, C.int64_t(textureID)) + return (Result)(res) +} + +// DataCallback is a function called when a PlatformMessage response send back +// to the embedder. +type DataCallback func(binaryReply []byte) + +// CreatePlatformMessageResponseHandle creates a platform message response +// handle that allows the embedder to set a native callback for a response to a +// message. +// Must be collected via `ReleasePlatformMessageResponseHandle` after the call +// to `SendPlatformMessage`. +func (flu *FlutterEngine) CreatePlatformMessageResponseHandle(callback DataCallback) (PlatformMessageResponseHandle, error) { + var responseHandle *C.FlutterPlatformMessageResponseHandle + + callbackPointer := uintptr(unsafe.Pointer(&callback)) + defer func() { + runtime.KeepAlive(callbackPointer) + }() + + res := C.createMessageResponseHandle(flu.Engine, unsafe.Pointer(&callbackPointer), &responseHandle) + if (Result)(res) != ResultSuccess { + return 0, errors.New("failed to create a response handle") + } + return PlatformMessageResponseHandle(unsafe.Pointer(responseHandle)), nil +} + +// ReleasePlatformMessageResponseHandle collects a platform message response +// handle. +func (flu *FlutterEngine) ReleasePlatformMessageResponseHandle(responseHandle PlatformMessageResponseHandle) { + cResponseHandle := (*C.FlutterPlatformMessageResponseHandle)(unsafe.Pointer(responseHandle)) + res := C.FlutterPlatformMessageReleaseResponseHandle(flu.Engine, cResponseHandle) + if (Result)(res) != ResultSuccess { + fmt.Printf("go-flutter: failed to collect platform response message handle") + } +} + +// FlutterEngineGetCurrentTime gets the current time in nanoseconds from the clock used by the flutter +// engine. +func FlutterEngineGetCurrentTime() uint64 { + return uint64(C.FlutterEngineGetCurrentTime()) } diff --git a/embedder/embedder.h b/embedder/embedder.h index 3a4bc97d..f75817e1 100644 --- a/embedder/embedder.h +++ b/embedder/embedder.h @@ -17,6 +17,16 @@ extern "C" { #define FLUTTER_EXPORT #endif // FLUTTER_EXPORT +#ifdef FLUTTER_API_SYMBOL_PREFIX +#define FLUTTER_EMBEDDING_CONCAT(a, b) a##b +#define FLUTTER_EMBEDDING_ADD_PREFIX(symbol, prefix) \ + FLUTTER_EMBEDDING_CONCAT(prefix, symbol) +#define FLUTTER_API_SYMBOL(symbol) \ + FLUTTER_EMBEDDING_ADD_PREFIX(symbol, FLUTTER_API_SYMBOL_PREFIX) +#else +#define FLUTTER_API_SYMBOL(symbol) symbol +#endif + #define FLUTTER_ENGINE_VERSION 1 typedef enum { @@ -153,6 +163,10 @@ typedef enum { // |PageView| widget does not have implicit scrolling, so that users don't // navigate to the next page when reaching the end of the current one. kFlutterSemanticsFlagHasImplicitScrolling = 1 << 18, + // Whether the semantic node is read only. + // + // Only applicable when kFlutterSemanticsFlagIsTextField flag is on. + kFlutterSemanticsFlagIsReadOnly = 1 << 20, } FlutterSemanticsFlag; typedef enum { @@ -164,7 +178,7 @@ typedef enum { kFlutterTextDirectionLTR = 2, } FlutterTextDirection; -typedef struct _FlutterEngine* FlutterEngine; +typedef struct _FlutterEngine* FLUTTER_API_SYMBOL(FlutterEngine); typedef struct { // horizontal scale factor @@ -369,12 +383,11 @@ typedef struct { size_t struct_size; const char* channel; const uint8_t* message; - const size_t message_size; + size_t message_size; // The response handle on which to invoke - // |FlutterEngineSendPlatformMessageResponse| when the response is ready. This - // field is ignored for messages being sent from the embedder to the - // framework. |FlutterEngineSendPlatformMessageResponse| must be called for - // all messages received by the embedder. Failure to call + // |FlutterEngineSendPlatformMessageResponse| when the response is ready. + // |FlutterEngineSendPlatformMessageResponse| must be called for all messages + // received by the embedder. Failure to call // |FlutterEngineSendPlatformMessageResponse| will cause a memory leak. It is // not safe to send multiple responses on a single response object. const FlutterPlatformMessageResponseHandle* response_handle; @@ -384,6 +397,10 @@ typedef void (*FlutterPlatformMessageCallback)( const FlutterPlatformMessage* /* message*/, void* /* user data */); +typedef void (*FlutterDataCallback)(const uint8_t* /* data */, + size_t /* size */, + void* /* user data */); + typedef struct { double left; double top; @@ -646,6 +663,9 @@ typedef struct { // Path to a directory used to store data that is cached across runs of a // Flutter application (such as compiled shader programs used by Skia). // This is optional. The string must be NULL terminated. + // + // This is different from the cache-path-dir argument defined in switches.h, + // which is used in |flutter::Settings| as |temp_directory_path|. const char* persistent_cache_path; // If true, we'll only read the existing cache, but not write new ones. @@ -682,30 +702,59 @@ FlutterEngineResult FlutterEngineRun(size_t version, const FlutterRendererConfig* config, const FlutterProjectArgs* args, void* user_data, - FlutterEngine* engine_out); + FLUTTER_API_SYMBOL(FlutterEngine) * + engine_out); FLUTTER_EXPORT -FlutterEngineResult FlutterEngineShutdown(FlutterEngine engine); +FlutterEngineResult FlutterEngineShutdown(FLUTTER_API_SYMBOL(FlutterEngine) + engine); FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendWindowMetricsEvent( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterWindowMetricsEvent* event); FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendPointerEvent( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterPointerEvent* events, size_t events_count); FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendPlatformMessage( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterPlatformMessage* message); +// Creates a platform message response handle that allows the embedder to set a +// native callback for a response to a message. This handle may be set on the +// |response_handle| field of any |FlutterPlatformMessage| sent to the engine. +// +// The handle must be collected via a call to +// |FlutterPlatformMessageReleaseResponseHandle|. This may be done immediately +// after a call to |FlutterEngineSendPlatformMessage| with a platform message +// whose response handle contains the handle created using this call. In case a +// handle is created but never sent in a message, the release call must still be +// made. Not calling release on the handle results in a small memory leak. +// +// The user data baton passed to the data callback is the one specified in this +// call as the third argument. +FLUTTER_EXPORT +FlutterEngineResult FlutterPlatformMessageCreateResponseHandle( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + FlutterDataCallback data_callback, + void* user_data, + FlutterPlatformMessageResponseHandle** response_out); + +// Collects the handle created using +// |FlutterPlatformMessageCreateResponseHandle|. +FLUTTER_EXPORT +FlutterEngineResult FlutterPlatformMessageReleaseResponseHandle( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + FlutterPlatformMessageResponseHandle* response); + FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendPlatformMessageResponse( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterPlatformMessageResponseHandle* handle, const uint8_t* data, size_t data_length); @@ -723,19 +772,19 @@ FlutterEngineResult __FlutterEngineFlushPendingTasksNow(); // |FlutterEngineMarkExternalTextureFrameAvailable|. FLUTTER_EXPORT FlutterEngineResult FlutterEngineRegisterExternalTexture( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, int64_t texture_identifier); // Unregister a previous texture registration. FLUTTER_EXPORT FlutterEngineResult FlutterEngineUnregisterExternalTexture( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, int64_t texture_identifier); // Mark that a new texture frame is available for a given texture identifier. FLUTTER_EXPORT FlutterEngineResult FlutterEngineMarkExternalTextureFrameAvailable( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, int64_t texture_identifier); // Enable or disable accessibility semantics. @@ -744,19 +793,20 @@ FlutterEngineResult FlutterEngineMarkExternalTextureFrameAvailable( // the |FlutterUpdateSemanticsNodeCallback| registered to // |update_semantics_node_callback| in |FlutterProjectArgs|; FLUTTER_EXPORT -FlutterEngineResult FlutterEngineUpdateSemanticsEnabled(FlutterEngine engine, - bool enabled); +FlutterEngineResult FlutterEngineUpdateSemanticsEnabled( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + bool enabled); // Sets additional accessibility features. FLUTTER_EXPORT FlutterEngineResult FlutterEngineUpdateAccessibilityFeatures( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, FlutterAccessibilityFeature features); // Dispatch a semantics action to the specified semantics node. FLUTTER_EXPORT FlutterEngineResult FlutterEngineDispatchSemanticsAction( - FlutterEngine engine, + FLUTTER_API_SYMBOL(FlutterEngine) engine, uint64_t id, FlutterSemanticsAction action, const uint8_t* data, @@ -779,7 +829,8 @@ FlutterEngineResult FlutterEngineDispatchSemanticsAction( // // That frame timepoints are in nanoseconds. FLUTTER_EXPORT -FlutterEngineResult FlutterEngineOnVsync(FlutterEngine engine, +FlutterEngineResult FlutterEngineOnVsync(FLUTTER_API_SYMBOL(FlutterEngine) + engine, intptr_t baton, uint64_t frame_start_time_nanos, uint64_t frame_target_time_nanos); @@ -814,9 +865,10 @@ void FlutterEngineTraceEventInstant(const char* name); // from any thread as long as a |FlutterEngineShutdown| on the specific engine // has not already been initiated. FLUTTER_EXPORT -FlutterEngineResult FlutterEnginePostRenderThreadTask(FlutterEngine engine, - VoidCallback callback, - void* callback_data); +FlutterEngineResult FlutterEnginePostRenderThreadTask( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + VoidCallback callback, + void* callback_data); // Get the current time in nanoseconds from the clock used by the flutter // engine. This is the system monotonic clock. @@ -828,11 +880,12 @@ uint64_t FlutterEngineGetCurrentTime(); // call must only be made at the target time specified in that callback. Running // the task before that time is undefined behavior. FLUTTER_EXPORT -FlutterEngineResult FlutterEngineRunTask(FlutterEngine engine, +FlutterEngineResult FlutterEngineRunTask(FLUTTER_API_SYMBOL(FlutterEngine) + engine, const FlutterTask* task); #if defined(__cplusplus) } // extern "C" #endif -#endif // FLUTTER_EMBEDDER_H_ \ No newline at end of file +#endif // FLUTTER_EMBEDDER_H_ diff --git a/embedder/embedder_helper.c b/embedder/embedder_helper.c index 410f0530..a5a12823 100644 --- a/embedder/embedder_helper.c +++ b/embedder/embedder_helper.c @@ -1,4 +1,4 @@ - +#include #include #include "embedder.h" @@ -10,36 +10,65 @@ bool proxy_present(void *user_data); uint32_t proxy_fbo_callback(void *user_data); bool proxy_make_resource_current(void *user_data); void *proxy_gl_proc_resolver(void *user_data, const char *procname); -void proxy_platform_message_callback(const FlutterPlatformMessage *message, void *user_data); +void proxy_platform_message_callback(const FlutterPlatformMessage *message, + void *user_data); +bool proxy_gl_external_texture_frame_callback(void *user_data, + int64_t texture_id, size_t width, + size_t height, + FlutterOpenGLTexture *texture); + +bool proxy_runs_task_on_current_thread_callback(void *user_data); +void proxy_post_task_callback(FlutterTask task, uint64_t target_time_nanos, + void *user_data); + +void proxy_desktop_binary_reply(const uint8_t *data, size_t data_size, + void *user_data); // C helper -FlutterEngineResult runFlutter(void *user_data, FlutterEngine *engine, FlutterProjectArgs *Args, - const char *const *vmArgs, int nVmAgrs) -{ - FlutterRendererConfig config = {}; - config.type = kOpenGL; - - config.open_gl.struct_size = sizeof(FlutterOpenGLRendererConfig); - config.open_gl.make_current = proxy_make_current; - config.open_gl.clear_current = proxy_clear_current; - config.open_gl.present = proxy_present; - config.open_gl.fbo_callback = proxy_fbo_callback; - config.open_gl.make_resource_current = proxy_make_resource_current; - config.open_gl.gl_proc_resolver = proxy_gl_proc_resolver; - - Args->command_line_argc = nVmAgrs; - Args->command_line_argv = vmArgs; - Args->platform_message_callback = proxy_platform_message_callback; - - return FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, Args, user_data, engine); -} +FlutterEngineResult runFlutter(void *user_data, FlutterEngine *engine, + FlutterProjectArgs *Args, + const char *const *vmArgs, int nVmAgrs) { + FlutterRendererConfig config = {}; + config.type = kOpenGL; + + config.open_gl.struct_size = sizeof(FlutterOpenGLRendererConfig); + config.open_gl.make_current = proxy_make_current; + config.open_gl.clear_current = proxy_clear_current; + config.open_gl.present = proxy_present; + config.open_gl.fbo_callback = proxy_fbo_callback; + config.open_gl.make_resource_current = proxy_make_resource_current; + config.open_gl.gl_proc_resolver = proxy_gl_proc_resolver; + config.open_gl.gl_external_texture_frame_callback = + proxy_gl_external_texture_frame_callback; -char **makeCharArray(int size) -{ - return calloc(sizeof(char *), size); + Args->command_line_argc = nVmAgrs; + Args->command_line_argv = vmArgs; + Args->platform_message_callback = proxy_platform_message_callback; + + // Configure task runner interop + FlutterTaskRunnerDescription platform_task_runner = {}; + platform_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription); + platform_task_runner.user_data = user_data; + platform_task_runner.runs_task_on_current_thread_callback = + proxy_runs_task_on_current_thread_callback; + platform_task_runner.post_task_callback = proxy_post_task_callback; + + FlutterCustomTaskRunners custom_task_runners = {}; + custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners); + custom_task_runners.platform_task_runner = &platform_task_runner; + Args->custom_task_runners = &custom_task_runners; + + return FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, Args, user_data, + engine); } -void setArrayString(char **a, char *s, int n) -{ - a[n] = s; +char **makeCharArray(int size) { return calloc(sizeof(char *), size); } + +void setArrayString(char **a, char *s, int n) { a[n] = s; } + +FlutterEngineResult +createMessageResponseHandle(FlutterEngine engine, void *user_data, + FlutterPlatformMessageResponseHandle **reply) { + return FlutterPlatformMessageCreateResponseHandle( + engine, proxy_desktop_binary_reply, user_data, reply); } diff --git a/embedder/embedder_proxy.go b/embedder/embedder_proxy.go index cf0999b9..8b3d0079 100644 --- a/embedder/embedder_proxy.go +++ b/embedder/embedder_proxy.go @@ -59,3 +59,44 @@ func proxy_gl_proc_resolver(userData unsafe.Pointer, procname *C.char) unsafe.Po flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer)) return flutterEngine.GLProcResolver(C.GoString(procname)) } + +//export proxy_gl_external_texture_frame_callback +func proxy_gl_external_texture_frame_callback( + userData unsafe.Pointer, + textureID int64, + width C.size_t, + height C.size_t, + texture *C.FlutterOpenGLTexture, +) C.bool { + flutterEnginePointer := *(*uintptr)(userData) + flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer)) + embedderGLTexture := flutterEngine.GLExternalTextureFrameCallback(textureID, int(width), int(height)) + if embedderGLTexture == nil { + return C.bool(false) + } + texture.target = C.uint32_t(embedderGLTexture.Target) + texture.name = C.uint32_t(embedderGLTexture.Name) + texture.format = C.uint32_t(embedderGLTexture.Format) + return C.bool(true) +} + +//export proxy_runs_task_on_current_thread_callback +func proxy_runs_task_on_current_thread_callback(userData unsafe.Pointer) C.bool { + flutterEnginePointer := *(*uintptr)(userData) + flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer)) + return C.bool(flutterEngine.TaskRunnerRunOnCurrentThread()) +} + +//export proxy_post_task_callback +func proxy_post_task_callback(task C.FlutterTask, targetTimeNanos C.uint64_t, userData unsafe.Pointer) { + flutterEnginePointer := *(*uintptr)(userData) + flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer)) + flutterEngine.TaskRunnerPostTask(task, uint64(targetTimeNanos)) +} + +//export proxy_desktop_binary_reply +func proxy_desktop_binary_reply(data *C.uint8_t, dataSize C.size_t, userData unsafe.Pointer) { + callbackPointer := *(*uintptr)(userData) + handler := *(*DataCallback)(unsafe.Pointer(callbackPointer)) + handler(C.GoBytes(unsafe.Pointer(data), C.int(dataSize))) +} diff --git a/event-loop.go b/event-loop.go new file mode 100644 index 00000000..2977198f --- /dev/null +++ b/event-loop.go @@ -0,0 +1,133 @@ +package flutter + +import ( + "container/heap" + "fmt" + "math" + "time" + + "github.com/go-flutter-desktop/go-flutter/embedder" + "github.com/go-flutter-desktop/go-flutter/internal/priorityqueue" +) + +// EventLoop is a event loop for the main thread that allows for delayed task +// execution.() +type EventLoop struct { + // store the task (event) by their priorities + priorityqueue *priorityqueue.PriorityQueue + // called when a task has been received, used to Wakeup the rendering event loop + postEmptyEvent func() + + onExpiredTask func(*embedder.FlutterTask) embedder.Result + + // timeout for non-Rendering events that needs to be processed in a polling manner + platformMessageRefreshRate time.Duration + + // indetifer for the current thread + mainThreadID uint64 +} + +// newEventLoop must ALWAYS be called if the calling goroutine is +// `runtime.LockOSThread()` +func newEventLoop(postEmptyEvent func(), onExpiredTask func(*embedder.FlutterTask) embedder.Result) *EventLoop { + pq := priorityqueue.NewPriorityQueue() + heap.Init(pq) + return &EventLoop{ + priorityqueue: pq, + postEmptyEvent: postEmptyEvent, + onExpiredTask: onExpiredTask, + + // 25 Millisecond is arbitrary value, not too high (adds too much delay to + // platform messages) and not too low (heavy CPU consumption). + // This value isn't related to FPS, as rendering events are process in a + // waiting manner. + // Platform message are fetched from the engine every time the rendering + // event loop process rendering event (e.g.: moving the cursor on the + // window), when no rendering event occur (e.g., window minimized) platform + // message are fetch every 25ms. + platformMessageRefreshRate: time.Duration(25) * time.Millisecond, + } +} + +// RunOnCurrentThread FlutterDocs: +// May be called from any thread. Should return true if tasks posted on the +// calling thread will be run on that same thread. +// +// The functions PostTask and onExpiredTask should be called from the same +// thread, this is ensured if the creation of the event loop (through +// `newEventLoop`) and the PostTask callback (through +// `a.engine.TaskRunnerPostTask = eventLoop.PostTask`) are done on a calling +// goroutine which always execute in that thread (`runtime.LockOSThread()`). +func (t *EventLoop) RunOnCurrentThread() bool { + return true +} + +// PostTask posts a Flutter engine tasks to the event loop for delayed execution. +// PostTask must ALWAYS be called on the same goroutine/thread as `newEventLoop` +func (t *EventLoop) PostTask(task embedder.FlutterTask, targetTimeNanos uint64) { + + taskDuration := time.Duration(targetTimeNanos) * time.Nanosecond + engineDuration := time.Duration(embedder.FlutterEngineGetCurrentTime()) + + t.priorityqueue.Lock() + item := &priorityqueue.Item{ + Value: task, + FireTime: time.Now().Add(taskDuration - engineDuration), + } + heap.Push(t.priorityqueue, item) + t.priorityqueue.Unlock() + + t.postEmptyEvent() +} + +// WaitForEvents waits for an any Rendering or pending Flutter Engine events +// and returns when either is encountered. +// Expired engine events are processed +func (t *EventLoop) WaitForEvents(rendererWaitEvents func(float64)) { + now := time.Now() + + expiredTasks := make([]*priorityqueue.Item, 0) + var top *priorityqueue.Item + + t.priorityqueue.Lock() + for t.priorityqueue.Len() > 0 { + + // Remove the item from the delayed tasks queue. + top = heap.Pop(t.priorityqueue).(*priorityqueue.Item) + + // If this task (and all tasks after this) has not yet expired, there is + // nothing more to do. Quit iterating. + if top.FireTime.After(now) { + heap.Push(t.priorityqueue, top) // push the item back into the queue + break + } + + // Make a record of the expired task. Do NOT service the task here + // because we are still holding onto the task queue mutex. We don't want + // other threads to block on posting tasks onto this thread till we are + // done processing expired tasks. + expiredTasks = append(expiredTasks, top) + + } + t.priorityqueue.Unlock() + + // Fire expired tasks. + for _, item := range expiredTasks { + task := item.Value + if t.onExpiredTask(&task) != embedder.ResultSuccess { + fmt.Printf("go-flutter: couldn't process task %v\n", task) + } + } + + // Sleep till the next task needs to be processed. If a new task comes + // along, the rendererWaitEvents will be resolved early because PostTask + // posts an empty event. + if t.priorityqueue.Len() == 0 { + rendererWaitEvents(t.platformMessageRefreshRate.Seconds()) + } else { + if top.FireTime.After(now) { + durationWait := math.Min(top.FireTime.Sub(now).Seconds(), t.platformMessageRefreshRate.Seconds()) + rendererWaitEvents(durationWait) + } + } +} diff --git a/glfw.go b/glfw.go index 0e73fda1..5ed129dc 100644 --- a/glfw.go +++ b/glfw.go @@ -19,16 +19,24 @@ const dpPerInch = 160.0 // glfwRenderer or glfwManager? All the attaching to glfw.Window must be done // during manager init in that case. Cannot be done by Application. type windowManager struct { - forcedPixelRatio float64 - oncePrintPixelRatioLimit sync.Once - pointerPhase embedder.PointerPhase + // forcedPixelRatio forces the pixelRatio to given value, when value is not zero. + forcedPixelRatio float64 + + // sync.Once to limit pixelRatio warning messages. + oncePrintPixelRatioLimit sync.Once + + // current pointer state + pointerPhase embedder.PointerPhase + pointerButton embedder.PointerButtonMouse + pointerCurrentlyAdded bool + + // caching of ppsc to avoid re-calculating every event pixelsPerScreenCoordinate float64 - pointerCurrentlyAdded bool - pointerButton embedder.PointerButtonMouse } -func newWindowManager() *windowManager { +func newWindowManager(forcedPixelRatio float64) *windowManager { return &windowManager{ + forcedPixelRatio: forcedPixelRatio, pixelsPerScreenCoordinate: 1.0, pointerPhase: embedder.PointerPhaseHover, } @@ -45,8 +53,6 @@ func (m *windowManager) sendPointerEvent(window *glfw.Window, phase embedder.Poi return } - // TODO(GeertJohan): sometimes the x and/or y given by glfw is negative or over window size, could this cause an issue? - // spew.Dump(event) event := embedder.PointerEvent{ Phase: phase, X: x * m.pixelsPerScreenCoordinate, @@ -58,6 +64,15 @@ func (m *windowManager) sendPointerEvent(window *glfw.Window, phase embedder.Poi flutterEnginePointer := *(*uintptr)(window.GetUserPointer()) flutterEngine := (*embedder.FlutterEngine)(unsafe.Pointer(flutterEnginePointer)) + // Always send a pointer event with PhaseMove before an eventual PhaseRemove. + // If x/y on the last move doesn't equal x/y on the PhaseRemove, the remove + // is canceled in Flutter. + if phase == embedder.PointerPhaseRemove { + event.Phase = embedder.PointerPhaseHover + flutterEngine.SendPointerEvent(event) + event.Phase = embedder.PointerPhaseRemove + } + flutterEngine.SendPointerEvent(event) if phase == embedder.PointerPhaseAdd { diff --git a/go.mod b/go.mod index caaf77ba..1300521c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/davecgh/go-spew v1.1.1 + github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 github.com/go-gl/glfw v0.0.0-20190519095719-e6da0acd62b1 github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum index ade1e344..5e5836b8 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-gl/glfw v0.0.0-20190217072633-93b30450e032 h1:WUDJN6o1AZlnNR0UZ11zsr0Quh44CV7svcg6VzEsySc= -github.com/go-gl/glfw v0.0.0-20190217072633-93b30450e032/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= github.com/go-gl/glfw v0.0.0-20190519095719-e6da0acd62b1 h1:noz9OnjV5PMOZWNOI+y1cS5rnxuJfpY6leIgQEEdBQw= github.com/go-gl/glfw v0.0.0-20190519095719-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= diff --git a/internal/priorityqueue/priorityqueue.go b/internal/priorityqueue/priorityqueue.go new file mode 100644 index 00000000..bce37d43 --- /dev/null +++ b/internal/priorityqueue/priorityqueue.go @@ -0,0 +1,62 @@ +package priorityqueue + +import ( + "sync" + "time" + + "github.com/go-flutter-desktop/go-flutter/embedder" +) + +// An Item is something we manage in a priority queue. +type Item struct { + Value embedder.FlutterTask // The value of the item + FireTime time.Time // The priority of the item in the queue. + + // The index is needed by update and is maintained by the heap.Interface methods. + index int // The index of the item in the heap. +} + +// A PriorityQueue implements heap.Interface and holds Items. +type PriorityQueue struct { + queue []*Item + sync.Mutex +} + +// NewPriorityQueue create a new PriorityQueue +func NewPriorityQueue() *PriorityQueue { + pq := &PriorityQueue{} + pq.queue = make([]*Item, 0) + return pq +} + +func (pq *PriorityQueue) Len() int { return len(pq.queue) } + +func (pq *PriorityQueue) Less(i, j int) bool { + // We want Pop to give us the lowest, not highest, priority so we use lower + // than here. + return pq.queue[i].FireTime.Before(pq.queue[j].FireTime) +} + +func (pq *PriorityQueue) Swap(i, j int) { + pq.queue[i], pq.queue[j] = pq.queue[j], pq.queue[i] + pq.queue[i].index = i + pq.queue[j].index = j +} + +// Push add a new priority/value pair in the queue. 0 priority = max. +func (pq *PriorityQueue) Push(x interface{}) { + n := len(pq.queue) + item := x.(*Item) + item.index = n + pq.queue = append(pq.queue, item) +} + +// Pop Remove and return the highest item (lowest priority) +func (pq *PriorityQueue) Pop() interface{} { + old := pq.queue + n := len(old) + item := old[n-1] + item.index = -1 // for safety + pq.queue = old[0 : n-1] + return item +} diff --git a/key-events.go b/key-events.go index de01099b..c4cb161e 100644 --- a/key-events.go +++ b/key-events.go @@ -81,7 +81,7 @@ func (p *keyeventPlugin) sendKeyEvent(window *glfw.Window, key glfw.Key, scancod ScanCode: scancode, Modifiers: int(mods), } - _, err := p.keyEventChannel.Send(event) + err := p.keyEventChannel.Send(event) if err != nil { fmt.Printf("go-flutter: Failed to send raw_keyboard event %v: %v\n", event, err) } diff --git a/lifecycle.go b/lifecycle.go index e7aae4fe..8fb82686 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -33,7 +33,7 @@ func (p *lifecyclePlugin) glfwIconifyCallback(w *glfw.Window, iconified bool) { case false: state = "AppLifecycleState.resumed" } - _, err := p.channel.Send(state) + err := p.channel.Send(state) if err != nil { fmt.Printf("go-flutter: Failed to send lifecycle event %s: %v\n", state, err) } diff --git a/messenger.go b/messenger.go index bff201ef..eeb9a275 100644 --- a/messenger.go +++ b/messenger.go @@ -30,13 +30,25 @@ func newMessenger(engine *embedder.FlutterEngine) *messenger { } } -// Send pushes a binary message on a channel to the Flutter side. Replies are -// not supported yet (https://github.com/flutter/flutter/issues/18852). This -// means that currently, binaryReply will be nil on success. -func (m *messenger) Send(channel string, binaryMessage []byte) (binaryReply []byte, err error) { +// SendWithReply pushes a binary message on a channel to the Flutter side and +// wait for a reply. +// NOTE: If no value are returned by the flutter handler, the function will +// wait forever. In case you don't want to wait for reply, use Send. +func (m *messenger) SendWithReply(channel string, binaryMessage []byte) (binaryReply []byte, err error) { + reply := make(chan []byte) + defer close(reply) + responseHandle, err := m.engine.CreatePlatformMessageResponseHandle(func(binaryMessage []byte) { + reply <- binaryMessage + }) + if err != nil { + return nil, err + } + defer m.engine.ReleasePlatformMessageResponseHandle(responseHandle) + msg := &embedder.PlatformMessage{ - Channel: channel, - Message: binaryMessage, + Channel: channel, + Message: binaryMessage, + ResponseHandle: responseHandle, } res := m.engine.SendPlatformMessage(msg) if err != nil { @@ -48,9 +60,28 @@ func (m *messenger) Send(channel string, binaryMessage []byte) (binaryReply []by return nil, errors.New("failed to send message") } - // NOTE: Response from engine is not yet supported by embedder. - // https://github.com/flutter/flutter/issues/18852 - return nil, nil + // wait for a reply and return + return <-reply, nil +} + +// Send pushes a binary message on a channel to the Flutter side without +// expecting replies. +func (m *messenger) Send(channel string, binaryMessage []byte) (err error) { + msg := &embedder.PlatformMessage{ + Channel: channel, + Message: binaryMessage, + } + res := m.engine.SendPlatformMessage(msg) + if err != nil { + if ferr, ok := err.(*plugin.FlutterError); ok { + return ferr + } + } + if res != embedder.ResultSuccess { + return errors.New("failed to send message") + } + + return nil } // SetChannelHandler satisfies plugin.BinaryMessenger diff --git a/option.go b/option.go index 43f628e3..5f07693d 100644 --- a/option.go +++ b/option.go @@ -16,7 +16,7 @@ type config struct { windowInitializerDeprecated func(*glfw.Window) error windowIconProvider func() ([]image.Image, error) windowInitialDimensions windowDimensions - windowInitialLocations windowLocations + windowInitialLocation windowLocation windowDimensionLimits windowDimensionLimits windowMode windowMode @@ -31,7 +31,7 @@ type windowDimensions struct { height int } -type windowLocations struct { +type windowLocation struct { xpos int ypos int } @@ -102,8 +102,8 @@ func OptionVMArguments(a []string) Option { // // Deprecated, please use WindowInitialDimensions(x, y). func ApplicationWindowDimension(x, y int) Option { - // deprecated on 2019-03-10 - fmt.Println("go-flutter: ApplicationWindowDimension is deprecated, use WindowInitialDimensions(x, y).") + // deprecated on 2019-03-10, to be removed 2020-01-01 + fmt.Println("go-flutter: ApplicationWindowDimension (singular) is deprecated, use WindowInitialDimensions (plural).") return WindowInitialDimensions(x, y) } @@ -127,7 +127,18 @@ func WindowInitialDimensions(width, height int) Option { // WindowInitialLocations specify the startup's position of the window. // Location, in screen coordinates, of the upper-left corner of the client area // of the window. +// +// Deprecated, please use WindowInitialLocation(xpos, ypos). func WindowInitialLocations(xpos, ypos int) Option { + // deprecated on 2019-08-18, to be removed 2020-06-01 + fmt.Println("go-flutter: WindowInitialLocations (plural) is deprecated, use WindowInitialLocation (singular).") + return WindowInitialLocation(xpos, ypos) +} + +// WindowInitialLocation specify the startup's position of the window. +// Location, in screen coordinates, of the upper-left corner of the client area +// of the window. +func WindowInitialLocation(xpos, ypos int) Option { if xpos < 1 { fmt.Println("go-flutter: invalid initial value for xpos location, must be 1 or greater.") os.Exit(1) @@ -138,8 +149,8 @@ func WindowInitialLocations(xpos, ypos int) Option { } return func(c *config) { - c.windowInitialLocations.xpos = xpos - c.windowInitialLocations.ypos = ypos + c.windowInitialLocation.xpos = xpos + c.windowInitialLocation.ypos = ypos } } @@ -175,7 +186,7 @@ func WindowDimensionLimits(minWidth, minHeight, maxWidth, maxHeight int) Option // // Deprecated, please use WindowIcon if you'd like to set the window icon. func OptionWindowInitializer(ini func(*glfw.Window) error) Option { - // deprecated on 2019-03-05 + // deprecated on 2019-03-05, to be removed 2020-01-01 fmt.Println("go-flutter: OptionWindowInitializer is deprecated. Please read https://is.gd/gflut_window_init_deprecated") return func(c *config) { c.windowInitializerDeprecated = ini diff --git a/platform.go b/platform.go index 6f76e645..fc477041 100644 --- a/platform.go +++ b/platform.go @@ -31,6 +31,7 @@ var _ PluginGLFW = &platformPlugin{} // compile-time type check func (p *platformPlugin) InitPlugin(messenger plugin.BinaryMessenger) error { p.messenger = messenger + p.glfwTasker = tasker.New() return nil } diff --git a/plugin.go b/plugin.go index 0f44731e..c2ed325a 100644 --- a/plugin.go +++ b/plugin.go @@ -22,7 +22,7 @@ type Plugin interface { // PluginGLFW defines the interface for plugins that are GLFW-aware. Plugins may // implement this interface to receive access to the *glfw.Window. Note that // plugins must still implement the Plugin interface. The call to InitPluginGLFW -// is made afther the call to InitPlugin. +// is made after the call to InitPlugin. // // PluginGLFW is separated because not all plugins need to know about glfw, // Adding glfw.Window to the InitPlugin call would add glfw as dependency to @@ -37,3 +37,19 @@ type PluginGLFW interface { // returned it is printend the application is stopped. InitPluginGLFW(window *glfw.Window) error } + +// PluginTexture defines the interface for plugins that needs to create and +// manage backend textures. Plugins may implement this interface to receive +// access to the TextureRegistry. Note that plugins must still implement the +// Plugin interface. The call to PluginTexture is made after the call to +// PluginGLFW. +// +// PluginTexture is separated because not all plugins need to send raw pixel to +// the Flutter scene. +type PluginTexture interface { + // Any type inmplementing PluginTexture must also implement Plugin. + Plugin + // InitPluginTexture is called after the call to InitPlugin. When an error is + // returned it is printend the application is stopped. + InitPluginTexture(registry *TextureRegistry) error +} diff --git a/plugin/basic-message-channel.go b/plugin/basic-message-channel.go index 9051920d..465073b5 100644 --- a/plugin/basic-message-channel.go +++ b/plugin/basic-message-channel.go @@ -53,16 +53,33 @@ func NewBasicMessageChannel(messenger BinaryMessenger, channelName string, codec return b } -// Send encodes and sends the specified message to the Flutter application and -// returns the reply, or an error. Results from the Flutter side are not yet -// implemented in the embedder. Until then, InvokeMethod will always return nil -// as reult. https://github.com/flutter/flutter/issues/18852 -func (b *BasicMessageChannel) Send(message interface{}) (reply interface{}, err error) { +// Send encodes and sends the specified message to the Flutter application +// without waiting for a reply. +func (b *BasicMessageChannel) Send(message interface{}) error { + encodedMessage, err := b.codec.EncodeMessage(message) + if err != nil { + return errors.Wrap(err, "failed to encode outgoing message") + } + err = b.messenger.Send(b.channelName, encodedMessage) + if err != nil { + return errors.Wrap(err, "failed to send outgoing message") + } + return nil +} + +// SendWithReply encodes and sends the specified message to the Flutter +// application and returns the reply, or an error. +// +// NOTE: If no value are returned by the handler setted in the +// setMessageHandler flutter method, the function will wait forever. In case +// you don't want to wait for reply, use Send or launch the +// function in a goroutine. +func (b *BasicMessageChannel) SendWithReply(message interface{}) (reply interface{}, err error) { encodedMessage, err := b.codec.EncodeMessage(message) if err != nil { return nil, errors.Wrap(err, "failed to encode outgoing message") } - encodedReply, err := b.messenger.Send(b.channelName, encodedMessage) + encodedReply, err := b.messenger.SendWithReply(b.channelName, encodedMessage) if err != nil { return nil, errors.Wrap(err, "failed to send outgoing message") } diff --git a/plugin/basic-message-channel_test.go b/plugin/basic-message-channel_test.go index e53db376..d3d1caf4 100644 --- a/plugin/basic-message-channel_test.go +++ b/plugin/basic-message-channel_test.go @@ -31,7 +31,7 @@ func TestBasicMethodChannelStringCodecSend(t *testing.T) { return nil }) channel := NewBasicMessageChannel(messenger, "ch", codec) - reply, err := channel.Send("hello") + reply, err := channel.SendWithReply("hello") if err != nil { t.Fatal(err) } @@ -100,7 +100,7 @@ func TestBasicMethodChannelBinaryCodecSend(t *testing.T) { return nil }) channel := NewBasicMessageChannel(messenger, "ch", codec) - reply, err := channel.Send([]byte{0x01}) + reply, err := channel.SendWithReply([]byte{0x01}) if err != nil { t.Fatal(err) } @@ -160,7 +160,7 @@ func TestBasicMethodChannelNilMockHandler(t *testing.T) { messenger := NewTestingBinaryMessenger() messenger.MockSetChannelHandler("ch", nil) channel := NewBasicMessageChannel(messenger, "ch", codec) - reply, err := channel.Send("hello") + reply, err := channel.SendWithReply("hello") Nil(t, reply) NotNil(t, err) Equal(t, "failed to send outgoing message: no handler set", err.Error()) @@ -170,7 +170,7 @@ func TestBasicMethodChannelEncodeFail(t *testing.T) { codec := StringCodec{} messenger := NewTestingBinaryMessenger() channel := NewBasicMessageChannel(messenger, "ch", codec) - reply, err := channel.Send(int(42)) // invalid value + reply, err := channel.SendWithReply(int(42)) // invalid value Nil(t, reply) NotNil(t, err) Equal(t, "failed to encode outgoing message: invalid type provided to message codec: expected message to be of type string", err.Error()) diff --git a/plugin/binary-messenger.go b/plugin/binary-messenger.go index da819ead..315f8451 100644 --- a/plugin/binary-messenger.go +++ b/plugin/binary-messenger.go @@ -2,8 +2,12 @@ package plugin // BinaryMessenger defines a bidirectional binary messenger. type BinaryMessenger interface { - // Send sends a binary message to the Flutter application. - Send(channel string, binaryMessage []byte) (binaryReply []byte, err error) + // SendWithReply sends a binary message to the Flutter application. + SendWithReply(channel string, binaryMessage []byte) (binaryReply []byte, err error) + + // Send sends a binary message to the Flutter application without + // expecting a reply. + Send(channel string, binaryMessage []byte) (err error) // SetChannelHandler registers a handler to be invoked when the Flutter // application sends a message to its host platform on given channel. diff --git a/plugin/event-sink.go b/plugin/event-sink.go index 1be8a0c0..0f1dfdd0 100644 --- a/plugin/event-sink.go +++ b/plugin/event-sink.go @@ -36,7 +36,10 @@ func (es *EventSink) Success(event interface{}) { if err != nil { fmt.Printf("go-flutter: failed to encode success envelope for event channel '%s', error: %v\n", es.eventChannel.channelName, err) } - es.eventChannel.messenger.Send(es.eventChannel.channelName, binaryMsg) + err = es.eventChannel.messenger.Send(es.eventChannel.channelName, binaryMsg) + if err != nil { + fmt.Printf("go-flutter: failed to send Success message on event channel '%s', error: %v\n", es.eventChannel.channelName, err) + } } // Error consumes an error event. @@ -51,7 +54,10 @@ func (es *EventSink) Error(errorCode string, errorMessage string, errorDetails i if err != nil { fmt.Printf("go-flutter: failed to encode success envelope for event channel '%s', error: %v\n", es.eventChannel.channelName, err) } - es.eventChannel.messenger.Send(es.eventChannel.channelName, binaryMsg) + err = es.eventChannel.messenger.Send(es.eventChannel.channelName, binaryMsg) + if err != nil { + fmt.Printf("go-flutter: failed to send Error message on event channel '%s', error: %v\n", es.eventChannel.channelName, err) + } } // EndOfStream consumes end of stream. @@ -63,5 +69,8 @@ func (es *EventSink) EndOfStream() { } es.hasEnded = true - es.eventChannel.messenger.Send(es.eventChannel.channelName, nil) + err := es.eventChannel.messenger.Send(es.eventChannel.channelName, nil) + if err != nil { + fmt.Printf("go-flutter: failed to send EndOfStream message on event channel '%s', error: %v\n", es.eventChannel.channelName, err) + } } diff --git a/plugin/helper_test.go b/plugin/helper_test.go index 0a85e44f..0a2ce6de 100644 --- a/plugin/helper_test.go +++ b/plugin/helper_test.go @@ -29,10 +29,15 @@ func NewTestingBinaryMessenger() *TestingBinaryMessenger { var _ BinaryMessenger = &TestingBinaryMessenger{} // compile-time type check +func (t *TestingBinaryMessenger) Send(channel string, message []byte) (err error) { + err = t.Send(channel, message) + return err +} + // Send sends the bytes onto the given channel. // In this testing implementation of a BinaryMessenger, the handler for the // channel may be set using MockSetMessageHandler -func (t *TestingBinaryMessenger) Send(channel string, message []byte) (reply []byte, err error) { +func (t *TestingBinaryMessenger) SendWithReply(channel string, message []byte) (reply []byte, err error) { t.mockChannelHandlersLock.Lock() handler := t.mockChannelHandlers[channel] t.mockChannelHandlersLock.Unlock() diff --git a/plugin/method-channel.go b/plugin/method-channel.go index c595a91a..f28fdd38 100644 --- a/plugin/method-channel.go +++ b/plugin/method-channel.go @@ -40,11 +40,31 @@ func NewMethodChannel(messenger BinaryMessenger, channelName string, methodCodec return mc } -// InvokeMethod sends a methodcall to the binary messenger and waits for a -// result. Results from the Flutter side are not yet implemented in the -// embedder. Until then, InvokeMethod will always return nil as result. -// https://github.com/flutter/flutter/issues/18852 -func (m *MethodChannel) InvokeMethod(name string, arguments interface{}) (result interface{}, err error) { +// InvokeMethod sends a methodcall to the binary messenger without waiting for +// a reply. +func (m *MethodChannel) InvokeMethod(name string, arguments interface{}) error { + encodedMessage, err := m.methodCodec.EncodeMethodCall(MethodCall{ + Method: name, + Arguments: arguments, + }) + if err != nil { + return errors.Wrap(err, "failed to encode methodcall") + } + err = m.messenger.Send(m.channelName, encodedMessage) + if err != nil { + return errors.Wrap(err, "failed to send methodcall") + } + return nil +} + +// InvokeMethodWithReply sends a methodcall to the binary messenger and wait +// for a reply. +// +// NOTE: If no value are returned by the handler setted in the +// setMethodCallHandler flutter method, the function will wait forever. In case +// you don't want to wait for reply, use InvokeMethod or launch the +// function in a goroutine. +func (m *MethodChannel) InvokeMethodWithReply(name string, arguments interface{}) (result interface{}, err error) { encodedMessage, err := m.methodCodec.EncodeMethodCall(MethodCall{ Method: name, Arguments: arguments, @@ -52,16 +72,10 @@ func (m *MethodChannel) InvokeMethod(name string, arguments interface{}) (result if err != nil { return nil, errors.Wrap(err, "failed to encode methodcall") } - encodedReply, err := m.messenger.Send(m.channelName, encodedMessage) + encodedReply, err := m.messenger.SendWithReply(m.channelName, encodedMessage) if err != nil { return nil, errors.Wrap(err, "failed to send methodcall") } - // TODO(GeertJohan): InvokeMethod may not return any JSON. In Java this is - // handled by not having a callback handler, which means no response is - // expected and response is never unmarshalled. We should perhaps define - // InvokeMethod(..) and InovkeMethodNoResponse(..) to avoid errors when no - // response is given. - // https://github.com/go-flutter-desktop/go-flutter/issues/141 result, err = m.methodCodec.DecodeEnvelope(encodedReply) if err != nil { return nil, err @@ -196,7 +210,16 @@ func (m *MethodChannel) handleMethodCall(handler MethodHandler, methodName strin reply, err := handler.HandleMethod(methodArgs) if err != nil { fmt.Printf("go-flutter: handler for method '%s' on channel '%s' returned an error: %v\n", methodName, m.channelName, err) - binaryReply, err := m.methodCodec.EncodeErrorEnvelope("error", err.Error(), nil) + + var errorCode string + switch t := err.(type) { + case *Error: + errorCode = t.code + default: + errorCode = "error" + } + + binaryReply, err := m.methodCodec.EncodeErrorEnvelope(errorCode, err.Error(), nil) if err != nil { fmt.Printf("go-flutter: failed to encode error envelope for method '%s' on channel '%s', error: %v\n", methodName, m.channelName, err) } @@ -209,3 +232,25 @@ func (m *MethodChannel) handleMethodCall(handler MethodHandler, methodName strin } responseSender.Send(binaryReply) } + +// Error implement the Go error interface, can be thrown from a go-flutter +// method channel plugin to return custom error codes. +// Normal Go error can also be used, the error code will default to "error". +type Error struct { + err string + code string +} + +// Error is needed to comply with the Golang error interface. +func (e *Error) Error() string { + return e.err +} + +// NewError create an error with an specific error code. +func NewError(code string, err error) *Error { + pe := &Error{ + code: code, + err: err.Error(), + } + return pe +} diff --git a/plugin/method-channel_test.go b/plugin/method-channel_test.go index 99b856e2..fbf91653 100644 --- a/plugin/method-channel_test.go +++ b/plugin/method-channel_test.go @@ -29,11 +29,11 @@ func TestMethodChannelJSONInvoke(t *testing.T) { r.Send(binaryReply) return nil }) - result, err := channel.InvokeMethod("sayHello", "hello") + result, err := channel.InvokeMethodWithReply("sayHello", "hello") Nil(t, err) Equal(t, json.RawMessage(`"hello world"`), result) - result, err = channel.InvokeMethod("invalidMethod", "") + result, err = channel.InvokeMethodWithReply("invalidMethod", "") Nil(t, result) expectedError := FlutterError{ Code: "unknown", diff --git a/textinput_model.go b/textinput-model.go similarity index 100% rename from textinput_model.go rename to textinput-model.go diff --git a/textinput.go b/textinput.go index f3d57598..58227be0 100644 --- a/textinput.go +++ b/textinput.go @@ -126,7 +126,7 @@ func (p *textinputPlugin) glfwKeyCallback(window *glfw.Window, key glfw.Key, sca keyboardShortcutBind := keyboardShortcutsGLFW{mod: mods} if key == glfw.KeyEscape && action == glfw.Press { - _, err := defaultNavigationPlugin.channel.InvokeMethod("popRoute", nil) + err := defaultNavigationPlugin.channel.InvokeMethod("popRoute", nil) if err != nil { fmt.Printf("go-flutter: failed to pop route after escape key press: %v\n", err) } diff --git a/texture-registry.go b/texture-registry.go new file mode 100644 index 00000000..075eec33 --- /dev/null +++ b/texture-registry.go @@ -0,0 +1,150 @@ +package flutter + +import ( + "fmt" + "sync" + + "github.com/go-flutter-desktop/go-flutter/embedder" + "github.com/go-gl/gl/v4.6-core/gl" + "github.com/go-gl/glfw/v3.2/glfw" + "github.com/pkg/errors" +) + +// once is used for the lazy initialization of go-gl/gl. +// The initialization occur on the first requested texture's frame. +var once sync.Once + +// TextureRegistry is a registry entry for a managed Texture. +type TextureRegistry struct { + window *glfw.Window + engine *embedder.FlutterEngine + channels map[int64]*externalTextureHanlder + channelsLock sync.RWMutex + + texture int64 + texturesLock sync.Mutex +} + +type externalTextureHanlder struct { + // handle is called when flutter needs the PixelBuffer + handle ExternalTextureHanlderFunc + // gl texture to refer to for this handler + texture uint32 +} + +func newTextureRegistry(engine *embedder.FlutterEngine, window *glfw.Window) *TextureRegistry { + return &TextureRegistry{ + window: window, + engine: engine, + channels: make(map[int64]*externalTextureHanlder), + } +} + +func (t *TextureRegistry) init() error { + t.window.MakeContextCurrent() + // Important! Call gl.Init only under the presence of an active OpenGL context, + // i.e., after MakeContextCurrent. + if err := gl.Init(); err != nil { + return errors.Wrap(err, "TextureRegistry gl init failed") + } + return nil +} + +// NewTexture creates a new Texture +func (t *TextureRegistry) NewTexture() Texture { + t.texturesLock.Lock() + defer t.texturesLock.Unlock() + t.texture++ + return Texture{ID: t.texture, registry: t} +} + +// ExternalTextureHanlderFunc describes the function that handles external +// Texture on a given ID. +type ExternalTextureHanlderFunc func(width int, height int) (bool, *PixelBuffer) + +// PixelBuffer is an in-memory (RGBA) image. +type PixelBuffer struct { + // Pix holds the image's pixels, in R, G, B, A order. + Pix []uint8 + // Width and Height of the image's bounds + Width, Height int +} + +// setTextureHandler registers a handler to be invoked when the Flutter +// application want to get a PixelBuffer to draw into the scene. +// +// Registration overwrites any previous registration for the same textureID +// name. Use nil as handler to deregister. +func (t *TextureRegistry) setTextureHandler(textureID int64, handler ExternalTextureHanlderFunc) { + t.channelsLock.Lock() + if handler == nil { + texture := t.channels[textureID] + if texture != nil { + gl.DeleteTextures(1, &texture.texture) + } + delete(t.channels, textureID) + } else { + t.channels[textureID] = &externalTextureHanlder{ + handle: handler, + } + } + t.channelsLock.Unlock() +} + +func (t *TextureRegistry) handleExternalTexture(textureID int64, + width int, height int) *embedder.FlutterOpenGLTexture { + + once.Do(func() { + t.init() + }) + + t.channelsLock.RLock() + registration, registrationExists := t.channels[textureID] + t.channelsLock.RUnlock() + + if !registrationExists { + fmt.Printf("go-flutter: no texture handler found for Texture ID: %v\n", textureID) + return nil + } + res, pixelBuffer := registration.handle(width, height) + if !res || pixelBuffer == nil { + return nil + } + + if len(pixelBuffer.Pix) == 0 { + return nil + } + + t.window.MakeContextCurrent() + + if registration.texture == 0 { + gl.GenTextures(1, ®istration.texture) + gl.BindTexture(gl.TEXTURE_2D, registration.texture) + // set the texture wrapping parameters + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_BORDER) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_BORDER) + // set texture filtering parameters + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + } + + gl.BindTexture(gl.TEXTURE_2D, registration.texture) + // It seems that current flutter/engine can only support RGBA texture. + gl.TexImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + int32(pixelBuffer.Width), + int32(pixelBuffer.Height), + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + gl.Ptr(pixelBuffer.Pix)) + + return &embedder.FlutterOpenGLTexture{ + Target: gl.TEXTURE_2D, + Name: registration.texture, + Format: gl.RGBA8, + } + +} diff --git a/texture.go b/texture.go new file mode 100644 index 00000000..333e7e8e --- /dev/null +++ b/texture.go @@ -0,0 +1,44 @@ +package flutter + +import ( + "errors" + "fmt" + + "github.com/go-flutter-desktop/go-flutter/embedder" +) + +// Texture is an identifier for texture declaration +type Texture struct { + ID int64 + registry *TextureRegistry +} + +// Register registers a textureID with his associated handler +func (t *Texture) Register(handler ExternalTextureHanlderFunc) error { + t.registry.setTextureHandler(t.ID, handler) + result := t.registry.engine.RegisterExternalTexture(t.ID) + if result != embedder.ResultSuccess { + t.registry.setTextureHandler(t.ID, nil) + return errors.New("'go-flutter' couldn't register texture with id: " + fmt.Sprint(t.ID)) + } + return nil +} + +// FrameAvailable mark a texture buffer is ready to be draw in the flutter scene +func (t *Texture) FrameAvailable() error { + result := t.registry.engine.MarkExternalTextureFrameAvailable(t.ID) + if result != embedder.ResultSuccess { + return errors.New("'go-flutter' couldn't mark frame available of texture with id: " + fmt.Sprint(t.ID)) + } + return nil +} + +// UnRegister unregisters a textureID with his associated handler +func (t *Texture) UnRegister() error { + result := t.registry.engine.UnregisterExternalTexture(t.ID) + if result != embedder.ResultSuccess { + return errors.New("'go-flutter' couldn't unregisters texture with id: " + fmt.Sprint(t.ID)) + } + t.registry.setTextureHandler(t.ID, nil) + return nil +}