changeset 2993:392f0b3dd502

Android: Add DW_FEATURE_RENDER_SAFE, initially just for Android. This will eventually be supported on other platforms that support unsafe rendering. On Android this moves EXPOSE events off the main thread and is enabled by default to provide extra safety. Since none of the callbacks happen on the main thread anymore, this means that none of the threading functions follow the potentially unsafe code paths that lead to instability. The trade off is performance, rendering performance, which was already kind of slow is even worse. I'll look for ways to improve this performance without sacrificing stability.
author bsmith@81767d24-ef19-dc11-ae90-00e081727c95
date Tue, 02 May 2023 11:37:48 +0000
parents c8bee3b6e7ce
children 8311be624877
files android/DWindows.kt android/dw.cpp dw.h dwtest.c dwtestoo.cpp readme.txt
diffstat 6 files changed, 195 insertions(+), 47 deletions(-) [+]
line wrap: on
line diff
--- a/android/DWindows.kt	Fri Apr 14 02:53:00 2023 +0000
+++ b/android/DWindows.kt	Tue May 02 11:37:48 2023 +0000
@@ -1859,24 +1859,53 @@
 
 class DWRender(context: Context) : View(context) {
     var cachedCanvas: Canvas? = null
+    var backingBitmap: Bitmap? = null
     var typeface: Typeface? = null
     var fontsize: Float? = null
     var evx: Float = 0f
     var evy: Float = 0f
     var button: Int = 1
 
+    fun createBitmap() {
+        // If we don't have a backing bitmap, or its size in invalid
+        if(backingBitmap == null ||
+            backingBitmap!!.width != this.width || backingBitmap!!.height != this.height) {
+            // Create the backing bitmap
+            backingBitmap = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)
+        }
+        cachedCanvas = Canvas(backingBitmap!!)
+    }
+
+    fun finishDraw() {
+        cachedCanvas = null
+        this.invalidate()
+    }
+
     override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
         super.onSizeChanged(width, height, oldWidth, oldHeight)
+        if(backingBitmap != null &&
+            (backingBitmap!!.width != width || backingBitmap!!.height != height)) {
+            backingBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+        }
         // Send DW_SIGNAL_CONFIGURE
         eventHandlerInt(DWEvent.CONFIGURE, width, height, 0, 0)
+        if(backingBitmap != null) {
+            // Send DW_SIGNAL_EXPOSE
+            eventHandlerInt(DWEvent.EXPOSE, 0, 0, width, height)
+        }
     }
 
     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)
-        cachedCanvas = canvas
-        // Send DW_SIGNAL_EXPOSE
-        eventHandlerInt(DWEvent.EXPOSE, 0, 0, this.width, this.height)
-        cachedCanvas = null
+        if(backingBitmap != null && cachedCanvas == null) {
+            canvas.drawBitmap(backingBitmap!!, 0f, 0f, null)
+        } else {
+            val savedCanvas = cachedCanvas
+            cachedCanvas = canvas
+            // Send DW_SIGNAL_EXPOSE
+            eventHandlerInt(DWEvent.EXPOSE, 0, 0, this.width, this.height)
+            cachedCanvas = savedCanvas
+        }
     }
 
     external fun eventHandlerInt(
@@ -6086,10 +6115,28 @@
         return render
     }
 
-    fun renderRedraw(render: DWRender)
+    fun renderRedraw(render: DWRender, safe: Int)
     {
         runOnUiThread {
-            render.invalidate()
+            if(safe != 0) {
+                render.eventHandlerInt(DWEvent.EXPOSE, 0, 0, render.width, render.height)
+            } else {
+                render.invalidate()
+            }
+        }
+    }
+
+    fun renderCreateBitmap(render: DWRender)
+    {
+        runOnUiThread {
+            render.createBitmap()
+        }
+    }
+
+    fun renderFinishDraw(render: DWRender)
+    {
+        runOnUiThread {
+            render.finishDraw()
         }
     }
 
--- a/android/dw.cpp	Fri Apr 14 02:53:00 2023 +0000
+++ b/android/dw.cpp	Tue May 02 11:37:48 2023 +0000
@@ -45,6 +45,7 @@
 static char _dw_user_dir[MAX_PATH+1] = {0};
 static int _dw_android_api = 0;
 static int _dw_container_mode = DW_CONTAINER_MODE_DEFAULT;
+static int _dw_render_safe_mode = DW_FEATURE_ENABLED;
 
 static pthread_key_t _dw_env_key;
 static pthread_key_t _dw_fgcolor_key;
@@ -277,6 +278,10 @@
 
 #define _DW_EVENT_PARAM_SIZE 10
 
+void _dw_render_create_bitmap(HWND handle);
+void _dw_render_finish_draw(HWND handle);
+
+
 int _dw_event_handler2(void **params)
 {
     DWSignalHandler *handler = (DWSignalHandler *)params[9];
@@ -358,7 +363,11 @@
                 exp.y = DW_POINTER_TO_INT(params[4]);
                 exp.width = DW_POINTER_TO_INT(params[5]);
                 exp.height = DW_POINTER_TO_INT(params[6]);
+                if(_dw_render_safe_mode)
+                    _dw_render_create_bitmap(handler->window);
                 retval = exposefunc(handler->window, &exp, handler->data);
+                if(_dw_render_safe_mode)
+                    _dw_render_finish_draw(handler->window);
                 /* Return here so we don't free params since we
                  * are always handling expose/draw on the UI thread.
                  */
@@ -494,29 +503,38 @@
     return retval;
 }
 
-/* We create a queue of events... the oldest event indexed by
- * _dw_event_head, the newest by _dw_event_tail.
- */
-#define _DW_EVENT_QUEUE_LENGTH 10
-void *_dw_event_queue[_DW_EVENT_QUEUE_LENGTH][_DW_EVENT_PARAM_SIZE];
-int _dw_event_head = -1, _dw_event_tail = -1, _dw_main_active = TRUE;
-HMTX _dw_event_mutex = nullptr;
+/* We create queues of events... the oldest event indexed by
+ * _dw_event_queue->head, the newest by _dw_event_queue->tail.
+ */
+#define _DW_EVENT_QUEUE_LENGTH 50
+
+typedef struct
+{
+    int head, tail;
+    HMTX mutex;
+    void *queue[_DW_EVENT_QUEUE_LENGTH][_DW_EVENT_PARAM_SIZE];
+} _dw_event_queue;
+
+/* Higher priority draw queue gets handled first */
+_dw_event_queue _dw_high = { -1, -1 };
+_dw_event_queue _dw_low = { -1, -1 };
+int _dw_main_active = TRUE;
 DWTID _dw_main_thread = -1;
 
 /* Add a new event to the queue if there is space.
  * This will be handled in the thread running dw_main()
  */
-int _dw_queue_event(void **params)
-{
-    int newtail = _dw_event_tail + 1;
+int _dw_queue_event(void **params, _dw_event_queue *queue)
+{
+    int newtail = queue->tail + 1;
     int retval = FALSE;
 
     /* Initialize the mutex if necessary... return on failure. */
-    if(!_dw_event_mutex && !(_dw_event_mutex = dw_mutex_new()))
+    if(!queue->mutex && !(queue->mutex = dw_mutex_new()))
         return retval;
 
     /* Protect the queue in a mutex... hold for as short as possible */
-    while(dw_mutex_trylock(_dw_event_mutex) != DW_ERROR_NONE)
+    while(dw_mutex_trylock(queue->mutex) != DW_ERROR_NONE)
         sched_yield();
 
     /* If we are at the end of the queue, loop back to the start. */
@@ -525,19 +543,19 @@
     /* If the new tail will be at the head, the event queue
      * if full... drop the event.
      */
-    if(newtail != _dw_event_head)
+    if(newtail != queue->head)
     {
         /* If the queue was empty, head and tail will be the same. */
-        if (_dw_event_head == -1)
-            _dw_event_head = newtail;
+        if (queue->head == -1)
+            queue->head = newtail;
         /* Copy the new event from the stack into the event queue. */
-        memcpy(&_dw_event_queue[newtail], params, _DW_EVENT_PARAM_SIZE * sizeof(void *));
+        memcpy(&queue->queue[newtail], params, _DW_EVENT_PARAM_SIZE * sizeof(void *));
         /* Update the tail index */
-        _dw_event_tail = newtail;
+        queue->tail = newtail;
         /* Successfully queued event */
         retval = TRUE;
     }
-    dw_mutex_unlock(_dw_event_mutex);
+    dw_mutex_unlock(queue->mutex);
     return retval;
 }
 
@@ -545,40 +563,40 @@
  * advance the head and return TRUE.
  * If there are no events waiting return FALSE
  */
-int _dw_dequeue_event(void **params)
+int _dw_dequeue_event(void **params, _dw_event_queue *queue)
 {
     int retval = FALSE;
 
     /* Initialize the mutex if necessary... return FALSE on failure. */
-    if(!_dw_event_mutex && !(_dw_event_mutex = dw_mutex_new()))
+    if(!queue->mutex && !(queue->mutex = dw_mutex_new()))
         return retval;
-    dw_mutex_lock(_dw_event_mutex);
-    if(_dw_event_head != -1)
+    dw_mutex_lock(queue->mutex);
+    if(queue->head != -1)
     {
         /* Copy the params out of the queue so it can be filled in by new events */
-        memcpy(params, &_dw_event_queue[_dw_event_head], _DW_EVENT_PARAM_SIZE * sizeof(void *));
+        memcpy(params, &queue->queue[queue->head], _DW_EVENT_PARAM_SIZE * sizeof(void *));
 
         /* If the head is the same as the tail...
          * there was only one event... so set the
          * head and tail to -1 to indicate empty.
          */
-        if(_dw_event_head == _dw_event_tail)
-            _dw_event_head = _dw_event_tail = -1;
+        if(queue->head == queue->tail)
+            queue->head = queue->tail = -1;
         else
         {
             /* Advance the head */
-            _dw_event_head++;
+            queue->head++;
 
             /* If we are at the end of the queue, loop back to the start. */
-            if(_dw_event_head >= _DW_EVENT_QUEUE_LENGTH)
-                _dw_event_head = 0;
+            if(queue->head >= _DW_EVENT_QUEUE_LENGTH)
+                queue->head = 0;
         }
         /* Successfully dequeued event */
         retval = TRUE;
         /* Notify dw_main() that there is an event to handle */
         dw_event_post(_dw_main_event);
     }
-    dw_mutex_unlock(_dw_event_mutex);
+    dw_mutex_unlock(queue->mutex);
     return retval;
 }
 
@@ -591,13 +609,19 @@
     {
         params[9] = (void *)handler;
 
-        /* We have to handle draw events in the main thread...
+        /* When we are not in safe rendering mode,
+         * We have to handle draw events in the main thread.
          * If it isn't a draw event, queue the event.
          */
         if(DW_POINTER_TO_INT(params[8]) != _DW_EVENT_EXPOSE)
         {
             /* Push the new event onto the queue if it fits */
-            _dw_queue_event(params);
+            _dw_queue_event(params, &_dw_high);
+        }
+        else if(_dw_render_safe_mode == DW_FEATURE_ENABLED)
+        {
+            /* Push the new event onto the high priority queue if it fits */
+            _dw_queue_event(params, &_dw_low);
         }
         else
             return _dw_event_handler2(params);
@@ -1016,12 +1040,20 @@
     do
     {
         void *params[_DW_EVENT_PARAM_SIZE];
+        int result;
 
         dw_event_reset(_dw_main_event);
 
-        /* Dequeue and handle any pending events */
-        while(_dw_dequeue_event(params))
-            _dw_event_handler2(params);
+        /* Dequeue and handle any pending events,
+         * always checking for high events first.
+         */
+        do
+        {
+            if((result = _dw_dequeue_event(params, &_dw_high)))
+                _dw_event_handler2(params);
+            else if((result = _dw_dequeue_event(params, &_dw_low)))
+                _dw_event_handler2(params);
+        } while(result);
 
         /* Wait for something to wake us up,
          * either a posted event, or dw_main_quit()
@@ -1064,12 +1096,20 @@
         do
         {
             void *params[_DW_EVENT_PARAM_SIZE];
+            int result;
 
             dw_event_reset(_dw_main_event);
 
-            /* Dequeue and handle any pending events */
-            while(_dw_dequeue_event(params))
-                _dw_event_handler2(params);
+            /* Dequeue and handle any pending events,
+             * always checking for high events first.
+             */
+            do
+            {
+                if((result = _dw_dequeue_event(params, &_dw_high)))
+                    _dw_event_handler2(params);
+                else if((result = _dw_dequeue_event(params, &_dw_low)))
+                    _dw_event_handler2(params);
+            } while(result);
 
             /* Wait for something to wake us up,
              * either a posted event, or dw_main_quit()
@@ -1102,8 +1142,10 @@
     {
         void *params[_DW_EVENT_PARAM_SIZE];
 
-        /* Dequeue a single pending event */
-        if (_dw_dequeue_event(params))
+        /* Dequeue a single pending event, try high first */
+        if (_dw_dequeue_event(params, &_dw_high))
+            _dw_event_handler2(params);
+        else if (_dw_dequeue_event(params, &_dw_low))
             _dw_event_handler2(params);
     }
     else
@@ -3124,13 +3166,50 @@
         jclass clazz = _dw_find_class(env, DW_CLASS_NAME);
         // Get the method that you want to call
         jmethodID renderRedraw = env->GetMethodID(clazz, "renderRedraw",
+                                                  "(Lorg/dbsoft/dwindows/DWRender;I)V");
+        // Call the method on the object
+        env->CallVoidMethod(_dw_obj, renderRedraw, handle, (jint)_dw_render_safe_mode);
+        _dw_jni_check_exception(env);
+    }
+}
+
+/* Internal function to create backing bitmap */
+void _dw_render_create_bitmap(HWND handle)
+{
+    JNIEnv *env;
+
+    if(handle && (env = (JNIEnv *)pthread_getspecific(_dw_env_key)))
+    {
+        // First get the class that contains the method you need to call
+        jclass clazz = _dw_find_class(env, DW_CLASS_NAME);
+        // Get the method that you want to call
+        jmethodID renderCreateBitmap = env->GetMethodID(clazz, "renderCreateBitmap",
                                                   "(Lorg/dbsoft/dwindows/DWRender;)V");
         // Call the method on the object
-        env->CallVoidMethod(_dw_obj, renderRedraw, handle);
+        env->CallVoidMethod(_dw_obj, renderCreateBitmap, handle);
         _dw_jni_check_exception(env);
     }
 }
 
+/* Internal function to trigger a redraw using the backing bitmap */
+void _dw_render_finish_draw(HWND handle)
+{
+    JNIEnv *env;
+
+    if(handle && (env = (JNIEnv *)pthread_getspecific(_dw_env_key)))
+    {
+        // First get the class that contains the method you need to call
+        jclass clazz = _dw_find_class(env, DW_CLASS_NAME);
+        // Get the method that you want to call
+        jmethodID renderFinishDraw = env->GetMethodID(clazz, "renderFinishDraw",
+                                                        "(Lorg/dbsoft/dwindows/DWRender;)V");
+        // Call the method on the object
+        env->CallVoidMethod(_dw_obj, renderFinishDraw, handle);
+        _dw_jni_check_exception(env);
+    }
+}
+
+
 /* Sets the current foreground drawing color.
  * Parameters:
  *       red: red value.
@@ -8211,6 +8290,8 @@
             return DW_FEATURE_ENABLED;
         case DW_FEATURE_CONTAINER_MODE:          /* Supports alternate container view modes */
             return _dw_container_mode;
+        case DW_FEATURE_RENDER_SAFE:             /* Supports render safe drawing mode, limited to expose */
+            return _dw_render_safe_mode;
         case DW_FEATURE_DARK_MODE:               /* Supports Dark Mode user interface */
         {
             /* Dark Mode on Android requires Android 10 (API 29) */
@@ -8252,6 +8333,15 @@
         case DW_FEATURE_TREE:                    /* Supports the Tree Widget */
             return DW_ERROR_GENERAL;
         /* These features are supported and configurable */
+        case DW_FEATURE_RENDER_SAFE:             /* Supports render safe drawing mode, limited to expose */
+        {
+            if (state == DW_FEATURE_ENABLED || state == DW_FEATURE_DISABLED)
+            {
+                _dw_render_safe_mode = state;
+                return DW_ERROR_NONE;
+            }
+            return DW_ERROR_GENERAL;
+        }
         case DW_FEATURE_CONTAINER_MODE:          /* Supports alternate container view modes */
         {
             if(state >= DW_CONTAINER_MODE_DEFAULT && state < DW_CONTAINER_MODE_MAX)
--- a/dw.h	Fri Apr 14 02:53:00 2023 +0000
+++ b/dw.h	Tue May 02 11:37:48 2023 +0000
@@ -1848,6 +1848,7 @@
     DW_FEATURE_WINDOW_PLACEMENT,        /* Supports arbitrary window placement */
     DW_FEATURE_CONTAINER_MODE,          /* Supports alternate container view modes */
     DW_FEATURE_HTML_MESSAGE,            /* Supports the DW_SIGNAL_HTML_MESSAGE callback */
+    DW_FEATURE_RENDER_SAFE,             /* Supports render safe drawing mode, limited to expose */
     DW_FEATURE_MAX
 } DWFEATURE;
 
--- a/dwtest.c	Fri Apr 14 02:53:00 2023 +0000
+++ b/dwtest.c	Tue May 02 11:37:48 2023 +0000
@@ -2172,6 +2172,7 @@
     "Supports arbitrary window placement",
     "Supports alternate container view modes",
     "Supports the DW_SIGNAL_HTML_MESSAGE callback",
+    "Supports render safe drawing mode, limited to expose",
     NULL };
 
 /*
--- a/dwtestoo.cpp	Fri Apr 14 02:53:00 2023 +0000
+++ b/dwtestoo.cpp	Tue May 02 11:37:48 2023 +0000
@@ -1967,7 +1967,8 @@
     "Supports the Tree Widget",
     "Supports arbitrary window placement",
     "Supports alternate container view modes",
-    "Supports the DW_SIGNAL_HTML_MESSAGE callback"
+    "Supports the DW_SIGNAL_HTML_MESSAGE callback",
+    "Supports render safe drawing mode, limited to expose"
 };
 
 // Let's demonstrate the functionality of this library. :)
--- a/readme.txt	Fri Apr 14 02:53:00 2023 +0000
+++ b/readme.txt	Tue May 02 11:37:48 2023 +0000
@@ -73,6 +73,14 @@
     Windows 7+ with Edge WebView2. MacOS 10.10+.
     GTK3/4 with WebKitGTK 2 or higher.
     iOS and Android, all supported versions.
+Added DW_FEATURE_RENDER_SAFE that requires safe rendering.
+    This means only allowing drawing in the EXPOSE callback.
+    On Android this also enables off main thread expose events.
+    Added high and low priority event queues on Android and
+    increased the queue length. This should prevent important
+    events from being dropped, only superfluous expose events.
+    This feature is enabled by default on Android.
+    This feature is disabled by default or unavailable on others.
 
 Dynamic Windows Documentation is available at: