From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- widget/gtk/AsyncDBus.cpp | 92 + widget/gtk/AsyncDBus.h | 40 + widget/gtk/AsyncGtkClipboardRequest.cpp | 120 + widget/gtk/AsyncGtkClipboardRequest.h | 61 + widget/gtk/CompositorWidgetChild.cpp | 51 + widget/gtk/CompositorWidgetChild.h | 41 + widget/gtk/CompositorWidgetParent.cpp | 54 + widget/gtk/CompositorWidgetParent.h | 42 + widget/gtk/DMABufLibWrapper.cpp | 325 + widget/gtk/DMABufLibWrapper.h | 228 + widget/gtk/DMABufSurface.cpp | 1654 ++++ widget/gtk/DMABufSurface.h | 376 + widget/gtk/GRefPtr.h | 89 + widget/gtk/GUniquePtr.h | 34 + widget/gtk/GfxInfo.cpp | 1403 +++ widget/gtk/GfxInfo.h | 133 + widget/gtk/GfxInfoUtils.h | 100 + widget/gtk/GtkCompositorWidget.cpp | 246 + widget/gtk/GtkCompositorWidget.h | 137 + widget/gtk/IMContextWrapper.cpp | 3360 +++++++ widget/gtk/IMContextWrapper.h | 688 ++ widget/gtk/InProcessGtkCompositorWidget.cpp | 44 + widget/gtk/InProcessGtkCompositorWidget.h | 30 + widget/gtk/MPRISInterfaceDescription.h | 91 + widget/gtk/MPRISServiceHandler.cpp | 856 ++ widget/gtk/MPRISServiceHandler.h | 187 + widget/gtk/MediaKeysEventSourceFactory.cpp | 14 + widget/gtk/MozContainer.cpp | 374 + widget/gtk/MozContainer.h | 90 + widget/gtk/MozContainerWayland.cpp | 819 ++ widget/gtk/MozContainerWayland.h | 108 + widget/gtk/NativeKeyBindings.cpp | 527 ++ widget/gtk/NativeKeyBindings.h | 62 + widget/gtk/NativeMenuGtk.cpp | 424 + widget/gtk/NativeMenuGtk.h | 64 + widget/gtk/NativeMenuSupport.cpp | 29 + widget/gtk/PCompositorWidget.ipdl | 35 + widget/gtk/PlatformWidgetTypes.ipdlh | 33 + widget/gtk/ScreenHelperGTK.cpp | 553 ++ widget/gtk/ScreenHelperGTK.h | 104 + widget/gtk/TaskbarProgress.cpp | 106 + widget/gtk/TaskbarProgress.h | 33 + widget/gtk/WakeLockListener.cpp | 546 ++ widget/gtk/WakeLockListener.h | 55 + widget/gtk/WaylandBuffer.cpp | 224 + widget/gtk/WaylandBuffer.h | 140 + widget/gtk/WaylandVsyncSource.cpp | 431 + widget/gtk/WaylandVsyncSource.h | 99 + widget/gtk/WidgetStyleCache.cpp | 1463 +++ widget/gtk/WidgetStyleCache.h | 63 + widget/gtk/WidgetTraceEvent.cpp | 68 + widget/gtk/WidgetUtilsGtk.cpp | 496 + widget/gtk/WidgetUtilsGtk.h | 80 + widget/gtk/WindowSurface.h | 41 + widget/gtk/WindowSurfaceProvider.cpp | 202 + widget/gtk/WindowSurfaceProvider.h | 101 + widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp | 417 + widget/gtk/WindowSurfaceWaylandMultiBuffer.h | 84 + widget/gtk/WindowSurfaceX11.cpp | 47 + widget/gtk/WindowSurfaceX11.h | 39 + widget/gtk/WindowSurfaceX11Image.cpp | 254 + widget/gtk/WindowSurfaceX11Image.h | 48 + widget/gtk/WindowSurfaceX11SHM.cpp | 27 + widget/gtk/WindowSurfaceX11SHM.h | 36 + widget/gtk/compat/gdk/gdkdnd.h | 29 + widget/gtk/compat/gdk/gdkkeysyms.h | 266 + widget/gtk/compat/gdk/gdkvisual.h | 15 + widget/gtk/compat/gdk/gdkwindow.h | 27 + widget/gtk/compat/gdk/gdkx.h | 42 + widget/gtk/compat/glib/gmem.h | 48 + widget/gtk/compat/gtk/gtkwidget.h | 43 + widget/gtk/compat/gtk/gtkwindow.h | 26 + widget/gtk/components.conf | 151 + widget/gtk/crashtests/540078-1.xhtml | 1 + widget/gtk/crashtests/673390-1.html | 1 + widget/gtk/crashtests/crashtests.list | 2 + widget/gtk/gtk3drawing.cpp | 2452 +++++ widget/gtk/gtkdrawing.h | 545 ++ widget/gtk/moz.build | 181 + widget/gtk/mozgtk/moz.build | 37 + widget/gtk/mozgtk/mozgtk.c | 30 + widget/gtk/mozwayland/moz.build | 16 + widget/gtk/mozwayland/mozwayland.c | 212 + widget/gtk/mozwayland/mozwayland.h | 134 + widget/gtk/nsAppShell.cpp | 424 + widget/gtk/nsAppShell.h | 46 + widget/gtk/nsApplicationChooser.cpp | 132 + widget/gtk/nsApplicationChooser.h | 32 + widget/gtk/nsBidiKeyboard.cpp | 52 + widget/gtk/nsBidiKeyboard.h | 25 + widget/gtk/nsClipboard.cpp | 1372 +++ widget/gtk/nsClipboard.h | 174 + widget/gtk/nsClipboardWayland.cpp | 75 + widget/gtk/nsClipboardWayland.h | 28 + widget/gtk/nsClipboardX11.cpp | 169 + widget/gtk/nsClipboardX11.h | 32 + widget/gtk/nsColorPicker.cpp | 253 + widget/gtk/nsColorPicker.h | 71 + widget/gtk/nsDeviceContextSpecG.cpp | 422 + widget/gtk/nsDeviceContextSpecG.h | 62 + widget/gtk/nsDragService.cpp | 2658 ++++++ widget/gtk/nsDragService.h | 264 + widget/gtk/nsFilePicker.cpp | 706 ++ widget/gtk/nsFilePicker.h | 89 + widget/gtk/nsGTKToolkit.h | 52 + widget/gtk/nsGtkCursors.h | 416 + widget/gtk/nsGtkKeyUtils.cpp | 2491 +++++ widget/gtk/nsGtkKeyUtils.h | 515 + widget/gtk/nsGtkUtils.h | 59 + widget/gtk/nsImageToPixbuf.cpp | 121 + widget/gtk/nsImageToPixbuf.h | 37 + widget/gtk/nsLookAndFeel.cpp | 2101 +++++ widget/gtk/nsLookAndFeel.h | 180 + widget/gtk/nsNativeThemeGTK.cpp | 1466 +++ widget/gtk/nsNativeThemeGTK.h | 118 + widget/gtk/nsPrintDialogGTK.cpp | 621 ++ widget/gtk/nsPrintDialogGTK.h | 35 + widget/gtk/nsPrintSettingsGTK.cpp | 677 ++ widget/gtk/nsPrintSettingsGTK.h | 147 + widget/gtk/nsPrintSettingsServiceGTK.cpp | 80 + widget/gtk/nsPrintSettingsServiceGTK.h | 33 + widget/gtk/nsShmImage.cpp | 326 + widget/gtk/nsShmImage.h | 75 + widget/gtk/nsSound.cpp | 397 + widget/gtk/nsSound.h | 33 + widget/gtk/nsToolkit.cpp | 24 + widget/gtk/nsUserIdleServiceGTK.cpp | 125 + widget/gtk/nsUserIdleServiceGTK.h | 55 + widget/gtk/nsWaylandDisplay.cpp | 185 + widget/gtk/nsWaylandDisplay.h | 111 + widget/gtk/nsWidgetFactory.cpp | 70 + widget/gtk/nsWidgetFactory.h | 21 + widget/gtk/nsWindow.cpp | 9813 ++++++++++++++++++++ widget/gtk/nsWindow.h | 1005 ++ widget/gtk/vaapitest/moz.build | 25 + widget/gtk/vaapitest/vaapitest.cpp | 254 + widget/gtk/wayland/gbm.h | 480 + .../idle-inhibit-unstable-v1-client-protocol.h | 228 + .../wayland/idle-inhibit-unstable-v1-protocol.c | 60 + .../linux-dmabuf-unstable-v1-client-protocol.h | 650 ++ .../wayland/linux-dmabuf-unstable-v1-protocol.c | 81 + widget/gtk/wayland/moz.build | 37 + ...inter-constraints-unstable-v1-client-protocol.h | 650 ++ .../pointer-constraints-unstable-v1-protocol.c | 97 + .../relative-pointer-unstable-v1-client-protocol.h | 293 + .../relative-pointer-unstable-v1-protocol.c | 69 + widget/gtk/wayland/va_drmcommon.h | 156 + widget/gtk/wayland/viewporter-client-protocol.h | 392 + widget/gtk/wayland/viewporter-protocol.c | 56 + .../wayland/xdg-activation-v1-client-protocol.h | 409 + widget/gtk/wayland/xdg-activation-v1-protocol.c | 82 + .../xdg-output-unstable-v1-client-protocol.h | 392 + .../gtk/wayland/xdg-output-unstable-v1-protocol.c | 74 + 153 files changed, 56036 insertions(+) create mode 100644 widget/gtk/AsyncDBus.cpp create mode 100644 widget/gtk/AsyncDBus.h create mode 100644 widget/gtk/AsyncGtkClipboardRequest.cpp create mode 100644 widget/gtk/AsyncGtkClipboardRequest.h create mode 100644 widget/gtk/CompositorWidgetChild.cpp create mode 100644 widget/gtk/CompositorWidgetChild.h create mode 100644 widget/gtk/CompositorWidgetParent.cpp create mode 100644 widget/gtk/CompositorWidgetParent.h create mode 100644 widget/gtk/DMABufLibWrapper.cpp create mode 100644 widget/gtk/DMABufLibWrapper.h create mode 100644 widget/gtk/DMABufSurface.cpp create mode 100644 widget/gtk/DMABufSurface.h create mode 100644 widget/gtk/GRefPtr.h create mode 100644 widget/gtk/GUniquePtr.h create mode 100644 widget/gtk/GfxInfo.cpp create mode 100644 widget/gtk/GfxInfo.h create mode 100644 widget/gtk/GfxInfoUtils.h create mode 100644 widget/gtk/GtkCompositorWidget.cpp create mode 100644 widget/gtk/GtkCompositorWidget.h create mode 100644 widget/gtk/IMContextWrapper.cpp create mode 100644 widget/gtk/IMContextWrapper.h create mode 100644 widget/gtk/InProcessGtkCompositorWidget.cpp create mode 100644 widget/gtk/InProcessGtkCompositorWidget.h create mode 100644 widget/gtk/MPRISInterfaceDescription.h create mode 100644 widget/gtk/MPRISServiceHandler.cpp create mode 100644 widget/gtk/MPRISServiceHandler.h create mode 100644 widget/gtk/MediaKeysEventSourceFactory.cpp create mode 100644 widget/gtk/MozContainer.cpp create mode 100644 widget/gtk/MozContainer.h create mode 100644 widget/gtk/MozContainerWayland.cpp create mode 100644 widget/gtk/MozContainerWayland.h create mode 100644 widget/gtk/NativeKeyBindings.cpp create mode 100644 widget/gtk/NativeKeyBindings.h create mode 100644 widget/gtk/NativeMenuGtk.cpp create mode 100644 widget/gtk/NativeMenuGtk.h create mode 100644 widget/gtk/NativeMenuSupport.cpp create mode 100644 widget/gtk/PCompositorWidget.ipdl create mode 100644 widget/gtk/PlatformWidgetTypes.ipdlh create mode 100644 widget/gtk/ScreenHelperGTK.cpp create mode 100644 widget/gtk/ScreenHelperGTK.h create mode 100644 widget/gtk/TaskbarProgress.cpp create mode 100644 widget/gtk/TaskbarProgress.h create mode 100644 widget/gtk/WakeLockListener.cpp create mode 100644 widget/gtk/WakeLockListener.h create mode 100644 widget/gtk/WaylandBuffer.cpp create mode 100644 widget/gtk/WaylandBuffer.h create mode 100644 widget/gtk/WaylandVsyncSource.cpp create mode 100644 widget/gtk/WaylandVsyncSource.h create mode 100644 widget/gtk/WidgetStyleCache.cpp create mode 100644 widget/gtk/WidgetStyleCache.h create mode 100644 widget/gtk/WidgetTraceEvent.cpp create mode 100644 widget/gtk/WidgetUtilsGtk.cpp create mode 100644 widget/gtk/WidgetUtilsGtk.h create mode 100644 widget/gtk/WindowSurface.h create mode 100644 widget/gtk/WindowSurfaceProvider.cpp create mode 100644 widget/gtk/WindowSurfaceProvider.h create mode 100644 widget/gtk/WindowSurfaceWaylandMultiBuffer.cpp create mode 100644 widget/gtk/WindowSurfaceWaylandMultiBuffer.h create mode 100644 widget/gtk/WindowSurfaceX11.cpp create mode 100644 widget/gtk/WindowSurfaceX11.h create mode 100644 widget/gtk/WindowSurfaceX11Image.cpp create mode 100644 widget/gtk/WindowSurfaceX11Image.h create mode 100644 widget/gtk/WindowSurfaceX11SHM.cpp create mode 100644 widget/gtk/WindowSurfaceX11SHM.h create mode 100644 widget/gtk/compat/gdk/gdkdnd.h create mode 100644 widget/gtk/compat/gdk/gdkkeysyms.h create mode 100644 widget/gtk/compat/gdk/gdkvisual.h create mode 100644 widget/gtk/compat/gdk/gdkwindow.h create mode 100644 widget/gtk/compat/gdk/gdkx.h create mode 100644 widget/gtk/compat/glib/gmem.h create mode 100644 widget/gtk/compat/gtk/gtkwidget.h create mode 100644 widget/gtk/compat/gtk/gtkwindow.h create mode 100644 widget/gtk/components.conf create mode 100644 widget/gtk/crashtests/540078-1.xhtml create mode 100644 widget/gtk/crashtests/673390-1.html create mode 100644 widget/gtk/crashtests/crashtests.list create mode 100644 widget/gtk/gtk3drawing.cpp create mode 100644 widget/gtk/gtkdrawing.h create mode 100644 widget/gtk/moz.build create mode 100644 widget/gtk/mozgtk/moz.build create mode 100644 widget/gtk/mozgtk/mozgtk.c create mode 100644 widget/gtk/mozwayland/moz.build create mode 100644 widget/gtk/mozwayland/mozwayland.c create mode 100644 widget/gtk/mozwayland/mozwayland.h create mode 100644 widget/gtk/nsAppShell.cpp create mode 100644 widget/gtk/nsAppShell.h create mode 100644 widget/gtk/nsApplicationChooser.cpp create mode 100644 widget/gtk/nsApplicationChooser.h create mode 100644 widget/gtk/nsBidiKeyboard.cpp create mode 100644 widget/gtk/nsBidiKeyboard.h create mode 100644 widget/gtk/nsClipboard.cpp create mode 100644 widget/gtk/nsClipboard.h create mode 100644 widget/gtk/nsClipboardWayland.cpp create mode 100644 widget/gtk/nsClipboardWayland.h create mode 100644 widget/gtk/nsClipboardX11.cpp create mode 100644 widget/gtk/nsClipboardX11.h create mode 100644 widget/gtk/nsColorPicker.cpp create mode 100644 widget/gtk/nsColorPicker.h create mode 100644 widget/gtk/nsDeviceContextSpecG.cpp create mode 100644 widget/gtk/nsDeviceContextSpecG.h create mode 100644 widget/gtk/nsDragService.cpp create mode 100644 widget/gtk/nsDragService.h create mode 100644 widget/gtk/nsFilePicker.cpp create mode 100644 widget/gtk/nsFilePicker.h create mode 100644 widget/gtk/nsGTKToolkit.h create mode 100644 widget/gtk/nsGtkCursors.h create mode 100644 widget/gtk/nsGtkKeyUtils.cpp create mode 100644 widget/gtk/nsGtkKeyUtils.h create mode 100644 widget/gtk/nsGtkUtils.h create mode 100644 widget/gtk/nsImageToPixbuf.cpp create mode 100644 widget/gtk/nsImageToPixbuf.h create mode 100644 widget/gtk/nsLookAndFeel.cpp create mode 100644 widget/gtk/nsLookAndFeel.h create mode 100644 widget/gtk/nsNativeThemeGTK.cpp create mode 100644 widget/gtk/nsNativeThemeGTK.h create mode 100644 widget/gtk/nsPrintDialogGTK.cpp create mode 100644 widget/gtk/nsPrintDialogGTK.h create mode 100644 widget/gtk/nsPrintSettingsGTK.cpp create mode 100644 widget/gtk/nsPrintSettingsGTK.h create mode 100644 widget/gtk/nsPrintSettingsServiceGTK.cpp create mode 100644 widget/gtk/nsPrintSettingsServiceGTK.h create mode 100644 widget/gtk/nsShmImage.cpp create mode 100644 widget/gtk/nsShmImage.h create mode 100644 widget/gtk/nsSound.cpp create mode 100644 widget/gtk/nsSound.h create mode 100644 widget/gtk/nsToolkit.cpp create mode 100644 widget/gtk/nsUserIdleServiceGTK.cpp create mode 100644 widget/gtk/nsUserIdleServiceGTK.h create mode 100644 widget/gtk/nsWaylandDisplay.cpp create mode 100644 widget/gtk/nsWaylandDisplay.h create mode 100644 widget/gtk/nsWidgetFactory.cpp create mode 100644 widget/gtk/nsWidgetFactory.h create mode 100644 widget/gtk/nsWindow.cpp create mode 100644 widget/gtk/nsWindow.h create mode 100644 widget/gtk/vaapitest/moz.build create mode 100644 widget/gtk/vaapitest/vaapitest.cpp create mode 100644 widget/gtk/wayland/gbm.h create mode 100644 widget/gtk/wayland/idle-inhibit-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/idle-inhibit-unstable-v1-protocol.c create mode 100644 widget/gtk/wayland/linux-dmabuf-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/linux-dmabuf-unstable-v1-protocol.c create mode 100644 widget/gtk/wayland/moz.build create mode 100644 widget/gtk/wayland/pointer-constraints-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/pointer-constraints-unstable-v1-protocol.c create mode 100644 widget/gtk/wayland/relative-pointer-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/relative-pointer-unstable-v1-protocol.c create mode 100644 widget/gtk/wayland/va_drmcommon.h create mode 100644 widget/gtk/wayland/viewporter-client-protocol.h create mode 100644 widget/gtk/wayland/viewporter-protocol.c create mode 100644 widget/gtk/wayland/xdg-activation-v1-client-protocol.h create mode 100644 widget/gtk/wayland/xdg-activation-v1-protocol.c create mode 100644 widget/gtk/wayland/xdg-output-unstable-v1-client-protocol.h create mode 100644 widget/gtk/wayland/xdg-output-unstable-v1-protocol.c (limited to 'widget/gtk') diff --git a/widget/gtk/AsyncDBus.cpp b/widget/gtk/AsyncDBus.cpp new file mode 100644 index 0000000000..cb1905dc17 --- /dev/null +++ b/widget/gtk/AsyncDBus.cpp @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AsyncDBus.h" + +#include +#include "mozilla/UniquePtrExtensions.h" + +namespace mozilla::widget { + +static void CreateProxyCallback(GObject*, GAsyncResult* aResult, + gpointer aUserData) { + RefPtr promise = + dont_AddRef(static_cast(aUserData)); + GUniquePtr error; + RefPtr proxy = dont_AddRef( + g_dbus_proxy_new_for_bus_finish(aResult, getter_Transfers(error))); + if (proxy) { + promise->Resolve(std::move(proxy), __func__); + } else { + promise->Reject(std::move(error), __func__); + } +} + +RefPtr CreateDBusProxyForBus( + GBusType aBusType, GDBusProxyFlags aFlags, + GDBusInterfaceInfo* aInterfaceInfo, const char* aName, + const char* aObjectPath, const char* aInterfaceName, + GCancellable* aCancellable) { + auto promise = MakeRefPtr(__func__); + g_dbus_proxy_new_for_bus(aBusType, aFlags, aInterfaceInfo, aName, aObjectPath, + aInterfaceName, aCancellable, CreateProxyCallback, + do_AddRef(promise).take()); + return promise.forget(); +} + +static void ProxyCallCallback(GObject* aSourceObject, GAsyncResult* aResult, + gpointer aUserData) { + RefPtr promise = + dont_AddRef(static_cast(aUserData)); + GUniquePtr error; + RefPtr result = dont_AddRef(g_dbus_proxy_call_finish( + G_DBUS_PROXY(aSourceObject), aResult, getter_Transfers(error))); + if (result) { + promise->Resolve(std::move(result), __func__); + } else { + promise->Reject(std::move(error), __func__); + } +} + +RefPtr DBusProxyCall(GDBusProxy* aProxy, const char* aMethod, + GVariant* aArgs, GDBusCallFlags aFlags, + gint aTimeout, + GCancellable* aCancellable) { + auto promise = MakeRefPtr(__func__); + g_dbus_proxy_call(aProxy, aMethod, aArgs, aFlags, aTimeout, aCancellable, + ProxyCallCallback, do_AddRef(promise).take()); + return promise.forget(); +} + +static void ProxyCallWithUnixFDListCallback(GObject* aSourceObject, + GAsyncResult* aResult, + gpointer aUserData) { + RefPtr promise = + dont_AddRef(static_cast(aUserData)); + GUniquePtr error; + GUnixFDList** aFDList = nullptr; + RefPtr result = + dont_AddRef(g_dbus_proxy_call_with_unix_fd_list_finish( + G_DBUS_PROXY(aSourceObject), aFDList, aResult, + getter_Transfers(error))); + if (result) { + promise->Resolve(std::move(result), __func__); + } else { + promise->Reject(std::move(error), __func__); + } +} + +RefPtr DBusProxyCallWithUnixFDList( + GDBusProxy* aProxy, const char* aMethod, GVariant* aArgs, + GDBusCallFlags aFlags, gint aTimeout, GUnixFDList* aFDList, + GCancellable* aCancellable) { + auto promise = MakeRefPtr(__func__); + g_dbus_proxy_call_with_unix_fd_list( + aProxy, aMethod, aArgs, aFlags, aTimeout, aFDList, aCancellable, + ProxyCallWithUnixFDListCallback, do_AddRef(promise).take()); + return promise.forget(); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/AsyncDBus.h b/widget/gtk/AsyncDBus.h new file mode 100644 index 0000000000..41558c8d70 --- /dev/null +++ b/widget/gtk/AsyncDBus.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_widget_AsyncDBus_h +#define mozilla_widget_AsyncDBus_h + +#include + +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/MozPromise.h" + +namespace mozilla::widget { + +using DBusProxyPromise = MozPromise, GUniquePtr, + /* IsExclusive = */ true>; + +using DBusCallPromise = MozPromise, GUniquePtr, + /* IsExclusive = */ true>; + +RefPtr CreateDBusProxyForBus( + GBusType aBusType, GDBusProxyFlags aFlags, + GDBusInterfaceInfo* aInterfaceInfo, const char* aName, + const char* aObjectPath, const char* aInterfaceName, + GCancellable* aCancellable = nullptr); + +RefPtr DBusProxyCall(GDBusProxy*, const char* aMethod, + GVariant* aArgs, GDBusCallFlags, + gint aTimeout = -1, + GCancellable* = nullptr); + +RefPtr DBusProxyCallWithUnixFDList( + GDBusProxy*, const char* aMethod, GVariant* aArgs, GDBusCallFlags, + gint aTimeout = -1, GUnixFDList* = nullptr, GCancellable* = nullptr); + +} // namespace mozilla::widget + +#endif diff --git a/widget/gtk/AsyncGtkClipboardRequest.cpp b/widget/gtk/AsyncGtkClipboardRequest.cpp new file mode 100644 index 0000000000..75801c698d --- /dev/null +++ b/widget/gtk/AsyncGtkClipboardRequest.cpp @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AsyncGtkClipboardRequest.h" + +namespace mozilla { + +AsyncGtkClipboardRequest::AsyncGtkClipboardRequest(ClipboardDataType aDataType, + int32_t aWhichClipboard, + const char* aMimeType) { + GtkClipboard* clipboard = + gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)); + mRequest = MakeUnique(aDataType); + + switch (aDataType) { + case ClipboardDataType::Data: + LOGCLIP(" getting DATA MIME %s\n", aMimeType); + gtk_clipboard_request_contents(clipboard, + gdk_atom_intern(aMimeType, FALSE), + OnDataReceived, mRequest.get()); + break; + case ClipboardDataType::Text: + LOGCLIP(" getting TEXT\n"); + gtk_clipboard_request_text(clipboard, OnTextReceived, mRequest.get()); + break; + case ClipboardDataType::Targets: + LOGCLIP(" getting TARGETS\n"); + gtk_clipboard_request_contents(clipboard, + gdk_atom_intern("TARGETS", FALSE), + OnDataReceived, mRequest.get()); + break; + } +} + +void AsyncGtkClipboardRequest::OnDataReceived(GtkClipboard* clipboard, + GtkSelectionData* selection_data, + gpointer data) { + int whichClipboard = GetGeckoClipboardType(clipboard); + LOGCLIP("OnDataReceived(%s) callback\n", + whichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + static_cast(data)->Complete(selection_data); +} + +void AsyncGtkClipboardRequest::OnTextReceived(GtkClipboard* clipboard, + const gchar* text, + gpointer data) { + int whichClipboard = GetGeckoClipboardType(clipboard); + LOGCLIP("OnTextReceived(%s) callback\n", + whichClipboard == nsClipboard::kSelectionClipboard ? "primary" + : "clipboard"); + static_cast(data)->Complete(text); +} + +void AsyncGtkClipboardRequest::Request::Complete(const void* aData) { + LOGCLIP("Request::Complete(), aData = %p, timedOut = %d\n", aData, mTimedOut); + + if (mTimedOut) { + delete this; + return; + } + + mData.emplace(); + + gint dataLength = 0; + if (mDataType == ClipboardDataType::Targets || + mDataType == ClipboardDataType::Data) { + dataLength = gtk_selection_data_get_length((GtkSelectionData*)aData); + } else { + dataLength = aData ? strlen((const char*)aData) : 0; + } + + // Negative size means no data or data error. + if (dataLength <= 0) { + LOGCLIP(" zero dataLength, quit.\n"); + return; + } + + switch (mDataType) { + case ClipboardDataType::Targets: { + LOGCLIP(" getting %d bytes of clipboard targets.\n", dataLength); + gint n_targets = 0; + GdkAtom* targets = nullptr; + if (!gtk_selection_data_get_targets((GtkSelectionData*)aData, &targets, + &n_targets) || + !n_targets) { + // We failed to get targets + return; + } + mData->SetTargets( + ClipboardTargets{GUniquePtr(targets), uint32_t(n_targets)}); + break; + } + case ClipboardDataType::Text: { + LOGCLIP(" getting %d bytes of text.\n", dataLength); + mData->SetText(Span(static_cast(aData), dataLength)); + LOGCLIP(" done, mClipboardData = %p\n", mData->AsSpan().data()); + break; + } + case ClipboardDataType::Data: { + LOGCLIP(" getting %d bytes of data.\n", dataLength); + mData->SetData(Span(gtk_selection_data_get_data((GtkSelectionData*)aData), + dataLength)); + LOGCLIP(" done, mClipboardData = %p\n", mData->AsSpan().data()); + break; + } + } +} + +AsyncGtkClipboardRequest::~AsyncGtkClipboardRequest() { + if (mRequest && mRequest->mData.isNothing()) { + mRequest->mTimedOut = true; + Unused << mRequest.release(); + } +} + +} // namespace mozilla diff --git a/widget/gtk/AsyncGtkClipboardRequest.h b/widget/gtk/AsyncGtkClipboardRequest.h new file mode 100644 index 0000000000..1d72691560 --- /dev/null +++ b/widget/gtk/AsyncGtkClipboardRequest.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_AsyncGtkClipboardRequest_h +#define mozilla_AsyncGtkClipboardRequest_h + +#include "nsClipboard.h" + +namespace mozilla { + +// An asynchronous clipboard request that we wait for synchronously by +// spinning the event loop. +class MOZ_STACK_CLASS AsyncGtkClipboardRequest { + // Heap-allocated object that we give GTK as a callback. + struct Request { + explicit Request(ClipboardDataType aDataType) : mDataType(aDataType) {} + + void Complete(const void*); + + const ClipboardDataType mDataType; + Maybe mData; + bool mTimedOut = false; + }; + + UniquePtr mRequest; + + static void OnDataReceived(GtkClipboard*, GtkSelectionData*, gpointer); + static void OnTextReceived(GtkClipboard*, const gchar*, gpointer); + + public: + // Launch a request for a particular GTK clipboard. The current status of the + // request can be observed by calling HasCompleted() and TakeResult(). + AsyncGtkClipboardRequest(ClipboardDataType, int32_t aWhichClipboard, + const char* aMimeType = nullptr); + + // Returns whether the request has been answered already. + bool HasCompleted() const { return mRequest->mData.isSome(); } + + // Takes the result from the current request if completed, or a + // default-constructed data otherwise. The destructor will take care of + // flagging the request as timed out in that case. + ClipboardData TakeResult() { + if (!HasCompleted()) { + return {}; + } + auto request = std::move(mRequest); + return request->mData.extract(); + } + + // If completed, frees the request if needed. Otherwise, marks it as a timed + // out request so that when it completes the Request object is properly + // freed. + ~AsyncGtkClipboardRequest(); +}; + +}; // namespace mozilla + +#endif diff --git a/widget/gtk/CompositorWidgetChild.cpp b/widget/gtk/CompositorWidgetChild.cpp new file mode 100644 index 0000000000..b7908a43d4 --- /dev/null +++ b/widget/gtk/CompositorWidgetChild.cpp @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CompositorWidgetChild.h" +#include "mozilla/Unused.h" +#include "gfxPlatform.h" + +namespace mozilla { +namespace widget { + +CompositorWidgetChild::CompositorWidgetChild( + RefPtr aVsyncDispatcher, + RefPtr aVsyncObserver, + const CompositorWidgetInitData&) + : mVsyncDispatcher(aVsyncDispatcher), mVsyncObserver(aVsyncObserver) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!gfxPlatform::IsHeadless()); +} + +CompositorWidgetChild::~CompositorWidgetChild() = default; + +bool CompositorWidgetChild::Initialize() { return true; } + +mozilla::ipc::IPCResult CompositorWidgetChild::RecvObserveVsync() { + mVsyncDispatcher->SetCompositorVsyncObserver(mVsyncObserver); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetChild::RecvUnobserveVsync() { + mVsyncDispatcher->SetCompositorVsyncObserver(nullptr); + return IPC_OK(); +} + +void CompositorWidgetChild::NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) { + Unused << SendNotifyClientSizeChanged(aClientSize); +} + +void CompositorWidgetChild::DisableRendering() { + Unused << SendDisableRendering(); +} + +void CompositorWidgetChild::EnableRendering(const uintptr_t aXWindow, + const bool aShaped) { + Unused << SendEnableRendering(aXWindow, aShaped); +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/CompositorWidgetChild.h b/widget/gtk/CompositorWidgetChild.h new file mode 100644 index 0000000000..b1cad75da3 --- /dev/null +++ b/widget/gtk/CompositorWidgetChild.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_gtk_CompositorWidgetChild_h +#define widget_gtk_CompositorWidgetChild_h + +#include "GtkCompositorWidget.h" +#include "mozilla/widget/PCompositorWidgetChild.h" +#include "mozilla/widget/CompositorWidgetVsyncObserver.h" + +namespace mozilla { +namespace widget { + +class CompositorWidgetChild final : public PCompositorWidgetChild, + public PlatformCompositorWidgetDelegate { + public: + CompositorWidgetChild(RefPtr aVsyncDispatcher, + RefPtr aVsyncObserver, + const CompositorWidgetInitData&); + ~CompositorWidgetChild() override; + + bool Initialize(); + + mozilla::ipc::IPCResult RecvObserveVsync() override; + mozilla::ipc::IPCResult RecvUnobserveVsync() override; + + void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override; + void DisableRendering() override; + void EnableRendering(const uintptr_t aXWindow, const bool aShaped) override; + + private: + RefPtr mVsyncDispatcher; + RefPtr mVsyncObserver; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_gtk_CompositorWidgetChild_h diff --git a/widget/gtk/CompositorWidgetParent.cpp b/widget/gtk/CompositorWidgetParent.cpp new file mode 100644 index 0000000000..998614622e --- /dev/null +++ b/widget/gtk/CompositorWidgetParent.cpp @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CompositorWidgetParent.h" +#include "mozilla/Unused.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "nsWindow.h" + +namespace mozilla::widget { + +CompositorWidgetParent::CompositorWidgetParent( + const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions) + : GtkCompositorWidget(aInitData.get_GtkCompositorWidgetInitData(), aOptions, + nullptr) { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU); +} + +CompositorWidgetParent::~CompositorWidgetParent() = default; + +void CompositorWidgetParent::ObserveVsync(VsyncObserver* aObserver) { + if (aObserver) { + Unused << SendObserveVsync(); + } else { + Unused << SendUnobserveVsync(); + } + mVsyncObserver = aObserver; +} + +RefPtr CompositorWidgetParent::GetVsyncObserver() const { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU); + return mVsyncObserver; +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvNotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) { + NotifyClientSizeChanged(aClientSize); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvDisableRendering() { + DisableRendering(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CompositorWidgetParent::RecvEnableRendering( + const uintptr_t& aXWindow, const bool& aShaped) { + EnableRendering(aXWindow, aShaped); + return IPC_OK(); +} + +} // namespace mozilla::widget diff --git a/widget/gtk/CompositorWidgetParent.h b/widget/gtk/CompositorWidgetParent.h new file mode 100644 index 0000000000..2bbc70af3e --- /dev/null +++ b/widget/gtk/CompositorWidgetParent.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_gtk_CompositorWidgetParent_h +#define widget_gtk_CompositorWidgetParent_h + +#include "GtkCompositorWidget.h" +#include "mozilla/VsyncDispatcher.h" +#include "mozilla/widget/PCompositorWidgetParent.h" + +namespace mozilla { +namespace widget { + +class CompositorWidgetParent final : public PCompositorWidgetParent, + public GtkCompositorWidget { + public: + explicit CompositorWidgetParent(const CompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions); + ~CompositorWidgetParent() override; + + void ActorDestroy(ActorDestroyReason aWhy) override {} + + void ObserveVsync(VsyncObserver* aObserver) override; + RefPtr GetVsyncObserver() const override; + + mozilla::ipc::IPCResult RecvNotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) override; + + mozilla::ipc::IPCResult RecvDisableRendering() override; + mozilla::ipc::IPCResult RecvEnableRendering(const uintptr_t& aXWindow, + const bool& aShaped) override; + + private: + RefPtr mVsyncObserver; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_gtk_CompositorWidgetParent_h diff --git a/widget/gtk/DMABufLibWrapper.cpp b/widget/gtk/DMABufLibWrapper.cpp new file mode 100644 index 0000000000..31cea31adb --- /dev/null +++ b/widget/gtk/DMABufLibWrapper.cpp @@ -0,0 +1,325 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "base/message_loop.h" // for MessageLoop +#include "nsWaylandDisplay.h" +#include "DMABufLibWrapper.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/gfx/gfxVars.h" +#include "WidgetUtilsGtk.h" +#include "gfxConfig.h" +#include "nsIGfxInfo.h" +#include "mozilla/Components.h" + +#include +#include +#include +#include +#include + +using namespace mozilla::gfx; + +namespace mozilla { +namespace widget { + +bool sUseWebGLDmabufBackend = true; + +#define GBMLIB_NAME "libgbm.so.1" +#define DRMLIB_NAME "libdrm.so.2" + +// Use static lock to protect dri operation as +// gbm_dri.c is not thread safe. +// https://gitlab.freedesktop.org/mesa/mesa/-/issues/4422 +mozilla::StaticMutex GbmLib::sDRILock MOZ_UNANNOTATED; + +bool GbmLib::sLoaded = false; +void* GbmLib::sGbmLibHandle = nullptr; +void* GbmLib::sXf86DrmLibHandle = nullptr; +CreateDeviceFunc GbmLib::sCreateDevice; +DestroyDeviceFunc GbmLib::sDestroyDevice; +CreateFunc GbmLib::sCreate; +CreateWithModifiersFunc GbmLib::sCreateWithModifiers; +GetModifierFunc GbmLib::sGetModifier; +GetStrideFunc GbmLib::sGetStride; +GetFdFunc GbmLib::sGetFd; +DestroyFunc GbmLib::sDestroy; +MapFunc GbmLib::sMap; +UnmapFunc GbmLib::sUnmap; +GetPlaneCountFunc GbmLib::sGetPlaneCount; +GetHandleForPlaneFunc GbmLib::sGetHandleForPlane; +GetStrideForPlaneFunc GbmLib::sGetStrideForPlane; +GetOffsetFunc GbmLib::sGetOffset; +DeviceIsFormatSupportedFunc GbmLib::sDeviceIsFormatSupported; +DrmPrimeHandleToFDFunc GbmLib::sDrmPrimeHandleToFD; +CreateSurfaceFunc GbmLib::sCreateSurface; +DestroySurfaceFunc GbmLib::sDestroySurface; + +bool GbmLib::IsLoaded() { + return sCreateDevice != nullptr && sDestroyDevice != nullptr && + sCreate != nullptr && sCreateWithModifiers != nullptr && + sGetModifier != nullptr && sGetStride != nullptr && + sGetFd != nullptr && sDestroy != nullptr && sMap != nullptr && + sUnmap != nullptr && sGetPlaneCount != nullptr && + sGetHandleForPlane != nullptr && sGetStrideForPlane != nullptr && + sGetOffset != nullptr && sDeviceIsFormatSupported != nullptr && + sDrmPrimeHandleToFD != nullptr && sCreateSurface != nullptr && + sDestroySurface != nullptr; +} + +bool GbmLib::Load() { + static bool sTriedToLoad = false; + if (sTriedToLoad) { + return sLoaded; + } + + sTriedToLoad = true; + + MOZ_ASSERT(!sGbmLibHandle); + MOZ_ASSERT(!sLoaded); + + LOGDMABUF(("Loading DMABuf system library %s ...\n", GBMLIB_NAME)); + + sGbmLibHandle = dlopen(GBMLIB_NAME, RTLD_LAZY | RTLD_LOCAL); + if (!sGbmLibHandle) { + LOGDMABUF(("Failed to load %s, dmabuf isn't available.\n", GBMLIB_NAME)); + return false; + } + + sCreateDevice = (CreateDeviceFunc)dlsym(sGbmLibHandle, "gbm_create_device"); + sDestroyDevice = + (DestroyDeviceFunc)dlsym(sGbmLibHandle, "gbm_device_destroy"); + sCreate = (CreateFunc)dlsym(sGbmLibHandle, "gbm_bo_create"); + sCreateWithModifiers = (CreateWithModifiersFunc)dlsym( + sGbmLibHandle, "gbm_bo_create_with_modifiers"); + sGetModifier = (GetModifierFunc)dlsym(sGbmLibHandle, "gbm_bo_get_modifier"); + sGetStride = (GetStrideFunc)dlsym(sGbmLibHandle, "gbm_bo_get_stride"); + sGetFd = (GetFdFunc)dlsym(sGbmLibHandle, "gbm_bo_get_fd"); + sDestroy = (DestroyFunc)dlsym(sGbmLibHandle, "gbm_bo_destroy"); + sMap = (MapFunc)dlsym(sGbmLibHandle, "gbm_bo_map"); + sUnmap = (UnmapFunc)dlsym(sGbmLibHandle, "gbm_bo_unmap"); + sGetPlaneCount = + (GetPlaneCountFunc)dlsym(sGbmLibHandle, "gbm_bo_get_plane_count"); + sGetHandleForPlane = (GetHandleForPlaneFunc)dlsym( + sGbmLibHandle, "gbm_bo_get_handle_for_plane"); + sGetStrideForPlane = (GetStrideForPlaneFunc)dlsym( + sGbmLibHandle, "gbm_bo_get_stride_for_plane"); + sGetOffset = (GetOffsetFunc)dlsym(sGbmLibHandle, "gbm_bo_get_offset"); + sDeviceIsFormatSupported = (DeviceIsFormatSupportedFunc)dlsym( + sGbmLibHandle, "gbm_device_is_format_supported"); + sCreateSurface = + (CreateSurfaceFunc)dlsym(sGbmLibHandle, "gbm_surface_create"); + sDestroySurface = + (DestroySurfaceFunc)dlsym(sGbmLibHandle, "gbm_surface_destroy"); + + sXf86DrmLibHandle = dlopen(DRMLIB_NAME, RTLD_LAZY | RTLD_LOCAL); + if (!sXf86DrmLibHandle) { + LOGDMABUF(("Failed to load %s, dmabuf isn't available.\n", DRMLIB_NAME)); + return false; + } + sDrmPrimeHandleToFD = + (DrmPrimeHandleToFDFunc)dlsym(sXf86DrmLibHandle, "drmPrimeHandleToFD"); + sLoaded = IsLoaded(); + if (!sLoaded) { + LOGDMABUF(("Failed to load all symbols from %s\n", GBMLIB_NAME)); + } + return sLoaded; +} + +int DMABufDevice::GetDmabufFD(uint32_t aGEMHandle) { + int fd; + return GbmLib::DrmPrimeHandleToFD(mDRMFd, aGEMHandle, 0, &fd) < 0 ? -1 : fd; +} + +gbm_device* DMABufDevice::GetGbmDevice() { + std::call_once(mFlagGbmDevice, [&] { + mGbmDevice = (mDRMFd != -1) ? GbmLib::CreateDevice(mDRMFd) : nullptr; + }); + return mGbmDevice; +} + +int DMABufDevice::OpenDRMFd() { return open(mDrmRenderNode.get(), O_RDWR); } + +bool DMABufDevice::IsEnabled(nsACString& aFailureId) { + if (mDRMFd == -1) { + aFailureId = mFailureId; + } + return mDRMFd != -1; +} + +DMABufDevice::DMABufDevice() + : mXRGBFormat({true, false, GBM_FORMAT_XRGB8888, {}}), + mARGBFormat({true, true, GBM_FORMAT_ARGB8888, {}}) { + Configure(); +} + +DMABufDevice::~DMABufDevice() { + if (mGbmDevice) { + GbmLib::DestroyDevice(mGbmDevice); + mGbmDevice = nullptr; + } + if (mDRMFd != -1) { + close(mDRMFd); + mDRMFd = -1; + } +} + +void DMABufDevice::Configure() { + LOGDMABUF(("DMABufDevice::Configure()")); + + if (!GbmLib::IsAvailable()) { + LOGDMABUF(("GbmLib is not available!")); + mFailureId = "FEATURE_FAILURE_NO_LIBGBM"; + return; + } + + mDrmRenderNode = nsAutoCString(getenv("MOZ_DRM_DEVICE")); + if (mDrmRenderNode.IsEmpty()) { + mDrmRenderNode.Assign(gfx::gfxVars::DrmRenderDevice()); + } + if (mDrmRenderNode.IsEmpty()) { + LOGDMABUF(("We're missing DRM render device!\n")); + mFailureId = "FEATURE_FAILURE_NO_DRM_DEVICE"; + return; + } + + LOGDMABUF(("Using DRM device %s", mDrmRenderNode.get())); + mDRMFd = open(mDrmRenderNode.get(), O_RDWR); + if (mDRMFd < 0) { + LOGDMABUF(("Failed to open drm render node %s error %s\n", + mDrmRenderNode.get(), strerror(errno))); + mFailureId = "FEATURE_FAILURE_NO_DRM_DEVICE"; + return; + } + + LoadFormatModifiers(); + + LOGDMABUF(("DMABuf is enabled")); +} + +#ifdef NIGHTLY_BUILD +bool DMABufDevice::IsDMABufTexturesEnabled() { + return gfx::gfxVars::UseDMABuf() && + StaticPrefs::widget_dmabuf_textures_enabled(); +} +#else +bool DMABufDevice::IsDMABufTexturesEnabled() { return false; } +#endif +bool DMABufDevice::IsDMABufWebGLEnabled() { + LOGDMABUF( + ("DMABufDevice::IsDMABufWebGLEnabled: UseDMABuf %d " + "sUseWebGLDmabufBackend %d " + "widget_dmabuf_webgl_enabled %d\n", + gfx::gfxVars::UseDMABuf(), sUseWebGLDmabufBackend, + StaticPrefs::widget_dmabuf_webgl_enabled())); + return gfx::gfxVars::UseDMABuf() && sUseWebGLDmabufBackend && + StaticPrefs::widget_dmabuf_webgl_enabled(); +} + +void DMABufDevice::SetModifiersToGfxVars() { + gfxVars::SetDMABufModifiersXRGB(mXRGBFormat.mModifiers); + gfxVars::SetDMABufModifiersARGB(mARGBFormat.mModifiers); +} + +void DMABufDevice::GetModifiersFromGfxVars() { + mXRGBFormat.mModifiers = gfxVars::DMABufModifiersXRGB().Clone(); + mARGBFormat.mModifiers = gfxVars::DMABufModifiersARGB().Clone(); +} + +void DMABufDevice::DisableDMABufWebGL() { sUseWebGLDmabufBackend = false; } + +GbmFormat* DMABufDevice::GetGbmFormat(bool aHasAlpha) { + GbmFormat* format = aHasAlpha ? &mARGBFormat : &mXRGBFormat; + return format->mIsSupported ? format : nullptr; +} + +void DMABufDevice::AddFormatModifier(bool aHasAlpha, int aFormat, + uint32_t mModifierHi, + uint32_t mModifierLo) { + GbmFormat* format = aHasAlpha ? &mARGBFormat : &mXRGBFormat; + format->mIsSupported = true; + format->mHasAlpha = aHasAlpha; + format->mFormat = aFormat; + format->mModifiers.AppendElement(((uint64_t)mModifierHi << 32) | mModifierLo); +} + +static void dmabuf_modifiers(void* data, + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf, + uint32_t format, uint32_t modifier_hi, + uint32_t modifier_lo) { + // skip modifiers marked as invalid + if (modifier_hi == (DRM_FORMAT_MOD_INVALID >> 32) && + modifier_lo == (DRM_FORMAT_MOD_INVALID & 0xffffffff)) { + return; + } + + auto* device = static_cast(data); + switch (format) { + case GBM_FORMAT_ARGB8888: + device->AddFormatModifier(true, format, modifier_hi, modifier_lo); + break; + case GBM_FORMAT_XRGB8888: + device->AddFormatModifier(false, format, modifier_hi, modifier_lo); + break; + default: + break; + } +} + +static void dmabuf_format(void* data, + struct zwp_linux_dmabuf_v1* zwp_linux_dmabuf, + uint32_t format) { + // XXX: deprecated +} + +static const struct zwp_linux_dmabuf_v1_listener dmabuf_listener = { + dmabuf_format, dmabuf_modifiers}; + +static void global_registry_handler(void* data, wl_registry* registry, + uint32_t id, const char* interface, + uint32_t version) { + if (strcmp(interface, "zwp_linux_dmabuf_v1") == 0 && version > 2) { + auto* dmabuf = WaylandRegistryBind( + registry, id, &zwp_linux_dmabuf_v1_interface, 3); + LOGDMABUF(("zwp_linux_dmabuf_v1 is available.")); + zwp_linux_dmabuf_v1_add_listener(dmabuf, &dmabuf_listener, data); + } else if (strcmp(interface, "wl_drm") == 0) { + LOGDMABUF(("wl_drm is available.")); + } +} + +static void global_registry_remover(void* data, wl_registry* registry, + uint32_t id) {} + +static const struct wl_registry_listener registry_listener = { + global_registry_handler, global_registry_remover}; + +void DMABufDevice::LoadFormatModifiers() { + if (!GdkIsWaylandDisplay()) { + return; + } + if (XRE_IsParentProcess()) { + MOZ_ASSERT(NS_IsMainThread()); + wl_display* display = WaylandDisplayGetWLDisplay(); + wl_registry* registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, this); + wl_display_roundtrip(display); + wl_display_roundtrip(display); + wl_registry_destroy(registry); + SetModifiersToGfxVars(); + } else { + GetModifiersFromGfxVars(); + } +} + +DMABufDevice* GetDMABufDevice() { + static DMABufDevice dmaBufDevice; + return &dmaBufDevice; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/DMABufLibWrapper.h b/widget/gtk/DMABufLibWrapper.h new file mode 100644 index 0000000000..9c1b05f1cd --- /dev/null +++ b/widget/gtk/DMABufLibWrapper.h @@ -0,0 +1,228 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __MOZ_DMABUF_LIB_WRAPPER_H__ +#define __MOZ_DMABUF_LIB_WRAPPER_H__ + +#include "mozilla/widget/gbm.h" +#include "mozilla/StaticMutex.h" +#include + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +extern mozilla::LazyLogModule gDmabufLog; +# define LOGDMABUF(args) MOZ_LOG(gDmabufLog, mozilla::LogLevel::Debug, args) +#else +# define LOGDMABUF(args) +#endif /* MOZ_LOGGING */ + +#ifndef DRM_FORMAT_MOD_INVALID +# define DRM_FORMAT_MOD_INVALID ((1ULL << 56) - 1) +#endif + +namespace mozilla { +namespace widget { + +typedef struct gbm_device* (*CreateDeviceFunc)(int); +typedef void (*DestroyDeviceFunc)(struct gbm_device*); +typedef struct gbm_bo* (*CreateFunc)(struct gbm_device*, uint32_t, uint32_t, + uint32_t, uint32_t); +typedef struct gbm_bo* (*CreateWithModifiersFunc)(struct gbm_device*, uint32_t, + uint32_t, uint32_t, + const uint64_t*, + const unsigned int); +typedef uint64_t (*GetModifierFunc)(struct gbm_bo*); +typedef uint32_t (*GetStrideFunc)(struct gbm_bo*); +typedef int (*GetFdFunc)(struct gbm_bo*); +typedef void (*DestroyFunc)(struct gbm_bo*); +typedef void* (*MapFunc)(struct gbm_bo*, uint32_t, uint32_t, uint32_t, uint32_t, + uint32_t, uint32_t*, void**); +typedef void (*UnmapFunc)(struct gbm_bo*, void*); +typedef int (*GetPlaneCountFunc)(struct gbm_bo*); +typedef union gbm_bo_handle (*GetHandleForPlaneFunc)(struct gbm_bo*, int); +typedef uint32_t (*GetStrideForPlaneFunc)(struct gbm_bo*, int); +typedef uint32_t (*GetOffsetFunc)(struct gbm_bo*, int); +typedef int (*DeviceIsFormatSupportedFunc)(struct gbm_device*, uint32_t, + uint32_t); +typedef int (*DrmPrimeHandleToFDFunc)(int, uint32_t, uint32_t, int*); +typedef struct gbm_surface* (*CreateSurfaceFunc)(struct gbm_device*, uint32_t, + uint32_t, uint32_t, uint32_t); +typedef void (*DestroySurfaceFunc)(struct gbm_surface*); + +class GbmLib { + public: + static bool IsAvailable() { return sLoaded || Load(); } + static bool IsModifierAvailable(); + + static struct gbm_device* CreateDevice(int fd) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreateDevice(fd); + }; + static void DestroyDevice(struct gbm_device* gdm) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDestroyDevice(gdm); + }; + static struct gbm_bo* Create(struct gbm_device* gbm, uint32_t width, + uint32_t height, uint32_t format, + uint32_t flags) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreate(gbm, width, height, format, flags); + } + static void Destroy(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + sDestroy(bo); + } + static uint32_t GetStride(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetStride(bo); + } + static int GetFd(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetFd(bo); + } + static void* Map(struct gbm_bo* bo, uint32_t x, uint32_t y, uint32_t width, + uint32_t height, uint32_t flags, uint32_t* stride, + void** map_data) { + StaticMutexAutoLock lockDRI(sDRILock); + return sMap(bo, x, y, width, height, flags, stride, map_data); + } + static void Unmap(struct gbm_bo* bo, void* map_data) { + StaticMutexAutoLock lockDRI(sDRILock); + sUnmap(bo, map_data); + } + static struct gbm_bo* CreateWithModifiers(struct gbm_device* gbm, + uint32_t width, uint32_t height, + uint32_t format, + const uint64_t* modifiers, + const unsigned int count) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreateWithModifiers(gbm, width, height, format, modifiers, count); + } + static uint64_t GetModifier(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetModifier(bo); + } + static int GetPlaneCount(struct gbm_bo* bo) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetPlaneCount(bo); + } + static union gbm_bo_handle GetHandleForPlane(struct gbm_bo* bo, int plane) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetHandleForPlane(bo, plane); + } + static uint32_t GetStrideForPlane(struct gbm_bo* bo, int plane) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetStrideForPlane(bo, plane); + } + static uint32_t GetOffset(struct gbm_bo* bo, int plane) { + StaticMutexAutoLock lockDRI(sDRILock); + return sGetOffset(bo, plane); + } + static int DeviceIsFormatSupported(struct gbm_device* gbm, uint32_t format, + uint32_t usage) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDeviceIsFormatSupported(gbm, format, usage); + } + static int DrmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, + int* prime_fd) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDrmPrimeHandleToFD(fd, handle, flags, prime_fd); + } + static struct gbm_surface* CreateSurface(struct gbm_device* gbm, + uint32_t width, uint32_t height, + uint32_t format, uint32_t flags) { + StaticMutexAutoLock lockDRI(sDRILock); + return sCreateSurface(gbm, width, height, format, flags); + } + static void DestroySurface(struct gbm_surface* surface) { + StaticMutexAutoLock lockDRI(sDRILock); + return sDestroySurface(surface); + } + + private: + static bool Load(); + static bool IsLoaded(); + + static CreateDeviceFunc sCreateDevice; + static DestroyDeviceFunc sDestroyDevice; + static CreateFunc sCreate; + static CreateWithModifiersFunc sCreateWithModifiers; + static GetModifierFunc sGetModifier; + static GetStrideFunc sGetStride; + static GetFdFunc sGetFd; + static DestroyFunc sDestroy; + static MapFunc sMap; + static UnmapFunc sUnmap; + static GetPlaneCountFunc sGetPlaneCount; + static GetHandleForPlaneFunc sGetHandleForPlane; + static GetStrideForPlaneFunc sGetStrideForPlane; + static GetOffsetFunc sGetOffset; + static DeviceIsFormatSupportedFunc sDeviceIsFormatSupported; + static DrmPrimeHandleToFDFunc sDrmPrimeHandleToFD; + static CreateSurfaceFunc sCreateSurface; + static DestroySurfaceFunc sDestroySurface; + static bool sLoaded; + + static void* sGbmLibHandle; + static void* sXf86DrmLibHandle; + static mozilla::StaticMutex sDRILock MOZ_UNANNOTATED; +}; + +struct GbmFormat { + bool mIsSupported; + bool mHasAlpha; + int mFormat; + nsTArray mModifiers; +}; + +class DMABufDevice { + public: + DMABufDevice(); + ~DMABufDevice(); + + int OpenDRMFd(); + gbm_device* GetGbmDevice(); + int GetDmabufFD(uint32_t aGEMHandle); + + bool IsEnabled(nsACString& aFailureId); + + // Use dmabuf for WebRender general web content + static bool IsDMABufTexturesEnabled(); + // Use dmabuf for WebGL content + static bool IsDMABufWebGLEnabled(); + static void DisableDMABufWebGL(); + + void AddFormatModifier(bool aHasAlpha, int aFormat, uint32_t mModifierHi, + uint32_t mModifierLo); + GbmFormat* GetGbmFormat(bool aHasAlpha); + + private: + void Configure(); + void LoadFormatModifiers(); + + void SetModifiersToGfxVars(); + void GetModifiersFromGfxVars(); + + private: + GbmFormat mXRGBFormat; + GbmFormat mARGBFormat; + + int mDRMFd = -1; + std::once_flag mFlagGbmDevice; + gbm_device* mGbmDevice = nullptr; + const char* mFailureId = nullptr; + nsAutoCString mDrmRenderNode; +}; + +DMABufDevice* GetDMABufDevice(); + +} // namespace widget +} // namespace mozilla + +#endif // __MOZ_DMABUF_LIB_WRAPPER_H__ diff --git a/widget/gtk/DMABufSurface.cpp b/widget/gtk/DMABufSurface.cpp new file mode 100644 index 0000000000..d2c3396469 --- /dev/null +++ b/widget/gtk/DMABufSurface.cpp @@ -0,0 +1,1654 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DMABufSurface.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mozilla/widget/gbm.h" +#include "mozilla/widget/va_drmcommon.h" +#include "YCbCrUtils.h" +#include "mozilla/gfx/2D.h" +#include "GLContextTypes.h" // for GLContext, etc +#include "GLContextEGL.h" +#include "GLContextProvider.h" +#include "ScopedGLHelpers.h" +#include "GLBlitHelper.h" +#include "GLReadTexImageHelper.h" +#include "nsGtkUtils.h" + +#include "mozilla/layers/LayersSurfaces.h" +#include "mozilla/ScopeExit.h" + +/* +TODO: +DRM device selection: +https://lists.freedesktop.org/archives/wayland-devel/2018-November/039660.html +*/ + +/* C++ / C typecast macros for special EGL handle values */ +#if defined(__cplusplus) +# define EGL_CAST(type, value) (static_cast(value)) +#else +# define EGL_CAST(type, value) ((type)(value)) +#endif + +using namespace mozilla; +using namespace mozilla::widget; +using namespace mozilla::gl; +using namespace mozilla::layers; +using namespace mozilla::gfx; + +#ifdef MOZ_LOGGING +# include "mozilla/Logging.h" +# include "nsTArray.h" +# include "Units.h" +static LazyLogModule gDmabufRefLog("DmabufRef"); +# define LOGDMABUFREF(args) \ + MOZ_LOG(gDmabufRefLog, mozilla::LogLevel::Debug, args) +#else +# define LOGDMABUFREF(args) +#endif /* MOZ_LOGGING */ + +#define BUFFER_FLAGS 0 + +static RefPtr sSnapshotContext; +static StaticMutex sSnapshotContextMutex MOZ_UNANNOTATED; +static Atomic gNewSurfaceUID(1); + +RefPtr ClaimSnapshotGLContext() { + if (!sSnapshotContext) { + nsCString discardFailureId; + sSnapshotContext = GLContextProvider::CreateHeadless({}, &discardFailureId); + if (!sSnapshotContext) { + LOGDMABUF( + ("ClaimSnapshotGLContext: Failed to create snapshot GLContext.")); + return nullptr; + } + sSnapshotContext->mOwningThreadId = Nothing(); // No singular owner. + } + if (!sSnapshotContext->MakeCurrent()) { + LOGDMABUF(("ClaimSnapshotGLContext: Failed to make GLContext current.")); + return nullptr; + } + return sSnapshotContext; +} + +void ReturnSnapshotGLContext(RefPtr aGLContext) { + // direct eglMakeCurrent() call breaks current context caching so make sure + // it's not used. + MOZ_ASSERT(!aGLContext->mUseTLSIsCurrent); + if (!aGLContext->IsCurrent()) { + LOGDMABUF(("ReturnSnapshotGLContext() failed, is not current!")); + return; + } + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + egl->fMakeCurrent(EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +bool DMABufSurface::IsGlobalRefSet() const { + if (!mGlobalRefCountFd) { + return false; + } + struct pollfd pfd; + pfd.fd = mGlobalRefCountFd; + pfd.events = POLLIN; + return poll(&pfd, 1, 0) == 1; +} + +void DMABufSurface::GlobalRefRelease() { + if (!mGlobalRefCountFd) { + return; + } + LOGDMABUFREF(("DMABufSurface::GlobalRefRelease UID %d", mUID)); + uint64_t counter; + if (read(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) { + if (errno == EAGAIN) { + LOGDMABUFREF( + (" GlobalRefRelease failed: already zero reference! UID %d", mUID)); + } + // EAGAIN means the refcount is already zero. It happens when we release + // last reference to the surface. + if (errno != EAGAIN) { + NS_WARNING(nsPrintfCString("Failed to unref dmabuf global ref count: %s", + strerror(errno)) + .get()); + } + } +} + +void DMABufSurface::GlobalRefAdd() { + LOGDMABUFREF(("DMABufSurface::GlobalRefAdd UID %d", mUID)); + MOZ_DIAGNOSTIC_ASSERT(mGlobalRefCountFd); + uint64_t counter = 1; + if (write(mGlobalRefCountFd, &counter, sizeof(counter)) != sizeof(counter)) { + NS_WARNING(nsPrintfCString("Failed to ref dmabuf global ref count: %s", + strerror(errno)) + .get()); + } +} + +void DMABufSurface::GlobalRefCountCreate() { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountCreate UID %d", mUID)); + MOZ_DIAGNOSTIC_ASSERT(!mGlobalRefCountFd); + // Create global ref count initialized to 0, + // i.e. is not referenced after create. + mGlobalRefCountFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | EFD_SEMAPHORE); + if (mGlobalRefCountFd < 0) { + NS_WARNING(nsPrintfCString("Failed to create dmabuf global ref count: %s", + strerror(errno)) + .get()); + mGlobalRefCountFd = 0; + return; + } +} + +void DMABufSurface::GlobalRefCountImport(int aFd) { + mGlobalRefCountFd = aFd; + if (mGlobalRefCountFd) { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountImport UID %d", mUID)); + GlobalRefAdd(); + } +} + +int DMABufSurface::GlobalRefCountExport() { +#ifdef MOZ_LOGGING + if (mGlobalRefCountFd) { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountExport UID %d", mUID)); + } +#endif + return mGlobalRefCountFd; +} + +void DMABufSurface::GlobalRefCountDelete() { + if (mGlobalRefCountFd) { + LOGDMABUFREF(("DMABufSurface::GlobalRefCountDelete UID %d", mUID)); + close(mGlobalRefCountFd); + mGlobalRefCountFd = 0; + } +} + +void DMABufSurface::ReleaseDMABuf() { + LOGDMABUF(("DMABufSurface::ReleaseDMABuf() UID %d", mUID)); + for (int i = 0; i < mBufferPlaneCount; i++) { + Unmap(i); + } + + MutexAutoLock lockFD(mSurfaceLock); + CloseFileDescriptors(lockFD, /* aForceClose */ true); + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mGbmBufferObject[i]) { + GbmLib::Destroy(mGbmBufferObject[i]); + mGbmBufferObject[i] = nullptr; + } + } + mBufferPlaneCount = 0; +} + +DMABufSurface::DMABufSurface(SurfaceType aSurfaceType) + : mSurfaceType(aSurfaceType), + mBufferPlaneCount(0), + mDrmFormats(), + mStrides(), + mOffsets(), + mGbmBufferObject(), + mMappedRegion(), + mMappedRegionStride(), + mSyncFd(-1), + mSync(nullptr), + mGlobalRefCountFd(0), + mUID(gNewSurfaceUID++), + mSurfaceLock("DMABufSurface") { + for (auto& slot : mDmabufFds) { + slot = -1; + } + for (auto& modifier : mBufferModifiers) { + modifier = DRM_FORMAT_MOD_INVALID; + } +} + +DMABufSurface::~DMABufSurface() { + FenceDelete(); + GlobalRefRelease(); + GlobalRefCountDelete(); +} + +already_AddRefed DMABufSurface::CreateDMABufSurface( + const mozilla::layers::SurfaceDescriptor& aDesc) { + const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf(); + RefPtr surf; + + switch (desc.bufferType()) { + case SURFACE_RGBA: + surf = new DMABufSurfaceRGBA(); + break; + case SURFACE_NV12: + case SURFACE_YUV420: + surf = new DMABufSurfaceYUV(); + break; + default: + return nullptr; + } + + if (!surf->Create(desc)) { + return nullptr; + } + return surf.forget(); +} + +void DMABufSurface::FenceDelete() { + if (mSyncFd > 0) { + close(mSyncFd); + mSyncFd = -1; + } + + if (!mGL) { + return; + } + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (mSync) { + egl->fDestroySync(mSync); + mSync = nullptr; + } +} + +void DMABufSurface::FenceSet() { + if (!mGL || !mGL->MakeCurrent()) { + MOZ_DIAGNOSTIC_ASSERT(mGL, + "DMABufSurface::FenceSet(): missing GL context!"); + return; + } + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (egl->IsExtensionSupported(EGLExtension::KHR_fence_sync) && + egl->IsExtensionSupported(EGLExtension::ANDROID_native_fence_sync)) { + FenceDelete(); + + mSync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr); + if (mSync) { + mSyncFd = egl->fDupNativeFenceFDANDROID(mSync); + mGL->fFlush(); + return; + } + } + + // ANDROID_native_fence_sync may not be supported so call glFinish() + // as a slow path. + mGL->fFinish(); +} + +void DMABufSurface::FenceWait() { + if (!mGL || mSyncFd < 0) { + MOZ_DIAGNOSTIC_ASSERT(mGL, + "DMABufSurface::FenceWait() missing GL context!"); + return; + } + + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + const EGLint attribs[] = {LOCAL_EGL_SYNC_NATIVE_FENCE_FD_ANDROID, mSyncFd, + LOCAL_EGL_NONE}; + EGLSync sync = egl->fCreateSync(LOCAL_EGL_SYNC_NATIVE_FENCE_ANDROID, attribs); + if (!sync) { + MOZ_ASSERT(false, "DMABufSurface::FenceWait(): Failed to create GLFence!"); + // We failed to create GLFence so clear mSyncFd to avoid another try. + close(mSyncFd); + mSyncFd = -1; + return; + } + + // mSyncFd is owned by GLFence so clear local reference to avoid double close + // at DMABufSurface::FenceDelete(). + mSyncFd = -1; + + egl->fClientWaitSync(sync, 0, LOCAL_EGL_FOREVER); + egl->fDestroySync(sync); +} + +bool DMABufSurface::OpenFileDescriptors(const MutexAutoLock& aProofOfLock) { + for (int i = 0; i < mBufferPlaneCount; i++) { + if (!OpenFileDescriptorForPlane(aProofOfLock, i)) { + return false; + } + } + return true; +} + +// We can safely close DMABuf file descriptors only when we have a valid +// GbmBufferObject. When we don't have a valid GbmBufferObject and a DMABuf +// file descriptor is closed, whole surface is released. +void DMABufSurface::CloseFileDescriptors(const MutexAutoLock& aProofOfLock, + bool aForceClose) { + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + CloseFileDescriptorForPlane(aProofOfLock, i, aForceClose); + } +} + +DMABufSurfaceRGBA::DMABufSurfaceRGBA() + : DMABufSurface(SURFACE_RGBA), + mSurfaceFlags(0), + mWidth(0), + mHeight(0), + mGmbFormat(nullptr), + mEGLImage(LOCAL_EGL_NO_IMAGE), + mTexture(0), + mGbmBufferFlags(0), + mWlBuffer(nullptr) {} + +DMABufSurfaceRGBA::~DMABufSurfaceRGBA() { + ReleaseWlBuffer(); + ReleaseSurface(); +} + +bool DMABufSurfaceRGBA::OpenFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane) { + if (mDmabufFds[aPlane] >= 0) { + return true; + } + gbm_bo* bo = mGbmBufferObject[0]; + if (NS_WARN_IF(!bo)) { + LOGDMABUF( + ("DMABufSurfaceRGBA::OpenFileDescriptorForPlane: Missing " + "mGbmBufferObject object!")); + return false; + } + + if (mBufferPlaneCount == 1) { + MOZ_ASSERT(aPlane == 0, "DMABuf: wrong surface plane!"); + mDmabufFds[0] = GbmLib::GetFd(bo); + } else { + mDmabufFds[aPlane] = GetDMABufDevice()->GetDmabufFD( + GbmLib::GetHandleForPlane(bo, aPlane).u32); + } + + if (mDmabufFds[aPlane] < 0) { + CloseFileDescriptors(aProofOfLock); + return false; + } + + return true; +} + +void DMABufSurfaceRGBA::CloseFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane, bool aForceClose = false) { + if ((aForceClose || mGbmBufferObject[0]) && mDmabufFds[aPlane] >= 0) { + close(mDmabufFds[aPlane]); + mDmabufFds[aPlane] = -1; + } +} + +bool DMABufSurfaceRGBA::Create(int aWidth, int aHeight, + int aDMABufSurfaceFlags) { + MOZ_ASSERT(mGbmBufferObject[0] == nullptr, "Already created?"); + + mSurfaceFlags = aDMABufSurfaceFlags; + mWidth = aWidth; + mHeight = aHeight; + + LOGDMABUF(("DMABufSurfaceRGBA::Create() UID %d size %d x %d\n", mUID, mWidth, + mHeight)); + + if (!GetDMABufDevice()->GetGbmDevice()) { + LOGDMABUF((" Missing GbmDevice!")); + return false; + } + + mGmbFormat = GetDMABufDevice()->GetGbmFormat(mSurfaceFlags & DMABUF_ALPHA); + if (!mGmbFormat) { + // Requested DRM format is not supported. + return false; + } + mDrmFormats[0] = mGmbFormat->mFormat; + + bool useModifiers = (aDMABufSurfaceFlags & DMABUF_USE_MODIFIERS) && + !mGmbFormat->mModifiers.IsEmpty(); + if (useModifiers) { + LOGDMABUF((" Creating with modifiers\n")); + mGbmBufferObject[0] = GbmLib::CreateWithModifiers( + GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight, mDrmFormats[0], + mGmbFormat->mModifiers.Elements(), mGmbFormat->mModifiers.Length()); + if (mGbmBufferObject[0]) { + mBufferModifiers[0] = GbmLib::GetModifier(mGbmBufferObject[0]); + } + } + + if (!mGbmBufferObject[0]) { + LOGDMABUF((" Creating without modifiers\n")); + mGbmBufferFlags = GBM_BO_USE_LINEAR; + mGbmBufferObject[0] = + GbmLib::Create(GetDMABufDevice()->GetGbmDevice(), mWidth, mHeight, + mDrmFormats[0], mGbmBufferFlags); + mBufferModifiers[0] = DRM_FORMAT_MOD_INVALID; + } + + if (!mGbmBufferObject[0]) { + LOGDMABUF((" Failed to create GbmBufferObject\n")); + return false; + } + + if (mBufferModifiers[0] != DRM_FORMAT_MOD_INVALID) { + mBufferPlaneCount = GbmLib::GetPlaneCount(mGbmBufferObject[0]); + if (mBufferPlaneCount > DMABUF_BUFFER_PLANES) { + LOGDMABUF((" There's too many dmabuf planes!")); + ReleaseSurface(); + return false; + } + + for (int i = 0; i < mBufferPlaneCount; i++) { + mStrides[i] = GbmLib::GetStrideForPlane(mGbmBufferObject[0], i); + mOffsets[i] = GbmLib::GetOffset(mGbmBufferObject[0], i); + } + } else { + mBufferPlaneCount = 1; + mStrides[0] = GbmLib::GetStride(mGbmBufferObject[0]); + } + + LOGDMABUF((" Success\n")); + return true; +} + +bool DMABufSurfaceRGBA::Create(mozilla::gl::GLContext* aGLContext, + const EGLImageKHR aEGLImage, int aWidth, + int aHeight) { + LOGDMABUF(("DMABufSurfaceRGBA::Create() from EGLImage UID = %d\n", mUID)); + if (!aGLContext) { + return false; + } + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + + mGL = aGLContext; + mWidth = aWidth; + mHeight = aHeight; + mEGLImage = aEGLImage; + if (!egl->fExportDMABUFImageQuery(mEGLImage, mDrmFormats, &mBufferPlaneCount, + mBufferModifiers)) { + LOGDMABUF((" ExportDMABUFImageQueryMESA failed, quit\n")); + return false; + } + if (mBufferPlaneCount > DMABUF_BUFFER_PLANES) { + LOGDMABUF((" wrong plane count %d, quit\n", mBufferPlaneCount)); + return false; + } + if (!egl->fExportDMABUFImage(mEGLImage, mDmabufFds, mStrides, mOffsets)) { + LOGDMABUF((" ExportDMABUFImageMESA failed, quit\n")); + return false; + } + + // A broken driver can return dmabuf without valid file descriptors + // which leads to fails later so quit now. + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mDmabufFds[i] < 0) { + LOGDMABUF( + (" ExportDMABUFImageMESA failed, mDmabufFds[%d] is invalid, quit", + i)); + return false; + } + } + + LOGDMABUF((" imported size %d x %d format %x planes %d modifiers %" PRIx64, + mWidth, mHeight, mDrmFormats[0], mBufferPlaneCount, + mBufferModifiers[0])); + return true; +} + +bool DMABufSurfaceRGBA::ImportSurfaceDescriptor( + const SurfaceDescriptor& aDesc) { + const SurfaceDescriptorDMABuf& desc = aDesc.get_SurfaceDescriptorDMABuf(); + + mWidth = desc.width()[0]; + mHeight = desc.height()[0]; + mBufferModifiers[0] = desc.modifier()[0]; + mDrmFormats[0] = desc.format()[0]; + mBufferPlaneCount = desc.fds().Length(); + mGbmBufferFlags = desc.flags(); + MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES); + mUID = desc.uid(); + + LOGDMABUF( + ("DMABufSurfaceRGBA::ImportSurfaceDescriptor() UID %d size %d x %d\n", + mUID, mWidth, mHeight)); + + for (int i = 0; i < mBufferPlaneCount; i++) { + mDmabufFds[i] = desc.fds()[i].ClonePlatformHandle().release(); + if (mDmabufFds[i] < 0) { + LOGDMABUF( + (" failed to get DMABuf file descriptor: %s", strerror(errno))); + return false; + } + mStrides[i] = desc.strides()[i]; + mOffsets[i] = desc.offsets()[i]; + } + + if (desc.fence().Length() > 0) { + mSyncFd = desc.fence()[0].ClonePlatformHandle().release(); + if (mSyncFd < 0) { + LOGDMABUF( + (" failed to get GL fence file descriptor: %s", strerror(errno))); + return false; + } + } + + if (desc.refCount().Length() > 0) { + GlobalRefCountImport(desc.refCount()[0].ClonePlatformHandle().release()); + } + + LOGDMABUF((" imported size %d x %d format %x planes %d", mWidth, mHeight, + mDrmFormats[0], mBufferPlaneCount)); + return true; +} + +bool DMABufSurfaceRGBA::Create(const SurfaceDescriptor& aDesc) { + return ImportSurfaceDescriptor(aDesc); +} + +bool DMABufSurfaceRGBA::Serialize( + mozilla::layers::SurfaceDescriptor& aOutDescriptor) { + AutoTArray width; + AutoTArray height; + AutoTArray format; + AutoTArray fds; + AutoTArray strides; + AutoTArray offsets; + AutoTArray images; + AutoTArray modifiers; + AutoTArray fenceFDs; + AutoTArray refCountFDs; + + LOGDMABUF(("DMABufSurfaceRGBA::Serialize() UID %d\n", mUID)); + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + + width.AppendElement(mWidth); + height.AppendElement(mHeight); + format.AppendElement(mDrmFormats[0]); + modifiers.AppendElement(mBufferModifiers[0]); + for (int i = 0; i < mBufferPlaneCount; i++) { + fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i])); + strides.AppendElement(mStrides[i]); + offsets.AppendElement(mOffsets[i]); + } + + CloseFileDescriptors(lockFD); + + if (mSync) { + fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd)); + } + + if (mGlobalRefCountFd) { + refCountFDs.AppendElement(ipc::FileDescriptor(GlobalRefCountExport())); + } + + aOutDescriptor = SurfaceDescriptorDMABuf( + mSurfaceType, modifiers, mGbmBufferFlags, fds, width, height, width, + height, format, strides, offsets, GetYUVColorSpace(), mColorRange, + fenceFDs, mUID, refCountFDs); + return true; +} + +bool DMABufSurfaceRGBA::CreateTexture(GLContext* aGLContext, int aPlane) { + LOGDMABUF(("DMABufSurfaceRGBA::CreateTexture() UID %d\n", mUID)); + MOZ_ASSERT(!mEGLImage && !mTexture, "EGLImage is already created!"); + + nsTArray attribs; + attribs.AppendElement(LOCAL_EGL_WIDTH); + attribs.AppendElement(mWidth); + attribs.AppendElement(LOCAL_EGL_HEIGHT); + attribs.AppendElement(mHeight); + attribs.AppendElement(LOCAL_EGL_LINUX_DRM_FOURCC_EXT); + attribs.AppendElement(mDrmFormats[0]); +#define ADD_PLANE_ATTRIBS(plane_idx) \ + { \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_FD_EXT); \ + attribs.AppendElement(mDmabufFds[plane_idx]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_OFFSET_EXT); \ + attribs.AppendElement((int)mOffsets[plane_idx]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_PITCH_EXT); \ + attribs.AppendElement((int)mStrides[plane_idx]); \ + if (mBufferModifiers[0] != DRM_FORMAT_MOD_INVALID) { \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_LO_EXT); \ + attribs.AppendElement(mBufferModifiers[0] & 0xFFFFFFFF); \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_HI_EXT); \ + attribs.AppendElement(mBufferModifiers[0] >> 32); \ + } \ + } + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + ADD_PLANE_ATTRIBS(0); + if (mBufferPlaneCount > 1) ADD_PLANE_ATTRIBS(1); + if (mBufferPlaneCount > 2) ADD_PLANE_ATTRIBS(2); + if (mBufferPlaneCount > 3) ADD_PLANE_ATTRIBS(3); +#undef ADD_PLANE_ATTRIBS + attribs.AppendElement(LOCAL_EGL_NONE); + + if (!aGLContext) return false; + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + mEGLImage = + egl->fCreateImage(LOCAL_EGL_NO_CONTEXT, LOCAL_EGL_LINUX_DMA_BUF_EXT, + nullptr, attribs.Elements()); + + CloseFileDescriptors(lockFD); + + if (mEGLImage == LOCAL_EGL_NO_IMAGE) { + LOGDMABUF(("EGLImageKHR creation failed")); + return false; + } + + if (!aGLContext->MakeCurrent()) { + LOGDMABUF( + ("DMABufSurfaceRGBA::CreateTexture(): failed to make GL context " + "current")); + return false; + } + aGLContext->fGenTextures(1, &mTexture); + const ScopedBindTexture savedTex(aGLContext, mTexture); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fEGLImageTargetTexture2D(LOCAL_GL_TEXTURE_2D, mEGLImage); + mGL = aGLContext; + + return true; +} + +void DMABufSurfaceRGBA::ReleaseTextures() { + LOGDMABUF(("DMABufSurfaceRGBA::ReleaseTextures() UID %d\n", mUID)); + FenceDelete(); + + if (!mTexture && !mEGLImage) { + return; + } + + if (!mGL) { +#ifdef NIGHTLY_BUILD + MOZ_DIAGNOSTIC_ASSERT(mGL, "Missing GL context!"); +#else + NS_WARNING( + "DMABufSurfaceRGBA::ReleaseTextures(): Missing GL context! We're " + "leaking textures!"); + return; +#endif + } + + const auto& gle = gl::GLContextEGL::Cast(mGL); + const auto& egl = gle->mEgl; + + if (mTexture && mGL->MakeCurrent()) { + mGL->fDeleteTextures(1, &mTexture); + mTexture = 0; + } + + if (mEGLImage != LOCAL_EGL_NO_IMAGE) { + egl->fDestroyImage(mEGLImage); + mEGLImage = LOCAL_EGL_NO_IMAGE; + } + mGL = nullptr; +} + +void DMABufSurfaceRGBA::ReleaseSurface() { + MOZ_ASSERT(!IsMapped(), "We can't release mapped buffer!"); + + ReleaseTextures(); + ReleaseDMABuf(); +} + +bool DMABufSurfaceRGBA::CreateWlBuffer() { + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + + nsWaylandDisplay* waylandDisplay = widget::WaylandDisplayGet(); + if (!waylandDisplay->GetDmabuf()) { + CloseFileDescriptors(lockFD); + return false; + } + + struct zwp_linux_buffer_params_v1* params = + zwp_linux_dmabuf_v1_create_params(waylandDisplay->GetDmabuf()); + zwp_linux_buffer_params_v1_add(params, mDmabufFds[0], 0, mOffsets[0], + mStrides[0], mBufferModifiers[0] >> 32, + mBufferModifiers[0] & 0xffffffff); + + mWlBuffer = zwp_linux_buffer_params_v1_create_immed( + params, GetWidth(), GetHeight(), mDrmFormats[0], 0); + + CloseFileDescriptors(lockFD); + + return mWlBuffer != nullptr; +} + +void DMABufSurfaceRGBA::ReleaseWlBuffer() { + MozClearPointer(mWlBuffer, wl_buffer_destroy); +} + +// We should synchronize DMA Buffer object access from CPU to avoid potential +// cache incoherency and data loss. +// See +// https://01.org/linuxgraphics/gfx-docs/drm/driver-api/dma-buf.html#cpu-access-to-dma-buffer-objects +struct dma_buf_sync { + uint64_t flags; +}; +#define DMA_BUF_SYNC_READ (1 << 0) +#define DMA_BUF_SYNC_WRITE (2 << 0) +#define DMA_BUF_SYNC_START (0 << 2) +#define DMA_BUF_SYNC_END (1 << 2) +#define DMA_BUF_BASE 'b' +#define DMA_BUF_IOCTL_SYNC _IOW(DMA_BUF_BASE, 0, struct dma_buf_sync) + +static void SyncDmaBuf(int aFd, uint64_t aFlags) { + struct dma_buf_sync sync = {0}; + + sync.flags = aFlags | DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE; + while (true) { + int ret; + ret = ioctl(aFd, DMA_BUF_IOCTL_SYNC, &sync); + if (ret == -1 && errno == EINTR) { + continue; + } else if (ret == -1) { + LOGDMABUF( + ("Failed to synchronize DMA buffer: %s FD %d", strerror(errno), aFd)); + break; + } else { + break; + } + } +} + +void* DMABufSurface::MapInternal(uint32_t aX, uint32_t aY, uint32_t aWidth, + uint32_t aHeight, uint32_t* aStride, + int aGbmFlags, int aPlane) { + NS_ASSERTION(!IsMapped(aPlane), "Already mapped!"); + if (!mGbmBufferObject[aPlane]) { + NS_WARNING("We can't map DMABufSurfaceRGBA without mGbmBufferObject"); + return nullptr; + } + + LOGDMABUF( + ("DMABufSurfaceRGBA::MapInternal() UID %d plane %d size %d x %d -> %d x " + "%d\n", + mUID, aPlane, aX, aY, aWidth, aHeight)); + + mMappedRegionStride[aPlane] = 0; + mMappedRegionData[aPlane] = nullptr; + mMappedRegion[aPlane] = + GbmLib::Map(mGbmBufferObject[aPlane], aX, aY, aWidth, aHeight, aGbmFlags, + &mMappedRegionStride[aPlane], &mMappedRegionData[aPlane]); + if (!mMappedRegion[aPlane]) { + LOGDMABUF((" Surface mapping failed: %s", strerror(errno))); + return nullptr; + } + if (aStride) { + *aStride = mMappedRegionStride[aPlane]; + } + + MutexAutoLock lockFD(mSurfaceLock); + if (OpenFileDescriptorForPlane(lockFD, aPlane)) { + SyncDmaBuf(mDmabufFds[aPlane], DMA_BUF_SYNC_START); + CloseFileDescriptorForPlane(lockFD, aPlane); + } + + return mMappedRegion[aPlane]; +} + +void* DMABufSurfaceRGBA::MapReadOnly(uint32_t aX, uint32_t aY, uint32_t aWidth, + uint32_t aHeight, uint32_t* aStride) { + return MapInternal(aX, aY, aWidth, aHeight, aStride, GBM_BO_TRANSFER_READ); +} + +void* DMABufSurfaceRGBA::MapReadOnly(uint32_t* aStride) { + return MapInternal(0, 0, mWidth, mHeight, aStride, GBM_BO_TRANSFER_READ); +} + +void* DMABufSurfaceRGBA::Map(uint32_t aX, uint32_t aY, uint32_t aWidth, + uint32_t aHeight, uint32_t* aStride) { + return MapInternal(aX, aY, aWidth, aHeight, aStride, + GBM_BO_TRANSFER_READ_WRITE); +} + +void* DMABufSurfaceRGBA::Map(uint32_t* aStride) { + return MapInternal(0, 0, mWidth, mHeight, aStride, + GBM_BO_TRANSFER_READ_WRITE); +} + +void DMABufSurface::Unmap(int aPlane) { + if (mMappedRegion[aPlane]) { + LOGDMABUF(("DMABufSurface::Unmap() UID %d plane %d\n", mUID, aPlane)); + MutexAutoLock lockFD(mSurfaceLock); + if (OpenFileDescriptorForPlane(lockFD, aPlane)) { + SyncDmaBuf(mDmabufFds[aPlane], DMA_BUF_SYNC_END); + CloseFileDescriptorForPlane(lockFD, aPlane); + } + GbmLib::Unmap(mGbmBufferObject[aPlane], mMappedRegionData[aPlane]); + mMappedRegion[aPlane] = nullptr; + mMappedRegionData[aPlane] = nullptr; + mMappedRegionStride[aPlane] = 0; + } +} + +#ifdef DEBUG +void DMABufSurfaceRGBA::DumpToFile(const char* pFile) { + uint32_t stride; + + if (!MapReadOnly(&stride)) { + return; + } + cairo_surface_t* surface = nullptr; + + auto unmap = MakeScopeExit([&] { + if (surface) { + cairo_surface_destroy(surface); + } + Unmap(); + }); + + surface = cairo_image_surface_create_for_data( + (unsigned char*)mMappedRegion[0], CAIRO_FORMAT_ARGB32, mWidth, mHeight, + stride); + if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { + cairo_surface_write_to_png(surface, pFile); + } +} +#endif + +#if 0 +// Copy from source surface by GL +# include "GLBlitHelper.h" + +bool DMABufSurfaceRGBA::CopyFrom(class DMABufSurface* aSourceSurface, + GLContext* aGLContext) { + MOZ_ASSERT(aSourceSurface->GetTexture()); + MOZ_ASSERT(GetTexture()); + + gfx::IntSize size(GetWidth(), GetHeight()); + aGLContext->BlitHelper()->BlitTextureToTexture(aSourceSurface->GetTexture(), + GetTexture(), size, size); + return true; +} +#endif + +// TODO - Clear the surface by EGL +void DMABufSurfaceRGBA::Clear() { + uint32_t destStride; + void* destData = Map(&destStride); + memset(destData, 0, GetHeight() * destStride); + Unmap(); +} + +bool DMABufSurfaceRGBA::HasAlpha() { + return !mGmbFormat || mGmbFormat->mHasAlpha; +} + +gfx::SurfaceFormat DMABufSurfaceRGBA::GetFormat() { + return HasAlpha() ? gfx::SurfaceFormat::B8G8R8A8 + : gfx::SurfaceFormat::B8G8R8X8; +} + +// GL uses swapped R and B components so report accordingly. +gfx::SurfaceFormat DMABufSurfaceRGBA::GetFormatGL() { + return HasAlpha() ? gfx::SurfaceFormat::R8G8B8A8 + : gfx::SurfaceFormat::R8G8B8X8; +} + +already_AddRefed DMABufSurfaceRGBA::CreateDMABufSurface( + int aWidth, int aHeight, int aDMABufSurfaceFlags) { + RefPtr surf = new DMABufSurfaceRGBA(); + if (!surf->Create(aWidth, aHeight, aDMABufSurfaceFlags)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed DMABufSurfaceRGBA::CreateDMABufSurface( + mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage, int aWidth, + int aHeight) { + RefPtr surf = new DMABufSurfaceRGBA(); + if (!surf->Create(aGLContext, aEGLImage, aWidth, aHeight)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed DMABufSurfaceYUV::CreateYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) { + RefPtr surf = new DMABufSurfaceYUV(); + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurface() UID %d from desc\n", + surf->GetUID())); + if (!surf->UpdateYUVData(aDesc, aWidth, aHeight, /* aCopy */ false)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed DMABufSurfaceYUV::CopyYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) { + RefPtr surf = new DMABufSurfaceYUV(); + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurfaceCopy() UID %d from desc\n", + surf->GetUID())); + if (!surf->UpdateYUVData(aDesc, aWidth, aHeight, /* aCopy */ true)) { + return nullptr; + } + return surf.forget(); +} + +already_AddRefed DMABufSurfaceYUV::CreateYUVSurface( + int aWidth, int aHeight, void** aPixelData, int* aLineSizes) { + RefPtr surf = new DMABufSurfaceYUV(); + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVSurface() UID %d %d x %d\n", + surf->GetUID(), aWidth, aHeight)); + if (!surf->Create(aWidth, aHeight, aPixelData, aLineSizes)) { + return nullptr; + } + return surf.forget(); +} + +DMABufSurfaceYUV::DMABufSurfaceYUV() + : DMABufSurface(SURFACE_NV12), + mWidth(), + mHeight(), + mWidthAligned(), + mHeightAligned(), + mTexture() { + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + mEGLImage[i] = LOCAL_EGL_NO_IMAGE; + } +} + +DMABufSurfaceYUV::~DMABufSurfaceYUV() { ReleaseSurface(); } + +bool DMABufSurfaceYUV::OpenFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane) { + // The fd is already opened, no need to reopen. + // This can happen when we import dmabuf surface from VA-API decoder, + // mGbmBufferObject is null and we don't close + // file descriptors for surface as they are our only reference to it. + if (mDmabufFds[aPlane] >= 0) { + return true; + } + + if (mGbmBufferObject[aPlane] == nullptr) { + LOGDMABUF( + ("DMABufSurfaceYUV::OpenFileDescriptorForPlane: Missing " + "mGbmBufferObject object!")); + return false; + } + + mDmabufFds[aPlane] = GbmLib::GetFd(mGbmBufferObject[aPlane]); + if (mDmabufFds[aPlane] < 0) { + CloseFileDescriptors(aProofOfLock); + return false; + } + return true; +} + +void DMABufSurfaceYUV::CloseFileDescriptorForPlane( + const MutexAutoLock& aProofOfLock, int aPlane, bool aForceClose = false) { + if ((aForceClose || mGbmBufferObject[aPlane]) && mDmabufFds[aPlane] >= 0) { + close(mDmabufFds[aPlane]); + mDmabufFds[aPlane] = -1; + } +} + +bool DMABufSurfaceYUV::ImportPRIMESurfaceDescriptor( + const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight) { + LOGDMABUF(("DMABufSurfaceYUV::ImportPRIMESurfaceDescriptor() UID %d", mUID)); + // Already exists? + MOZ_DIAGNOSTIC_ASSERT(mDmabufFds[0] < 0); + + if (aDesc.num_layers > DMABUF_BUFFER_PLANES || + aDesc.num_objects > DMABUF_BUFFER_PLANES) { + LOGDMABUF((" Can't import, wrong layers/objects number (%d, %d)", + aDesc.num_layers, aDesc.num_objects)); + return false; + } + if (aDesc.fourcc == VA_FOURCC_NV12) { + mSurfaceType = SURFACE_NV12; + } else if (aDesc.fourcc == VA_FOURCC_P010) { + mSurfaceType = SURFACE_NV12; + } else if (aDesc.fourcc == VA_FOURCC_YV12) { + mSurfaceType = SURFACE_YUV420; + } else { + LOGDMABUF((" Can't import surface data of 0x%x format", aDesc.fourcc)); + return false; + } + + mBufferPlaneCount = aDesc.num_layers; + + for (unsigned int i = 0; i < aDesc.num_layers; i++) { + // All supported formats have 4:2:0 chroma sub-sampling. + unsigned int subsample = i == 0 ? 0 : 1; + + unsigned int object = aDesc.layers[i].object_index[0]; + mBufferModifiers[i] = aDesc.objects[object].drm_format_modifier; + mDrmFormats[i] = aDesc.layers[i].drm_format; + mOffsets[i] = aDesc.layers[i].offset[0]; + mStrides[i] = aDesc.layers[i].pitch[0]; + mWidthAligned[i] = aDesc.width >> subsample; + mHeightAligned[i] = aDesc.height >> subsample; + mWidth[i] = aWidth >> subsample; + mHeight[i] = aHeight >> subsample; + LOGDMABUF((" plane %d size %d x %d format %x", i, mWidth[i], mHeight[i], + mDrmFormats[i])); + } + return true; +} + +bool DMABufSurfaceYUV::MoveYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight) { + if (!ImportPRIMESurfaceDescriptor(aDesc, aWidth, aHeight)) { + return false; + } + for (unsigned int i = 0; i < aDesc.num_layers; i++) { + unsigned int object = aDesc.layers[i].object_index[0]; + // Keep VADRMPRIMESurfaceDescriptor untouched and dup() dmabuf + // file descriptors. + mDmabufFds[i] = dup(aDesc.objects[object].fd); + } + return true; +} + +void DMABufSurfaceYUV::ReleaseVADRMPRIMESurfaceDescriptor( + VADRMPRIMESurfaceDescriptor& aDesc) { + for (unsigned int i = 0; i < aDesc.num_layers; i++) { + unsigned int object = aDesc.layers[i].object_index[0]; + if (aDesc.objects[object].fd != -1) { + close(aDesc.objects[object].fd); + aDesc.objects[object].fd = -1; + } + } +} + +bool DMABufSurfaceYUV::CreateYUVPlane(int aPlane) { + LOGDMABUF(("DMABufSurfaceYUV::CreateYUVPlane() UID %d size %d x %d", mUID, + mWidth[aPlane], mHeight[aPlane])); + + if (!GetDMABufDevice()->GetGbmDevice()) { + LOGDMABUF((" Missing GbmDevice!")); + return false; + } + + MOZ_DIAGNOSTIC_ASSERT(mGbmBufferObject[aPlane] == nullptr); + bool useModifiers = (mBufferModifiers[aPlane] != DRM_FORMAT_MOD_INVALID); + if (useModifiers) { + LOGDMABUF((" Creating with modifiers")); + mGbmBufferObject[aPlane] = GbmLib::CreateWithModifiers( + GetDMABufDevice()->GetGbmDevice(), mWidth[aPlane], mHeight[aPlane], + mDrmFormats[aPlane], mBufferModifiers + aPlane, 1); + } + if (!mGbmBufferObject[aPlane]) { + LOGDMABUF((" Creating without modifiers")); + mGbmBufferObject[aPlane] = GbmLib::Create( + GetDMABufDevice()->GetGbmDevice(), mWidth[aPlane], mHeight[aPlane], + mDrmFormats[aPlane], GBM_BO_USE_RENDERING); + mBufferModifiers[aPlane] = DRM_FORMAT_MOD_INVALID; + } + if (!mGbmBufferObject[aPlane]) { + LOGDMABUF((" Failed to create GbmBufferObject: %s", strerror(errno))); + return false; + } + + mStrides[aPlane] = GbmLib::GetStride(mGbmBufferObject[aPlane]); + mOffsets[aPlane] = GbmLib::GetOffset(mGbmBufferObject[aPlane], 0); + mWidthAligned[aPlane] = mWidth[aPlane]; + mHeightAligned[aPlane] = mHeight[aPlane]; + return true; +} + +bool DMABufSurfaceYUV::CopyYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight) { + RefPtr tmpSurf = CreateYUVSurface(aDesc, aWidth, aHeight); + if (!tmpSurf) { + return false; + } + + if (!ImportPRIMESurfaceDescriptor(aDesc, aWidth, aHeight)) { + return false; + } + + StaticMutexAutoLock lock(sSnapshotContextMutex); + RefPtr context = ClaimSnapshotGLContext(); + auto releaseTextures = MakeScopeExit([&] { + tmpSurf->ReleaseTextures(); + ReleaseTextures(); + ReturnSnapshotGLContext(context); + }); + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (!tmpSurf->CreateTexture(context, i)) { + return false; + } + if (!CreateYUVPlane(i) || !CreateTexture(context, i)) { + return false; + } + gfx::IntSize size(GetWidth(i), GetHeight(i)); + context->BlitHelper()->BlitTextureToTexture( + tmpSurf->GetTexture(i), GetTexture(i), size, size, LOCAL_GL_TEXTURE_2D, + LOCAL_GL_TEXTURE_2D); + } + return true; +} + +bool DMABufSurfaceYUV::UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight, bool aCopy) { + LOGDMABUF(("DMABufSurfaceYUV::UpdateYUVData() UID %d copy %d", mUID, aCopy)); + return aCopy ? CopyYUVDataImpl(aDesc, aWidth, aHeight) + : MoveYUVDataImpl(aDesc, aWidth, aHeight); +} + +bool DMABufSurfaceYUV::CreateLinearYUVPlane(int aPlane, int aWidth, int aHeight, + int aDrmFormat) { + LOGDMABUF(("DMABufSurfaceYUV::CreateLinearYUVPlane() UID %d size %d x %d", + mUID, aWidth, aHeight)); + + if (!GetDMABufDevice()->GetGbmDevice()) { + LOGDMABUF((" Missing GbmDevice!")); + return false; + } + + mWidth[aPlane] = aWidth; + mHeight[aPlane] = aHeight; + mDrmFormats[aPlane] = aDrmFormat; + + mGbmBufferObject[aPlane] = + GbmLib::Create(GetDMABufDevice()->GetGbmDevice(), aWidth, aHeight, + aDrmFormat, GBM_BO_USE_LINEAR); + if (!mGbmBufferObject[aPlane]) { + LOGDMABUF((" Failed to create GbmBufferObject: %s", strerror(errno))); + return false; + } + + mStrides[aPlane] = GbmLib::GetStride(mGbmBufferObject[aPlane]); + mDmabufFds[aPlane] = -1; + + return true; +} + +void DMABufSurfaceYUV::UpdateYUVPlane(int aPlane, void* aPixelData, + int aLineSize) { + LOGDMABUF( + ("DMABufSurfaceYUV::UpdateYUVPlane() UID %d plane %d", mUID, aPlane)); + if (aLineSize == mWidth[aPlane] && + (int)mMappedRegionStride[aPlane] == mWidth[aPlane]) { + memcpy(mMappedRegion[aPlane], aPixelData, aLineSize * mHeight[aPlane]); + } else { + char* src = (char*)aPixelData; + char* dest = (char*)mMappedRegion[aPlane]; + for (int i = 0; i < mHeight[aPlane]; i++) { + memcpy(dest, src, mWidth[aPlane]); + src += aLineSize; + dest += mMappedRegionStride[aPlane]; + } + } +} + +bool DMABufSurfaceYUV::UpdateYUVData(void** aPixelData, int* aLineSizes) { + LOGDMABUF(("DMABufSurfaceYUV::UpdateYUVData() UID %d", mUID)); + if (mSurfaceType != SURFACE_YUV420) { + LOGDMABUF((" UpdateYUVData can upload YUV420 surface type only!")); + return false; + } + + if (mBufferPlaneCount != 3) { + LOGDMABUF((" DMABufSurfaceYUV planes does not match!")); + return false; + } + + auto unmapBuffers = MakeScopeExit([&] { + Unmap(0); + Unmap(1); + Unmap(2); + }); + + // Map planes + for (int i = 0; i < mBufferPlaneCount; i++) { + MapInternal(0, 0, mWidth[i], mHeight[i], nullptr, GBM_BO_TRANSFER_WRITE, i); + if (!mMappedRegion[i]) { + LOGDMABUF((" DMABufSurfaceYUV plane can't be mapped!")); + return false; + } + if ((int)mMappedRegionStride[i] < mWidth[i]) { + LOGDMABUF((" DMABufSurfaceYUV plane size stride does not match!")); + return false; + } + } + + // Copy planes + for (int i = 0; i < mBufferPlaneCount; i++) { + UpdateYUVPlane(i, aPixelData[i], aLineSizes[i]); + } + + return true; +} + +bool DMABufSurfaceYUV::Create(int aWidth, int aHeight, void** aPixelData, + int* aLineSizes) { + LOGDMABUF(("DMABufSurfaceYUV::Create() UID %d size %d x %d", mUID, aWidth, + aHeight)); + + mSurfaceType = SURFACE_YUV420; + mBufferPlaneCount = 3; + + if (!CreateLinearYUVPlane(0, aWidth, aHeight, GBM_FORMAT_R8)) { + return false; + } + if (!CreateLinearYUVPlane(1, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) { + return false; + } + if (!CreateLinearYUVPlane(2, aWidth >> 1, aHeight >> 1, GBM_FORMAT_R8)) { + return false; + } + if (!aPixelData || !aLineSizes) { + return true; + } + return UpdateYUVData(aPixelData, aLineSizes); +} + +bool DMABufSurfaceYUV::Create(const SurfaceDescriptor& aDesc) { + return ImportSurfaceDescriptor(aDesc); +} + +bool DMABufSurfaceYUV::ImportSurfaceDescriptor( + const SurfaceDescriptorDMABuf& aDesc) { + mBufferPlaneCount = aDesc.fds().Length(); + mSurfaceType = (mBufferPlaneCount == 2) ? SURFACE_NV12 : SURFACE_YUV420; + mColorSpace = aDesc.yUVColorSpace(); + mColorRange = aDesc.colorRange(); + mUID = aDesc.uid(); + + LOGDMABUF(("DMABufSurfaceYUV::ImportSurfaceDescriptor() UID %d", mUID)); + + MOZ_RELEASE_ASSERT(mBufferPlaneCount <= DMABUF_BUFFER_PLANES); + for (int i = 0; i < mBufferPlaneCount; i++) { + mDmabufFds[i] = aDesc.fds()[i].ClonePlatformHandle().release(); + if (mDmabufFds[i] < 0) { + LOGDMABUF((" failed to get DMABuf plane file descriptor: %s", + strerror(errno))); + return false; + } + mWidth[i] = aDesc.width()[i]; + mHeight[i] = aDesc.height()[i]; + mWidthAligned[i] = aDesc.widthAligned()[i]; + mHeightAligned[i] = aDesc.heightAligned()[i]; + mDrmFormats[i] = aDesc.format()[i]; + mStrides[i] = aDesc.strides()[i]; + mOffsets[i] = aDesc.offsets()[i]; + mBufferModifiers[i] = aDesc.modifier()[i]; + LOGDMABUF((" plane %d fd %d size %d x %d format %x", i, mDmabufFds[i], + mWidth[i], mHeight[i], mDrmFormats[i])); + } + + if (aDesc.fence().Length() > 0) { + mSyncFd = aDesc.fence()[0].ClonePlatformHandle().release(); + if (mSyncFd < 0) { + LOGDMABUF( + (" failed to get GL fence file descriptor: %s", strerror(errno))); + return false; + } + } + + if (aDesc.refCount().Length() > 0) { + GlobalRefCountImport(aDesc.refCount()[0].ClonePlatformHandle().release()); + } + + return true; +} + +bool DMABufSurfaceYUV::Serialize( + mozilla::layers::SurfaceDescriptor& aOutDescriptor) { + AutoTArray width; + AutoTArray height; + AutoTArray widthBytes; + AutoTArray heightBytes; + AutoTArray format; + AutoTArray fds; + AutoTArray strides; + AutoTArray offsets; + AutoTArray modifiers; + AutoTArray fenceFDs; + AutoTArray refCountFDs; + + LOGDMABUF(("DMABufSurfaceYUV::Serialize() UID %d", mUID)); + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptors(lockFD)) { + return false; + } + + for (int i = 0; i < mBufferPlaneCount; i++) { + width.AppendElement(mWidth[i]); + height.AppendElement(mHeight[i]); + widthBytes.AppendElement(mWidthAligned[i]); + heightBytes.AppendElement(mHeightAligned[i]); + format.AppendElement(mDrmFormats[i]); + fds.AppendElement(ipc::FileDescriptor(mDmabufFds[i])); + strides.AppendElement(mStrides[i]); + offsets.AppendElement(mOffsets[i]); + modifiers.AppendElement(mBufferModifiers[i]); + } + + CloseFileDescriptors(lockFD); + + if (mSync) { + fenceFDs.AppendElement(ipc::FileDescriptor(mSyncFd)); + } + + if (mGlobalRefCountFd) { + refCountFDs.AppendElement(ipc::FileDescriptor(GlobalRefCountExport())); + } + + aOutDescriptor = SurfaceDescriptorDMABuf( + mSurfaceType, modifiers, 0, fds, width, height, widthBytes, heightBytes, + format, strides, offsets, GetYUVColorSpace(), mColorRange, fenceFDs, mUID, + refCountFDs); + return true; +} + +bool DMABufSurfaceYUV::CreateEGLImage(GLContext* aGLContext, int aPlane) { + LOGDMABUF( + ("DMABufSurfaceYUV::CreateEGLImage() UID %d plane %d", mUID, aPlane)); + MOZ_ASSERT(mEGLImage[aPlane] == LOCAL_EGL_NO_IMAGE, + "EGLImage is already created!"); + MOZ_ASSERT(aGLContext, "Missing GLContext!"); + + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + + MutexAutoLock lockFD(mSurfaceLock); + if (!OpenFileDescriptorForPlane(lockFD, aPlane)) { + LOGDMABUF((" failed to open dmabuf file descriptors")); + return false; + } + + nsTArray attribs; + attribs.AppendElement(LOCAL_EGL_WIDTH); + attribs.AppendElement(mWidthAligned[aPlane]); + attribs.AppendElement(LOCAL_EGL_HEIGHT); + attribs.AppendElement(mHeightAligned[aPlane]); + attribs.AppendElement(LOCAL_EGL_LINUX_DRM_FOURCC_EXT); + attribs.AppendElement(mDrmFormats[aPlane]); +#define ADD_PLANE_ATTRIBS_NV12(plane_idx) \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_FD_EXT); \ + attribs.AppendElement(mDmabufFds[aPlane]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_OFFSET_EXT); \ + attribs.AppendElement((int)mOffsets[aPlane]); \ + attribs.AppendElement(LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_PITCH_EXT); \ + attribs.AppendElement((int)mStrides[aPlane]); \ + if (mBufferModifiers[aPlane] != DRM_FORMAT_MOD_INVALID) { \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_LO_EXT); \ + attribs.AppendElement(mBufferModifiers[aPlane] & 0xFFFFFFFF); \ + attribs.AppendElement( \ + LOCAL_EGL_DMA_BUF_PLANE##plane_idx##_MODIFIER_HI_EXT); \ + attribs.AppendElement(mBufferModifiers[aPlane] >> 32); \ + } + ADD_PLANE_ATTRIBS_NV12(0); +#undef ADD_PLANE_ATTRIBS_NV12 + attribs.AppendElement(LOCAL_EGL_NONE); + + mEGLImage[aPlane] = + egl->fCreateImage(LOCAL_EGL_NO_CONTEXT, LOCAL_EGL_LINUX_DMA_BUF_EXT, + nullptr, attribs.Elements()); + + CloseFileDescriptorForPlane(lockFD, aPlane); + + if (mEGLImage[aPlane] == LOCAL_EGL_NO_IMAGE) { + LOGDMABUF((" EGLImageKHR creation failed")); + return false; + } + + LOGDMABUF((" Success.")); + return true; +} + +void DMABufSurfaceYUV::ReleaseEGLImages(GLContext* aGLContext) { + LOGDMABUF(("DMABufSurfaceYUV::ReleaseEGLImages() UID %d", mUID)); + MOZ_ASSERT(aGLContext, "Missing GLContext!"); + + const auto& gle = gl::GLContextEGL::Cast(aGLContext); + const auto& egl = gle->mEgl; + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mEGLImage[i] != LOCAL_EGL_NO_IMAGE) { + egl->fDestroyImage(mEGLImage[i]); + mEGLImage[i] = LOCAL_EGL_NO_IMAGE; + } + } +} + +bool DMABufSurfaceYUV::CreateTexture(GLContext* aGLContext, int aPlane) { + LOGDMABUF( + ("DMABufSurfaceYUV::CreateTexture() UID %d plane %d", mUID, aPlane)); + MOZ_ASSERT(!mTexture[aPlane], "Texture is already created!"); + MOZ_ASSERT(aGLContext, "Missing GLContext!"); + + if (!CreateEGLImage(aGLContext, aPlane)) { + return false; + } + if (!aGLContext->MakeCurrent()) { + LOGDMABUF((" Failed to make GL context current.")); + return false; + } + aGLContext->fGenTextures(1, &mTexture[aPlane]); + const ScopedBindTexture savedTex(aGLContext, mTexture[aPlane]); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, + LOCAL_GL_CLAMP_TO_EDGE); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, + LOCAL_GL_LINEAR); + aGLContext->fEGLImageTargetTexture2D(LOCAL_GL_TEXTURE_2D, mEGLImage[aPlane]); + mGL = aGLContext; + return true; +} + +void DMABufSurfaceYUV::ReleaseTextures() { + LOGDMABUF(("DMABufSurfaceYUV::ReleaseTextures() UID %d", mUID)); + + FenceDelete(); + + bool textureActive = false; + for (int i = 0; i < mBufferPlaneCount; i++) { + if (mTexture[i] || mEGLImage[i]) { + textureActive = true; + break; + } + } + + if (!textureActive) { + return; + } + + if (!mGL) { +#ifdef NIGHTLY_BUILD + MOZ_DIAGNOSTIC_ASSERT(mGL, "Missing GL context!"); +#else + NS_WARNING( + "DMABufSurfaceYUV::ReleaseTextures(): Missing GL context! We're " + "leaking textures!"); + return; +#endif + } + + if (!mGL->MakeCurrent()) { + NS_WARNING( + "DMABufSurfaceYUV::ReleaseTextures(): MakeCurrent failed. We're " + "leaking textures!"); + return; + } + + mGL->fDeleteTextures(DMABUF_BUFFER_PLANES, mTexture); + for (int i = 0; i < DMABUF_BUFFER_PLANES; i++) { + mTexture[i] = 0; + } + ReleaseEGLImages(mGL); + mGL = nullptr; +} + +bool DMABufSurfaceYUV::VerifyTextureCreation() { + LOGDMABUF(("DMABufSurfaceYUV::VerifyTextureCreation() UID %d", mUID)); + + StaticMutexAutoLock lock(sSnapshotContextMutex); + RefPtr context = ClaimSnapshotGLContext(); + auto release = MakeScopeExit([&] { + ReleaseEGLImages(context); + ReturnSnapshotGLContext(context); + }); + + for (int i = 0; i < mBufferPlaneCount; i++) { + if (!CreateEGLImage(context, i)) { + LOGDMABUF((" failed to create EGL image!")); + return false; + } + } + + LOGDMABUF((" success")); + return true; +} + +gfx::SurfaceFormat DMABufSurfaceYUV::GetFormat() { + switch (mSurfaceType) { + case SURFACE_NV12: + return gfx::SurfaceFormat::NV12; + case SURFACE_YUV420: + return gfx::SurfaceFormat::YUV; + default: + NS_WARNING("DMABufSurfaceYUV::GetFormat(): Wrong surface format!"); + return gfx::SurfaceFormat::UNKNOWN; + } +} + +// GL uses swapped R and B components so report accordingly. +gfx::SurfaceFormat DMABufSurfaceYUV::GetFormatGL() { return GetFormat(); } + +int DMABufSurfaceYUV::GetTextureCount() { + switch (mSurfaceType) { + case SURFACE_NV12: + return 2; + case SURFACE_YUV420: + return 3; + default: + NS_WARNING("DMABufSurfaceYUV::GetTextureCount(): Wrong surface format!"); + return 1; + } +} + +void DMABufSurfaceYUV::ReleaseSurface() { + LOGDMABUF(("DMABufSurfaceYUV::ReleaseSurface() UID %d", mUID)); + ReleaseTextures(); + ReleaseDMABuf(); +} + +already_AddRefed +DMABufSurfaceYUV::GetAsSourceSurface() { + LOGDMABUF(("DMABufSurfaceYUV::GetAsSourceSurface UID %d", mUID)); + + StaticMutexAutoLock lock(sSnapshotContextMutex); + RefPtr context = ClaimSnapshotGLContext(); + auto releaseTextures = mozilla::MakeScopeExit([&] { + ReleaseTextures(); + ReturnSnapshotGLContext(context); + }); + + for (int i = 0; i < GetTextureCount(); i++) { + if (!GetTexture(i) && !CreateTexture(context, i)) { + LOGDMABUF(("GetAsSourceSurface: Failed to create DMABuf textures.")); + return nullptr; + } + } + + ScopedTexture scopedTex(context); + ScopedBindTexture boundTex(context, scopedTex.Texture()); + + gfx::IntSize size(GetWidth(), GetHeight()); + context->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGBA, size.width, + size.height, 0, LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE, + nullptr); + + ScopedFramebufferForTexture autoFBForTex(context, scopedTex.Texture()); + if (!autoFBForTex.IsComplete()) { + LOGDMABUF(("GetAsSourceSurface: ScopedFramebufferForTexture failed.")); + return nullptr; + } + + const gl::OriginPos destOrigin = gl::OriginPos::BottomLeft; + { + const ScopedBindFramebuffer bindFB(context, autoFBForTex.FB()); + if (!context->BlitHelper()->Blit(this, size, destOrigin)) { + LOGDMABUF(("GetAsSourceSurface: Blit failed.")); + return nullptr; + } + } + + RefPtr source = + gfx::Factory::CreateDataSourceSurface(size, gfx::SurfaceFormat::B8G8R8A8); + if (NS_WARN_IF(!source)) { + LOGDMABUF(("GetAsSourceSurface: CreateDataSourceSurface failed.")); + return nullptr; + } + + ScopedBindFramebuffer bind(context, autoFBForTex.FB()); + ReadPixelsIntoDataSurface(context, source); + + return source.forget(); +} + +#if 0 +void DMABufSurfaceYUV::ClearPlane(int aPlane) { + if (!MapInternal(0, 0, mWidth[aPlane], mHeight[aPlane], nullptr, + GBM_BO_TRANSFER_WRITE, aPlane)) { + return; + } + if ((int)mMappedRegionStride[aPlane] < mWidth[aPlane]) { + return; + } + memset((char*)mMappedRegion[aPlane], 0, + mMappedRegionStride[aPlane] * mHeight[aPlane]); + Unmap(aPlane); +} + +# include "gfxUtils.h" + +void DMABufSurfaceYUV::DumpToFile(const char* aFile) { + RefPtr surf = GetAsSourceSurface(); + gfxUtils::WriteAsPNG(surf, aFile); +} +#endif diff --git a/widget/gtk/DMABufSurface.h b/widget/gtk/DMABufSurface.h new file mode 100644 index 0000000000..d11530ced3 --- /dev/null +++ b/widget/gtk/DMABufSurface.h @@ -0,0 +1,376 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DMABufSurface_h__ +#define DMABufSurface_h__ + +#include +#include "mozilla/widget/nsWaylandDisplay.h" +#include "mozilla/widget/va_drmcommon.h" +#include "GLTypes.h" + +typedef void* EGLImageKHR; +typedef void* EGLSyncKHR; + +#define DMABUF_BUFFER_PLANES 4 + +#ifndef VA_FOURCC_NV12 +# define VA_FOURCC_NV12 0x3231564E +#endif +#ifndef VA_FOURCC_YV12 +# define VA_FOURCC_YV12 0x32315659 +#endif +#ifndef VA_FOURCC_P010 +# define VA_FOURCC_P010 0x30313050 +#endif + +namespace mozilla { +namespace gfx { +class DataSourceSurface; +} +namespace layers { +class SurfaceDescriptor; +class SurfaceDescriptorDMABuf; +} // namespace layers +namespace gl { +class GLContext; +} +} // namespace mozilla + +typedef enum { + // Use alpha pixel format + DMABUF_ALPHA = 1 << 0, + // Surface is used as texture and may be also shared + DMABUF_TEXTURE = 1 << 1, + // Use modifiers. Such dmabuf surface may have more planes + // and complex internal structure (tiling/compression/etc.) + // so we can't do direct rendering to it. + DMABUF_USE_MODIFIERS = 1 << 3, +} DMABufSurfaceFlags; + +class DMABufSurfaceRGBA; +class DMABufSurfaceYUV; + +class DMABufSurface { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DMABufSurface) + + enum SurfaceType { + SURFACE_RGBA, + SURFACE_NV12, + SURFACE_YUV420, + }; + + // Import surface from SurfaceDescriptor. This is usually + // used to copy surface from another process over IPC. + // When a global reference counter was created for the surface + // (see bellow) it's automatically referenced. + static already_AddRefed CreateDMABufSurface( + const mozilla::layers::SurfaceDescriptor& aDesc); + + // Export surface to another process via. SurfaceDescriptor. + virtual bool Serialize( + mozilla::layers::SurfaceDescriptor& aOutDescriptor) = 0; + + virtual int GetWidth(int aPlane = 0) = 0; + virtual int GetHeight(int aPlane = 0) = 0; + virtual mozilla::gfx::SurfaceFormat GetFormat() = 0; + virtual mozilla::gfx::SurfaceFormat GetFormatGL() = 0; + + virtual bool CreateTexture(mozilla::gl::GLContext* aGLContext, + int aPlane = 0) = 0; + virtual void ReleaseTextures() = 0; + virtual GLuint GetTexture(int aPlane = 0) = 0; + virtual EGLImageKHR GetEGLImage(int aPlane = 0) = 0; + + SurfaceType GetSurfaceType() { return mSurfaceType; }; + virtual int GetTextureCount() = 0; + + bool IsMapped(int aPlane = 0) { return (mMappedRegion[aPlane] != nullptr); }; + void Unmap(int aPlane = 0); + + virtual DMABufSurfaceRGBA* GetAsDMABufSurfaceRGBA() { return nullptr; } + virtual DMABufSurfaceYUV* GetAsDMABufSurfaceYUV() { return nullptr; } + virtual already_AddRefed + GetAsSourceSurface() { + return nullptr; + } + + virtual mozilla::gfx::YUVColorSpace GetYUVColorSpace() { + return mozilla::gfx::YUVColorSpace::Default; + }; + + bool IsFullRange() { return mColorRange == mozilla::gfx::ColorRange::FULL; }; + void SetColorRange(mozilla::gfx::ColorRange aColorRange) { + mColorRange = aColorRange; + }; + + void FenceSet(); + void FenceWait(); + void FenceDelete(); + + // Set and get a global surface UID. The UID is shared across process + // and it's used to track surface lifetime in various parts of rendering + // engine. + uint32_t GetUID() const { return mUID; }; + + // Creates a global reference counter objects attached to the surface. + // It's created as unreferenced, i.e. IsGlobalRefSet() returns false + // right after GlobalRefCountCreate() call. + // + // The counter is shared by all surface instances across processes + // so it tracks global surface usage. + // + // The counter is automatically referenced when a new surface instance is + // created with SurfaceDescriptor (usually copied to another process over IPC) + // and it's unreferenced when surface is deleted. + // + // So without any additional GlobalRefAdd()/GlobalRefRelease() calls + // the IsGlobalRefSet() returns true if any other process use the surface. + void GlobalRefCountCreate(); + void GlobalRefCountDelete(); + + // If global reference counter was created by GlobalRefCountCreate() + // returns true when there's an active surface reference. + bool IsGlobalRefSet() const; + + // Add/Remove additional reference to the surface global reference counter. + void GlobalRefAdd(); + void GlobalRefRelease(); + + // Release all underlying data. + virtual void ReleaseSurface() = 0; + +#ifdef DEBUG + virtual void DumpToFile(const char* pFile){}; +#endif + + DMABufSurface(SurfaceType aSurfaceType); + + protected: + virtual bool Create(const mozilla::layers::SurfaceDescriptor& aDesc) = 0; + + // Import global ref count object from IPC by file descriptor. + // This adds global ref count reference to the surface. + void GlobalRefCountImport(int aFd); + // Export global ref count object by file descriptor. + int GlobalRefCountExport(); + + void ReleaseDMABuf(); + + void* MapInternal(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight, + uint32_t* aStride, int aGbmFlags, int aPlane = 0); + + // We want to keep number of opened file descriptors low so open/close + // DMABuf file handles only when we need them, i.e. when DMABuf is exported + // to another process or to EGL. + virtual bool OpenFileDescriptorForPlane( + const mozilla::MutexAutoLock& aProofOfLock, int aPlane) = 0; + virtual void CloseFileDescriptorForPlane( + const mozilla::MutexAutoLock& aProofOfLock, int aPlane, + bool aForceClose = false) = 0; + bool OpenFileDescriptors(const mozilla::MutexAutoLock& aProofOfLock); + void CloseFileDescriptors(const mozilla::MutexAutoLock& aProofOfLock, + bool aForceClose = false); + + virtual ~DMABufSurface(); + + SurfaceType mSurfaceType; + uint64_t mBufferModifiers[DMABUF_BUFFER_PLANES]; + + int mBufferPlaneCount; + int mDmabufFds[DMABUF_BUFFER_PLANES]; + int32_t mDrmFormats[DMABUF_BUFFER_PLANES]; + int32_t mStrides[DMABUF_BUFFER_PLANES]; + int32_t mOffsets[DMABUF_BUFFER_PLANES]; + + struct gbm_bo* mGbmBufferObject[DMABUF_BUFFER_PLANES]; + void* mMappedRegion[DMABUF_BUFFER_PLANES]; + void* mMappedRegionData[DMABUF_BUFFER_PLANES]; + uint32_t mMappedRegionStride[DMABUF_BUFFER_PLANES]; + + int mSyncFd; + EGLSyncKHR mSync; + RefPtr mGL; + + int mGlobalRefCountFd; + uint32_t mUID; + mozilla::Mutex mSurfaceLock MOZ_UNANNOTATED; + + mozilla::gfx::ColorRange mColorRange = mozilla::gfx::ColorRange::LIMITED; +}; + +class DMABufSurfaceRGBA : public DMABufSurface { + public: + static already_AddRefed CreateDMABufSurface( + int aWidth, int aHeight, int aDMABufSurfaceFlags); + + static already_AddRefed CreateDMABufSurface( + mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage, + int aWidth, int aHeight); + + bool Serialize(mozilla::layers::SurfaceDescriptor& aOutDescriptor); + + DMABufSurfaceRGBA* GetAsDMABufSurfaceRGBA() { return this; } + + void Clear(); + + void ReleaseSurface(); + + bool CopyFrom(class DMABufSurface* aSourceSurface); + + int GetWidth(int aPlane = 0) { return mWidth; }; + int GetHeight(int aPlane = 0) { return mHeight; }; + mozilla::gfx::SurfaceFormat GetFormat(); + mozilla::gfx::SurfaceFormat GetFormatGL(); + bool HasAlpha(); + + void* MapReadOnly(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight, + uint32_t* aStride = nullptr); + void* MapReadOnly(uint32_t* aStride = nullptr); + void* Map(uint32_t aX, uint32_t aY, uint32_t aWidth, uint32_t aHeight, + uint32_t* aStride = nullptr); + void* Map(uint32_t* aStride = nullptr); + void* GetMappedRegion(int aPlane = 0) { return mMappedRegion[aPlane]; }; + uint32_t GetMappedRegionStride(int aPlane = 0) { + return mMappedRegionStride[aPlane]; + }; + + bool CreateTexture(mozilla::gl::GLContext* aGLContext, int aPlane = 0); + void ReleaseTextures(); + GLuint GetTexture(int aPlane = 0) { return mTexture; }; + EGLImageKHR GetEGLImage(int aPlane = 0) { return mEGLImage; }; + + bool CreateWlBuffer(); + void ReleaseWlBuffer(); + wl_buffer* GetWlBuffer() { return mWlBuffer; }; + + int GetTextureCount() { return 1; }; + +#ifdef DEBUG + virtual void DumpToFile(const char* pFile); +#endif + + DMABufSurfaceRGBA(); + + private: + DMABufSurfaceRGBA(const DMABufSurfaceRGBA&) = delete; + DMABufSurfaceRGBA& operator=(const DMABufSurfaceRGBA&) = delete; + ~DMABufSurfaceRGBA(); + + bool Create(int aWidth, int aHeight, int aDMABufSurfaceFlags); + bool Create(const mozilla::layers::SurfaceDescriptor& aDesc); + bool Create(mozilla::gl::GLContext* aGLContext, const EGLImageKHR aEGLImage, + int aWidth, int aHeight); + + bool ImportSurfaceDescriptor(const mozilla::layers::SurfaceDescriptor& aDesc); + + bool OpenFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane); + void CloseFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane, bool aForceClose); + + private: + int mSurfaceFlags; + + int mWidth; + int mHeight; + mozilla::widget::GbmFormat* mGmbFormat; + + EGLImageKHR mEGLImage; + GLuint mTexture; + uint32_t mGbmBufferFlags; + wl_buffer* mWlBuffer; +}; + +class DMABufSurfaceYUV : public DMABufSurface { + public: + static already_AddRefed CreateYUVSurface( + int aWidth, int aHeight, void** aPixelData = nullptr, + int* aLineSizes = nullptr); + static already_AddRefed CreateYUVSurface( + const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, int aHeight); + static already_AddRefed CopyYUVSurface( + const VADRMPRIMESurfaceDescriptor& aVaDesc, int aWidth, int aHeight); + static void ReleaseVADRMPRIMESurfaceDescriptor( + VADRMPRIMESurfaceDescriptor& aDesc); + + bool Serialize(mozilla::layers::SurfaceDescriptor& aOutDescriptor); + + DMABufSurfaceYUV* GetAsDMABufSurfaceYUV() { return this; }; + already_AddRefed GetAsSourceSurface(); + + int GetWidth(int aPlane = 0) { return mWidth[aPlane]; } + int GetHeight(int aPlane = 0) { return mHeight[aPlane]; } + mozilla::gfx::SurfaceFormat GetFormat(); + mozilla::gfx::SurfaceFormat GetFormatGL(); + + bool CreateTexture(mozilla::gl::GLContext* aGLContext, int aPlane = 0); + void ReleaseTextures(); + + void ReleaseSurface(); + + GLuint GetTexture(int aPlane = 0) { return mTexture[aPlane]; }; + EGLImageKHR GetEGLImage(int aPlane = 0) { return mEGLImage[aPlane]; }; + + int GetTextureCount(); + + void SetYUVColorSpace(mozilla::gfx::YUVColorSpace aColorSpace) { + mColorSpace = aColorSpace; + } + mozilla::gfx::YUVColorSpace GetYUVColorSpace() { return mColorSpace; } + + DMABufSurfaceYUV(); + + bool UpdateYUVData(void** aPixelData, int* aLineSizes); + bool UpdateYUVData(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, + int aHeight, bool aCopy); + bool VerifyTextureCreation(); + + private: + DMABufSurfaceYUV(const DMABufSurfaceYUV&) = delete; + DMABufSurfaceYUV& operator=(const DMABufSurfaceYUV&) = delete; + ~DMABufSurfaceYUV(); + + bool Create(const mozilla::layers::SurfaceDescriptor& aDesc); + bool Create(int aWidth, int aHeight, void** aPixelData, int* aLineSizes); + bool CreateYUVPlane(int aPlane); + bool CreateLinearYUVPlane(int aPlane, int aWidth, int aHeight, + int aDrmFormat); + void UpdateYUVPlane(int aPlane, void* aPixelData, int aLineSize); + + bool MoveYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, + int aHeight); + bool CopyYUVDataImpl(const VADRMPRIMESurfaceDescriptor& aDesc, int aWidth, + int aHeight); + + bool ImportPRIMESurfaceDescriptor(const VADRMPRIMESurfaceDescriptor& aDesc, + int aWidth, int aHeight); + bool ImportSurfaceDescriptor( + const mozilla::layers::SurfaceDescriptorDMABuf& aDesc); + + bool OpenFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane); + void CloseFileDescriptorForPlane(const mozilla::MutexAutoLock& aProofOfLock, + int aPlane, bool aForceClose); + + bool CreateEGLImage(mozilla::gl::GLContext* aGLContext, int aPlane); + void ReleaseEGLImages(mozilla::gl::GLContext* aGLContext); + + int mWidth[DMABUF_BUFFER_PLANES]; + int mHeight[DMABUF_BUFFER_PLANES]; + // Aligned size of the surface imported from VADRMPRIMESurfaceDescriptor. + // It's used only internally to create EGLImage as some GL drivers + // needs that (Bug 1724385). + int mWidthAligned[DMABUF_BUFFER_PLANES]; + int mHeightAligned[DMABUF_BUFFER_PLANES]; + EGLImageKHR mEGLImage[DMABUF_BUFFER_PLANES]; + GLuint mTexture[DMABUF_BUFFER_PLANES]; + mozilla::gfx::YUVColorSpace mColorSpace = + mozilla::gfx::YUVColorSpace::Default; +}; + +#endif diff --git a/widget/gtk/GRefPtr.h b/widget/gtk/GRefPtr.h new file mode 100644 index 0000000000..ca3838758c --- /dev/null +++ b/widget/gtk/GRefPtr.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GRefPtr_h_ +#define GRefPtr_h_ + +// Allows to use RefPtr with various kinds of GObjects + +#include +#include +#include +#include "mozilla/RefPtr.h" + +#ifdef MOZ_ENABLE_DBUS +// TODO: Remove this (we should use GDBus instead, which is not deprecated). +# include +#endif + +namespace mozilla { + +template +struct GObjectRefPtrTraits { + static void AddRef(T* aObject) { g_object_ref(aObject); } + static void Release(T* aObject) { g_object_unref(aObject); } +}; + +#define GOBJECT_TRAITS(type_) \ + template <> \ + struct RefPtrTraits : public GObjectRefPtrTraits {}; + +GOBJECT_TRAITS(GtkWidget) +GOBJECT_TRAITS(GFile) +GOBJECT_TRAITS(GMenu) +GOBJECT_TRAITS(GMenuItem) +GOBJECT_TRAITS(GSimpleAction) +GOBJECT_TRAITS(GSimpleActionGroup) +GOBJECT_TRAITS(GDBusProxy) +GOBJECT_TRAITS(GAppInfo) +GOBJECT_TRAITS(GAppLaunchContext) +GOBJECT_TRAITS(GdkDragContext) +GOBJECT_TRAITS(GDBusMessage) +GOBJECT_TRAITS(GdkPixbuf) +GOBJECT_TRAITS(GCancellable) +GOBJECT_TRAITS(GtkIMContext) +GOBJECT_TRAITS(GUnixFDList) + +#ifdef MOZ_ENABLE_DBUS +GOBJECT_TRAITS(DBusGProxy) +#endif + +#undef GOBJECT_TRAITS + +template <> +struct RefPtrTraits { + static void AddRef(GVariant* aVariant) { g_variant_ref(aVariant); } + static void Release(GVariant* aVariant) { g_variant_unref(aVariant); } +}; + +template <> +struct RefPtrTraits { + static void AddRef(GHashTable* aObject) { g_hash_table_ref(aObject); } + static void Release(GHashTable* aObject) { g_hash_table_unref(aObject); } +}; + +template <> +struct RefPtrTraits { + static void AddRef(GDBusNodeInfo* aObject) { g_dbus_node_info_ref(aObject); } + static void Release(GDBusNodeInfo* aObject) { + g_dbus_node_info_unref(aObject); + } +}; + +#ifdef MOZ_ENABLE_DBUS +template <> +struct RefPtrTraits { + static void AddRef(DBusGConnection* aObject) { + dbus_g_connection_ref(aObject); + } + static void Release(DBusGConnection* aObject) { + dbus_g_connection_unref(aObject); + } +}; +#endif + +} // namespace mozilla + +#endif diff --git a/widget/gtk/GUniquePtr.h b/widget/gtk/GUniquePtr.h new file mode 100644 index 0000000000..0cd5de7b60 --- /dev/null +++ b/widget/gtk/GUniquePtr.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GUniquePtr_h_ +#define GUniquePtr_h_ + +// Provides GUniquePtr to g_free a given pointer. + +#include +#include +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +struct GFreeDeleter { + constexpr GFreeDeleter() = default; + void operator()(GdkEventCrossing* aPtr) const { + gdk_event_free((GdkEvent*)aPtr); + } + void operator()(GdkEventKey* aPtr) const { gdk_event_free((GdkEvent*)aPtr); } + void operator()(GdkEvent* aPtr) const { gdk_event_free(aPtr); } + void operator()(GError* aPtr) const { g_error_free(aPtr); } + void operator()(void* aPtr) const { g_free(aPtr); } + void operator()(GtkPaperSize* aPtr) const { gtk_paper_size_free(aPtr); } +}; + +template +using GUniquePtr = UniquePtr; + +} // namespace mozilla + +#endif diff --git a/widget/gtk/GfxInfo.cpp b/widget/gtk/GfxInfo.cpp new file mode 100644 index 0000000000..7382b066d1 --- /dev/null +++ b/widget/gtk/GfxInfo.cpp @@ -0,0 +1,1403 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=8 et : + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GfxInfo.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mozilla/gfx/Logging.h" +#include "mozilla/SSE.h" +#include "mozilla/Telemetry.h" +#include "mozilla/XREAppData.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/StaticPrefs_media.h" +#include "nsCRTGlue.h" +#include "nsExceptionHandler.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "nsStringFwd.h" +#include "nsUnicharUtils.h" +#include "nsWhitespaceTokenizer.h" +#include "prenv.h" +#include "WidgetUtilsGtk.h" +#include "MediaCodecsSupport.h" +#include "nsAppRunner.h" + +// How long we wait for data from glxtest/vaapi test process in milliseconds. +#define GFX_TEST_TIMEOUT 4000 +#define VAAPI_TEST_TIMEOUT 2000 + +#define GLX_PROBE_BINARY u"glxtest"_ns +#define VAAPI_PROBE_BINARY u"vaapitest"_ns + +namespace mozilla::widget { + +#ifdef DEBUG +NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug) +#endif + +int GfxInfo::sGLXTestPipe = -1; +pid_t GfxInfo::sGLXTestPID = 0; + +// bits to use decoding codec information returned from glxtest +constexpr int CODEC_HW_H264 = 1 << 4; +constexpr int CODEC_HW_VP8 = 1 << 5; +constexpr int CODEC_HW_VP9 = 1 << 6; +constexpr int CODEC_HW_AV1 = 1 << 7; + +nsresult GfxInfo::Init() { + mGLMajorVersion = 0; + mGLMinorVersion = 0; + mHasTextureFromPixmap = false; + mIsMesa = false; + mIsAccelerated = true; + mIsWayland = false; + mIsXWayland = false; + mHasMultipleGPUs = false; + mGlxTestError = false; + return GfxInfoBase::Init(); +} + +void GfxInfo::AddCrashReportAnnotations() { + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterVendorID, + mVendorId); + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::AdapterDeviceID, + mDeviceId); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AdapterDriverVendor, mDriverVendor); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::AdapterDriverVersion, mDriverVersion); + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::IsWayland, + mIsWayland); + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::DesktopEnvironment, + GetDesktopEnvironmentIdentifier()); + + if (mHasMultipleGPUs) { + nsAutoCString note; + note.AppendLiteral("Has dual GPUs."); + if (!mSecondaryVendorId.IsEmpty()) { + note.AppendLiteral(" GPU #2: AdapterVendorID2: "); + note.Append(mSecondaryVendorId); + note.AppendLiteral(", AdapterDeviceID2: "); + note.Append(mSecondaryDeviceId); + } + CrashReporter::AppendAppNotesToCrashReport(note); + } +} + +static bool MakeFdNonBlocking(int fd) { + return fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) != -1; +} + +static bool ManageChildProcess(const char* aProcessName, int* aPID, int* aPipe, + int aTimeout, char** aData) { + // Don't try anything if we failed before + if (*aPID < 0) { + return false; + } + + GIOChannel* channel = nullptr; + *aData = nullptr; + + auto free = mozilla::MakeScopeExit([&] { + if (channel) { + g_io_channel_unref(channel); + } + if (*aPipe >= 0) { + close(*aPipe); + *aPipe = -1; + } + }); + + const TimeStamp deadline = + TimeStamp::Now() + TimeDuration::FromMilliseconds(aTimeout); + + struct pollfd pfd {}; + pfd.fd = *aPipe; + pfd.events = POLLIN; + + while (poll(&pfd, 1, aTimeout) != 1) { + if (errno != EAGAIN && errno != EINTR) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): poll failed: " << strerror(errno) << "\n"; + return false; + } + if (TimeStamp::Now() > deadline) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): process hangs\n"; + return false; + } + } + + channel = g_io_channel_unix_new(*aPipe); + MakeFdNonBlocking(*aPipe); + + GUniquePtr error; + gsize length = 0; + int ret; + do { + error = nullptr; + ret = g_io_channel_read_to_end(channel, aData, &length, + getter_Transfers(error)); + } while (ret == G_IO_STATUS_AGAIN && TimeStamp::Now() < deadline); + if (error || ret != G_IO_STATUS_NORMAL) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): failed to read data from child process: "; + if (error) { + gfxCriticalNote << error->message; + } else { + gfxCriticalNote << "timeout"; + } + return false; + } + + int status = 0; + int pid = *aPID; + *aPID = -1; + + while (true) { + int ret = waitpid(pid, &status, WNOHANG); + if (ret > 0) { + break; + } + if (ret < 0) { + if (errno == ECHILD) { + // Bug 718629 + // ECHILD happens when the glxtest process got reaped got reaped after a + // PR_CreateProcess as per bug 227246. This shouldn't matter, as we + // still seem to get the data from the pipe, and if we didn't, the + // outcome would be to blocklist anyway. + return true; + } + if (errno != EAGAIN && errno != EINTR) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): waitpid failed: " << strerror(errno) << "\n"; + return false; + } + } + if (TimeStamp::Now() > deadline) { + gfxCriticalNote << "ManageChildProcess(" << aProcessName + << "): process hangs\n"; + return false; + } + // Wait 50ms to another waitpid() check. + usleep(50000); + } + + return WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS; +} + +// to understand this function, see bug 639842. We retrieve the OpenGL driver +// information in a separate process to protect against bad drivers. +void GfxInfo::GetData() { + if (mInitialized) { + return; + } + mInitialized = true; + + // In some cases (xpcshell test, Profile manager etc.) + // FireGLXTestProcess() is not fired in advance + // so we call it here. + GfxInfo::FireGLXTestProcess(); + + GfxInfoBase::GetData(); + + char* glxData = nullptr; + auto free = mozilla::MakeScopeExit([&] { g_free((void*)glxData); }); + + bool error = !ManageChildProcess("glxtest", &sGLXTestPID, &sGLXTestPipe, + GFX_TEST_TIMEOUT, &glxData); + if (error) { + gfxCriticalNote << "glxtest: ManageChildProcess failed\n"; + } + + nsCString glVendor; + nsCString glRenderer; + nsCString glVersion; + nsCString textureFromPixmap; + nsCString testType; + + // Available if GLX_MESA_query_renderer is supported. + nsCString mesaVendor; + nsCString mesaDevice; + nsCString mesaAccelerated; + // Available if using a DRI-based libGL stack. + nsCString driDriver; + nsCString adapterRam; + + nsCString drmRenderDevice; + + nsCString ddxDriver; + + AutoTArray pciVendors; + AutoTArray pciDevices; + + nsCString* stringToFill = nullptr; + bool logString = false; + bool errorLog = false; + + char* bufptr = glxData; + + while (true) { + char* line = NS_strtok("\n", &bufptr); + if (!line) break; + if (stringToFill) { + stringToFill->Assign(line); + stringToFill = nullptr; + } else if (logString) { + gfxCriticalNote << "glxtest: " << line; + logString = false; + } else if (!strcmp(line, "VENDOR")) { + stringToFill = &glVendor; + } else if (!strcmp(line, "RENDERER")) { + stringToFill = &glRenderer; + } else if (!strcmp(line, "VERSION")) { + stringToFill = &glVersion; + } else if (!strcmp(line, "TFP")) { + stringToFill = &textureFromPixmap; + } else if (!strcmp(line, "MESA_VENDOR_ID")) { + stringToFill = &mesaVendor; + } else if (!strcmp(line, "MESA_DEVICE_ID")) { + stringToFill = &mesaDevice; + } else if (!strcmp(line, "MESA_ACCELERATED")) { + stringToFill = &mesaAccelerated; + } else if (!strcmp(line, "MESA_VRAM")) { + stringToFill = &adapterRam; + } else if (!strcmp(line, "DDX_DRIVER")) { + stringToFill = &ddxDriver; + } else if (!strcmp(line, "DRI_DRIVER")) { + stringToFill = &driDriver; + } else if (!strcmp(line, "PCI_VENDOR_ID")) { + stringToFill = pciVendors.AppendElement(); + } else if (!strcmp(line, "PCI_DEVICE_ID")) { + stringToFill = pciDevices.AppendElement(); + } else if (!strcmp(line, "DRM_RENDERDEVICE")) { + stringToFill = &drmRenderDevice; + } else if (!strcmp(line, "TEST_TYPE")) { + stringToFill = &testType; + } else if (!strcmp(line, "WARNING")) { + logString = true; + } else if (!strcmp(line, "ERROR")) { + logString = true; + errorLog = true; + } + } + + MOZ_ASSERT(pciDevices.Length() == pciVendors.Length(), + "Missing PCI vendors/devices"); + + size_t pciLen = std::min(pciVendors.Length(), pciDevices.Length()); + mHasMultipleGPUs = pciLen > 1; + + if (!strcmp(textureFromPixmap.get(), "TRUE")) mHasTextureFromPixmap = true; + + // only useful for Linux kernel version check for FGLRX driver. + // assumes X client == X server, which is sad. + struct utsname unameobj {}; + if (uname(&unameobj) >= 0) { + mOS.Assign(unameobj.sysname); + mOSRelease.Assign(unameobj.release); + } + + const char* spoofedVendor = PR_GetEnv("MOZ_GFX_SPOOF_GL_VENDOR"); + if (spoofedVendor) glVendor.Assign(spoofedVendor); + const char* spoofedRenderer = PR_GetEnv("MOZ_GFX_SPOOF_GL_RENDERER"); + if (spoofedRenderer) glRenderer.Assign(spoofedRenderer); + const char* spoofedVersion = PR_GetEnv("MOZ_GFX_SPOOF_GL_VERSION"); + if (spoofedVersion) glVersion.Assign(spoofedVersion); + const char* spoofedOS = PR_GetEnv("MOZ_GFX_SPOOF_OS"); + if (spoofedOS) mOS.Assign(spoofedOS); + const char* spoofedOSRelease = PR_GetEnv("MOZ_GFX_SPOOF_OS_RELEASE"); + if (spoofedOSRelease) mOSRelease.Assign(spoofedOSRelease); + + // Scan the GL_VERSION string for the GL and driver versions. + nsCWhitespaceTokenizer tokenizer(glVersion); + while (tokenizer.hasMoreTokens()) { + nsCString token(tokenizer.nextToken()); + unsigned int major = 0, minor = 0, revision = 0, patch = 0; + if (sscanf(token.get(), "%u.%u.%u.%u", &major, &minor, &revision, &patch) >= + 2) { + // A survey of GL_VENDOR strings indicates that the first version is + // always the GL version, the second is usually the driver version. + if (mGLMajorVersion == 0) { + mGLMajorVersion = major; + mGLMinorVersion = minor; + } else if (mDriverVersion.IsEmpty()) { // Not already spoofed. + mDriverVersion = + nsPrintfCString("%u.%u.%u.%u", major, minor, revision, patch); + } + } + } + + if (mGLMajorVersion == 0) { + NS_WARNING("Failed to parse GL version!"); + } + + mDrmRenderDevice = std::move(drmRenderDevice); + mTestType = std::move(testType); + + // Mesa always exposes itself in the GL_VERSION string, but not always the + // GL_VENDOR string. + mIsMesa = glVersion.Find("Mesa") != -1; + + // We need to use custom driver vendor IDs for mesa so we can treat them + // differently than the proprietary drivers. + if (mIsMesa) { + mIsAccelerated = !mesaAccelerated.Equals("FALSE"); + // Process software rasterizers before the DRI driver string; we may be + // forcing software rasterization on a DRI-accelerated X server by using + // LIBGL_ALWAYS_SOFTWARE or a similar restriction. + if (strcasestr(glRenderer.get(), "llvmpipe")) { + CopyUTF16toUTF8( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaLLVMPipe), + mDriverVendor); + mIsAccelerated = false; + } else if (strcasestr(glRenderer.get(), "softpipe")) { + CopyUTF16toUTF8( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSoftPipe), + mDriverVendor); + mIsAccelerated = false; + } else if (strcasestr(glRenderer.get(), "software rasterizer")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSWRast), + mDriverVendor); + mIsAccelerated = false; + } else if (strcasestr(driDriver.get(), "vmwgfx")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaVM), + mDriverVendor); + mIsAccelerated = false; + } else if (!mIsAccelerated) { + CopyUTF16toUTF8( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaSWUnknown), + mDriverVendor); + } else if (!driDriver.IsEmpty()) { + mDriverVendor = nsPrintfCString("mesa/%s", driDriver.get()); + } else { + // Some other mesa configuration where we couldn't get enough info. + NS_WARNING("Failed to detect Mesa driver being used!"); + CopyUTF16toUTF8(GfxDriverInfo::GetDriverVendor(DriverVendor::MesaUnknown), + mDriverVendor); + } + + if (!mesaVendor.IsEmpty()) { + mVendorId = mesaVendor; + } + + if (!mesaDevice.IsEmpty()) { + mDeviceId = mesaDevice; + } + + if (!mIsAccelerated && mVendorId.IsEmpty()) { + mVendorId.Assign(glVendor.get()); + } + + if (!mIsAccelerated && mDeviceId.IsEmpty()) { + mDeviceId.Assign(glRenderer.get()); + } + } else if (glVendor.EqualsLiteral("NVIDIA Corporation")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA), + mVendorId); + mDriverVendor.AssignLiteral("nvidia/unknown"); + // TODO: Use NV-CONTROL X11 extension to query Device ID and VRAM. + } else if (glVendor.EqualsLiteral("ATI Technologies Inc.")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI), + mVendorId); + mDriverVendor.AssignLiteral("ati/unknown"); + // TODO: Look into ways to find the device ID on FGLRX. + } else { + NS_WARNING("Failed to detect GL vendor!"); + } + + if (!adapterRam.IsEmpty()) { + mAdapterRAM = (uint32_t)atoi(adapterRam.get()); + } + + // If we have the DRI driver, we can derive the vendor ID from that if needed. + if (mVendorId.IsEmpty() && !driDriver.IsEmpty()) { + const char* nvidiaDrivers[] = {"nouveau", "tegra", nullptr}; + for (size_t i = 0; nvidiaDrivers[i]; ++i) { + if (driDriver.Equals(nvidiaDrivers[i])) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::NVIDIA), + mVendorId); + break; + } + } + + if (mVendorId.IsEmpty()) { + const char* intelDrivers[] = {"iris", "crocus", "i915", "i965", + "i810", "intel", nullptr}; + for (size_t i = 0; intelDrivers[i]; ++i) { + if (driDriver.Equals(intelDrivers[i])) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::Intel), + mVendorId); + break; + } + } + } + + if (mVendorId.IsEmpty()) { + const char* amdDrivers[] = {"r600", "r200", "r100", + "radeon", "radeonsi", nullptr}; + for (size_t i = 0; amdDrivers[i]; ++i) { + if (driDriver.Equals(amdDrivers[i])) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::ATI), + mVendorId); + break; + } + } + } + + if (mVendorId.IsEmpty()) { + if (driDriver.EqualsLiteral("freedreno")) { + CopyUTF16toUTF8(GfxDriverInfo::GetDeviceVendor(DeviceVendor::Qualcomm), + mVendorId); + } + } + } + + // If we still don't have a vendor ID, we can try the PCI vendor list. + if (mVendorId.IsEmpty()) { + if (pciVendors.IsEmpty()) { + gfxCriticalNote << "No GPUs detected via PCI\n"; + } else { + for (size_t i = 0; i < pciVendors.Length(); ++i) { + if (mVendorId.IsEmpty()) { + mVendorId = pciVendors[i]; + } else if (mVendorId != pciVendors[i]) { + gfxCriticalNote << "More than 1 GPU vendor detected via PCI, cannot " + "deduce vendor\n"; + mVendorId.Truncate(); + break; + } + } + } + } + + // If we know the vendor ID, but didn't get a device ID, we can guess from the + // PCI device list. + if (mDeviceId.IsEmpty() && !mVendorId.IsEmpty()) { + for (size_t i = 0; i < pciLen; ++i) { + if (mVendorId.Equals(pciVendors[i])) { + if (mDeviceId.IsEmpty()) { + mDeviceId = pciDevices[i]; + } else if (mDeviceId != pciDevices[i]) { + gfxCriticalNote << "More than 1 GPU from same vendor detected via " + "PCI, cannot deduce device\n"; + mDeviceId.Truncate(); + break; + } + } + } + } + + // Assuming we know the vendor, we should check for a secondary card. + if (!mVendorId.IsEmpty()) { + if (pciLen > 2) { + gfxCriticalNote + << "More than 2 GPUs detected via PCI, secondary GPU is arbitrary\n"; + } + for (size_t i = 0; i < pciLen; ++i) { + if (!mVendorId.Equals(pciVendors[i]) || + (!mDeviceId.IsEmpty() && !mDeviceId.Equals(pciDevices[i]))) { + mSecondaryVendorId = pciVendors[i]; + mSecondaryDeviceId = pciDevices[i]; + break; + } + } + } + + // If we couldn't choose, log them. + if (mVendorId.IsEmpty()) { + for (size_t i = 0; i < pciLen; ++i) { + gfxCriticalNote << "PCI candidate " << pciVendors[i].get() << "/" + << pciDevices[i].get() << "\n"; + } + } + + // Fallback to GL_VENDOR and GL_RENDERER. + if (mVendorId.IsEmpty()) { + mVendorId.Assign(glVendor.get()); + } + if (mDeviceId.IsEmpty()) { + mDeviceId.Assign(glRenderer.get()); + } + + mAdapterDescription.Assign(glRenderer); + + // Make a best effort guess at whether or not we are using the XWayland compat + // layer. For all intents and purposes, we should otherwise believe we are + // using X11. + mIsWayland = GdkIsWaylandDisplay(); + mIsXWayland = IsXWaylandProtocol(); + + if (!ddxDriver.IsEmpty()) { + PRInt32 start = 0; + PRInt32 loc = ddxDriver.Find(";", start); + while (loc != kNotFound) { + nsCString line(ddxDriver.get() + start, loc - start); + mDdxDrivers.AppendElement(std::move(line)); + + start = loc + 1; + loc = ddxDriver.Find(";", start); + } + } + + if (error || errorLog || mTestType.IsEmpty()) { + if (!mAdapterDescription.IsEmpty()) { + mAdapterDescription.AppendLiteral(" (See failure log)"); + } else { + mAdapterDescription.AppendLiteral("See failure log"); + } + + mGlxTestError = true; + } + + AddCrashReportAnnotations(); +} + +int GfxInfo::FireTestProcess(const nsAString& aBinaryFile, int* aOutPipe, + const char** aStringArgs) { + nsCOMPtr appFile; + nsresult rv = XRE_GetBinaryPath(getter_AddRefs(appFile)); + if (NS_FAILED(rv)) { + gfxCriticalNote << "Couldn't find application file.\n"; + return false; + } + nsCOMPtr exePath; + rv = appFile->GetParent(getter_AddRefs(exePath)); + if (NS_FAILED(rv)) { + gfxCriticalNote << "Couldn't get application directory.\n"; + return false; + } + exePath->Append(aBinaryFile); + +#define MAX_ARGS 8 + char* argv[MAX_ARGS + 2]; + + argv[0] = strdup(exePath->NativePath().get()); + for (int i = 0; i < MAX_ARGS; i++) { + if (aStringArgs[i]) { + argv[i + 1] = strdup(aStringArgs[i]); + } else { + argv[i + 1] = nullptr; + break; + } + } + + // Use G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_DO_NOT_REAP_CHILD flags + // to g_spawn_async_with_pipes() run posix_spawn() directly. + int pid; + GUniquePtr err; + g_spawn_async_with_pipes( + nullptr, argv, nullptr, + GSpawnFlags(G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_DO_NOT_REAP_CHILD), + nullptr, nullptr, &pid, nullptr, aOutPipe, nullptr, + getter_Transfers(err)); + if (err) { + gfxCriticalNote << "FireTestProcess failed: " << err->message << "\n"; + pid = 0; + } + for (auto& arg : argv) { + if (!arg) { + break; + } + free(arg); + } + return pid; +} + +bool GfxInfo::FireGLXTestProcess() { + if (sGLXTestPID != 0) { + return true; + } + + int pfd[2]; + if (pipe(pfd) == -1) { + gfxCriticalNote << "FireGLXTestProcess failed to create pipe\n"; + return false; + } + sGLXTestPipe = pfd[0]; + + auto pipeID = std::to_string(pfd[1]); + const char* args[] = {"-f", pipeID.c_str(), + IsWaylandEnabled() ? "-w" : nullptr, nullptr}; + sGLXTestPID = FireTestProcess(GLX_PROBE_BINARY, nullptr, args); + // Set pid to -1 to avoid further test launch. + if (!sGLXTestPID) { + sGLXTestPID = -1; + } + close(pfd[1]); + return true; +} + +void GfxInfo::GetDataVAAPI() { + if (mIsVAAPISupported.isSome()) { + return; + } + mIsVAAPISupported = Some(false); + +#ifdef MOZ_ENABLE_VAAPI + char* vaapiData = nullptr; + auto free = mozilla::MakeScopeExit([&] { g_free((void*)vaapiData); }); + + int vaapiPipe = -1; + int vaapiPID = 0; + const char* args[] = {"-d", mDrmRenderDevice.get(), nullptr}; + vaapiPID = FireTestProcess(VAAPI_PROBE_BINARY, &vaapiPipe, args); + if (!vaapiPID) { + return; + } + + if (!ManageChildProcess("vaapitest", &vaapiPID, &vaapiPipe, + VAAPI_TEST_TIMEOUT, &vaapiData)) { + gfxCriticalNote << "vaapitest: ManageChildProcess failed\n"; + return; + } + + char* bufptr = vaapiData; + char* line; + while ((line = NS_strtok("\n", &bufptr))) { + if (!strcmp(line, "VAAPI_SUPPORTED")) { + line = NS_strtok("\n", &bufptr); + if (!line) { + gfxCriticalNote << "vaapitest: Failed to get VAAPI support\n"; + return; + } + mIsVAAPISupported = Some(!strcmp(line, "TRUE")); + } else if (!strcmp(line, "VAAPI_HWCODECS")) { + line = NS_strtok("\n", &bufptr); + if (!line) { + gfxCriticalNote << "vaapitest: Failed to get VAAPI codecs\n"; + return; + } + + std::istringstream(line) >> mVAAPISupportedCodecs; + if (mVAAPISupportedCodecs & CODEC_HW_H264) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::H264HardwareDecode); + } + if (mVAAPISupportedCodecs & CODEC_HW_VP8) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::VP8HardwareDecode); + } + if (mVAAPISupportedCodecs & CODEC_HW_VP9) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::VP9HardwareDecode); + } + if (mVAAPISupportedCodecs & CODEC_HW_AV1) { + media::MCSInfo::AddSupport( + media::MediaCodecsSupport::AV1HardwareDecode); + } + } else if (!strcmp(line, "WARNING") || !strcmp(line, "ERROR")) { + gfxCriticalNote << "vaapitest: " << line; + line = NS_strtok("\n", &bufptr); + if (line) { + gfxCriticalNote << "vaapitest: " << line << "\n"; + } + return; + } + } +#endif +} + +const nsTArray& GfxInfo::GetGfxDriverInfo() { + if (!sDriverInfo->Length()) { + // Mesa 10.0 provides the GLX_MESA_query_renderer extension, which allows us + // to query device IDs backing a GL context for blocklisting. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(10, 0, 0, 0), "FEATURE_FAILURE_OLD_MESA", + "Mesa 10.0"); + + // NVIDIA Mesa baseline (see bug 1714391). + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaNouveau, DeviceFamily::All, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(11, 0, 0, 0), "FEATURE_FAILURE_OLD_NV_MESA", + "Mesa 11.0"); + + // NVIDIA baseline (ported from old blocklist) + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(257, 21, 0, 0), "FEATURE_FAILURE_OLD_NVIDIA", + "NVIDIA 257.21"); + + // fglrx baseline (chosen arbitrarily as 2013-07-22 release). + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::AtiAll, + GfxDriverInfo::allFeatures, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(13, 15, 100, 1), "FEATURE_FAILURE_OLD_FGLRX", + "fglrx 13.15.100.1"); + + //////////////////////////////////// + // FEATURE_WEBRENDER + + // All Mesa software drivers, they should get Software WebRender instead. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::SoftwareMesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_SOFTWARE_GL", + ""); + + // Older generation Intel devices do not perform well with WebRender. + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::IntelWebRenderBlocked, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "INTEL_DEVICE_GEN5_OR_OLDER", + ""); + + // Nvidia Mesa baseline, see bug 1563859. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(18, 2, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA", "Mesa 18.2.0.0"); + + // Disable on all older Nvidia drivers due to stability issues. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_LESS_THAN, V(470, 82, 0, 0), + "FEATURE_FAILURE_WEBRENDER_OLD_NVIDIA", "470.82.0"); + + // Older generation NVIDIA devices do not perform well with WebRender. + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::NvidiaWebRenderBlocked, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "NVIDIA_EARLY_TESLA_AND_C67_C68", ""); + + // Mesa baseline, chosen arbitrarily. Linux users are generally good about + // updating their Mesa libraries so we don't want to arbitarily support + // WebRender on old drivers with outstanding bugs to work around. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(17, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA", "Mesa 17.0.0.0"); + + // Mesa baseline for non-Intel/NVIDIA/ATI devices. These other devices will + // often have less mature drivers so let's block older Mesa versions. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaNonIntelNvidiaAtiAll, + DeviceFamily::All, nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(22, 2, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA_OTHER", + "Mesa 22.2.0.0"); + + // Bug 1690568 / Bug 1393793 - Require Mesa 17.3.0+ for devices using the + // AMD r600 driver to avoid shader compilation issues. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaR600, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(17, 3, 0, 0), "FEATURE_FAILURE_WEBRENDER_OLD_MESA_R600", + "Mesa 17.3.0.0"); + + // Disable on all ATI devices not using Mesa for now. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_WEBRENDER_NO_LINUX_ATI", ""); + + // Disable R600 GPUs with Mesa drivers. + // Bug 1673939 - Garbled text on RS880 GPUs with Mesa drivers. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AmdR600, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_WEBRENDER_BUG_1673939", + "https://gitlab.freedesktop.org/mesa/mesa/-/issues/3720"); + + // Bug 1635186 - Poor performance with video playing in a background window + // on XWayland. Keep in sync with FEATURE_X11_EGL below to only enable them + // together by default. Only Mesa and Nvidia binary drivers are expected + // on Wayland rigth now. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::XWayland, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(21, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_BUG_1635186", + "Mesa 21.0.0.0"); + + // Bug 1815481 - Disable mesa drivers in virtual machines. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaVM, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_WEBRENDER_MESA_VM", ""); + // Disable hardware mesa drivers in virtual machines due to instability. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaVM, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBGL_USE_HARDWARE, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_WEBGL_MESA_VM", ""); + + //////////////////////////////////// + // FEATURE_WEBRENDER_COMPOSITOR + APPEND_TO_DRIVER_BLOCKLIST( + OperatingSystem::Linux, DeviceFamily::All, + nsIGfxInfo::FEATURE_WEBRENDER_COMPOSITOR, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_WEBRENDER_COMPOSITOR_DISABLED", ""); + + //////////////////////////////////// + // FEATURE_X11_EGL + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(17, 0, 0, 0), "FEATURE_X11_EGL_OLD_MESA", + "Mesa 17.0.0.0"); + + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(18, 2, 0, 0), "FEATURE_X11_EGL_OLD_MESA_NOUVEAU", + "Mesa 18.2.0.0"); + + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_LESS_THAN, V(470, 82, 0, 0), + "FEATURE_ROLLOUT_X11_EGL_NVIDIA_BINARY", "470.82.0"); + + // Disable on all AMD devices not using Mesa. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_X11_EGL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_X11_EGL_NO_LINUX_ATI", ""); + + //////////////////////////////////// + // FEATURE_DMABUF + // Disabled due to high volume crash tracked in bug 1788573. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_DMABUF, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1788573", + ""); + + //////////////////////////////////// + // FEATURE_DMABUF_SURFACE_EXPORT + // Disabled due to: + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6666 + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6796 + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", ""); + + // Disabled due to: + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6688 + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::IntelAll, + nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", ""); + + // Disabled due to: + // https://gitlab.freedesktop.org/mesa/mesa/-/issues/6988 + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::QualcommAll, + nsIGfxInfo::FEATURE_DMABUF_SURFACE_EXPORT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_FAILURE_BROKEN_DRIVER", ""); + + //////////////////////////////////// + // FEATURE_HARDWARE_VIDEO_DECODING + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::All, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_LESS_THAN, + V(21, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_MESA", + "Mesa 21.0.0.0"); + + // Disable on all NVIDIA hardware + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::All, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_LINUX_NVIDIA", ""); + + // Disable on all AMD devices not using Mesa. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_LINUX_AMD", ""); + + // Disable on r600 driver due to decoding artifacts (Bug 1824307) + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaR600, DeviceFamily::All, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_DECODING_NO_R600", ""); + + // Disable on Release/late Beta on AMD +#if !defined(EARLY_BETA_OR_EARLIER) + APPEND_TO_DRIVER_BLOCKLIST(OperatingSystem::Linux, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_HARDWARE_VIDEO_DECODING_DISABLE", ""); +#endif + //////////////////////////////////// + // FEATURE_HW_DECODED_VIDEO_ZERO_COPY - ALLOWLIST + APPEND_TO_DRIVER_BLOCKLIST2(OperatingSystem::Linux, DeviceFamily::All, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_ALLOW_ALWAYS, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_ROLLOUT_ALL"); + + // Disable on all AMD devices using Mesa (Bug 1802844). + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaAll, DeviceFamily::AtiAll, + nsIGfxInfo::FEATURE_HW_DECODED_VIDEO_ZERO_COPY, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_HARDWARE_VIDEO_ZERO_COPY_LINUX_AMD_DISABLE", + ""); + + //////////////////////////////////// + // FEATURE_WEBRENDER_PARTIAL_PRESENT + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::X11, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_WEBRENDER_PARTIAL_PRESENT, + nsIGfxInfo::FEATURE_BLOCKED_DEVICE, DRIVER_COMPARISON_IGNORED, + V(0, 0, 0, 0), "FEATURE_ROLLOUT_WR_PARTIAL_PRESENT_NVIDIA_BINARY", ""); + + //////////////////////////////////// + + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::MesaNouveau, DeviceFamily::All, + nsIGfxInfo::FEATURE_THREADSAFE_GL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), + "FEATURE_FAILURE_THREADSAFE_GL_NOUVEAU", ""); + + // Disabled due to high volume crash tracked in bug 1788573. + APPEND_TO_DRIVER_BLOCKLIST_EXT( + OperatingSystem::Linux, ScreenSizeStatus::All, BatteryStatus::All, + WindowProtocol::All, DriverVendor::NonMesaAll, DeviceFamily::NvidiaAll, + nsIGfxInfo::FEATURE_THREADSAFE_GL, nsIGfxInfo::FEATURE_BLOCKED_DEVICE, + DRIVER_COMPARISON_IGNORED, V(0, 0, 0, 0), "FEATURE_FAILURE_BUG_1788573", + ""); + } + return *sDriverInfo; +} + +bool GfxInfo::DoesWindowProtocolMatch(const nsAString& aBlocklistWindowProtocol, + const nsAString& aWindowProtocol) { + if (mIsWayland && + aBlocklistWindowProtocol.Equals( + GfxDriverInfo::GetWindowProtocol(WindowProtocol::WaylandAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (!mIsWayland && + aBlocklistWindowProtocol.Equals( + GfxDriverInfo::GetWindowProtocol(WindowProtocol::X11All), + nsCaseInsensitiveStringComparator)) { + return true; + } + return GfxInfoBase::DoesWindowProtocolMatch(aBlocklistWindowProtocol, + aWindowProtocol); +} + +bool GfxInfo::DoesDriverVendorMatch(const nsAString& aBlocklistVendor, + const nsAString& aDriverVendor) { + if (mIsMesa) { + if (aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::MesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (mIsAccelerated && + aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::HardwareMesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (!mIsAccelerated && + aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::SoftwareMesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + if (aBlocklistVendor.Equals(GfxDriverInfo::GetDriverVendor( + DriverVendor::MesaNonIntelNvidiaAtiAll), + nsCaseInsensitiveStringComparator)) { + return !mVendorId.Equals("0x8086") && !mVendorId.Equals("0x10de") && + !mVendorId.Equals("0x1002"); + } + } + if (!mIsMesa && aBlocklistVendor.Equals( + GfxDriverInfo::GetDriverVendor(DriverVendor::NonMesaAll), + nsCaseInsensitiveStringComparator)) { + return true; + } + return GfxInfoBase::DoesDriverVendorMatch(aBlocklistVendor, aDriverVendor); +} + +nsresult GfxInfo::GetFeatureStatusImpl( + int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion, + const nsTArray& aDriverInfo, nsACString& aFailureId, + OperatingSystem* aOS /* = nullptr */) + +{ + NS_ENSURE_ARG_POINTER(aStatus); + *aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN; + aSuggestedDriverVersion.SetIsVoid(true); + OperatingSystem os = OperatingSystem::Linux; + if (aOS) *aOS = os; + + if (sShutdownOccurred) { + return NS_OK; + } + + GetData(); + + if (aFeature == nsIGfxInfo::FEATURE_BACKDROP_FILTER) { + *aStatus = nsIGfxInfo::FEATURE_STATUS_OK; + return NS_OK; + } + + if (mGlxTestError) { + // If glxtest failed, block all features by default. + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_GLXTEST_FAILED"; + return NS_OK; + } + + if (mGLMajorVersion == 1) { + // We're on OpenGL 1. In most cases that indicates really old hardware. + // We better block them, rather than rely on them to fail gracefully, + // because they don't! see bug 696636 + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_OPENGL_1"; + return NS_OK; + } + + // Blocklist software GL implementations from using layers acceleration. + // On the test infrastructure, we'll force-enable layers acceleration. + if (aFeature == nsIGfxInfo::FEATURE_OPENGL_LAYERS && !mIsAccelerated && + !PR_GetEnv("MOZ_LAYERS_ALLOW_SOFTWARE_GL")) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_SOFTWARE_GL"; + return NS_OK; + } + + if (aFeature == nsIGfxInfo::FEATURE_WEBRENDER) { + // Don't try Webrender on devices where we are guaranteed to fail. + if (mGLMajorVersion < 3) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_OPENGL_LESS_THAN_3"; + return NS_OK; + } + + // Bug 1710400: Disable Webrender on the deprecated Intel DDX driver + for (const nsCString& driver : mDdxDrivers) { + if (strcasestr(driver.get(), "Intel")) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_DEVICE; + aFailureId = "FEATURE_FAILURE_DDX_INTEL"; + return NS_OK; + } + } + } + + const struct { + int32_t mFeature; + int32_t mCodec; + } kFeatureToCodecs[] = {{nsIGfxInfo::FEATURE_H264_HW_DECODE, CODEC_HW_H264}, + {nsIGfxInfo::FEATURE_VP8_HW_DECODE, CODEC_HW_VP8}, + {nsIGfxInfo::FEATURE_VP9_HW_DECODE, CODEC_HW_VP9}, + {nsIGfxInfo::FEATURE_AV1_HW_DECODE, CODEC_HW_AV1}}; + + for (const auto& pair : kFeatureToCodecs) { + if (aFeature != pair.mFeature) { + continue; + } + if (mVAAPISupportedCodecs & pair.mCodec) { + *aStatus = nsIGfxInfo::FEATURE_STATUS_OK; + } else { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_PLATFORM_TEST; + aFailureId = "FEATURE_FAILURE_VIDEO_DECODING_MISSING"; + } + return NS_OK; + } + + auto ret = GfxInfoBase::GetFeatureStatusImpl( + aFeature, aStatus, aSuggestedDriverVersion, aDriverInfo, aFailureId, &os); + + // Probe VA-API on supported devices only + if (aFeature == nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING) { + if (!StaticPrefs::media_hardware_video_decoding_enabled_AtStartup()) { + return ret; + } + bool probeHWDecode = false; +#ifdef MOZ_WAYLAND + probeHWDecode = + mIsAccelerated && + (*aStatus == nsIGfxInfo::FEATURE_STATUS_OK || + StaticPrefs::media_hardware_video_decoding_force_enabled_AtStartup() || + StaticPrefs::media_ffmpeg_vaapi_enabled_AtStartup()); +#endif + if (probeHWDecode) { + GetDataVAAPI(); + } else { + mIsVAAPISupported = Some(false); + } + if (!mIsVAAPISupported.value()) { + *aStatus = nsIGfxInfo::FEATURE_BLOCKED_PLATFORM_TEST; + aFailureId = "FEATURE_FAILURE_VIDEO_DECODING_TEST_FAILED"; + } + } + + return ret; +} + +NS_IMETHODIMP +GfxInfo::GetD2DEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +GfxInfo::GetDWriteEnabled(bool* aEnabled) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +GfxInfo::GetDWriteVersion(nsAString& aDwriteVersion) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP GfxInfo::GetHasBattery(bool* aHasBattery) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GfxInfo::GetEmbeddedInFirefoxReality(bool* aEmbeddedInFirefoxReality) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetCleartypeParameters(nsAString& aCleartypeParams) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetWindowProtocol(nsAString& aWindowProtocol) { + GetData(); + if (mIsWayland) { + aWindowProtocol = GfxDriverInfo::GetWindowProtocol(WindowProtocol::Wayland); + } else if (mIsXWayland) { + aWindowProtocol = + GfxDriverInfo::GetWindowProtocol(WindowProtocol::XWayland); + } else { + aWindowProtocol = GfxDriverInfo::GetWindowProtocol(WindowProtocol::X11); + } + Telemetry::ScalarSet(Telemetry::ScalarID::GFX_LINUX_WINDOW_PROTOCOL, + aWindowProtocol); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetTestType(nsAString& aTestType) { + GetData(); + AppendASCIItoUTF16(mTestType, aTestType); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDescription(nsAString& aAdapterDescription) { + GetData(); + AppendASCIItoUTF16(mAdapterDescription, aAdapterDescription); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDescription2(nsAString& aAdapterDescription) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterRAM(uint32_t* aAdapterRAM) { + GetData(); + *aAdapterRAM = mAdapterRAM; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterRAM2(uint32_t* aAdapterRAM) { return NS_ERROR_FAILURE; } + +NS_IMETHODIMP +GfxInfo::GetAdapterDriver(nsAString& aAdapterDriver) { + aAdapterDriver.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriver2(nsAString& aAdapterDriver) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) { + GetData(); + CopyASCIItoUTF16(mDriverVendor, aAdapterDriverVendor); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) { + GetData(); + CopyASCIItoUTF16(mDriverVersion, aAdapterDriverVersion); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverVersion2(nsAString& aAdapterDriverVersion) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverDate(nsAString& aAdapterDriverDate) { + aAdapterDriverDate.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDriverDate2(nsAString& aAdapterDriverDate) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID(nsAString& aAdapterVendorID) { + GetData(); + CopyUTF8toUTF16(mVendorId, aAdapterVendorID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterVendorID2(nsAString& aAdapterVendorID) { + GetData(); + CopyUTF8toUTF16(mSecondaryVendorId, aAdapterVendorID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID(nsAString& aAdapterDeviceID) { + GetData(); + CopyUTF8toUTF16(mDeviceId, aAdapterDeviceID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterDeviceID2(nsAString& aAdapterDeviceID) { + GetData(); + CopyUTF8toUTF16(mSecondaryDeviceId, aAdapterDeviceID); + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterSubsysID(nsAString& aAdapterSubsysID) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetAdapterSubsysID2(nsAString& aAdapterSubsysID) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +GfxInfo::GetIsGPU2Active(bool* aIsGPU2Active) { + // This is never the case, as the active GPU should be the primary GPU. + *aIsGPU2Active = false; + return NS_OK; +} + +NS_IMETHODIMP +GfxInfo::GetDrmRenderDevice(nsACString& aDrmRenderDevice) { + GetData(); + aDrmRenderDevice.Assign(mDrmRenderDevice); + return NS_OK; +} + +#ifdef DEBUG + +// Implement nsIGfxInfoDebug +// We don't support spoofing anything on Linux + +NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString& aVendorID) { + GetData(); + CopyUTF16toUTF8(aVendorID, mVendorId); + mIsAccelerated = true; + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString& aDeviceID) { + GetData(); + CopyUTF16toUTF8(aDeviceID, mDeviceId); + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString& aDriverVersion) { + GetData(); + CopyUTF16toUTF8(aDriverVersion, mDriverVersion); + return NS_OK; +} + +NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t aVersion) { + // We don't support OS versioning on Linux. There's just "Linux". + return NS_OK; +} + +#endif + +} // namespace mozilla::widget diff --git a/widget/gtk/GfxInfo.h b/widget/gtk/GfxInfo.h new file mode 100644 index 0000000000..639d793b9a --- /dev/null +++ b/widget/gtk/GfxInfo.h @@ -0,0 +1,133 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIDGET_GTK_GFXINFO_h__ +#define WIDGET_GTK_GFXINFO_h__ + +#include "GfxInfoBase.h" +#include "nsString.h" + +namespace mozilla { +namespace widget { + +class GfxInfo final : public GfxInfoBase { + public: + // We only declare the subset of nsIGfxInfo that we actually implement. The + // rest is brought forward from GfxInfoBase. + NS_IMETHOD GetD2DEnabled(bool* aD2DEnabled) override; + NS_IMETHOD GetDWriteEnabled(bool* aDWriteEnabled) override; + NS_IMETHOD GetDWriteVersion(nsAString& aDwriteVersion) override; + NS_IMETHOD GetEmbeddedInFirefoxReality( + bool* aEmbeddedInFirefoxReality) override; + NS_IMETHOD GetHasBattery(bool* aHasBattery) override; + NS_IMETHOD GetCleartypeParameters(nsAString& aCleartypeParams) override; + NS_IMETHOD GetWindowProtocol(nsAString& aWindowProtocol) override; + NS_IMETHOD GetTestType(nsAString& aTestType) override; + NS_IMETHOD GetAdapterDescription(nsAString& aAdapterDescription) override; + NS_IMETHOD GetAdapterDriver(nsAString& aAdapterDriver) override; + NS_IMETHOD GetAdapterVendorID(nsAString& aAdapterVendorID) override; + NS_IMETHOD GetAdapterDeviceID(nsAString& aAdapterDeviceID) override; + NS_IMETHOD GetAdapterSubsysID(nsAString& aAdapterSubsysID) override; + NS_IMETHOD GetAdapterRAM(uint32_t* aAdapterRAM) override; + NS_IMETHOD GetAdapterDriverVendor(nsAString& aAdapterDriverVendor) override; + NS_IMETHOD GetAdapterDriverVersion(nsAString& aAdapterDriverVersion) override; + NS_IMETHOD GetAdapterDriverDate(nsAString& aAdapterDriverDate) override; + NS_IMETHOD GetAdapterDescription2(nsAString& aAdapterDescription) override; + NS_IMETHOD GetAdapterDriver2(nsAString& aAdapterDriver) override; + NS_IMETHOD GetAdapterVendorID2(nsAString& aAdapterVendorID) override; + NS_IMETHOD GetAdapterDeviceID2(nsAString& aAdapterDeviceID) override; + NS_IMETHOD GetAdapterSubsysID2(nsAString& aAdapterSubsysID) override; + NS_IMETHOD GetAdapterRAM2(uint32_t* aAdapterRAM) override; + NS_IMETHOD GetAdapterDriverVendor2(nsAString& aAdapterDriverVendor) override; + NS_IMETHOD GetAdapterDriverVersion2( + nsAString& aAdapterDriverVersion) override; + NS_IMETHOD GetAdapterDriverDate2(nsAString& aAdapterDriverDate) override; + NS_IMETHOD GetIsGPU2Active(bool* aIsGPU2Active) override; + NS_IMETHOD GetDrmRenderDevice(nsACString& aDrmRenderDevice) override; + using GfxInfoBase::GetFeatureStatus; + using GfxInfoBase::GetFeatureSuggestedDriverVersion; + + virtual nsresult Init() override; + + NS_IMETHOD_(void) GetData() override; + + static bool FireGLXTestProcess(); + +#ifdef DEBUG + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIGFXINFODEBUG +#endif + + protected: + ~GfxInfo() = default; + + OperatingSystem GetOperatingSystem() override { + return OperatingSystem::Linux; + } + virtual nsresult GetFeatureStatusImpl( + int32_t aFeature, int32_t* aStatus, nsAString& aSuggestedDriverVersion, + const nsTArray& aDriverInfo, nsACString& aFailureId, + OperatingSystem* aOS = nullptr) override; + virtual const nsTArray& GetGfxDriverInfo() override; + + virtual bool DoesWindowProtocolMatch( + const nsAString& aBlocklistWindowProtocol, + const nsAString& aWindowProtocol) override; + + virtual bool DoesDriverVendorMatch(const nsAString& aBlocklistVendor, + const nsAString& aDriverVendor) override; + static int FireTestProcess(const nsAString& aBinaryFile, int* aOutPipe, + const char** aStringArgs); + + private: + bool mInitialized = false; + nsCString mVendorId; + nsCString mDeviceId; + nsCString mDriverVendor; + nsCString mDriverVersion; + nsCString mAdapterDescription; + uint32_t mAdapterRAM; + nsCString mOS; + nsCString mOSRelease; + nsCString mTestType; + + nsCString mSecondaryVendorId; + nsCString mSecondaryDeviceId; + + nsCString mDrmRenderDevice; + + nsTArray mDdxDrivers; + + struct ScreenInfo { + uint32_t mWidth; + uint32_t mHeight; + bool mIsDefault; + }; + + nsTArray mScreenInfo; + bool mHasTextureFromPixmap; + unsigned int mGLMajorVersion, mGLMinorVersion; + bool mIsMesa; + bool mIsAccelerated; + bool mIsWayland; + bool mIsXWayland; + bool mHasMultipleGPUs; + bool mGlxTestError; + mozilla::Maybe mIsVAAPISupported; + int mVAAPISupportedCodecs = 0; + + static int sGLXTestPipe; + static pid_t sGLXTestPID; + + void GetDataVAAPI(); + void AddCrashReportAnnotations(); +}; + +} // namespace widget +} // namespace mozilla + +#endif /* WIDGET_GTK_GFXINFO_h__ */ diff --git a/widget/gtk/GfxInfoUtils.h b/widget/gtk/GfxInfoUtils.h new file mode 100644 index 0000000000..b6174842f2 --- /dev/null +++ b/widget/gtk/GfxInfoUtils.h @@ -0,0 +1,100 @@ +/* vim: se cin sw=2 ts=2 et : */ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WIDGET_GTK_GFXINFO_UTILS_h__ +#define WIDGET_GTK_GFXINFO_UTILS_h__ + +// An alternative to mozilla::Unused for use in (a) C code and (b) code where +// linking with unused.o is difficult. +#define MOZ_UNUSED(expr) \ + do { \ + if (expr) { \ + (void)0; \ + } \ + } while (0) + +#define LOG_PIPE 2 + +static bool enable_logging = false; +static void log(const char* format, ...) { + if (!enable_logging) { + return; + } + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + +static int output_pipe = 1; +static void close_logging() { + // we want to redirect to /dev/null stdout, stderr, and while we're at it, + // any PR logging file descriptors. To that effect, we redirect all positive + // file descriptors up to what open() returns here. In particular, 1 is stdout + // and 2 is stderr. + int fd = open("/dev/null", O_WRONLY); + for (int i = 1; i < fd; i++) { + if (output_pipe != i) { + dup2(fd, i); + } + } + close(fd); +} + +// C++ standard collides with C standard in that it doesn't allow casting void* +// to function pointer types. So the work-around is to convert first to size_t. +// http://www.trilithium.com/johan/2004/12/problem-with-dlsym/ +template +static func_ptr_type cast(void* ptr) { + return reinterpret_cast(reinterpret_cast(ptr)); +} + +#define BUFFER_SIZE_STEP 4000 + +static char* test_buf = nullptr; +static int test_bufsize = 0; +static int test_length = 0; + +static void record_value(const char* format, ...) { + if (!test_buf || test_length + BUFFER_SIZE_STEP / 2 > test_bufsize) { + test_bufsize += BUFFER_SIZE_STEP; + test_buf = (char*)realloc(test_buf, test_bufsize); + } + int remaining = test_bufsize - test_length; + + // Append the new values to the buffer, not to exceed the remaining space. + va_list args; + va_start(args, format); + int max_added = vsnprintf(test_buf + test_length, remaining, format, args); + va_end(args); + + if (max_added >= remaining) { + test_length += remaining; + } else { + test_length += max_added; + } +} + +static void record_error(const char* str) { record_value("ERROR\n%s\n", str); } + +static void record_warning(const char* str) { + record_value("WARNING\n%s\n", str); +} + +static void record_flush() { + if (!test_buf) { + return; + } + MOZ_UNUSED(write(output_pipe, test_buf, test_length)); + if (enable_logging) { + MOZ_UNUSED(write(LOG_PIPE, test_buf, test_length)); + } + free(test_buf); + test_buf = nullptr; +} + +#endif /* WIDGET_GTK_GFXINFO_h__ */ diff --git a/widget/gtk/GtkCompositorWidget.cpp b/widget/gtk/GtkCompositorWidget.cpp new file mode 100644 index 0000000000..2c29d5de03 --- /dev/null +++ b/widget/gtk/GtkCompositorWidget.cpp @@ -0,0 +1,246 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GtkCompositorWidget.h" + +#include "mozilla/gfx/gfxVars.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/widget/InProcessCompositorWidget.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "nsWindow.h" + +#ifdef MOZ_X11 +# include "mozilla/X11Util.h" +#endif + +#ifdef MOZ_WAYLAND +# include "mozilla/layers/NativeLayerWayland.h" +#endif + +#ifdef MOZ_LOGGING +# undef LOG +# define LOG(str, ...) \ + MOZ_LOG(IsPopup() ? gWidgetPopupLog : gWidgetLog, \ + mozilla::LogLevel::Debug, \ + ("[%p]: " str, mWidget.get(), ##__VA_ARGS__)) +#endif /* MOZ_LOGGING */ + +namespace mozilla { +namespace widget { + +GtkCompositorWidget::GtkCompositorWidget( + const GtkCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, RefPtr aWindow) + : CompositorWidget(aOptions), + mWidget(std::move(aWindow)), + mClientSize(LayoutDeviceIntSize(aInitData.InitialClientSize()), + "GtkCompositorWidget::mClientSize") { +#if defined(MOZ_X11) + if (GdkIsX11Display()) { + mXWindow = (Window)aInitData.XWindow(); + ConfigureX11Backend(mXWindow, aInitData.Shaped()); + LOG("GtkCompositorWidget::GtkCompositorWidget() [%p] mXWindow %p " + "mIsRenderingSuspended %d\n", + (void*)mWidget.get(), (void*)mXWindow, !!mIsRenderingSuspended); + } +#endif +#if defined(MOZ_WAYLAND) + if (GdkIsWaylandDisplay()) { + ConfigureWaylandBackend(); + LOG("GtkCompositorWidget::GtkCompositorWidget() [%p] mWidget %p " + "mIsRenderingSuspended %d\n", + (void*)mWidget.get(), (void*)mWidget, !!mIsRenderingSuspended); + } +#endif +} + +GtkCompositorWidget::~GtkCompositorWidget() { + LOG("GtkCompositorWidget::~GtkCompositorWidget [%p]\n", (void*)mWidget.get()); + DisableRendering(); + RefPtr widget = mWidget.forget(); + NS_ReleaseOnMainThread("GtkCompositorWidget::mWidget", widget.forget()); +} + +already_AddRefed GtkCompositorWidget::StartRemoteDrawing() { + return nullptr; +} +void GtkCompositorWidget::EndRemoteDrawing() {} + +already_AddRefed +GtkCompositorWidget::StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode) { + return mProvider.StartRemoteDrawingInRegion(aInvalidRegion, aBufferMode); +} + +void GtkCompositorWidget::EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) { + mProvider.EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion); +} + +nsIWidget* GtkCompositorWidget::RealWidget() { return mWidget; } + +void GtkCompositorWidget::NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) { + LOG("GtkCompositorWidget::NotifyClientSizeChanged() to %d x %d", + aClientSize.width, aClientSize.height); + + auto size = mClientSize.Lock(); + *size = aClientSize; +} + +LayoutDeviceIntSize GtkCompositorWidget::GetClientSize() { + auto size = mClientSize.Lock(); + return *size; +} + +void GtkCompositorWidget::RemoteLayoutSizeUpdated( + const LayoutDeviceRect& aSize) { + if (!mWidget || !mWidget->IsWaitingForCompositorResume()) { + return; + } + + LOG("GtkCompositorWidget::RemoteLayoutSizeUpdated() %d x %d", + (int)aSize.width, (int)aSize.height); + + // We're waiting for layout to match widget size. + auto clientSize = mClientSize.Lock(); + if (clientSize->width != (int)aSize.width || + clientSize->height != (int)aSize.height) { + LOG("quit, client size doesn't match (%d x %d)", clientSize->width, + clientSize->height); + return; + } + + mWidget->ResumeCompositorFromCompositorThread(); +} + +EGLNativeWindowType GtkCompositorWidget::GetEGLNativeWindow() { + EGLNativeWindowType window = nullptr; + if (mWidget) { + window = (EGLNativeWindowType)mWidget->GetNativeData(NS_NATIVE_EGL_WINDOW); + } +#if defined(MOZ_X11) + if (mXWindow) { + window = (EGLNativeWindowType)mXWindow; + } +#endif + LOG("GtkCompositorWidget::GetEGLNativeWindow [%p] window %p\n", mWidget.get(), + window); + return window; +} + +#if defined(MOZ_WAYLAND) +void GtkCompositorWidget::SetEGLNativeWindowSize( + const LayoutDeviceIntSize& aEGLWindowSize) { + if (mWidget) { + mWidget->SetEGLNativeWindowSize(aEGLWindowSize); + } +} +#endif + +LayoutDeviceIntRegion GtkCompositorWidget::GetTransparentRegion() { + // We need to clear target buffer alpha values of popup windows as + // SW-WR paints with alpha blending (see Bug 1674473). + if (!mWidget || mWidget->IsPopup()) { + return LayoutDeviceIntRect(LayoutDeviceIntPoint(0, 0), GetClientSize()); + } + + // Clear background of titlebar area to render titlebar + // transparent corners correctly. + return mWidget->GetTitlebarRect(); +} + +#ifdef MOZ_WAYLAND +RefPtr +GtkCompositorWidget::GetNativeLayerRoot() { + if (gfx::gfxVars::UseWebRenderCompositor()) { + if (!mNativeLayerRoot) { + MOZ_ASSERT(mWidget && mWidget->GetMozContainer()); + mNativeLayerRoot = NativeLayerRootWayland::CreateForMozContainer( + mWidget->GetMozContainer()); + } + return mNativeLayerRoot; + } + return nullptr; +} +#endif + +void GtkCompositorWidget::DisableRendering() { + LOG("GtkCompositorWidget::DisableRendering [%p]\n", (void*)mWidget.get()); + mIsRenderingSuspended = true; + mProvider.CleanupResources(); +#if defined(MOZ_X11) + mXWindow = {}; +#endif +} + +#if defined(MOZ_WAYLAND) +bool GtkCompositorWidget::ConfigureWaylandBackend() { + mProvider.Initialize(this); + return true; +} +#endif + +#if defined(MOZ_X11) +bool GtkCompositorWidget::ConfigureX11Backend(Window aXWindow, bool aShaped) { + mXWindow = aXWindow; + + // We don't have X window yet. + if (!mXWindow) { + mIsRenderingSuspended = true; + return false; + } + + // Grab the window's visual and depth + XWindowAttributes windowAttrs; + if (!XGetWindowAttributes(DefaultXDisplay(), mXWindow, &windowAttrs)) { + NS_WARNING("GtkCompositorWidget(): XGetWindowAttributes() failed!"); + return false; + } + + Visual* visual = windowAttrs.visual; + int depth = windowAttrs.depth; + + // Initialize the window surface provider + mProvider.Initialize(mXWindow, visual, depth, aShaped); + return true; +} +#endif + +void GtkCompositorWidget::EnableRendering(const uintptr_t aXWindow, + const bool aShaped) { + LOG("GtkCompositorWidget::EnableRendering() [%p]\n", mWidget.get()); + + if (!mIsRenderingSuspended) { + LOG(" quit, mIsRenderingSuspended = false\n"); + return; + } +#if defined(MOZ_WAYLAND) + if (GdkIsWaylandDisplay()) { + LOG(" configure widget %p\n", mWidget.get()); + if (!ConfigureWaylandBackend()) { + return; + } + } +#endif +#if defined(MOZ_X11) + if (GdkIsX11Display()) { + LOG(" configure XWindow %p shaped %d\n", (void*)aXWindow, aShaped); + if (!ConfigureX11Backend((Window)aXWindow, aShaped)) { + return; + } + } +#endif + mIsRenderingSuspended = false; +} +#ifdef MOZ_LOGGING +bool GtkCompositorWidget::IsPopup() { + return mWidget ? mWidget->IsPopup() : false; +} +#endif + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/GtkCompositorWidget.h b/widget/gtk/GtkCompositorWidget.h new file mode 100644 index 0000000000..601172d09c --- /dev/null +++ b/widget/gtk/GtkCompositorWidget.h @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef widget_gtk_GtkCompositorWidget_h +#define widget_gtk_GtkCompositorWidget_h + +#include "GLDefs.h" +#include "mozilla/DataMutex.h" +#include "mozilla/widget/CompositorWidget.h" +#include "WindowSurfaceProvider.h" + +class nsIWidget; +class nsWindow; + +namespace mozilla { + +namespace layers { +class NativeLayerRootWayland; +} // namespace layers + +namespace widget { + +class PlatformCompositorWidgetDelegate : public CompositorWidgetDelegate { + public: + virtual void NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) = 0; + virtual GtkCompositorWidget* AsGtkCompositorWidget() { return nullptr; }; + + virtual void DisableRendering() = 0; + virtual void EnableRendering(const uintptr_t aXWindow, + const bool aShaped) = 0; + + // CompositorWidgetDelegate Overrides + + PlatformCompositorWidgetDelegate* AsPlatformSpecificDelegate() override { + return this; + } +}; + +class GtkCompositorWidgetInitData; + +class GtkCompositorWidget : public CompositorWidget, + public PlatformCompositorWidgetDelegate { + public: + GtkCompositorWidget(const GtkCompositorWidgetInitData& aInitData, + const layers::CompositorOptions& aOptions, + RefPtr aWindow /* = nullptr*/); + ~GtkCompositorWidget(); + + // CompositorWidget Overrides + + already_AddRefed StartRemoteDrawing() override; + void EndRemoteDrawing() override; + + already_AddRefed StartRemoteDrawingInRegion( + const LayoutDeviceIntRegion& aInvalidRegion, + layers::BufferMode* aBufferMode) override; + void EndRemoteDrawingInRegion( + gfx::DrawTarget* aDrawTarget, + const LayoutDeviceIntRegion& aInvalidRegion) override; + + LayoutDeviceIntSize GetClientSize() override; + void RemoteLayoutSizeUpdated(const LayoutDeviceRect& aSize); + + nsIWidget* RealWidget() override; + GtkCompositorWidget* AsGTK() override { return this; } + CompositorWidgetDelegate* AsDelegate() override { return this; } + + EGLNativeWindowType GetEGLNativeWindow(); + + LayoutDeviceIntRegion GetTransparentRegion() override; + + // Suspend rendering of this remote widget and clear all resources. + // Can be used when underlying window is hidden/unmapped. + void DisableRendering() override; + + // Resume rendering with to given aXWindow (X11) or nsWindow (Wayland). + void EnableRendering(const uintptr_t aXWindow, const bool aShaped) override; + +#if defined(MOZ_X11) + Window XWindow() const { return mXWindow; } +#endif +#if defined(MOZ_WAYLAND) + void SetEGLNativeWindowSize(const LayoutDeviceIntSize& aEGLWindowSize); + RefPtr GetNativeLayerRoot() override; +#endif + + bool PreRender(WidgetRenderingContext* aContext) override { + return !mIsRenderingSuspended; + } + bool IsHidden() const override { return mIsRenderingSuspended; } + + // PlatformCompositorWidgetDelegate Overrides + + void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize) override; + GtkCompositorWidget* AsGtkCompositorWidget() override { return this; } + + private: +#if defined(MOZ_WAYLAND) + bool ConfigureWaylandBackend(); +#endif +#if defined(MOZ_X11) + bool ConfigureX11Backend(Window aXWindow, bool aShaped); +#endif +#ifdef MOZ_LOGGING + bool IsPopup(); +#endif + + protected: + RefPtr mWidget; + + private: + // This field is written to on the main thread and read from on the compositor + // or renderer thread. During window resizing, this is subject to a (largely + // benign) read/write race, see bug 1665726. The DataMutex doesn't prevent the + // read/write race, but it does make it Not Undefined Behaviour, and also + // ensures we only ever use the old or new size, and not some weird synthesis + // of the two. + DataMutex mClientSize; + + WindowSurfaceProvider mProvider; + +#if defined(MOZ_X11) + Window mXWindow = {}; +#endif +#ifdef MOZ_WAYLAND + RefPtr mNativeLayerRoot; +#endif + Atomic mIsRenderingSuspended{true}; +}; + +} // namespace widget +} // namespace mozilla + +#endif // widget_gtk_GtkCompositorWidget_h diff --git a/widget/gtk/IMContextWrapper.cpp b/widget/gtk/IMContextWrapper.cpp new file mode 100644 index 0000000000..2e438bbc5c --- /dev/null +++ b/widget/gtk/IMContextWrapper.cpp @@ -0,0 +1,3360 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Logging.h" +#include "nsString.h" +#include "prtime.h" +#include "prenv.h" + +#include "IMContextWrapper.h" + +#include "GRefPtr.h" +#include "nsGtkKeyUtils.h" +#include "nsWindow.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Likely.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_intl.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEventDispatcher.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "mozilla/WritingModes.h" + +// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync` +// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too +// big file. +// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior. +mozilla::LazyLogModule gIMELog("IMEHandler"); + +namespace mozilla { +namespace widget { + +static inline const char* ToChar(bool aBool) { + return aBool ? "true" : "false"; +} + +static const char* GetEventType(GdkEventKey* aKeyEvent) { + switch (aKeyEvent->type) { + case GDK_KEY_PRESS: + return "GDK_KEY_PRESS"; + case GDK_KEY_RELEASE: + return "GDK_KEY_RELEASE"; + default: + return "Unknown"; + } +} + +class GetEventStateName : public nsAutoCString { + public: + explicit GetEventStateName(guint aState, + IMContextWrapper::IMContextID aIMContextID = + IMContextWrapper::IMContextID::Unknown) { + if (aState & GDK_SHIFT_MASK) { + AppendModifier("shift"); + } + if (aState & GDK_CONTROL_MASK) { + AppendModifier("control"); + } + if (aState & GDK_MOD1_MASK) { + AppendModifier("mod1"); + } + if (aState & GDK_MOD2_MASK) { + AppendModifier("mod2"); + } + if (aState & GDK_MOD3_MASK) { + AppendModifier("mod3"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod4"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod5"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod5"); + } + switch (aIMContextID) { + case IMContextWrapper::IMContextID::IBus: + static const guint IBUS_HANDLED_MASK = 1 << 24; + static const guint IBUS_IGNORED_MASK = 1 << 25; + if (aState & IBUS_HANDLED_MASK) { + AppendModifier("IBUS_HANDLED_MASK"); + } + if (aState & IBUS_IGNORED_MASK) { + AppendModifier("IBUS_IGNORED_MASK"); + } + break; + case IMContextWrapper::IMContextID::Fcitx: + case IMContextWrapper::IMContextID::Fcitx5: + static const guint FcitxKeyState_HandledMask = 1 << 24; + static const guint FcitxKeyState_IgnoredMask = 1 << 25; + if (aState & FcitxKeyState_HandledMask) { + AppendModifier("FcitxKeyState_HandledMask"); + } + if (aState & FcitxKeyState_IgnoredMask) { + AppendModifier("FcitxKeyState_IgnoredMask"); + } + break; + default: + break; + } + } + + private: + void AppendModifier(const char* aModifierName) { + if (!IsEmpty()) { + AppendLiteral(" + "); + } + Append(aModifierName); + } +}; + +class GetTextRangeStyleText final : public nsAutoCString { + public: + explicit GetTextRangeStyleText(const TextRangeStyle& aStyle) { + if (!aStyle.IsDefined()) { + AssignLiteral("{ IsDefined()=false }"); + return; + } + + if (aStyle.IsLineStyleDefined()) { + AppendLiteral("{ mLineStyle="); + AppendLineStyle(aStyle.mLineStyle); + if (aStyle.IsUnderlineColorDefined()) { + AppendLiteral(", mUnderlineColor="); + AppendColor(aStyle.mUnderlineColor); + } else { + AppendLiteral(", IsUnderlineColorDefined=false"); + } + } else { + AppendLiteral("{ IsLineStyleDefined()=false"); + } + + if (aStyle.IsForegroundColorDefined()) { + AppendLiteral(", mForegroundColor="); + AppendColor(aStyle.mForegroundColor); + } else { + AppendLiteral(", IsForegroundColorDefined()=false"); + } + + if (aStyle.IsBackgroundColorDefined()) { + AppendLiteral(", mBackgroundColor="); + AppendColor(aStyle.mBackgroundColor); + } else { + AppendLiteral(", IsBackgroundColorDefined()=false"); + } + + AppendLiteral(" }"); + } + void AppendLineStyle(TextRangeStyle::LineStyle aLineStyle) { + switch (aLineStyle) { + case TextRangeStyle::LineStyle::None: + AppendLiteral("LineStyle::None"); + break; + case TextRangeStyle::LineStyle::Solid: + AppendLiteral("LineStyle::Solid"); + break; + case TextRangeStyle::LineStyle::Dotted: + AppendLiteral("LineStyle::Dotted"); + break; + case TextRangeStyle::LineStyle::Dashed: + AppendLiteral("LineStyle::Dashed"); + break; + case TextRangeStyle::LineStyle::Double: + AppendLiteral("LineStyle::Double"); + break; + case TextRangeStyle::LineStyle::Wavy: + AppendLiteral("LineStyle::Wavy"); + break; + default: + AppendPrintf("Invalid(0x%02X)", + static_cast(aLineStyle)); + break; + } + } + void AppendColor(nscolor aColor) { + AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }", NS_GET_R(aColor), + NS_GET_G(aColor), NS_GET_B(aColor), NS_GET_A(aColor)); + } + virtual ~GetTextRangeStyleText() = default; +}; + +const static bool kUseSimpleContextDefault = false; + +/****************************************************************************** + * SelectionStyleProvider + * + * IME (e.g., fcitx, ~4.2.8.3) may look up selection colors of widget, which + * is related to the window associated with the IM context, to support any + * colored widgets. Our editor (like ) is rendered as + * native GtkTextView as far as possible by default and if editor color is + * changed by web apps, nsTextFrame may swap background color of foreground + * color of composition string for making composition string is always + * visually distinct in normal text. + * + * So, we would like IME to set style of composition string to good colors + * in GtkTextView. Therefore, this class overwrites selection colors of + * our widget with selection colors of GtkTextView so that it's possible IME + * to refer selection colors of GtkTextView via our widget. + ******************************************************************************/ + +static Maybe GetSystemColor(LookAndFeel::ColorID aId) { + return LookAndFeel::GetColor(aId, LookAndFeel::ColorScheme::Light, + LookAndFeel::UseStandins::No); +} + +class SelectionStyleProvider final { + public: + static SelectionStyleProvider* GetExistingInstance() { return sInstance; } + + static SelectionStyleProvider* GetInstance() { + if (sHasShutDown) { + return nullptr; + } + if (!sInstance) { + sInstance = new SelectionStyleProvider(); + } + return sInstance; + } + + static void Shutdown() { + if (sInstance) { + g_object_unref(sInstance->mProvider); + } + delete sInstance; + sInstance = nullptr; + sHasShutDown = true; + } + + // aGDKWindow is a GTK window which will be associated with an IM context. + void AttachTo(GdkWindow* aGDKWindow) { + GtkWidget* widget = nullptr; + // gdk_window_get_user_data() typically returns pointer to widget that + // window belongs to. If it's widget, fcitx retrieves selection colors + // of them. So, we need to overwrite its style. + gdk_window_get_user_data(aGDKWindow, (gpointer*)&widget); + if (GTK_IS_WIDGET(widget)) { + gtk_style_context_add_provider(gtk_widget_get_style_context(widget), + GTK_STYLE_PROVIDER(mProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + } + + void OnThemeChanged() { + // fcitx refers GtkStyle::text[GTK_STATE_SELECTED] and + // GtkStyle::bg[GTK_STATE_SELECTED] (although pair of text and *base* + // or *fg* and bg is correct). gtk_style_update_from_context() will + // set these colors using the widget's GtkStyleContext and so the + // colors can be controlled by a ":selected" CSS rule. + nsAutoCString style(":selected{"); + // FYI: LookAndFeel always returns selection colors of GtkTextView. + if (auto selectionForegroundColor = + GetSystemColor(LookAndFeel::ColorID::Highlight)) { + double alpha = + static_cast(NS_GET_A(*selectionForegroundColor)) / 0xFF; + style.AppendPrintf("color:rgba(%u,%u,%u,", + NS_GET_R(*selectionForegroundColor), + NS_GET_G(*selectionForegroundColor), + NS_GET_B(*selectionForegroundColor)); + // We can't use AppendPrintf here, because it does locale-specific + // formatting of floating-point values. + style.AppendFloat(alpha); + style.AppendPrintf(");"); + } + if (auto selectionBackgroundColor = + GetSystemColor(LookAndFeel::ColorID::Highlighttext)) { + double alpha = + static_cast(NS_GET_A(*selectionBackgroundColor)) / 0xFF; + style.AppendPrintf("background-color:rgba(%u,%u,%u,", + NS_GET_R(*selectionBackgroundColor), + NS_GET_G(*selectionBackgroundColor), + NS_GET_B(*selectionBackgroundColor)); + style.AppendFloat(alpha); + style.AppendPrintf(");"); + } + style.AppendLiteral("}"); + gtk_css_provider_load_from_data(mProvider, style.get(), -1, nullptr); + } + + private: + static SelectionStyleProvider* sInstance; + static bool sHasShutDown; + GtkCssProvider* const mProvider; + + SelectionStyleProvider() : mProvider(gtk_css_provider_new()) { + OnThemeChanged(); + } +}; + +SelectionStyleProvider* SelectionStyleProvider::sInstance = nullptr; +bool SelectionStyleProvider::sHasShutDown = false; + +/****************************************************************************** + * IMContextWrapper + ******************************************************************************/ + +IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr; +guint16 IMContextWrapper::sWaitingSynthesizedKeyPressHardwareKeyCode = 0; +bool IMContextWrapper::sUseSimpleContext; + +NS_IMPL_ISUPPORTS(IMContextWrapper, TextEventDispatcherListener, + nsISupportsWeakReference) + +IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow) + : mOwnerWindow(aOwnerWindow), + mLastFocusedWindow(nullptr), + mContext(nullptr), + mSimpleContext(nullptr), + mDummyContext(nullptr), + mComposingContext(nullptr), + mCompositionStart(UINT32_MAX), + mProcessingKeyEvent(nullptr), + mCompositionState(eCompositionState_NotComposing), + mIMContextID(IMContextID::Unknown), + mFallbackToKeyEvent(false), + mKeyboardEventWasDispatched(false), + mKeyboardEventWasConsumed(false), + mIsDeletingSurrounding(false), + mLayoutChanged(false), + mSetCursorPositionOnKeyEvent(true), + mPendingResettingIMContext(false), + mRetrieveSurroundingSignalReceived(false), + mMaybeInDeadKeySequence(false), + mIsIMInAsyncKeyHandlingMode(false), + mSetInputPurposeAndInputHints(false) { + static bool sFirstInstance = true; + if (sFirstInstance) { + sFirstInstance = false; + sUseSimpleContext = + Preferences::GetBool("intl.ime.use_simple_context_on_password_field", + kUseSimpleContextDefault); + } + Init(); +} + +static bool IsIBusInSyncMode() { + // See ibus_im_context_class_init() in client/gtk2/ibusimcontext.c + // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L610 + const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE"); + + // See _get_boolean_env() in client/gtk2/ibusimcontext.c + // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L520-L537 + if (!env) { + return false; + } + nsDependentCString envStr(env); + if (envStr.IsEmpty() || envStr.EqualsLiteral("0") || + envStr.EqualsLiteral("false") || envStr.EqualsLiteral("False") || + envStr.EqualsLiteral("FALSE")) { + return false; + } + return true; +} + +static bool GetFcitxBoolEnv(const char* aEnv) { + // See fcitx_utils_get_boolean_env in src/lib/fcitx-utils/utils.c + // https://github.com/fcitx/fcitx/blob/0c87840dc7d9460c2cb5feaeefec299d0d3d62ec/src/lib/fcitx-utils/utils.c#L721-L736 + const char* env = PR_GetEnv(aEnv); + if (!env) { + return false; + } + nsDependentCString envStr(env); + if (envStr.IsEmpty() || envStr.EqualsLiteral("0") || + envStr.EqualsLiteral("false")) { + return false; + } + return true; +} + +static bool IsFcitxInSyncMode() { + // See fcitx_im_context_class_init() in src/frontend/gtk2/fcitximcontext.c + // https://github.com/fcitx/fcitx/blob/78b98d9230dc9630e99d52e3172bdf440ffd08c4/src/frontend/gtk2/fcitximcontext.c#L395-L398 + return GetFcitxBoolEnv("IBUS_ENABLE_SYNC_MODE") || + GetFcitxBoolEnv("FCITX_ENABLE_SYNC_MODE"); +} + +nsDependentCSubstring IMContextWrapper::GetIMName() const { + const char* contextIDChar = + gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)); + if (!contextIDChar) { + return nsDependentCSubstring(); + } + + nsDependentCSubstring im(contextIDChar, strlen(contextIDChar)); + + // If the context is XIM, actual engine must be specified with + // |XMODIFIERS=@im=foo|. + const char* xmodifiersChar = PR_GetEnv("XMODIFIERS"); + if (!xmodifiersChar || !im.EqualsLiteral("xim")) { + return im; + } + + nsDependentCString xmodifiers(xmodifiersChar); + int32_t atIMValueStart = xmodifiers.Find("@im=") + 4; + if (atIMValueStart < 4 || + xmodifiers.Length() <= static_cast(atIMValueStart)) { + return im; + } + + int32_t atIMValueEnd = xmodifiers.Find("@", atIMValueStart); + if (atIMValueEnd > atIMValueStart) { + return nsDependentCSubstring(xmodifiersChar + atIMValueStart, + atIMValueEnd - atIMValueStart); + } + + if (atIMValueEnd == kNotFound) { + return nsDependentCSubstring(xmodifiersChar + atIMValueStart, + strlen(xmodifiersChar) - atIMValueStart); + } + + return im; +} + +void IMContextWrapper::Init() { + MozContainer* container = mOwnerWindow->GetMozContainer(); + MOZ_ASSERT(container, "container is null"); + GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container)); + + // Overwrite selection colors of the window before associating the window + // with IM context since IME may look up selection colors via IM context + // to support any colored widgets. + SelectionStyleProvider::GetInstance()->AttachTo(gdkWindow); + + // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails. + // So, we don't need to check the result. + + // Normal context. + mContext = gtk_im_multicontext_new(); + gtk_im_context_set_client_window(mContext, gdkWindow); + g_signal_connect(mContext, "preedit_changed", + G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback), + this); + g_signal_connect(mContext, "retrieve_surrounding", + G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback), + this); + g_signal_connect(mContext, "delete_surrounding", + G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback), + this); + g_signal_connect(mContext, "commit", + G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback), + this); + g_signal_connect(mContext, "preedit_start", + G_CALLBACK(IMContextWrapper::OnStartCompositionCallback), + this); + g_signal_connect(mContext, "preedit_end", + G_CALLBACK(IMContextWrapper::OnEndCompositionCallback), + this); + nsDependentCSubstring im = GetIMName(); + if (im.EqualsLiteral("ibus")) { + mIMContextID = IMContextID::IBus; + mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode(); + // Although ibus has key snooper mode, it's forcibly disabled on Firefox + // in default settings by its whitelist since we always send key events + // to IME before handling shortcut keys. The whitelist can be + // customized with env, IBUS_NO_SNOOPER_APPS, but we don't need to + // support such rare cases for reducing maintenance cost. + mIsKeySnooped = false; + } else if (im.EqualsLiteral("fcitx")) { + mIMContextID = IMContextID::Fcitx; + mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode(); + // Although Fcitx has key snooper mode similar to ibus, it's also + // disabled on Firefox in default settings by its whitelist. The + // whitelist can be customized with env, IBUS_NO_SNOOPER_APPS or + // FCITX_NO_SNOOPER_APPS, but we don't need to support such rare cases + // for reducing maintenance cost. + mIsKeySnooped = false; + } else if (im.EqualsLiteral("fcitx5")) { + mIMContextID = IMContextID::Fcitx5; + mIsIMInAsyncKeyHandlingMode = true; // does not have sync mode. + mIsKeySnooped = false; // never use key snooper. + } else if (im.EqualsLiteral("uim")) { + mIMContextID = IMContextID::Uim; + mIsIMInAsyncKeyHandlingMode = false; + // We cannot know if uim uses key snooper since it's build option of + // uim. Therefore, we need to retrieve the consideration from the + // pref for making users and distributions allowed to choose their + // preferred value. + mIsKeySnooped = + Preferences::GetBool("intl.ime.hack.uim.using_key_snooper", true); + } else if (im.EqualsLiteral("scim")) { + mIMContextID = IMContextID::Scim; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = false; + } else if (im.EqualsLiteral("iiim")) { + mIMContextID = IMContextID::IIIMF; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = false; + } else if (im.EqualsLiteral("wayland")) { + mIMContextID = IMContextID::Wayland; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = true; + } else { + mIMContextID = IMContextID::Unknown; + mIsIMInAsyncKeyHandlingMode = false; + mIsKeySnooped = false; + } + + // Simple context + if (sUseSimpleContext) { + mSimpleContext = gtk_im_context_simple_new(); + gtk_im_context_set_client_window(mSimpleContext, gdkWindow); + g_signal_connect(mSimpleContext, "preedit_changed", + G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback), + this); + g_signal_connect( + mSimpleContext, "retrieve_surrounding", + G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback), this); + g_signal_connect(mSimpleContext, "delete_surrounding", + G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback), + this); + g_signal_connect(mSimpleContext, "commit", + G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback), + this); + g_signal_connect(mSimpleContext, "preedit_start", + G_CALLBACK(IMContextWrapper::OnStartCompositionCallback), + this); + g_signal_connect(mSimpleContext, "preedit_end", + G_CALLBACK(IMContextWrapper::OnEndCompositionCallback), + this); + } + + // Dummy context + mDummyContext = gtk_im_multicontext_new(); + gtk_im_context_set_client_window(mDummyContext, gdkWindow); + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p Init(), mOwnerWindow=%p, mContext=%p (im=\"%s\"), " + "mIsIMInAsyncKeyHandlingMode=%s, mIsKeySnooped=%s, " + "mSimpleContext=%p, mDummyContext=%p, " + "gtk_im_multicontext_get_context_id()=\"%s\", " + "PR_GetEnv(\"XMODIFIERS\")=\"%s\"", + this, mOwnerWindow, mContext, nsAutoCString(im).get(), + ToChar(mIsIMInAsyncKeyHandlingMode), ToChar(mIsKeySnooped), + mSimpleContext, mDummyContext, + gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)), + PR_GetEnv("XMODIFIERS"))); +} + +/* static */ +void IMContextWrapper::Shutdown() { SelectionStyleProvider::Shutdown(); } + +IMContextWrapper::~IMContextWrapper() { + MOZ_ASSERT(!mContext); + MOZ_ASSERT(!mComposingContext); + if (this == sLastFocusedContext) { + sLastFocusedContext = nullptr; + } + MOZ_LOG(gIMELog, LogLevel::Info, ("0x%p ~IMContextWrapper()", this)); +} + +NS_IMETHODIMP +IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher, + const IMENotification& aNotification) { + switch (aNotification.mMessage) { + case REQUEST_TO_COMMIT_COMPOSITION: + case REQUEST_TO_CANCEL_COMPOSITION: { + nsWindow* window = + static_cast(aTextEventDispatcher->GetWidget()); + return IsComposing() ? EndIMEComposition(window) : NS_OK; + } + case NOTIFY_IME_OF_FOCUS: + OnFocusChangeInGecko(true); + return NS_OK; + case NOTIFY_IME_OF_BLUR: + OnFocusChangeInGecko(false); + return NS_OK; + case NOTIFY_IME_OF_POSITION_CHANGE: + OnLayoutChange(); + return NS_OK; + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: + OnUpdateComposition(); + return NS_OK; + case NOTIFY_IME_OF_SELECTION_CHANGE: { + nsWindow* window = + static_cast(aTextEventDispatcher->GetWidget()); + OnSelectionChange(window, aNotification); + return NS_OK; + } + default: + return NS_ERROR_NOT_IMPLEMENTED; + } +} + +NS_IMETHODIMP_(void) +IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) { + // XXX When input transaction is being stolen by add-on, what should we do? +} + +NS_IMETHODIMP_(void) +IMContextWrapper::WillDispatchKeyboardEvent( + TextEventDispatcher* aTextEventDispatcher, + WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress, + void* aData) { + KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent, + static_cast(aData)); +} + +TextEventDispatcher* IMContextWrapper::GetTextEventDispatcher() { + if (NS_WARN_IF(!mLastFocusedWindow)) { + return nullptr; + } + TextEventDispatcher* dispatcher = + mLastFocusedWindow->GetTextEventDispatcher(); + // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr. + MOZ_RELEASE_ASSERT(dispatcher); + return dispatcher; +} + +NS_IMETHODIMP_(IMENotificationRequests) +IMContextWrapper::GetIMENotificationRequests() { + IMENotificationRequests::Notifications notifications = + IMENotificationRequests::NOTIFY_NOTHING; + // If it's not enabled, we don't need position change notification. + if (IsEnabled()) { + notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE; + } + return IMENotificationRequests(notifications); +} + +void IMContextWrapper::OnDestroyWindow(nsWindow* aWindow) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, " + "mOwnerWindow=0x%p, mLastFocusedModule=0x%p", + this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext)); + + MOZ_ASSERT(aWindow, "aWindow must not be null"); + + if (mLastFocusedWindow == aWindow) { + if (IsComposing()) { + EndIMEComposition(aWindow); + } + NotifyIMEOfFocusChange(IMEFocusState::Blurred); + mLastFocusedWindow = nullptr; + } + + if (mOwnerWindow != aWindow) { + return; + } + + if (sLastFocusedContext == this) { + sLastFocusedContext = nullptr; + } + + /** + * NOTE: + * The given window is the owner of this, so, we must disconnect from the + * contexts now. But that might be referred from other nsWindows + * (they are children of this. But we don't know why there are the + * cases). So, we need to clear the pointers that refers to contexts + * and this if the other referrers are still alive. See bug 349727. + */ + if (mContext) { + PrepareToDestroyContext(mContext); + gtk_im_context_set_client_window(mContext, nullptr); + g_signal_handlers_disconnect_by_data(mContext, this); + g_object_unref(mContext); + mContext = nullptr; + } + + if (mSimpleContext) { + gtk_im_context_set_client_window(mSimpleContext, nullptr); + g_signal_handlers_disconnect_by_data(mSimpleContext, this); + g_object_unref(mSimpleContext); + mSimpleContext = nullptr; + } + + if (mDummyContext) { + // mContext and mDummyContext have the same slaveType and signal_data + // so no need for another workaround_gtk_im_display_closed. + gtk_im_context_set_client_window(mDummyContext, nullptr); + g_object_unref(mDummyContext); + mDummyContext = nullptr; + } + + if (NS_WARN_IF(mComposingContext)) { + g_object_unref(mComposingContext); + mComposingContext = nullptr; + } + + mOwnerWindow = nullptr; + mLastFocusedWindow = nullptr; + mInputContext.mIMEState.mEnabled = IMEEnabled::Disabled; + mPostingKeyEvents.Clear(); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p OnDestroyWindow(), succeeded, Completely destroyed", this)); +} + +void IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext) { + if (mIMContextID == IMContextID::IIIMF) { + // IIIM module registers handlers for the "closed" signal on the + // display, but the signal handler is not disconnected when the module + // is unloaded. To prevent the module from being unloaded, use static + // variable to hold reference of slave context class declared by IIIM. + // Note that this does not grab any instance, it grabs the "class". + static gpointer sGtkIIIMContextClass = nullptr; + if (!sGtkIIIMContextClass) { + // We retrieved slave context class with g_type_name() and actual + // slave context instance when our widget was GTK2. That must be + // _GtkIMContext::priv::slave in GTK3. However, _GtkIMContext::priv + // is an opacity struct named _GtkIMMulticontextPrivate, i.e., it's + // not exposed by GTK3. Therefore, we cannot access the instance + // safely. So, we need to retrieve the slave context class with + // g_type_from_name("GtkIMContextIIIM") directly (anyway, we needed + // to compare the class name with "GtkIMContextIIIM"). + GType IIMContextType = g_type_from_name("GtkIMContextIIIM"); + if (IIMContextType) { + sGtkIIIMContextClass = g_type_class_ref(IIMContextType); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p PrepareToDestroyContext(), added to reference to " + "GtkIMContextIIIM class to prevent it from being unloaded", + this)); + } else { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p PrepareToDestroyContext(), FAILED to prevent the " + "IIIM module from being uploaded", + this)); + } + } + } +} + +void IMContextWrapper::OnFocusWindow(nsWindow* aWindow) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this, + aWindow, mLastFocusedWindow)); + mLastFocusedWindow = aWindow; +} + +void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, " + "mIMEFocusState=%s", + this, aWindow, mLastFocusedWindow, ToString(mIMEFocusState).c_str())); + + if (mLastFocusedWindow != aWindow) { + return; + } + + NotifyIMEOfFocusChange(IMEFocusState::Blurred); +} + +KeyHandlingState IMContextWrapper::OnKeyEvent( + nsWindow* aCaller, GdkEventKey* aEvent, + bool aKeyboardEventWasDispatched /* = false */) { + MOZ_ASSERT(aEvent, "aEvent must be non-null"); + + if (!mInputContext.mIMEState.IsEditable() || MOZ_UNLIKELY(IsDestroyed())) { + return KeyHandlingState::eNotHandled; + } + + MOZ_LOG(gIMELog, LogLevel::Info, (">>>>>>>>>>>>>>>>")); + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p OnKeyEvent(aCaller=0x%p, " + "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X, state=%s, " + "time=%u, hardware_keycode=%u, group=%u }, " + "aKeyboardEventWasDispatched=%s)", + this, aCaller, aEvent, GetEventType(aEvent), + gdk_keyval_name(aEvent->keyval), gdk_keyval_to_unicode(aEvent->keyval), + GetEventStateName(aEvent->state, mIMContextID).get(), aEvent->time, + aEvent->hardware_keycode, aEvent->group, + ToChar(aKeyboardEventWasDispatched))); + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p OnKeyEvent(), mMaybeInDeadKeySequence=%s, " + "mCompositionState=%s, current context=%p, active context=%p, " + "mIMContextID=%s, mIsIMInAsyncKeyHandlingMode=%s", + this, ToChar(mMaybeInDeadKeySequence), GetCompositionStateName(), + GetCurrentContext(), GetActiveContext(), ToString(mIMContextID).c_str(), + ToChar(mIsIMInAsyncKeyHandlingMode))); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnKeyEvent(), FAILED, the caller isn't focused " + "window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return KeyHandlingState::eNotHandled; + } + + // Even if old IM context has composition, key event should be sent to + // current context since the user expects so. + GtkIMContext* currentContext = GetCurrentContext(); + if (MOZ_UNLIKELY(!currentContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnKeyEvent(), FAILED, there are no context", this)); + return KeyHandlingState::eNotHandled; + } + + if (mSetCursorPositionOnKeyEvent) { + SetCursorPosition(currentContext); + mSetCursorPositionOnKeyEvent = false; + } + + // Let's support dead key event even if active keyboard layout also + // supports complicated composition like CJK IME. + bool isDeadKey = + KeymapWrapper::ComputeDOMKeyNameIndex(aEvent) == KEY_NAME_INDEX_Dead; + mMaybeInDeadKeySequence |= isDeadKey; + + // If current context is mSimpleContext, both ibus and fcitx handles key + // events synchronously. So, only when current context is mContext which + // is GtkIMMulticontext, the key event may be handled by IME asynchronously. + bool probablyHandledAsynchronously = + mIsIMInAsyncKeyHandlingMode && currentContext == mContext; + + // If we're not sure whether the event is handled asynchronously, this is + // set to true. + bool maybeHandledAsynchronously = false; + + // If aEvent is a synthesized event for async handling, this will be set to + // true. + bool isHandlingAsyncEvent = false; + + // If we've decided that the event won't be synthesized asyncrhonously + // by IME, but actually IME did it, this is set to true. + bool isUnexpectedAsyncEvent = false; + + // If IM is ibus or fcitx and it handles key events asynchronously, + // they mark aEvent->state as "handled by me" when they post key event + // to another process. Unfortunately, we need to check this hacky + // flag because it's difficult to store all pending key events by + // an array or a hashtable. + if (probablyHandledAsynchronously) { + switch (mIMContextID) { + case IMContextID::IBus: { + // See src/ibustypes.h + static const guint IBUS_IGNORED_MASK = 1 << 25; + // If IBUS_IGNORED_MASK was set to aEvent->state, the event + // has already been handled by another process and it wasn't + // used by IME. + isHandlingAsyncEvent = !!(aEvent->state & IBUS_IGNORED_MASK); + if (!isHandlingAsyncEvent) { + // On some environments, IBUS_IGNORED_MASK flag is not set as + // expected. In such case, we keep pusing all events into the queue. + // I.e., that causes eating a lot of memory until it's blurred. + // Therefore, we need to check whether there is same timestamp event + // in the queue. This redundant cost should be low because in most + // causes, key events in the queue should be 2 or 4. + isHandlingAsyncEvent = + mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex(); + if (isHandlingAsyncEvent) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state does not have " + "IBUS_IGNORED_MASK but " + "same event in the queue. So, assuming it's a " + "synthesized event", + this)); + } + } + + // If it's a synthesized event, let's remove it from the posting + // event queue first. Otherwise the following blocks cannot use + // `break`. + if (isHandlingAsyncEvent) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state has IBUS_IGNORED_MASK " + "or aEvent is in the " + "posting event queue, so, it won't be handled " + "asynchronously anymore. Removing " + "the posted events from the queue", + this)); + probablyHandledAsynchronously = false; + mPostingKeyEvents.RemoveEvent(aEvent); + } + + // ibus won't send back key press events in a dead key sequcne. + if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) { + probablyHandledAsynchronously = false; + if (isHandlingAsyncEvent) { + isUnexpectedAsyncEvent = true; + break; + } + // Some keyboard layouts which have dead keys may send + // "empty" key event to make us call + // gtk_im_context_filter_keypress() to commit composed + // character during a GDK_KEY_PRESS event dispatching. + if (!gdk_keyval_to_unicode(aEvent->keyval) && + !aEvent->hardware_keycode) { + isUnexpectedAsyncEvent = true; + break; + } + break; + } + // ibus may handle key events synchronously if focused editor is + // or |ime-mode: disabled;|. However, in + // some environments, not so actually. Therefore, we need to check + // the result of gtk_im_context_filter_keypress() later. + if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) { + probablyHandledAsynchronously = false; + maybeHandledAsynchronously = !isHandlingAsyncEvent; + break; + } + break; + } + case IMContextID::Fcitx: + case IMContextID::Fcitx5: { + // See src/lib/fcitx-utils/keysym.h + static const guint FcitxKeyState_IgnoredMask = 1 << 25; + // If FcitxKeyState_IgnoredMask was set to aEvent->state, + // the event has already been handled by another process and + // it wasn't used by IME. + isHandlingAsyncEvent = !!(aEvent->state & FcitxKeyState_IgnoredMask); + if (!isHandlingAsyncEvent) { + // On some environments, FcitxKeyState_IgnoredMask flag *might* be not + // set as expected. If there were such cases, we'd keep pusing all + // events into the queue. I.e., that would cause eating a lot of + // memory until it'd be blurred. Therefore, we should check whether + // there is same timestamp event in the queue. This redundant cost + // should be low because in most causes, key events in the queue + // should be 2 or 4. + isHandlingAsyncEvent = + mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex(); + if (isHandlingAsyncEvent) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state does not have " + "FcitxKeyState_IgnoredMask " + "but same event in the queue. So, assuming it's a " + "synthesized event", + this)); + } + } + + // fcitx won't send back key press events in a dead key sequcne. + if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) { + probablyHandledAsynchronously = false; + if (isHandlingAsyncEvent) { + isUnexpectedAsyncEvent = true; + break; + } + // Some keyboard layouts which have dead keys may send + // "empty" key event to make us call + // gtk_im_context_filter_keypress() to commit composed + // character during a GDK_KEY_PRESS event dispatching. + if (!gdk_keyval_to_unicode(aEvent->keyval) && + !aEvent->hardware_keycode) { + isUnexpectedAsyncEvent = true; + break; + } + } + + // fcitx handles key events asynchronously even if focused + // editor cannot use IME actually. + + if (isHandlingAsyncEvent) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state has " + "FcitxKeyState_IgnoredMask or aEvent is in " + "the posting event queue, so, it won't be handled " + "asynchronously anymore. " + "Removing the posted events from the queue", + this)); + probablyHandledAsynchronously = false; + mPostingKeyEvents.RemoveEvent(aEvent); + break; + } + break; + } + default: + MOZ_ASSERT_UNREACHABLE( + "IME may handle key event " + "asyncrhonously, but not yet confirmed if it comes agian " + "actually"); + } + } + + if (!isUnexpectedAsyncEvent) { + mKeyboardEventWasDispatched = aKeyboardEventWasDispatched; + mKeyboardEventWasConsumed = false; + } else { + // If we didn't expect this event, we've alreday dispatched eKeyDown + // event or eKeyUp event for that. + mKeyboardEventWasDispatched = true; + // And in this case, we need to assume that another key event hasn't + // been receivied and mKeyboardEventWasConsumed keeps storing the + // dispatched eKeyDown or eKeyUp event's state. + } + mFallbackToKeyEvent = false; + mProcessingKeyEvent = aEvent; + gboolean isFiltered = gtk_im_context_filter_keypress(currentContext, aEvent); + + // If we're not sure whether the event is handled by IME asynchronously or + // synchronously, we need to trust the result of + // gtk_im_context_filter_keypress(). If it consumed and but did nothing, + // we can assume that another event will be synthesized. + if (!isHandlingAsyncEvent && maybeHandledAsynchronously) { + probablyHandledAsynchronously |= + isFiltered && !mFallbackToKeyEvent && !mKeyboardEventWasDispatched; + } + + if (aEvent->type == GDK_KEY_PRESS) { + if (isFiltered && probablyHandledAsynchronously) { + sWaitingSynthesizedKeyPressHardwareKeyCode = aEvent->hardware_keycode; + } else { + sWaitingSynthesizedKeyPressHardwareKeyCode = 0; + } + } + + // The caller of this shouldn't handle aEvent anymore if we've dispatched + // composition events or modified content with other events. + bool filterThisEvent = isFiltered && !mFallbackToKeyEvent; + + if (IsComposingOnCurrentContext() && !isFiltered && + aEvent->type == GDK_KEY_PRESS && mDispatchedCompositionString.IsEmpty()) { + // A Hangul input engine for SCIM doesn't emit preedit_end + // signal even when composition string becomes empty. On the + // other hand, we should allow to make composition with empty + // string for other languages because there *might* be such + // IM. For compromising this issue, we should dispatch + // compositionend event, however, we don't need to reset IM + // actually. + // NOTE: Don't dispatch key events as "processed by IME" since + // we need to dispatch keyboard events as IME wasn't handled it. + mProcessingKeyEvent = nullptr; + DispatchCompositionCommitEvent(currentContext, &EmptyString()); + mProcessingKeyEvent = aEvent; + // In this case, even though we handle the keyboard event here, + // but we should dispatch keydown event as + filterThisEvent = false; + } + + if (filterThisEvent && !mKeyboardEventWasDispatched) { + // If IME handled the key event but we've not dispatched eKeyDown nor + // eKeyUp event yet, we need to dispatch here unless the key event is + // now being handled by other IME process. + if (!probablyHandledAsynchronously) { + MaybeDispatchKeyEventAsProcessedByIME(eVoidEvent); + // Be aware, the widget might have been gone here. + } + // If we need to wait reply from IM, IM may send some signals to us + // without sending the key event again. In such case, we need to + // dispatch keyboard events with a copy of aEvent. Therefore, we + // need to use information of this key event to dispatch an KeyDown + // or eKeyUp event later. + else { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnKeyEvent(), putting aEvent into the queue...", this)); + mPostingKeyEvents.PutEvent(aEvent); + } + } + + mProcessingKeyEvent = nullptr; + + if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) { + // If the key event hasn't been handled by active IME nor keyboard + // layout, we can assume that the dead key sequence has been or was + // ended. Note that we should not reset it when the key event is + // GDK_KEY_RELEASE since it may not be filtered by active keyboard + // layout even in composition. + mMaybeInDeadKeySequence = false; + } + + if (aEvent->type == GDK_KEY_RELEASE) { + if (const GdkEventKey* pendingKeyPressEvent = + mPostingKeyEvents.GetCorrespondingKeyPressEvent(aEvent)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnKeyEvent(), forgetting a pending GDK_KEY_PRESS event " + "because GDK_KEY_RELEASE for the event is handled", + this)); + mPostingKeyEvents.RemoveEvent(pendingKeyPressEvent); + } + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s " + "(isFiltered=%s, mFallbackToKeyEvent=%s, " + "probablyHandledAsynchronously=%s, maybeHandledAsynchronously=%s), " + "mPostingKeyEvents.Length()=%zu, mCompositionState=%s, " + "mMaybeInDeadKeySequence=%s, mKeyboardEventWasDispatched=%s, " + "mKeyboardEventWasConsumed=%s", + this, ToChar(filterThisEvent), ToChar(isFiltered), + ToChar(mFallbackToKeyEvent), ToChar(probablyHandledAsynchronously), + ToChar(maybeHandledAsynchronously), mPostingKeyEvents.Length(), + GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence), + ToChar(mKeyboardEventWasDispatched), ToChar(mKeyboardEventWasConsumed))); + MOZ_LOG(gIMELog, LogLevel::Info, ("<<<<<<<<<<<<<<<<\n\n")); + + if (filterThisEvent) { + return KeyHandlingState::eHandled; + } + // If another call of this method has already dispatched eKeyDown event, + // we should return KeyHandlingState::eNotHandledButEventDispatched because + // the caller should've stopped handling the event if preceding eKeyDown + // event was consumed. + if (aKeyboardEventWasDispatched) { + return KeyHandlingState::eNotHandledButEventDispatched; + } + if (!mKeyboardEventWasDispatched) { + return KeyHandlingState::eNotHandled; + } + return mKeyboardEventWasConsumed + ? KeyHandlingState::eNotHandledButEventConsumed + : KeyHandlingState::eNotHandledButEventDispatched; +} + +void IMContextWrapper::OnFocusChangeInGecko(bool aFocus) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnFocusChangeInGecko(aFocus=%s),mCompositionState=%s, " + "mIMEFocusState=%s, mSetInputPurposeAndInputHints=%s", + this, ToChar(aFocus), GetCompositionStateName(), + ToString(mIMEFocusState).c_str(), + ToChar(mSetInputPurposeAndInputHints))); + + // We shouldn't carry over the removed string to another editor. + mSelectedStringRemovedByComposition.Truncate(); + mContentSelection.reset(); + + if (aFocus) { + if (mSetInputPurposeAndInputHints) { + mSetInputPurposeAndInputHints = false; + SetInputPurposeAndInputHints(); + } + NotifyIMEOfFocusChange(IMEFocusState::Focused); + } else { + NotifyIMEOfFocusChange(IMEFocusState::Blurred); + } + + // When the focus changes, we need to inform IM about the new cursor + // position. Chinese input methods generally rely on this because they + // usually don't start composition until a character is picked. + if (aFocus && EnsureToCacheContentSelection()) { + SetCursorPosition(GetActiveContext()); + } +} + +void IMContextWrapper::ResetIME() { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p ResetIME(), mCompositionState=%s, mIMEFocusState=%s", this, + GetCompositionStateName(), ToString(mIMEFocusState).c_str())); + + GtkIMContext* activeContext = GetActiveContext(); + if (MOZ_UNLIKELY(!activeContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p ResetIME(), FAILED, there are no context", this)); + return; + } + + RefPtr kungFuDeathGrip(this); + RefPtr lastFocusedWindow(mLastFocusedWindow); + + mPendingResettingIMContext = false; + gtk_im_context_reset(activeContext); + + // The last focused window might have been destroyed by a DOM event handler + // which was called by us during a call of gtk_im_context_reset(). + if (!lastFocusedWindow || + NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) || + lastFocusedWindow->Destroyed()) { + return; + } + + nsAutoString compositionString; + GetCompositionString(activeContext, compositionString); + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p ResetIME() called gtk_im_context_reset(), " + "activeContext=0x%p, mCompositionState=%s, compositionString=%s, " + "mIMEFocusState=%s", + this, activeContext, GetCompositionStateName(), + NS_ConvertUTF16toUTF8(compositionString).get(), + ToString(mIMEFocusState).c_str())); + + // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still + // used in Japan!) sends only "preedit_changed" signal with empty + // composition string synchronously. Therefore, if composition string + // is now empty string, we should assume that the IME won't send + // "commit" signal. + if (IsComposing() && compositionString.IsEmpty()) { + // WARNING: The widget might have been gone after this. + DispatchCompositionCommitEvent(activeContext, &EmptyString()); + } +} + +nsresult IMContextWrapper::EndIMEComposition(nsWindow* aCaller) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return NS_OK; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p EndIMEComposition(aCaller=0x%p), " + "mCompositionState=%s", + this, aCaller, GetCompositionStateName())); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p EndIMEComposition(), FAILED, the caller isn't " + "focused window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return NS_OK; + } + + if (!IsComposing()) { + return NS_OK; + } + + // Currently, GTK has API neither to commit nor to cancel composition + // forcibly. Therefore, TextComposition will recompute commit string for + // the request even if native IME will cause unexpected commit string. + // So, we don't need to emulate commit or cancel composition with + // proper composition events. + // XXX ResetIME() might not enough for finishing compositoin on some + // environments. We should emulate focus change too because some IMEs + // may commit or cancel composition at blur. + ResetIME(); + + return NS_OK; +} + +void IMContextWrapper::OnLayoutChange() { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + if (IsComposing()) { + SetCursorPosition(GetActiveContext()); + } else { + // If not composing, candidate window position is updated before key + // down + mSetCursorPositionOnKeyEvent = true; + } + mLayoutChanged = true; +} + +void IMContextWrapper::OnUpdateComposition() { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + if (!IsComposing()) { + // Composition has been committed. So we need update selection for + // caret later + mContentSelection.reset(); + EnsureToCacheContentSelection(); + mSetCursorPositionOnKeyEvent = true; + } + + // If we've already set candidate window position, we don't need to update + // the position with update composition notification. + if (!mLayoutChanged) { + SetCursorPosition(GetActiveContext()); + } +} + +void IMContextWrapper::SetInputContext(nsWindow* aCaller, + const InputContext* aContext, + const InputContextAction* aAction) { + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ " + "mEnabled=%s }, mHTMLInputType=%s })", + this, aCaller, ToString(aContext->mIMEState.mEnabled).c_str(), + NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get())); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetInputContext(), FAILED, " + "the caller isn't focused window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return; + } + + if (!mContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetInputContext(), FAILED, " + "there are no context", + this)); + return; + } + + if (sLastFocusedContext != this) { + mInputContext = *aContext; + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p SetInputContext(), succeeded, " + "but we're not active", + this)); + return; + } + + const bool changingEnabledState = + aContext->IsInputAttributeChanged(mInputContext); + + // Release current IME focus if IME is enabled. + if (changingEnabledState && mInputContext.mIMEState.IsEditable()) { + if (IsComposing()) { + EndIMEComposition(mLastFocusedWindow); + } + if (mIMEFocusState == IMEFocusState::Focused) { + NotifyIMEOfFocusChange(IMEFocusState::BlurredWithoutFocusChange); + } + } + + mInputContext = *aContext; + mSetInputPurposeAndInputHints = false; + + if (!changingEnabledState || !mInputContext.mIMEState.IsEditable()) { + return; + } + + // If the input context was temporarily disabled without a focus change, + // it must be ready to query content even if the focused content is in + // a remote process. In this case, we should set IME focus right now. + if (mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) { + SetInputPurposeAndInputHints(); + NotifyIMEOfFocusChange(IMEFocusState::Focused); + return; + } + + // Otherwise, we cannot set input-purpose and input-hints right now because + // setting them may require to set focus immediately for IME own's UI. + // However, at this moment, `ContentCacheInParent` does not have content + // cache, it'll be available after `NOTIFY_IME_OF_FOCUS` notification. + // Therefore, we set them at receiving the notification. + mSetInputPurposeAndInputHints = true; +} + +void IMContextWrapper::SetInputPurposeAndInputHints() { + GtkIMContext* currentContext = GetCurrentContext(); + if (!currentContext) { + return; + } + + GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM; + const nsString& inputType = mInputContext.mHTMLInputType; + // Password case has difficult issue. Desktop IMEs disable composition if + // input-purpose is password. For disabling IME on |ime-mode: disabled;|, we + // need to check mEnabled value instead of inputType value. This hack also + // enables composition on . + // This is right behavior of ime-mode on desktop. + // + // On the other hand, IME for tablet devices may provide a specific software + // keyboard for password field. If so, the behavior might look strange on + // both: + // + // + // + // Temporarily, we should focus on desktop environment for now. I.e., let's + // ignore tablet devices for now. When somebody reports actual trouble on + // tablet devices, we should try to look for a way to solve actual problem. + if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) { + purpose = GTK_INPUT_PURPOSE_PASSWORD; + } else if (inputType.EqualsLiteral("email")) { + purpose = GTK_INPUT_PURPOSE_EMAIL; + } else if (inputType.EqualsLiteral("url")) { + purpose = GTK_INPUT_PURPOSE_URL; + } else if (inputType.EqualsLiteral("tel")) { + purpose = GTK_INPUT_PURPOSE_PHONE; + } else if (inputType.EqualsLiteral("number")) { + purpose = GTK_INPUT_PURPOSE_NUMBER; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("decimal")) { + purpose = GTK_INPUT_PURPOSE_NUMBER; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("email")) { + purpose = GTK_INPUT_PURPOSE_EMAIL; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("numeric")) { + purpose = GTK_INPUT_PURPOSE_DIGITS; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("tel")) { + purpose = GTK_INPUT_PURPOSE_PHONE; + } else if (mInputContext.mHTMLInputMode.EqualsLiteral("url")) { + purpose = GTK_INPUT_PURPOSE_URL; + } + // Search by type and inputmode isn't supported on GTK. + + g_object_set(currentContext, "input-purpose", purpose, nullptr); + + // Although GtkInputHints is enum type, value is bit field. + gint hints = GTK_INPUT_HINT_NONE; + if (mInputContext.mHTMLInputMode.EqualsLiteral("none")) { + hints |= GTK_INPUT_HINT_INHIBIT_OSK; + } + + if (mInputContext.mAutocapitalize.EqualsLiteral("characters")) { + hints |= GTK_INPUT_HINT_UPPERCASE_CHARS; + } else if (mInputContext.mAutocapitalize.EqualsLiteral("sentences")) { + hints |= GTK_INPUT_HINT_UPPERCASE_SENTENCES; + } else if (mInputContext.mAutocapitalize.EqualsLiteral("words")) { + hints |= GTK_INPUT_HINT_UPPERCASE_WORDS; + } + + g_object_set(currentContext, "input-hints", hints, nullptr); +} + +InputContext IMContextWrapper::GetInputContext() { + mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED; + return mInputContext; +} + +GtkIMContext* IMContextWrapper::GetCurrentContext() const { + if (IsEnabled()) { + return mContext; + } + if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) { + return mSimpleContext; + } + return mDummyContext; +} + +bool IMContextWrapper::IsValidContext(GtkIMContext* aContext) const { + if (!aContext) { + return false; + } + return aContext == mContext || aContext == mSimpleContext || + aContext == mDummyContext; +} + +bool IMContextWrapper::IsEnabled() const { + return mInputContext.mIMEState.mEnabled == IMEEnabled::Enabled || + (!sUseSimpleContext && + mInputContext.mIMEState.mEnabled == IMEEnabled::Password); +} + +void IMContextWrapper::NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState) { + MOZ_ASSERT_IF(aIMEFocusState == IMEFocusState::BlurredWithoutFocusChange, + mIMEFocusState != IMEFocusState::Blurred); + if (mIMEFocusState == aIMEFocusState) { + return; + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p NotifyIMEOfFocusChange(aIMEFocusState=%s), mIMEFocusState=%s, " + "sLastFocusedContext=0x%p", + this, ToString(aIMEFocusState).c_str(), + ToString(mIMEFocusState).c_str(), sLastFocusedContext)); + MOZ_ASSERT(!mSetInputPurposeAndInputHints); + + // If we've already made IME blurred at setting the input context disabled + // and it's now completely blurred by a focus move, we need only to update + // mIMEFocusState and when the input context gets enabled, we cannot set + // IME focus immediately. + if (aIMEFocusState == IMEFocusState::Blurred && + mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) { + mIMEFocusState = IMEFocusState::Blurred; + return; + } + + auto Blur = [&](IMEFocusState aInternalState) { + GtkIMContext* currentContext = GetCurrentContext(); + if (MOZ_UNLIKELY(!currentContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p NotifyIMEOfFocusChange()::Blur(), FAILED, " + "there is no context", + this)); + return; + } + gtk_im_context_focus_out(currentContext); + mIMEFocusState = aInternalState; + }; + + if (aIMEFocusState != IMEFocusState::Focused) { + return Blur(aIMEFocusState); + } + + GtkIMContext* currentContext = GetCurrentContext(); + if (MOZ_UNLIKELY(!currentContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p NotifyIMEOfFocusChange(), FAILED, " + "there is no context", + this)); + return; + } + + if (sLastFocusedContext && sLastFocusedContext != this) { + sLastFocusedContext->NotifyIMEOfFocusChange(IMEFocusState::Blurred); + } + + sLastFocusedContext = this; + + // Forget all posted key events when focus is moved since they shouldn't + // be fired in different editor. + sWaitingSynthesizedKeyPressHardwareKeyCode = 0; + mPostingKeyEvents.Clear(); + + gtk_im_context_focus_in(currentContext); + mIMEFocusState = aIMEFocusState; + mSetCursorPositionOnKeyEvent = true; + + if (!IsEnabled()) { + // We should release IME focus for uim and scim. + // These IMs are using snooper that is released at losing focus. + Blur(IMEFocusState::BlurredWithoutFocusChange); + } +} + +void IMContextWrapper::OnSelectionChange( + nsWindow* aCaller, const IMENotification& aIMENotification) { + const bool isSelectionRangeChanged = + mContentSelection.isNothing() || + !aIMENotification.mSelectionChangeData.EqualsRange( + mContentSelection.ref()); + mContentSelection = + Some(ContentSelection(aIMENotification.mSelectionChangeData)); + const bool retrievedSurroundingSignalReceived = + mRetrieveSurroundingSignalReceived; + mRetrieveSurroundingSignalReceived = false; + + if (MOZ_UNLIKELY(IsDestroyed())) { + return; + } + + const IMENotification::SelectionChangeDataBase& selectionChangeData = + aIMENotification.mSelectionChangeData; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ " + "mSelectionChangeData=%s }), " + "mCompositionState=%s, mIsDeletingSurrounding=%s, " + "mRetrieveSurroundingSignalReceived=%s, isSelectionRangeChanged=%s", + this, aCaller, ToString(selectionChangeData).c_str(), + GetCompositionStateName(), ToChar(mIsDeletingSurrounding), + ToChar(retrievedSurroundingSignalReceived), + ToChar(isSelectionRangeChanged))); + + if (aCaller != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnSelectionChange(), FAILED, " + "the caller isn't focused window, mLastFocusedWindow=0x%p", + this, mLastFocusedWindow)); + return; + } + + if (!IsComposing()) { + // Now we have no composition (mostly situation on calling this method) + // If we have it, it will set by + // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED. + mSetCursorPositionOnKeyEvent = true; + } + + // The focused editor might have placeholder text with normal text node. + // In such case, the text node must be removed from a compositionstart + // event handler. So, we're dispatching eCompositionStart, + // we should ignore selection change notification. + if (mCompositionState == eCompositionState_CompositionStartDispatched) { + if (NS_WARN_IF(mContentSelection.isNothing())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnSelectionChange(), FAILED, " + "new offset is too large, cannot keep composing", + this)); + } else if (mContentSelection->HasRange()) { + // Modify the selection start offset with new offset. + mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset(); + // XXX We should modify mSelectedStringRemovedByComposition? + // But how? + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p OnSelectionChange(), ignored, mCompositionStart " + "is updated to %u, the selection change doesn't cause " + "resetting IM context", + this, mCompositionStart)); + // And don't reset the IM context. + return; + } else { + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p OnSelectionChange(), ignored, because of no selection range", + this)); + return; + } + // Otherwise, reset the IM context due to impossible to keep composing. + } + + // If the selection change is caused by deleting surrounding text, + // we shouldn't need to notify IME of selection change. + if (mIsDeletingSurrounding) { + return; + } + + bool occurredBeforeComposition = + IsComposing() && !selectionChangeData.mOccurredDuringComposition && + !selectionChangeData.mCausedByComposition; + if (occurredBeforeComposition) { + mPendingResettingIMContext = true; + } + + // When the selection change is caused by dispatching composition event, + // selection set event and/or occurred before starting current composition, + // we shouldn't notify IME of that and commit existing composition. + // Don't do this even if selection is not changed actually. For example, + // fcitx has direct input mode which does not insert composing string, but + // inserts commited text for each key sequence (i.e., there is "invisible" + // composition string). In the world after bug 1712269, we don't use a + // set of composition events for this kind of IME. Therefore, + // SelectionChangeData.mCausedByComposition is not expected value for here + // if this call is caused by a preceding commit. And if the preceding commit + // is triggered by a key type for next word, resetting IME state makes fcitx + // discard the pending input for the next word. Thus, we need to check + // whether the selection range is actually changed here. + if (!selectionChangeData.mCausedByComposition && + !selectionChangeData.mCausedBySelectionEvent && isSelectionRangeChanged && + !occurredBeforeComposition) { + // Hack for ibus-pinyin. ibus-pinyin will synthesize a set of + // composition which commits with empty string after calling + // gtk_im_context_reset(). Therefore, selecting text causes + // unexpectedly removing it. For preventing it but not breaking the + // other IMEs which use surrounding text, we should call it only when + // surrounding text has been retrieved after last selection range was + // set. If it's not retrieved, that means that current IME doesn't + // have any content cache, so, it must not need the notification of + // selection change. + if (IsComposing() || retrievedSurroundingSignalReceived) { + ResetIME(); + } + } +} + +/* static */ +void IMContextWrapper::OnThemeChanged() { + if (auto* provider = SelectionStyleProvider::GetExistingInstance()) { + provider->OnThemeChanged(); + } +} + +/* static */ +void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule) { + aModule->OnStartCompositionNative(aContext); +} + +void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) { + // IME may synthesize composition asynchronously after filtering a + // GDK_KEY_PRESS event. In that case, we should handle composition with + // emulating the usual case, i.e., this is called in the stack of + // OnKeyEvent(). + Maybe> maybeRestoreProcessingKeyEvent; + if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) { + GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent(); + if (keyEvent && keyEvent->type == GDK_KEY_PRESS && + KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) == + KEY_NAME_INDEX_USE_STRING) { + maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent); + mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent(); + } + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnStartCompositionNative(aContext=0x%p), " + "current context=0x%p, mComposingContext=0x%p", + this, aContext, GetCurrentContext(), mComposingContext)); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (GetCurrentContext() != aContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnStartCompositionNative(), FAILED, " + "given context doesn't match", + this)); + return; + } + + if (mComposingContext && aContext != mComposingContext) { + // XXX For now, we should ignore this odd case, just logging. + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnStartCompositionNative(), Warning, " + "there is already a composing context but starting new " + "composition with different context", + this)); + } + + // IME may start composition without "preedit_start" signal. Therefore, + // mComposingContext will be initialized in DispatchCompositionStart(). + + if (!DispatchCompositionStart(aContext)) { + return; + } + mCompositionTargetRange.mOffset = mCompositionStart; + mCompositionTargetRange.mLength = 0; +} + +/* static */ +void IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule) { + aModule->OnEndCompositionNative(aContext); +} + +void IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnEndCompositionNative(aContext=0x%p), mComposingContext=0x%p", + this, aContext, mComposingContext)); + + // See bug 472635, we should do nothing if IM context doesn't match. + // Note that if this is called after focus move, the context may different + // from any our owning context. + if (!IsValidContext(aContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnEndCompositionNative(), FAILED, " + "given context doesn't match with any context", + this)); + return; + } + + // If we've not started composition with aContext, we should ignore it. + if (aContext != mComposingContext) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnEndCompositionNative(), Warning, " + "given context doesn't match with mComposingContext", + this)); + return; + } + + g_object_unref(mComposingContext); + mComposingContext = nullptr; + + // If we already handled the commit event, we should do nothing here. + if (IsComposing()) { + if (!DispatchCompositionCommitEvent(aContext)) { + // If the widget is destroyed, we should do nothing anymore. + return; + } + } + + if (mPendingResettingIMContext) { + ResetIME(); + } +} + +/* static */ +void IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext, + IMContextWrapper* aModule) { + RefPtr module = aModule; + module->OnChangeCompositionNative(aContext); + + if (module->IsDestroyed()) { + // A strong reference is already held during "preedit-changed" emission, + // but _ibus_context_destroy_cb() in ibus 1.5.28 and + // _fcitx_im_context_close_im_cb() in fcitx 4.2.9.9 want their + // GtkIMContexts to live a little longer. See bug 1824634. + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, [context = RefPtr{aContext}]() {})); + } +} + +void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) { + // IME may synthesize composition asynchronously after filtering a + // GDK_KEY_PRESS event. In that case, we should handle composition with + // emulating the usual case, i.e., this is called in the stack of + // OnKeyEvent(). + Maybe> maybeRestoreProcessingKeyEvent; + if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) { + GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent(); + if (keyEvent && keyEvent->type == GDK_KEY_PRESS && + KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) == + KEY_NAME_INDEX_USE_STRING) { + maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent); + mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent(); + } + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnChangeCompositionNative(aContext=0x%p), " + "mComposingContext=0x%p", + this, aContext, mComposingContext)); + + // See bug 472635, we should do nothing if IM context doesn't match. + // Note that if this is called after focus move, the context may different + // from any our owning context. + if (!IsValidContext(aContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnChangeCompositionNative(), FAILED, " + "given context doesn't match with any context", + this)); + return; + } + + if (mComposingContext && aContext != mComposingContext) { + // XXX For now, we should ignore this odd case, just logging. + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnChangeCompositionNative(), Warning, " + "given context doesn't match with composing context", + this)); + } + + nsAutoString compositionString; + GetCompositionString(aContext, compositionString); + if (!IsComposing() && compositionString.IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnChangeCompositionNative(), Warning, does nothing " + "because has not started composition and composing string is " + "empty", + this)); + mDispatchedCompositionString.Truncate(); + return; // Don't start the composition with empty string. + } + + // Be aware, widget can be gone + DispatchCompositionChangeEvent(aContext, compositionString); +} + +/* static */ +gboolean IMContextWrapper::OnRetrieveSurroundingCallback( + GtkIMContext* aContext, IMContextWrapper* aModule) { + return aModule->OnRetrieveSurroundingNative(aContext); +} + +gboolean IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), " + "current context=0x%p", + this, aContext, GetCurrentContext())); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (GetCurrentContext() != aContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnRetrieveSurroundingNative(), FAILED, " + "given context doesn't match", + this)); + return FALSE; + } + + nsAutoString uniStr; + uint32_t cursorPos; + if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) { + return FALSE; + } + + // Despite taking a pointer and a length, IBus wants the string to be + // zero-terminated and doesn't like U+0000 within the string. + uniStr.ReplaceChar(char16_t(0), char16_t(0xFFFD)); + + NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos)); + uint32_t cursorPosInUTF8 = utf8Str.Length(); + AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str); + gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(), + cursorPosInUTF8); + mRetrieveSurroundingSignalReceived = true; + return TRUE; +} + +/* static */ +gboolean IMContextWrapper::OnDeleteSurroundingCallback( + GtkIMContext* aContext, gint aOffset, gint aNChars, + IMContextWrapper* aModule) { + return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars); +} + +gboolean IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext, + gint aOffset, + gint aNChars) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, " + "aNChar=%d), current context=0x%p", + this, aContext, aOffset, aNChars, GetCurrentContext())); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (GetCurrentContext() != aContext) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnDeleteSurroundingNative(), FAILED, " + "given context doesn't match", + this)); + return FALSE; + } + + AutoRestore saveDeletingSurrounding(mIsDeletingSurrounding); + mIsDeletingSurrounding = true; + if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) { + return TRUE; + } + + // failed + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnDeleteSurroundingNative(), FAILED, " + "cannot delete text", + this)); + return FALSE; +} + +/* static */ +void IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext, + const gchar* aString, + IMContextWrapper* aModule) { + aModule->OnCommitCompositionNative(aContext, aString); +} + +void IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext, + const gchar* aUTF8Char) { + const gchar emptyStr = 0; + const gchar* commitString = aUTF8Char ? aUTF8Char : &emptyStr; + NS_ConvertUTF8toUTF16 utf16CommitString(commitString); + + // IME may synthesize composition asynchronously after filtering a + // GDK_KEY_PRESS event. In that case, we should handle composition with + // emulating the usual case, i.e., this is called in the stack of + // OnKeyEvent(). + Maybe> maybeRestoreProcessingKeyEvent; + if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) { + GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent(); + if (keyEvent && keyEvent->type == GDK_KEY_PRESS && + KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) == + KEY_NAME_INDEX_USE_STRING) { + maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent); + mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent(); + } + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(aContext=0x%p), " + "current context=0x%p, active context=0x%p, commitString=\"%s\", " + "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s", + this, aContext, GetCurrentContext(), GetActiveContext(), + commitString, mProcessingKeyEvent, ToChar(IsComposingOn(aContext)))); + + // See bug 472635, we should do nothing if IM context doesn't match. + if (!IsValidContext(aContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnCommitCompositionNative(), FAILED, " + "given context doesn't match", + this)); + return; + } + + // If we are not in composition and committing with empty string, + // we need to do nothing because if we continued to handle this + // signal, we would dispatch compositionstart, text, compositionend + // events with empty string. Of course, they are unnecessary events + // for Web applications and our editor. + if (!IsComposingOn(aContext) && utf16CommitString.IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnCommitCompositionNative(), Warning, does nothing " + "because has not started composition and commit string is empty", + this)); + return; + } + + // If IME doesn't change their keyevent that generated this commit, + // we should treat that IME didn't handle the key event because + // web applications want to receive "keydown" and "keypress" event + // in such case. + // NOTE: While a key event is being handled, this might be caused on + // current context. Otherwise, this may be caused on active context. + if (!IsComposingOn(aContext) && mProcessingKeyEvent && + mProcessingKeyEvent->type == GDK_KEY_PRESS && + aContext == GetCurrentContext()) { + char keyval_utf8[8]; /* should have at least 6 bytes of space */ + gint keyval_utf8_len; + guint32 keyval_unicode; + + keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval); + keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8); + keyval_utf8[keyval_utf8_len] = '\0'; + + // If committing string is exactly same as a character which is + // produced by the key, eKeyDown and eKeyPress event should be + // dispatched by the caller of OnKeyEvent() normally. Note that + // mMaybeInDeadKeySequence will be set to false by OnKeyEvent() + // since we set mFallbackToKeyEvent to true here. + if (!strcmp(commitString, keyval_utf8)) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "we'll send normal key event", + this)); + mFallbackToKeyEvent = true; + return; + } + + // If we're in a dead key sequence, commit string is a character in + // the BMP and mProcessingKeyEvent produces some characters but it's + // not same as committing string, we should dispatch an eKeyPress + // event from here. + WidgetKeyboardEvent keyDownEvent(true, eKeyDown, mLastFocusedWindow); + KeymapWrapper::InitKeyEvent(keyDownEvent, mProcessingKeyEvent, false); + if (mMaybeInDeadKeySequence && utf16CommitString.Length() == 1 && + keyDownEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) { + mKeyboardEventWasDispatched = true; + // Anyway, we're not in dead key sequence anymore. + mMaybeInDeadKeySequence = false; + + RefPtr dispatcher = GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p OnCommitCompositionNative(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return; + } + + // First, dispatch eKeyDown event. + keyDownEvent.mKeyValue = utf16CommitString; + nsEventStatus status = nsEventStatus_eIgnore; + bool dispatched = dispatcher->DispatchKeyboardEvent( + eKeyDown, keyDownEvent, status, mProcessingKeyEvent); + if (!dispatched || status == nsEventStatus_eConsumeNoDefault) { + mKeyboardEventWasConsumed = true; + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "doesn't dispatch eKeyPress event because the preceding " + "eKeyDown event was not dispatched or was consumed", + this)); + return; + } + if (mLastFocusedWindow != keyDownEvent.mWidget || + mLastFocusedWindow->Destroyed()) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p OnCommitCompositionNative(), Warning, " + "stop dispatching eKeyPress event because the preceding " + "eKeyDown event caused changing focused widget or " + "destroyed", + this)); + return; + } + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "dispatched eKeyDown event for the committed character", + this)); + + // Next, dispatch eKeyPress event. + dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status, + mProcessingKeyEvent); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p OnCommitCompositionNative(), " + "dispatched eKeyPress event for the committed character", + this)); + return; + } + } + + NS_ConvertUTF8toUTF16 str(commitString); + // Be aware, widget can be gone + DispatchCompositionCommitEvent(aContext, &str); +} + +void IMContextWrapper::GetCompositionString(GtkIMContext* aContext, + nsAString& aCompositionString) { + gchar* preedit_string; + gint cursor_pos; + PangoAttrList* feedback_list; + gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list, + &cursor_pos); + if (preedit_string && *preedit_string) { + CopyUTF8toUTF16(MakeStringSpan(preedit_string), aCompositionString); + } else { + aCompositionString.Truncate(); + } + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p GetCompositionString(aContext=0x%p), " + "aCompositionString=\"%s\"", + this, aContext, preedit_string)); + + pango_attr_list_unref(feedback_list); + g_free(preedit_string); +} + +bool IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME( + EventMessage aFollowingEvent) { + if (!mLastFocusedWindow) { + return false; + } + + if (!mIsKeySnooped && + ((!mProcessingKeyEvent && mPostingKeyEvents.IsEmpty()) || + (mProcessingKeyEvent && mKeyboardEventWasDispatched))) { + return true; + } + + // A "keydown" or "keyup" event handler may change focus with the + // following event. In such case, we need to cancel this composition. + // So, we need to store IM context now because mComposingContext may be + // overwritten with different context if calling this method recursively. + // Note that we don't need to grab the context here because |context| + // will be used only for checking if it's same as mComposingContext. + GtkIMContext* oldCurrentContext = GetCurrentContext(); + GtkIMContext* oldComposingContext = mComposingContext; + + RefPtr lastFocusedWindow(mLastFocusedWindow); + + if (mProcessingKeyEvent || !mPostingKeyEvents.IsEmpty()) { + if (mProcessingKeyEvent) { + mKeyboardEventWasDispatched = true; + } + // If we're not handling a key event synchronously, the signal may be + // sent by IME without sending key event to us. In such case, we + // should dispatch keyboard event for the last key event which was + // posted to other IME process. + GdkEventKey* sourceEvent = mProcessingKeyEvent + ? mProcessingKeyEvent + : mPostingKeyEvents.GetFirstEvent(); + + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(" + "aFollowingEvent=%s), dispatch %s %s " + "event: { type=%s, keyval=%s, unicode=0x%X, state=%s, " + "time=%u, hardware_keycode=%u, group=%u }", + this, ToChar(aFollowingEvent), + ToChar(sourceEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp), + mProcessingKeyEvent ? "processing" : "posted", + GetEventType(sourceEvent), gdk_keyval_name(sourceEvent->keyval), + gdk_keyval_to_unicode(sourceEvent->keyval), + GetEventStateName(sourceEvent->state, mIMContextID).get(), + sourceEvent->time, sourceEvent->hardware_keycode, sourceEvent->group)); + + // Let's dispatch eKeyDown event or eKeyUp event now. Note that only + // when we're not in a dead key composition, we should mark the + // eKeyDown and eKeyUp event as "processed by IME" since we should + // expose raw keyCode and key value to web apps the key event is a + // part of a dead key sequence. + // FYI: We should ignore if default of preceding keydown or keyup + // event is prevented since even on the other browsers, web + // applications cannot cancel the following composition event. + // Spec bug: https://github.com/w3c/uievents/issues/180 + KeymapWrapper::DispatchKeyDownOrKeyUpEvent(lastFocusedWindow, sourceEvent, + !mMaybeInDeadKeySequence, + &mKeyboardEventWasConsumed); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup " + "event is dispatched", + this)); + + if (!mProcessingKeyEvent) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), removing first " + "event from the queue", + this)); + mPostingKeyEvents.RemoveEvent(sourceEvent); + } + } else { + MOZ_ASSERT(mIsKeySnooped); + // Currently, we support key snooper mode of uim and wayland only. + MOZ_ASSERT(mIMContextID == IMContextID::Uim || + mIMContextID == IMContextID::Wayland); + // uim sends "preedit_start" signal and "preedit_changed" separately + // at starting composition, "commit" and "preedit_end" separately at + // committing composition. + + // Currently, we should dispatch only fake eKeyDown event because + // we cannot decide which is the last signal of each key operation + // and Chromium also dispatches only "keydown" event in this case. + bool dispatchFakeKeyDown = false; + switch (aFollowingEvent) { + case eCompositionStart: + case eCompositionCommit: + case eCompositionCommitAsIs: + case eContentCommandInsertText: + dispatchFakeKeyDown = true; + break; + // XXX Unfortunately, I don't have a good idea to prevent to + // dispatch redundant eKeyDown event for eCompositionStart + // immediately after "delete_surrounding" signal. However, + // not dispatching eKeyDown event is worse than dispatching + // redundant eKeyDown events. + case eContentCommandDelete: + dispatchFakeKeyDown = true; + break; + // We need to prevent to dispatch redundant eKeyDown event for + // eCompositionChange immediately after eCompositionStart. So, + // We should not dispatch eKeyDown event if dispatched composition + // string is still empty string. + case eCompositionChange: + dispatchFakeKeyDown = !mDispatchedCompositionString.IsEmpty(); + break; + default: + MOZ_ASSERT_UNREACHABLE("Do you forget to handle the case?"); + break; + } + + if (dispatchFakeKeyDown) { + WidgetKeyboardEvent fakeKeyDownEvent(true, eKeyDown, lastFocusedWindow); + fakeKeyDownEvent.mKeyCode = NS_VK_PROCESSKEY; + fakeKeyDownEvent.mKeyNameIndex = KEY_NAME_INDEX_Process; + // It's impossible to get physical key information in this case but + // this should be okay since web apps shouldn't do anything with + // physical key information during composition. + fakeKeyDownEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN; + + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(" + "aFollowingEvent=%s), dispatch fake eKeyDown event", + this, ToChar(aFollowingEvent))); + + KeymapWrapper::DispatchKeyDownOrKeyUpEvent( + lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), " + "fake keydown event is dispatched", + this)); + } + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the " + "focused widget was destroyed/changed by a key event", + this)); + return false; + } + + // If the dispatched keydown event caused moving focus and that also + // caused changing active context, we need to cancel composition here. + if (GetCurrentContext() != oldCurrentContext) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the key " + "event causes changing active IM context", + this)); + if (mComposingContext == oldComposingContext) { + // Only when the context is still composing, we should call + // ResetIME() here. Otherwise, it should've already been + // cleaned up. + ResetIME(); + } + return false; + } + + return true; +} + +bool IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext)); + + if (IsComposing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "we're already in composition", + this)); + return true; + } + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + if (NS_WARN_IF(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "cannot query the selection offset", + this)); + return false; + } + + if (NS_WARN_IF(!mContentSelection->HasRange())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "due to no selection", + this)); + return false; + } + + mComposingContext = static_cast(g_object_ref(aContext)); + MOZ_ASSERT(mComposingContext); + + // Keep the last focused window alive + RefPtr lastFocusedWindow(mLastFocusedWindow); + + // XXX The composition start point might be changed by composition events + // even though we strongly hope it doesn't happen. + // Every composition event should have the start offset for the result + // because it may high cost if we query the offset every time. + mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset(); + mDispatchedCompositionString.Truncate(); + + // If this composition is started by a key press, we need to dispatch + // eKeyDown or eKeyUp event before dispatching eCompositionStart event. + // Note that dispatching a keyboard event which is marked as "processed + // by IME" is okay since Chromium also dispatches keyboard event as so. + if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionStart)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionStart(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return false; + } + + RefPtr dispatcher = GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + static bool sHasSetTelemetry = false; + if (!sHasSetTelemetry) { + sHasSetTelemetry = true; + NS_ConvertUTF8toUTF16 im(GetIMName()); + // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp + if (im.Length() > 72) { + if (NS_IS_SURROGATE_PAIR(im[72 - 2], im[72 - 1])) { + im.Truncate(72 - 2); + } else { + im.Truncate(72 - 1); + } + // U+2026 is "..." + im.Append(char16_t(0x2026)); + } + Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_LINUX, im, + true); + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p DispatchCompositionStart(), dispatching " + "compositionstart... (mCompositionStart=%u)", + this, mCompositionStart)); + mCompositionState = eCompositionState_CompositionStartDispatched; + nsEventStatus status; + dispatcher->StartComposition(status); + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionStart(), FAILED, the focused " + "widget was destroyed/changed by compositionstart event", + this)); + return false; + } + + return true; +} + +bool IMContextWrapper::DispatchCompositionChangeEvent( + GtkIMContext* aContext, const nsAString& aCompositionString) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext)); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + if (!IsComposing()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p DispatchCompositionChangeEvent(), the composition " + "wasn't started, force starting...", + this)); + if (!DispatchCompositionStart(aContext)) { + return false; + } + } + // If this composition string change caused by a key press, we need to + // dispatch eKeyDown or eKeyUp before dispatching eCompositionChange event. + else if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionChange)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionChangeEvent(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return false; + } + + RefPtr dispatcher = GetTextEventDispatcher(); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + // Store the selected string which will be removed by following + // compositionchange event. + if (mCompositionState == eCompositionState_CompositionStartDispatched) { + if (NS_WARN_IF(!EnsureToCacheContentSelection( + &mSelectedStringRemovedByComposition))) { + // XXX How should we behave in this case?? + } else if (mContentSelection->HasRange()) { + // XXX We should assume, for now, any web applications don't change + // selection at handling this compositionchange event. + mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset(); + } else { + // If there is no selection range, we should keep previously storing + // mCompositionStart. + } + } + + RefPtr rangeArray = + CreateTextRangeArray(aContext, aCompositionString); + + rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to SetPendingComposition() failure", + this)); + return false; + } + + mCompositionState = eCompositionState_CompositionChangeEventDispatched; + + // We cannot call SetCursorPosition for e10s-aware. + // DispatchEvent is async on e10s, so composition rect isn't updated now + // on tab parent. + mDispatchedCompositionString = aCompositionString; + mLayoutChanged = false; + mCompositionTargetRange.mOffset = + mCompositionStart + rangeArray->TargetClauseOffset(); + mCompositionTargetRange.mLength = rangeArray->TargetClauseLength(); + + RefPtr lastFocusedWindow(mLastFocusedWindow); + nsEventStatus status; + rv = dispatcher->FlushPendingComposition(status); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to FlushPendingComposition() failure", + this)); + return false; + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, the " + "focused widget was destroyed/changed by " + "compositionchange event", + this)); + return false; + } + return true; +} + +bool IMContextWrapper::DispatchCompositionCommitEvent( + GtkIMContext* aContext, const nsAString* aCommitString) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, " + "aCommitString=0x%p, (\"%s\"))", + this, aContext, aCommitString, + aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "")); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "there are no focused window in this module", + this)); + return false; + } + + // TODO: We need special care to handle request to commit composition + // by content while we're committing composition because we have + // commit string information now but IME may not have composition + // anymore. Therefore, we may not be able to handle commit as + // expected. However, this is rare case because this situation + // never occurs with remote content. So, it's okay to fix this + // issue later. (Perhaps, TextEventDisptcher should do it for + // all platforms. E.g., creating WillCommitComposition()?) + RefPtr lastFocusedWindow(mLastFocusedWindow); + RefPtr dispatcher; + if (!IsComposing() && + !StaticPrefs::intl_ime_use_composition_events_for_insert_text()) { + if (!aCommitString || aCommitString->IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "did nothing due to inserting empty string without composition", + this)); + return true; + } + if (MOZ_UNLIKELY(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionCommitEvent(), Warning, " + "Failed to cache selection before dispatching " + "eContentCommandInsertText event", + this)); + } + if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandInsertText)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionCommitEvent(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return false; + } + // Emulate selection until receiving actual selection range. This is + // important for OnSelectionChange. If selection is not changed by web + // apps, i.e., selection range is same as what selection expects, we + // shouldn't reset IME because the trigger of causing this commit may be an + // input for next composition and we shouldn't cancel it. + if (mContentSelection.isSome()) { + mContentSelection->Collapse( + (mContentSelection->HasRange() + ? mContentSelection->OffsetAndDataRef().StartOffset() + : mCompositionStart) + + aCommitString->Length()); + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p DispatchCompositionCommitEvent(), mContentSelection=%s", + this, ToString(mContentSelection).c_str())); + } + MOZ_ASSERT(!dispatcher); + } else { + if (!IsComposing()) { + if (!aCommitString || aCommitString->IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "there is no composition and empty commit string", + this)); + return true; + } + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p DispatchCompositionCommitEvent(), " + "the composition wasn't started, force starting...", + this)); + if (!DispatchCompositionStart(aContext)) { + return false; + } + } + // If this commit caused by a key press, we need to dispatch eKeyDown or + // eKeyUp before dispatching composition events. + else if (!MaybeDispatchKeyEventAsProcessedByIME( + aCommitString ? eCompositionCommit : eCompositionCommitAsIs)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DispatchCompositionCommitEvent(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + mCompositionState = eCompositionState_NotComposing; + return false; + } + + dispatcher = GetTextEventDispatcher(); + MOZ_ASSERT(dispatcher); + nsresult rv = dispatcher->BeginNativeInputTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "due to BeginNativeInputTransaction() failure", + this)); + return false; + } + + // Emulate selection until receiving actual selection range. + const uint32_t offsetToPutCaret = + mCompositionStart + (aCommitString + ? aCommitString->Length() + : mDispatchedCompositionString.Length()); + if (mContentSelection.isSome()) { + mContentSelection->Collapse(offsetToPutCaret); + } else { + // TODO: We should guarantee that there should be at least fake selection + // for IME at here. Then, we can keep the last writing mode. + mContentSelection.emplace(offsetToPutCaret, WritingMode()); + } + } + + mCompositionState = eCompositionState_NotComposing; + // Reset dead key sequence too because GTK doesn't support dead key chain + // (i.e., a key press doesn't cause both producing some characters and + // restarting new dead key sequence at one time). So, committing + // composition means end of a dead key sequence. + mMaybeInDeadKeySequence = false; + mCompositionStart = UINT32_MAX; + mCompositionTargetRange.Clear(); + mDispatchedCompositionString.Truncate(); + mSelectedStringRemovedByComposition.Truncate(); + + if (!dispatcher) { + MOZ_ASSERT(aCommitString); + MOZ_ASSERT(!aCommitString->IsEmpty()); + nsEventStatus status = nsEventStatus_eIgnore; + WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText, + lastFocusedWindow); + insertTextEvent.mString.emplace(*aCommitString); + lastFocusedWindow->DispatchEvent(&insertTextEvent, status); + + if (!insertTextEvent.mSucceeded) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, inserting " + "text failed", + this)); + return false; + } + } else { + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = dispatcher->CommitComposition(status, aCommitString); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionChangeEvent(), FAILED, " + "due to CommitComposition() failure", + this)); + return false; + } + } + + if (lastFocusedWindow->IsDestroyed() || + lastFocusedWindow != mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DispatchCompositionCommitEvent(), FAILED, " + "the focused widget was destroyed/changed by " + "compositioncommit event", + this)); + return false; + } + + return true; +} + +already_AddRefed IMContextWrapper::CreateTextRangeArray( + GtkIMContext* aContext, const nsAString& aCompositionString) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p CreateTextRangeArray(aContext=0x%p, " + "aCompositionString=\"%s\" (Length()=%zu))", + this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(), + aCompositionString.Length())); + + RefPtr textRangeArray = new TextRangeArray(); + + gchar* preedit_string; + gint cursor_pos_in_chars; + PangoAttrList* feedback_list; + gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list, + &cursor_pos_in_chars); + if (!preedit_string || !*preedit_string) { + if (!aCompositionString.IsEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p CreateTextRangeArray(), FAILED, due to " + "preedit_string is null", + this)); + } + pango_attr_list_unref(feedback_list); + g_free(preedit_string); + return textRangeArray.forget(); + } + + // Convert caret offset from offset in characters to offset in UTF-16 + // string. If we couldn't proper offset in UTF-16 string, we should + // assume that the caret is at the end of the composition string. + uint32_t caretOffsetInUTF16 = aCompositionString.Length(); + if (NS_WARN_IF(cursor_pos_in_chars < 0)) { + // Note that this case is undocumented. We should assume that the + // caret is at the end of the composition string. + } else if (cursor_pos_in_chars == 0) { + caretOffsetInUTF16 = 0; + } else { + gchar* charAfterCaret = + g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars); + if (NS_WARN_IF(!charAfterCaret)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), failed to get UTF-8 " + "string before the caret (cursor_pos_in_chars=%d)", + this, cursor_pos_in_chars)); + } else { + glong caretOffset = 0; + gunichar2* utf16StrBeforeCaret = + g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string, + nullptr, &caretOffset, nullptr); + if (NS_WARN_IF(!utf16StrBeforeCaret) || NS_WARN_IF(caretOffset < 0)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), WARNING, failed to " + "convert to UTF-16 string before the caret " + "(cursor_pos_in_chars=%d, caretOffset=%ld)", + this, cursor_pos_in_chars, caretOffset)); + } else { + caretOffsetInUTF16 = static_cast(caretOffset); + uint32_t compositionStringLength = aCompositionString.Length(); + if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), WARNING, " + "caretOffsetInUTF16=%u is larger than " + "compositionStringLength=%u", + this, caretOffsetInUTF16, compositionStringLength)); + caretOffsetInUTF16 = compositionStringLength; + } + } + if (utf16StrBeforeCaret) { + g_free(utf16StrBeforeCaret); + } + } + } + + PangoAttrIterator* iter; + iter = pango_attr_list_get_iterator(feedback_list); + if (!iter) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p CreateTextRangeArray(), FAILED, iterator couldn't " + "be allocated", + this)); + pango_attr_list_unref(feedback_list); + g_free(preedit_string); + return textRangeArray.forget(); + } + + uint32_t minOffsetOfClauses = aCompositionString.Length(); + uint32_t maxOffsetOfClauses = 0; + do { + TextRange range; + if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) { + continue; + } + MOZ_ASSERT(range.Length()); + minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset); + maxOffsetOfClauses = std::max(maxOffsetOfClauses, range.mEndOffset); + textRangeArray->AppendElement(range); + } while (pango_attr_iterator_next(iter)); + + // If the IME doesn't define clause from the start of the composition, + // we should insert dummy clause information since TextRangeArray assumes + // that there must be a clause whose start is 0 when there is one or + // more clauses. + if (minOffsetOfClauses) { + TextRange dummyClause; + dummyClause.mStartOffset = 0; + dummyClause.mEndOffset = minOffsetOfClauses; + dummyClause.mRangeType = TextRangeType::eRawClause; + textRangeArray->InsertElementAt(0, dummyClause); + maxOffsetOfClauses = std::max(maxOffsetOfClauses, dummyClause.mEndOffset); + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), inserting a dummy clause " + "at the beginning of the composition string mStartOffset=%u, " + "mEndOffset=%u, mRangeType=%s", + this, dummyClause.mStartOffset, dummyClause.mEndOffset, + ToChar(dummyClause.mRangeType))); + } + + // If the IME doesn't define clause at end of the composition, we should + // insert dummy clause information since TextRangeArray assumes that there + // must be a clase whose end is the length of the composition string when + // there is one or more clauses. + if (!textRangeArray->IsEmpty() && + maxOffsetOfClauses < aCompositionString.Length()) { + TextRange dummyClause; + dummyClause.mStartOffset = maxOffsetOfClauses; + dummyClause.mEndOffset = aCompositionString.Length(); + dummyClause.mRangeType = TextRangeType::eRawClause; + textRangeArray->AppendElement(dummyClause); + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p CreateTextRangeArray(), inserting a dummy clause " + "at the end of the composition string mStartOffset=%u, " + "mEndOffset=%u, mRangeType=%s", + this, dummyClause.mStartOffset, dummyClause.mEndOffset, + ToChar(dummyClause.mRangeType))); + } + + TextRange range; + range.mStartOffset = range.mEndOffset = caretOffsetInUTF16; + range.mRangeType = TextRangeType::eCaret; + textRangeArray->AppendElement(range); + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p CreateTextRangeArray(), mStartOffset=%u, " + "mEndOffset=%u, mRangeType=%s", + this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType))); + + pango_attr_iterator_destroy(iter); + pango_attr_list_unref(feedback_list); + g_free(preedit_string); + + return textRangeArray.forget(); +} + +/* static */ +nscolor IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor) { + PangoColor& pangoColor = aPangoAttrColor->color; + uint8_t r = pangoColor.red / 0x100; + uint8_t g = pangoColor.green / 0x100; + uint8_t b = pangoColor.blue / 0x100; + return NS_RGB(r, g, b); +} + +bool IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter, + const gchar* aUTF8CompositionString, + uint32_t aUTF16CaretOffset, + TextRange& aTextRange) const { + // Set the range offsets in UTF-16 string. + gint utf8ClauseStart, utf8ClauseEnd; + pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd); + if (utf8ClauseStart == utf8ClauseEnd) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetTextRange(), FAILED, due to collapsed range", this)); + return false; + } + + if (!utf8ClauseStart) { + aTextRange.mStartOffset = 0; + } else { + glong utf16PreviousClausesLength; + gunichar2* utf16PreviousClausesString = + g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr, + &utf16PreviousClausesLength, nullptr); + + if (NS_WARN_IF(!utf16PreviousClausesString)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() " + "failure (retrieving previous string of current clause)", + this)); + return false; + } + + aTextRange.mStartOffset = utf16PreviousClausesLength; + g_free(utf16PreviousClausesString); + } + + glong utf16CurrentClauseLength; + gunichar2* utf16CurrentClauseString = g_utf8_to_utf16( + aUTF8CompositionString + utf8ClauseStart, utf8ClauseEnd - utf8ClauseStart, + nullptr, &utf16CurrentClauseLength, nullptr); + + if (NS_WARN_IF(!utf16CurrentClauseString)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() " + "failure (retrieving current clause)", + this)); + return false; + } + + // iBus Chewing IME tells us that there is an empty clause at the end of + // the composition string but we should ignore it since our code doesn't + // assume that there is an empty clause. + if (!utf16CurrentClauseLength) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p SetTextRange(), FAILED, due to current clause length " + "is 0", + this)); + return false; + } + + aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength; + g_free(utf16CurrentClauseString); + utf16CurrentClauseString = nullptr; + + // Set styles + TextRangeStyle& style = aTextRange.mRangeStyle; + + // Underline + PangoAttrInt* attrUnderline = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE)); + if (attrUnderline) { + switch (attrUnderline->value) { + case PANGO_UNDERLINE_NONE: + style.mLineStyle = TextRangeStyle::LineStyle::None; + break; + case PANGO_UNDERLINE_DOUBLE: + style.mLineStyle = TextRangeStyle::LineStyle::Double; + break; + case PANGO_UNDERLINE_ERROR: + style.mLineStyle = TextRangeStyle::LineStyle::Wavy; + break; + case PANGO_UNDERLINE_SINGLE: + case PANGO_UNDERLINE_LOW: + style.mLineStyle = TextRangeStyle::LineStyle::Solid; + break; + default: + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p SetTextRange(), retrieved unknown underline " + "style: %d", + this, attrUnderline->value)); + style.mLineStyle = TextRangeStyle::LineStyle::Solid; + break; + } + style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE; + + // Underline color + PangoAttrColor* attrUnderlineColor = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE_COLOR)); + if (attrUnderlineColor) { + style.mUnderlineColor = ToNscolor(attrUnderlineColor); + style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR; + } + } else { + style.mLineStyle = TextRangeStyle::LineStyle::None; + style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE; + } + + // Don't set colors if they are not specified. They should be computed by + // textframe if only one of the colors are specified. + + // Foreground color (text color) + PangoAttrColor* attrForeground = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND)); + if (attrForeground) { + style.mForegroundColor = ToNscolor(attrForeground); + style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR; + } + + // Background color + PangoAttrColor* attrBackground = reinterpret_cast( + pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND)); + if (attrBackground) { + style.mBackgroundColor = ToNscolor(attrBackground); + style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR; + } + + /** + * We need to judge the meaning of the clause for a11y. Before we support + * IME specific composition string style, we used following rules: + * + * 1: If attrUnderline and attrForground are specified, we assumed the + * clause is TextRangeType::eSelectedClause. + * 2: If only attrUnderline is specified, we assumed the clause is + * TextRangeType::eConvertedClause. + * 3: If only attrForground is specified, we assumed the clause is + * TextRangeType::eSelectedRawClause. + * 4: If neither attrUnderline nor attrForeground is specified, we assumed + * the clause is TextRangeType::eRawClause. + * + * However, this rules are odd since there can be two or more selected + * clauses. Additionally, our old rules caused that IME developers/users + * cannot specify composition string style as they want. + * + * So, we shouldn't guess the meaning from its visual style. + */ + + // If the range covers whole of composition string and the caret is at + // the end of the composition string, the range is probably not converted. + if (!utf8ClauseStart && + utf8ClauseEnd == static_cast(strlen(aUTF8CompositionString)) && + aTextRange.mEndOffset == aUTF16CaretOffset) { + aTextRange.mRangeType = TextRangeType::eRawClause; + } + // Typically, the caret is set at the start of the selected clause. + // So, if the caret is in the clause, we can assume that the clause is + // selected. + else if (aTextRange.mStartOffset <= aUTF16CaretOffset && + aTextRange.mEndOffset > aUTF16CaretOffset) { + aTextRange.mRangeType = TextRangeType::eSelectedClause; + } + // Otherwise, we should assume that the clause is converted but not + // selected. + else { + aTextRange.mRangeType = TextRangeType::eConvertedClause; + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p SetTextRange(), succeeded, aTextRange= { " + "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }", + this, aTextRange.mStartOffset, aTextRange.mEndOffset, + ToChar(aTextRange.mRangeType), + GetTextRangeStyleText(aTextRange.mRangeStyle).get())); + + return true; +} + +void IMContextWrapper::SetCursorPosition(GtkIMContext* aContext) { + MOZ_LOG( + gIMELog, LogLevel::Info, + ("0x%p SetCursorPosition(aContext=0x%p), " + "mCompositionTargetRange={ mOffset=%u, mLength=%u }, " + "mContentSelection=%s", + this, aContext, mCompositionTargetRange.mOffset, + mCompositionTargetRange.mLength, ToString(mContentSelection).c_str())); + + bool useCaret = false; + if (!mCompositionTargetRange.IsValid()) { + if (mContentSelection.isNothing()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, " + "mCompositionTargetRange and mContentSelection are invalid", + this)); + return; + } + if (!mContentSelection->HasRange()) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p SetCursorPosition(), FAILED, " + "mCompositionTargetRange is invalid and there is no selection", + this)); + return; + } + useCaret = true; + } + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, due to no focused " + "window", + this)); + return; + } + + if (MOZ_UNLIKELY(!aContext)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, due to no context", this)); + return; + } + + WidgetQueryContentEvent queryCaretOrTextRectEvent( + true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow); + if (useCaret) { + queryCaretOrTextRectEvent.InitForQueryCaretRect( + mContentSelection->OffsetAndDataRef().StartOffset()); + } else { + if (mContentSelection->WritingModeRef().IsVertical()) { + // For preventing the candidate window to overlap the target + // clause, we should set fake (typically, very tall) caret rect. + uint32_t length = + mCompositionTargetRange.mLength ? mCompositionTargetRange.mLength : 1; + queryCaretOrTextRectEvent.InitForQueryTextRect( + mCompositionTargetRange.mOffset, length); + } else { + queryCaretOrTextRectEvent.InitForQueryTextRect( + mCompositionTargetRange.mOffset, 1); + } + } + nsEventStatus status; + mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status); + if (queryCaretOrTextRectEvent.Failed()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p SetCursorPosition(), FAILED, %s was failed", this, + useCaret ? "eQueryCaretRect" : "eQueryTextRect")); + return; + } + + nsWindow* rootWindow = + static_cast(mLastFocusedWindow->GetTopLevelWidget()); + + // Get the position of the rootWindow in screen. + LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset(); + + // Get the position of IM context owner window in screen. + LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset(); + + // Compute the caret position in the IM owner window. + LayoutDeviceIntRect rect = + queryCaretOrTextRectEvent.mReply->mRect + root - owner; + rect.width = 0; + GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect); + + gtk_im_context_set_cursor_location(aContext, &area); +} + +nsresult IMContextWrapper::GetCurrentParagraph(nsAString& aText, + uint32_t& aCursorPos) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p GetCurrentParagraph(), mCompositionState=%s", this, + GetCompositionStateName())); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, there are no " + "focused window in this module", + this)); + return NS_ERROR_NULL_POINTER; + } + + nsEventStatus status; + + uint32_t selOffset = mCompositionStart; + uint32_t selLength = mSelectedStringRemovedByComposition.Length(); + + // If focused editor doesn't have composition string, we should use + // current selection. + if (!EditorHasCompositionString()) { + // Query cursor position & selection + if (NS_WARN_IF(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, due to no " + "valid selection information", + this)); + return NS_ERROR_FAILURE; + } + + if (mContentSelection.isSome() && mContentSelection->HasRange()) { + selOffset = mContentSelection->OffsetAndDataRef().StartOffset(); + selLength = mContentSelection->OffsetAndDataRef().Length(); + } else { + // If there is no range, let's get all text instead... + selOffset = 0u; + selLength = INT32_MAX; // TODO: Change to UINT32_MAX, but see below + } + } + + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u", this, + selOffset, selLength)); + + // XXX nsString::Find and nsString::RFind take int32_t for offset, so, + // we cannot support this request when the current offset is larger + // than INT32_MAX. + if (selOffset > INT32_MAX || selLength > INT32_MAX || + selOffset + selLength > INT32_MAX) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, The selection is " + "out of range", + this)); + return NS_ERROR_FAILURE; + } + + // Get all text contents of the focused editor + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + mLastFocusedWindow); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + return NS_ERROR_FAILURE; + } + + if (selOffset + selLength > queryTextContentEvent.mReply->DataLength()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p GetCurrentParagraph(), FAILED, The selection is " + "invalid, queryTextContentEvent={ mReply=%s }", + this, ToString(queryTextContentEvent.mReply).c_str())); + return NS_ERROR_FAILURE; + } + + // Remove composing string and restore the selected string because + // GtkEntry doesn't remove selected string until committing, however, + // our editor does it. We should emulate the behavior for IME. + nsAutoString textContent(queryTextContentEvent.mReply->DataRef()); + if (EditorHasCompositionString() && + mDispatchedCompositionString != mSelectedStringRemovedByComposition) { + textContent.Replace(mCompositionStart, + mDispatchedCompositionString.Length(), + mSelectedStringRemovedByComposition); + } + + // Get only the focused paragraph, by looking for newlines + int32_t parStart = 0; + if (selOffset > 0) { + parStart = Substring(textContent, 0, selOffset - 1).RFind(u"\n") + 1; + } + int32_t parEnd = textContent.Find(u"\n", selOffset + selLength); + if (parEnd < 0) { + parEnd = textContent.Length(); + } + aText = nsDependentSubstring(textContent, parStart, parEnd - parStart); + aCursorPos = selOffset - uint32_t(parStart); + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p GetCurrentParagraph(), succeeded, aText=%s, " + "aText.Length()=%zu, aCursorPos=%u", + this, NS_ConvertUTF16toUTF8(aText).get(), aText.Length(), aCursorPos)); + + return NS_OK; +} + +nsresult IMContextWrapper::DeleteText(GtkIMContext* aContext, int32_t aOffset, + uint32_t aNChars) { + MOZ_LOG(gIMELog, LogLevel::Info, + ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), " + "mCompositionState=%s", + this, aContext, aOffset, aNChars, GetCompositionStateName())); + + if (!mLastFocusedWindow) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, there are no focused window " + "in this module", + this)); + return NS_ERROR_NULL_POINTER; + } + + if (!aNChars) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, aNChars must not be zero", this)); + return NS_ERROR_INVALID_ARG; + } + + RefPtr lastFocusedWindow(mLastFocusedWindow); + nsEventStatus status; + + // First, we should cancel current composition because editor cannot + // handle changing selection and deleting text. + uint32_t selOffset; + bool wasComposing = IsComposing(); + bool editorHadCompositionString = EditorHasCompositionString(); + if (wasComposing) { + selOffset = mCompositionStart; + if (!DispatchCompositionCommitEvent(aContext, + &mSelectedStringRemovedByComposition)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, quitting from DeletText", this)); + return NS_ERROR_FAILURE; + } + } else { + if (NS_WARN_IF(!EnsureToCacheContentSelection())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, due to no valid selection " + "information", + this)); + return NS_ERROR_FAILURE; + } + if (!mContentSelection->HasRange()) { + MOZ_LOG(gIMELog, LogLevel::Debug, + ("0x%p DeleteText(), does nothing, due to no selection range", + this)); + return NS_OK; + } + selOffset = mContentSelection->OffsetAndDataRef().StartOffset(); + } + + // Get all text contents of the focused editor + WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent, + mLastFocusedWindow); + queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX); + mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status); + if (NS_WARN_IF(queryTextContentEvent.Failed())) { + return NS_ERROR_FAILURE; + } + if (queryTextContentEvent.mReply->IsDataEmpty()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, there is no contents", this)); + return NS_ERROR_FAILURE; + } + + NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring( + queryTextContentEvent.mReply->DataRef(), 0, selOffset)); + glong offsetInUTF8Characters = + g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset; + if (offsetInUTF8Characters < 0) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, aOffset is too small for " + "current cursor pos (computed offset: %ld)", + this, offsetInUTF8Characters)); + return NS_ERROR_FAILURE; + } + + AppendUTF16toUTF8( + nsDependentSubstring(queryTextContentEvent.mReply->DataRef(), selOffset), + utf8Str); + glong countOfCharactersInUTF8 = + g_utf8_strlen(utf8Str.get(), utf8Str.Length()); + glong endInUTF8Characters = offsetInUTF8Characters + aNChars; + if (countOfCharactersInUTF8 < endInUTF8Characters) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, aNChars is too large for " + "current contents (content length: %ld, computed end offset: %ld)", + this, countOfCharactersInUTF8, endInUTF8Characters)); + return NS_ERROR_FAILURE; + } + + gchar* charAtOffset = + g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters); + gchar* charAtEnd = + g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters); + + // Set selection to delete + WidgetSelectionEvent selectionEvent(true, eSetSelection, mLastFocusedWindow); + + nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0, + charAtOffset - utf8Str.get()); + selectionEvent.mOffset = NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length(); + + nsDependentCSubstring utf8DeletingStr(utf8Str, utf8StrBeforeOffset.Length(), + charAtEnd - charAtOffset); + selectionEvent.mLength = NS_ConvertUTF8toUTF16(utf8DeletingStr).Length(); + + selectionEvent.mReversed = false; + selectionEvent.mExpandToClusterBoundary = false; + lastFocusedWindow->DispatchEvent(&selectionEvent, status); + + if (!selectionEvent.mSucceeded || lastFocusedWindow != mLastFocusedWindow || + lastFocusedWindow->Destroyed()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, setting selection caused " + "focus change or window destroyed", + this)); + return NS_ERROR_FAILURE; + } + + // If this deleting text caused by a key press, we need to dispatch + // eKeyDown or eKeyUp before dispatching eContentCommandDelete event. + if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandDelete)) { + MOZ_LOG(gIMELog, LogLevel::Warning, + ("0x%p DeleteText(), Warning, " + "MaybeDispatchKeyEventAsProcessedByIME() returned false", + this)); + return NS_ERROR_FAILURE; + } + + // Delete the selection + WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete, + mLastFocusedWindow); + mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status); + + if (!contentCommandEvent.mSucceeded || + lastFocusedWindow != mLastFocusedWindow || + lastFocusedWindow->Destroyed()) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, deleting the selection caused " + "focus change or window destroyed", + this)); + return NS_ERROR_FAILURE; + } + + if (!wasComposing) { + return NS_OK; + } + + // Restore the composition at new caret position. + if (!DispatchCompositionStart(aContext)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, resterting composition start", this)); + return NS_ERROR_FAILURE; + } + + if (!editorHadCompositionString) { + return NS_OK; + } + + nsAutoString compositionString; + GetCompositionString(aContext, compositionString); + if (!DispatchCompositionChangeEvent(aContext, compositionString)) { + MOZ_LOG( + gIMELog, LogLevel::Error, + ("0x%p DeleteText(), FAILED, restoring composition string", this)); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +bool IMContextWrapper::EnsureToCacheContentSelection( + nsAString* aSelectedString) { + if (aSelectedString) { + aSelectedString->Truncate(); + } + + if (mContentSelection.isSome()) { + if (mContentSelection->HasRange() && aSelectedString) { + aSelectedString->Assign(mContentSelection->OffsetAndDataRef().DataRef()); + } + return true; + } + + RefPtr dispatcherWindow = + mLastFocusedWindow ? mLastFocusedWindow : mOwnerWindow; + if (NS_WARN_IF(!dispatcherWindow)) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p EnsureToCacheContentSelection(), FAILED, due to " + "no focused window", + this)); + return false; + } + + nsEventStatus status; + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + dispatcherWindow); + dispatcherWindow->DispatchEvent(&querySelectedTextEvent, status); + if (NS_WARN_IF(querySelectedTextEvent.Failed())) { + MOZ_LOG(gIMELog, LogLevel::Error, + ("0x%p EnsureToCacheContentSelection(), FAILED, due to " + "failure of query selection event", + this)); + return false; + } + + mContentSelection = Some(ContentSelection(querySelectedTextEvent)); + if (mContentSelection->HasRange()) { + if (!mContentSelection->OffsetAndDataRef().IsDataEmpty() && + aSelectedString) { + aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef()); + } + } + + MOZ_LOG( + gIMELog, LogLevel::Debug, + ("0x%p EnsureToCacheContentSelection(), Succeeded, mContentSelection=%s", + this, ToString(mContentSelection).c_str())); + return true; +} + +} // namespace widget +} // namespace mozilla diff --git a/widget/gtk/IMContextWrapper.h b/widget/gtk/IMContextWrapper.h new file mode 100644 index 0000000000..cf1d2638e0 --- /dev/null +++ b/widget/gtk/IMContextWrapper.h @@ -0,0 +1,688 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef IMContextWrapper_h_ +#define IMContextWrapper_h_ + +#include +#include + +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsIWidget.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ContentData.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Maybe.h" +#include "mozilla/TextEventDispatcherListener.h" +#include "mozilla/WritingModes.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/widget/IMEData.h" + +class nsWindow; + +namespace mozilla { +namespace widget { + +/** + * KeyHandlingState is result of IMContextWrapper::OnKeyEvent(). + */ +enum class KeyHandlingState { + // The native key event has not been handled by IMContextWrapper. + eNotHandled, + // The native key event was handled by IMContextWrapper. + eHandled, + // The native key event has not been handled by IMContextWrapper, + // but eKeyDown or eKeyUp event has been dispatched. + eNotHandledButEventDispatched, + // The native key event has not been handled by IMContextWrapper, + // but eKeyDown or eKeyUp event has been dispatched and consumed. + eNotHandledButEventConsumed, +}; + +class IMContextWrapper final : public TextEventDispatcherListener { + public: + // TextEventDispatcherListener implementation + NS_DECL_ISUPPORTS + + NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher, + const IMENotification& aNotification) override; + NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override; + NS_IMETHOD_(void) + OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override; + NS_IMETHOD_(void) + WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher, + WidgetKeyboardEvent& aKeyboardEvent, + uint32_t aIndexOfKeypress, void* aData) override; + + public: + // aOwnerWindow is a pointer of the owner window. When aOwnerWindow is + // destroyed, the related IME contexts are released (i.e., IME cannot be + // used with the instance after that). + explicit IMContextWrapper(nsWindow* aOwnerWindow); + + // Called when the process is being shut down. + static void Shutdown(); + + // "Enabled" means the users can use all IMEs. + // I.e., the focus is in the normal editors. + bool IsEnabled() const; + + // OnFocusWindow is a notification that aWindow is going to be focused. + void OnFocusWindow(nsWindow* aWindow); + // OnBlurWindow is a notification that aWindow is going to be unfocused. + void OnBlurWindow(nsWindow* aWindow); + // OnDestroyWindow is a notification that aWindow is going to be destroyed. + void OnDestroyWindow(nsWindow* aWindow); + // OnFocusChangeInGecko is a notification that an editor gets focus. + void OnFocusChangeInGecko(bool aFocus); + // OnSelectionChange is a notification that selection (caret) is changed + // in the focused editor. + void OnSelectionChange(nsWindow* aCaller, + const IMENotification& aIMENotification); + // OnThemeChanged is called when desktop theme is changed. + static void OnThemeChanged(); + + /** + * OnKeyEvent() is called when aWindow gets a native key press event or a + * native key release event. If this returns true, the key event was + * filtered by IME. Otherwise, this returns false. + * NOTE: When the native key press event starts composition, this returns + * true but dispatches an eKeyDown event or eKeyUp event before + * dispatching composition events or content command event. + * + * @param aWindow A window on which user operate the + * key. + * @param aEvent A native key press or release + * event. + * @param aKeyboardEventWasDispatched true if eKeyDown or eKeyUp event + * for aEvent has already been + * dispatched. In this case, + * this class doesn't dispatch + * keyboard event anymore. + */ + KeyHandlingState OnKeyEvent(nsWindow* aWindow, GdkEventKey* aEvent, + bool aKeyboardEventWasDispatched = false); + + // IME related nsIWidget methods. + nsresult EndIMEComposition(nsWindow* aCaller); + void SetInputContext(nsWindow* aCaller, const InputContext* aContext, + const InputContextAction* aAction); + InputContext GetInputContext(); + void OnUpdateComposition(); + void OnLayoutChange(); + + TextEventDispatcher* GetTextEventDispatcher(); + + // TODO: Typically, new IM comes every several years. And now, our code + // becomes really IM behavior dependent. So, perhaps, we need prefs + // to control related flags for IM developers. + enum class IMContextID : uint8_t { + Fcitx, // 4.x or earlier + Fcitx5, + IBus, + IIIMF, + Scim, + Uim, + Wayland, + Unknown, + }; + + friend std::ostream& operator<<(std::ostream& aStream, + const IMContextID& aIMContextID) { + switch (aIMContextID) { + case IMContextID::Fcitx: + return aStream << "Fcitx"; + case IMContextID::Fcitx5: + return aStream << "Fcitx5"; + case IMContextID::IBus: + return aStream << "IBus"; + case IMContextID::IIIMF: + return aStream << "IIIMF"; + case IMContextID::Scim: + return aStream << "Scim"; + case IMContextID::Uim: + return aStream << "Uim"; + case IMContextID::Wayland: + return aStream << "Wayland"; + case IMContextID::Unknown: + return aStream << "Unknown"; + } + MOZ_ASSERT_UNREACHABLE("Add new case for the new IM support"); + return aStream << "Unknown"; + } + + /** + * GetIMName() returns IM name associated with mContext. If the context is + * xim, this look for actual engine from XMODIFIERS environment variable. + */ + nsDependentCSubstring GetIMName() const; + + /** + * GetWaitingSynthesizedKeyPressHardwareKeyCode() returns hardware_keycode + * value of last handled GDK_KEY_PRESS event which is probable handled by + * IME asynchronously and we have not received synthesized GDK_KEY_PRESS + * event yet. + */ + static guint16 GetWaitingSynthesizedKeyPressHardwareKeyCode() { + return sWaitingSynthesizedKeyPressHardwareKeyCode; + } + + protected: + ~IMContextWrapper(); + + /** + * SetInputPurposeAndInputHints() sets input-purpose and input-hints of + * current IM context to the values computed with mInputContext. + */ + void SetInputPurposeAndInputHints(); + + // Owner of an instance of this class. This should be top level window. + // The owner window must release the contexts when it's destroyed because + // the IME contexts need the native window. If OnDestroyWindow() is called + // with the owner window, it'll release IME contexts. Otherwise, it'll + // just clean up any existing composition if it's related to the destroying + // child window. + nsWindow* mOwnerWindow; + + // A last focused window in this class's context. + nsWindow* mLastFocusedWindow; + + // Actual context. This is used for handling the user's input. + GtkIMContext* mContext; + + // mSimpleContext is used for the password field and + // the |ime-mode: disabled;| editors if sUseSimpleContext is true. + // These editors disable IME. But dead keys should work. Fortunately, + // the simple IM context of GTK2 support only them. + GtkIMContext* mSimpleContext; + + // mDummyContext is a dummy context and will be used in Focus() + // when the state of mEnabled means disabled. This context's IME state is + // always "closed", so it closes IME forcedly. + GtkIMContext* mDummyContext; + + // mComposingContext is not nullptr while one of mContext, mSimpleContext + // and mDummyContext has composition. + // XXX: We don't assume that two or more context have composition same time. + GtkIMContext* mComposingContext; + + // IME enabled state and other things defined in InputContext. + // Use following helper methods if you don't need the detail of the status. + InputContext mInputContext; + + // mCompositionStart is the start offset of the composition string in the + // current content. When