四月, 2024 以后的博文

背景

众所周知,android系统是基于linux内核运行的,而安卓丰富且成熟的软件生态和linux的生态形成了鲜明的对比,所以,都是基于一个内核,为什么不能让安卓应用直接在linux上跑呢?

目前市面上能在linux上跑android的技术方案主要就是两个,一个是虚拟机,二是用容器化的方案,前者会吃掉太多电脑硬件性能,且两个系统在完全隔离之后是无法交互,而容器化则没有这些缺点,所以成为了我们的首选,具体来说就是相当于在linux的地盘上隔出一个安卓运行的空间,但是两个系统之间还保有交互的通道。

OpenFDE软件架构

现在,我们来看看OpenFDE的软件架构。自底向上可以分为分为标准Linux内核、LXC隔离和融合层、API框架层、应用层和Linux桌面图形栈几个部分。

image-20240425-085148.png

应用层

OpenFDE应用层主要包含安卓应用和Linux的X11类应用。其中Android应用可以原生运行。但Linux的X11类应用还是运行在linux系统中,我们是通过运行在FDE中的vnc client将Linux X11类应用的显示融合到Android系统中,同时进行(键鼠信号和输入法)事件转发,目前我们正在开发基于安卓系统的XServer,以替换Linux上的vnc server X11,这样可以获得更高的显示性能和更强的兼容性。

API框架层

API框架层提供了一组标准的API接口,使应用程序能够与底层系统进行通信和交互,这些API包括访问设备驱动、管理文件系统、处理网络通信等功能。OpenFDE是直接继承Android API设计框架,使两者遵循一套统一的外设、功耗管理API,能极大的缓解API版本代次共存、图形界面使用与开发过程中修改较为随意和混乱的问题。

LXC隔离层

LXC隔离层负责将Linux和Android的文件系统及API层进行隔离,但同时又共享Linux内核。融合层则充分利用标准桌面协议(Wayland)和Linux桌面图形栈,提供图形显示、输入设备管理、窗口管理等关键功能。另外我们正在做将Linux桌面图形栈的OpenGL/ES融合成动态库的形式,提供到Android端直接进行调用,以兼容更多的硬件。

Linux融合方案详解

Linux上的DRI显示框架的逻辑(也是目前最高效的显示框架)是,客户端直接调用gpu渲染,渲染完毕后,提交给合成器,合成器合成后提交显示。

image-20240425-092821.png

安卓的显示逻辑是应用向surfaceflinger申请缓存,surfaceflinger调用gralloc完成申请,再同步给应用程序使用。渲染过程由应用调取gpu直接完成,最后交给surfaceflinger调用hwcomposer完成合成。

image-20240425-092858.png

在Linux上跑安卓,就可以将这两个流程进行合并,前面部分保持安卓原流程不变,将hwcomposer改造成wayland client(因此就失去了硬件合成功能),在surfaceflinger请求hwcomposer合成的时候,hwcomposer返回全部让gpu合成的数据,此时surfaceflinger会调用gpu完成合成,再将合成后的结果发给hwcomposer进行提交。此时hwcomposer就通过wayland协议将缓存发送到linux合成器——mutter,最终由mutter发到linux的dpu上进行显示。

image-20240425-092951.png

当然这种方法会有一定的性能损耗,但是可以很好的达到我们的目的。

背景介绍

请先读本段!请先读本段!请先读本段!

本文是在OpenFDE上实现linux窗口和Android窗口融合的一种方案预研和设想,因为方案有些复杂,这段文字作为一个前情提要,描述一下背景知识,不然直接开始容易让人懵逼。

首先,需求是这样的,因为OpenFDE是运行在linux上的Android桌面,那如果在OpenFDE上运行一个xserver,是不是linux程序就可以直接在OpenFDE上运行了,并且效果跟原生运行在linux一样。在Android上已经有一些xserver的实现了,比如google的最简版Java xserver,还有Xsever-SDL,经过各种研究对比,termux-x11是实现最完整,工程化也最好的项目。

其次,仅仅在Android上运行一个Xserver,在一个Android窗口里模拟一个Xserver屏幕,那效果跟任何平台都没有区别了,因为linux就运行在本机,我们团队提出的需求是,linux的单个程序就像Android的单个程序一样运行,就是窗口融合的概念。简单理解就是每个linux窗口对应一个activity,在Xserver-SDL的github,笔者也看到别人有这样的发散思维https://github.com/pelya/xserver-xsdl/issues/65

经过相当长时间的文档熟悉,实验验证,方案探索(当然本方案也还在探索),目标似乎是可以实现的了,下文是大致记录可行性研究过程和思路描述,在具体的实现过程中,这些思路也会有修正。每一个大致的环节,后续都应该会有各种设计,验证,调整的代码和文档输出。

什么是Termux和Termux-x11?

Termux 是一个 Android 终端应用程序和 Linux 环境。直观的来看,Termux 是运行在 Android 上的 terminal,不需要root,运行于内部存储(不在SD卡上),目录位于本应用的file目录,配置了相应的环境变量,包安装命令是pkg install xxx,实际是一个封装APT的脚本。

源码地址:https://github.com/termux/termux-app

image-20240411-033058.png

环境变量配置:

image-20240411-033123.png

安装库路径:/data/data/com.termux/files/usr/lib

image-20240411-033159.png

当你使用termux作为终端来安装和运行程序时,应该是从代码仓库编译https://github.com/termux/termux-packages ,并发布到apt源的程序。

Termux-packages该项目包含用于构建 Termux Android 应用程序包的脚本和补丁。简单对比了一下补丁文件,项目构建的方式主要是将linux的编译选项改为Android,如果原本项目就支持Android编译,即可以最简单的直接编译出程序,否则就要先安装在Android上本来不存在的依赖。

Termux-X11 是一个成熟的 X 服务器。它是使用 Android NDK 构建的,并针对与 Termux 一起使用进行了优化。

源码地址:https://github.com/termux/termux-x11

经过学习源码,termux-x11支持了大部分xserver扩展,运行效率好,项目工程化好,可以直接编译运行。

在Termux-x11上运行X程序

实验步骤

  1. 安装termux

编译运行https://github.com/termux/termux-app

或者在 F-droid https://f-droid.org/en/packages/com.termux/   下载APK。

  1. 安装termux-x11命令行

在termux终端执行如下命令,即相当于在termux环境安装了termux-x11库或者脚本命令,真正的运行程序需要安装termux-x11 APK。

pkg install x11-repo //x11依赖
​
pkg install termux-x11            //termux-x11命令行
pkg install termux-x11-nightly    //termux-x11命令行
  1. 编译安装termux-x11 APK

编译环境必须是linux,并且需要安装以下依赖。

sudo apt update && sudo apt install -y libarchive-tools binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf gcc bison

https://github.com/termux/termux-x11 导入 Android studio,编译安装到FDE。

编译配置可以参考workflow:

https://github.com/termux/termux-x11/blob/71d1243bc68548a46703ca4a53e30d3b75083b2f/.github/workflows/debug_build.yml

从编译产出看,应该是相关依赖也一并生成了,或者可以取代pkg从apt源安装。

   files: |
     ./app/build/outputs/apk/debug/app-*-debug.apk
     ./app/build/outputs/apk/debug/shell-loader-nightly.apk
     ./app/build/outputs/apk/debug/termux-x11-*-all.deb
     ./app/build/outputs/apk/debug/termux-x11-*-any.pkg.tar.xz
  1. 运行termux-x11即x11 server,在termux执行

TERMUX_X11_DEBUG=1 termux-x11 :0 -legacy-drawing -listen tcp
​
//或者
termux-x11 :0 -listen tcp //listen tcp支持是远程连接,怎么去掉还没有查清楚
  1. 在linux端启动X程序连接

export DISPLAY=:0
firefox
wps
...
//启动合成器、窗口管理器
xfwm4
i3

效果图:

image-20240411-033840.png

Termux-x11原理浅析

从以上的执行过程来看,启动x11 server(即termux-x11)的方式是在termux终端运行termux-x11 :0命令。

waydroid:/data/data/com.termux/files/usr/bin # cat termux-x11
#!/data/data/com.termux/files/usr/bin/sh
export CLASSPATH=/data/data/com.termux/files/usr/libexec/termux-x11/loader.apk
unset LD_LIBRARY_PATH LD_PRELOAD
exec /system/bin/app_process / com.termux.x11.Loader "$@"

Termux-x11是通过pkg安装的脚本,直接启动x11 server的代码是在脚本中。/system/bin/app_process 是 Android 系统上的应用程序启动器,它负责启动指定的 Java 类。

这是com.termux.x11.Loader的源码,

   public static void main(String[] args) {
        try {
            android.content.pm.PackageInfo targetInfo = (android.os.Build.VERSION.SDK_INT <= 32) ?
                    android.app.ActivityThread.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, android.content.pm.PackageManager.GET_SIGNATURES, 0) :
                    android.app.ActivityThread.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, (long) android.content.pm.PackageManager.GET_SIGNATURES, 0);
            assert targetInfo != null : BuildConfig.packageNotInstalledErrorText.replace("ARCH", android.os.Build.SUPPORTED_ABIS[0]);
            assert targetInfo.signatures.length == 1 && BuildConfig.SIGNATURE == targetInfo.signatures[0].hashCode() : BuildConfig.packageSignatureMismatchErrorText;
​
            android.util.Log.i(BuildConfig.logTag, "loading " + targetInfo.applicationInfo.sourceDir + "::" + BuildConfig.CLASS_ID + "::main of " + BuildConfig.APPLICATION_ID + " application (commit " + BuildConfig.COMMIT + ")");
            Class<?> targetClass = Class.forName(BuildConfig.CLASS_ID, true,
                    new dalvik.system.PathClassLoader(targetInfo.applicationInfo.sourceDir, null, ClassLoader.getSystemClassLoader()));
            targetClass.getMethod("main", String[].class).invoke(null, (Object) args);
        } catch (AssertionError e) {
            System.err.println(e.getMessage());
        } catch (java.lang.reflect.InvocationTargetException e) {
            e.getCause().printStackTrace(System.err);
        } catch (Throwable e) {
            android.util.Log.e(BuildConfig.logTag, "Loader error", e);
            e.printStackTrace(System.err);
        }
    }
 public static final String CLASS_ID = "com.termux.x11.CmdEntryPoint";

实际是使用当前环境变量,通过反射启动上面这个类的main函数,相当于执行了一个java程序,CmdEntryPoint是termu-x11源码中的类,是x11 server真正启动的地方。

CmdEntryPoint又是一个AIDL的stub,意味着他作为一个AIDL service启动,可以接收原termux-x11 APK的接口函数。当你手动从桌面点击termux-x11的图标的时候,会启动activity传入显示用的SurfaceView。

public class CmdEntryPoint extends ICmdEntryInterface.Stub {
    public static final String ACTION_START = "com.termux.x11.CmdEntryPoint.ACTION_START";
    public static final int PORT = 7892;
    public static final byte[] MAGIC = "0xDEADBEEF".getBytes();
    private static final Handler handler;
    public static Context ctx;
     ...

笔者理解Termux-x11有两条运行线:

  • termux-->app_process-->com.termux.x11.CmdEntryPoint-->startserver

  • termux-x11-->SurfaceView-->com.termux.x11.ICmdEntryInterface.windowChanged-->xserver

两条线的最后代码都是在termux-x11,并且两条线不互相干扰,server启动后不置入SurfaceView,不显示图像,server也可以正常在后台运行。

Action 1 : 改造termux/termux-x11

简单描述这个问题:将termux、termux-x11安装启动xserver的过程,做到用户无感知,交互正常,不需要pkg安装,也不需要命令行操作。

(?笔者认为的关键是,解决环境变量,库依赖的问题)

可能的最终形态:

  1. 改成普通安卓service或者系统安卓service,运行在后台;

  2. 提供一个融合应用的入口,类似VNC的《Fusion Linux Application》,用户启动后再拉起server,列出X程序,点击X程序再启动linux端的连接。

怎么使用Compositor实现窗口融合?

合成器扩展Compositor,又叫混成器,一句话形容,是用来将不同的X程序的显示内容合成成最终图像的扩展模块。既然是窗口管理程序,使用的肯定也是X协议的窗口管理协议,ICCCM/EWMH,参见 https://tronche.com/gui/x/icccm/ 。他的主要函数是从窗口获取图像,可以用来实现各种窗口特效。

使用Compositor扩展后,xserver服务端和compositor端都有相应的回调函数拿到想要的窗口的图像。

image-20240411-034025.png

Compositor源码说明

[Composite extension protocol specification]  https://cgit.freedesktop.org/xorg/proto/compositeproto/tree/compositeproto.txt  
[合成器扩展功能练习]  http://www.talisman.org/~erlkonig/misc/x11-composite-tutorial/  

从composite源码来看关键函数是:

Hierarchy Redirection
​
    RedirectWindow
​
        window:             Window
        update:             UPDATETYPE
//重定向窗口
​
Associating a Pixmap ID with the off-screen storage (0.2 and later)
​
    NameWindowPixmap
​
        window:             Window
        pixmap:             Pixmap
//获取窗口图像
​

合成器连接上server后,会对所有窗口重定向,在重定向的回调函数中就可以通过NameWindowPixmap获取对应程序的图像,其本质就是GetWindowPixmap,不过需要在重定向窗口之后执行。

合成器的一种简单使用是实现X程序截图,很多X程序在最小化之后,需要将隐藏了的图像取帧做为提示图,如果使用合成器扩展,就是简单两步,重定向窗口,保存图像:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <libpng16/png.h>
​
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xcomposite.h>
#include <X11/extensions/Xrender.h>
//Compile hint: gcc -shared -O3 -lX11 -fPIC -Wl,-soname,prtscn -o prtscn.so prtscn.c
​
typedef int bool;
#define true 1
#define false 0
​
const int DEBUG = 0;
​
void getScreen2(const int, const int, const int, const int, const XID,
  unsigned char *);
void write_png_for_image(XImage *image, XID xid, int width, int height,
  char *filename);
typedef int (*handler)(Display *, XErrorEvent *);
XID getWindows(Display *display, Window parent, Window window, XID xid,
  int depth);
​
int main() {
​
 Display *display = XOpenDisplay("10.31.91.87:5");
 Window root = DefaultRootWindow(display);
​
 uint nwindows;
 Window root_return, parent_return, *windows;
​
 Atom a = XInternAtom(display, "_NET_CLIENT_LIST", true);
 Atom actualType;
​
 int format;
 unsigned long numItems, bytesAfter;
 unsigned char *data = 0;
​
 int status = XGetWindowProperty(display, root, a, 0L, (~0L),
 false,
 AnyPropertyType, &actualType, &format, &numItems, &bytesAfter, &data);
​
 char* window_name_return;
​
 if (status >= Success && numItems) {
  long *array = (long*) data;
  for (long k = 0; k < numItems; k++) {
   Window window = (Window) array[k];
​
   //not finding chrome window name
   printf("window found was %d \n", window);
   if (XFetchName(display, window, &window_name_return)) {
    printf("found window name for %d : %s \n", window,
      window_name_return);
   }
​
   //XMapWindow(display, parent);
   XMapWindow(display, window);
​
   XWindowAttributes attr;
​
   Status status = XGetWindowAttributes(display, window, &attr);
   if (status == 0)
    printf("Fail to get window attributes!\n");
​
   unsigned char outdata[attr.width * attr.height * 3];
​
   getScreen2(0, 0, attr.width, attr.height, window, outdata);
​
  }
  XFree(data);
 }
​
 return 0;
}
​
void getScreen2(const int xx, const int yy, const int W, const int H,
  const XID xid, /*out*/unsigned char * data) {
​
 Display *display = XOpenDisplay("10.31.91.87:5");
 Window root = DefaultRootWindow(display);
​
 // turn on --sync to force error on correct method
 //https://www.x.org/releases/X11R7.6/doc/man/man3/XSynchronize.3.xhtml
 XSynchronize(display, True);
​
 int counter = 1;
​
 // select which xid to operate on, the winder or its parent
 //XID xwid = fparent;
 XID xwid = xid;
​
 // Requests the X server to direct the hierarchy starting at window to off-screen storage
 XCompositeRedirectWindow(display, xwid, CompositeRedirectAutomatic);
​
 XWindowAttributes attr;
 Status status = XGetWindowAttributes(display, xwid, &attr);
​
 int width = attr.width;
 int height = attr.height;
 int depth = attr.depth;
​
 Pixmap xc_pixmap = XCompositeNameWindowPixmap(display, xwid);
 if (!xc_pixmap) {
  printf("xc_pixmap not found\n");
 }
​
 //XWriteBitmapFile(display, "test1.xpm", pixmap, W, H, -1, -1);
​
 XRenderPictFormat *format = XRenderFindVisualFormat(display, attr.visual);
​
 XRenderPictureAttributes pa;
​
 pa.subwindow_mode = IncludeInferiors;
​
 Picture picture = XRenderCreatePicture(display, xwid, format,
 CPSubwindowMode, &pa);
​
 char buffer[50];
 int n;
 int file_counter = 1;
​
 n = sprintf(buffer, "/tmp/%d_test%d.xpm", xid, file_counter++);
 XWriteBitmapFile(display, buffer, xc_pixmap, W, H, -1, -1);
​
 n = sprintf(buffer, "/tmp/%d_test%d.xpm", xid, file_counter++);
 XWriteBitmapFile(display, buffer, xid, W, H, -1, -1);
​
 XImage *image = XGetImage(display, xid, 0, 0, W, H, AllPlanes, ZPixmap);
 if (!image) {
  printf("XGetImage failed\n");
 }
​
 char filename[255];
 int n2;
​
 n2 = sprintf(filename, "/tmp/png_out/%d_test%d.png", xid, file_counter++);
​
 printf("filename %s \n", filename);
 write_png_for_image(image, xid, W, H, filename);
​
 //XFree(image);
 XDestroyImage(image);
 XDestroyWindow(display, root);
 XCloseDisplay(display);
}
​
void write_png_for_image(XImage *image, XID xid, int width, int height,
  char *filename) {
​
 int code = 0;
 FILE *fp;
 png_structp png_ptr;
 png_infop png_info_ptr;
 png_bytep png_row;
​
 char buffer[50];
 int n;
​
 n = sprintf(buffer, filename, xid);
​
// Open file
 fp = fopen(buffer, "wb");
 if (fp == NULL) {
  fprintf(stderr, "Could not open file for writing\n");
  code = 1;
 }
​
// Initialize write structure
 png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
 if (png_ptr == NULL) {
  fprintf(stderr, "Could not allocate write struct\n");
  code = 1;
 }
​
// Initialize info structure
 png_info_ptr = png_create_info_struct(png_ptr);
 if (png_info_ptr == NULL) {
  fprintf(stderr, "Could not allocate info struct\n");
  code = 1;
 }
​
// Setup Exception handling
 if (setjmp(png_jmpbuf (png_ptr))) {
  fprintf(stderr, "Error during png creation\n");
  code = 1;
 }
​
 png_init_io(png_ptr, fp);
​
// Write header (8 bit colour depth)
 png_set_IHDR(png_ptr, png_info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB,
 PNG_INTERLACE_NONE,
 PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
​
// Set title
 char *title = "Screenshot";
 if (title != NULL) {
  png_text title_text;
  title_text.compression = PNG_TEXT_COMPRESSION_NONE;
  title_text.key = "Title";
  title_text.text = title;
  png_set_text(png_ptr, png_info_ptr, &title_text, 1);
 }
​
 png_write_info(png_ptr, png_info_ptr);
​
// Allocate memory for one row (3 bytes per pixel - RGB)
 png_row = (png_bytep) malloc(3 * width * sizeof(png_byte));
​
 unsigned long red_mask = image->red_mask;
 unsigned long green_mask = image->green_mask;
 unsigned long blue_mask = image->blue_mask;
​
// Write image data
//int xxx, yyy;
 for (int y = 0; y < height; y++) {
  for (int x = 0; x < width; x++) {
   unsigned long pixel = XGetPixel(image, x, y);
   unsigned char blue = pixel & blue_mask;
   unsigned char green = (pixel & green_mask) >> 8;
   unsigned char red = (pixel & red_mask) >> 16;
   png_byte *ptr = &(png_row[x * 3]);
   ptr[0] = red;
   ptr[1] = green;
   ptr[2] = blue;
  }
  png_write_row(png_ptr, png_row);
 }
​
// End write
 png_write_end(png_ptr, NULL);
​
// Free
 fclose(fp);
 if (png_info_ptr != NULL)
  png_free_data(png_ptr, png_info_ptr, PNG_FREE_ALL, -1);
 if (png_ptr != NULL)
  png_destroy_write_struct(&png_ptr, (png_infopp) NULL);
 if (png_row != NULL)
  free(png_row);
​
}

窗口融合原理

FDE上要实现的窗口融合效果是Linux的每个X程序跟安卓的每个应用显示效果一致,使自己的程序独有窗口。当我们能够将每个X程序的图像从xserver的合成器中分离出来的时候,方案的前置条件就成立了。当然具体实现还要从以下几个方面着手。

1. 显示重定向

显示重定向是最容易理解的,就是把X程序对应用窗口的显示内容转到另一个单独的buffer中,FDE再把这个buffer拿去安卓中做显示,即是output的处理。

与compositor配合使用的一个扩展是damage,在重定向后,damage注册在不同的window或者screen上,就可以实现监听哪些区域有变化,再选择更新图像。在termux-x11中的实现是定时器检查damage状况,但是却不是把damage注册在对应的window上,这个是可以优化的。

static void lorieTimerCallback(int fd, unused int r, void *arg) {
   logh("lorieTimerCallback");
   char dummy[8];
   read(fd, dummy, 8);
   if (renderer_should_redraw() && RegionNotEmpty(DamageRegion(pvfb->damage))) {
       int redrawn = FALSE;
       ScreenPtr pScreen = (ScreenPtr) arg;
​
       loriePixmapUnlock(pScreen->GetScreenPixmap(pScreen));
       redrawn = renderer_redraw(pvfb->env, pvfb->root.flip);
       if (loriePixmapLock(pScreen->GetScreenPixmap(pScreen)) && redrawn)
           DamageEmpty(pvfb->damage);
   } else if (pvfb->cursorMoved)
       renderer_redraw(pvfb->env, pvfb->root.flip);
​
   pvfb->cursorMoved = FALSE;
}

Action 2 : 重定向后buffer的生命周期管理或者说X程序生命周期的管理

可能要拿到以下确切数据,来构建安卓窗口:

  1. connection建立过程,window创建过程,window的详细参数;

  2. window分类,哪些需要新窗口显示,哪些在内部处理;

  3. 优化damage机制;

  4. ....

2. 事件重定向

Compositor扩展并没有处理事件重定向,意思就是说他不重定向输入事件。他会将OverlayWindow 的所有输入直接传到下层窗口,相当于什么也没做。

那如果窗口重定向后,显示到一个离屏的区域,用户在新渲染的安卓窗口上操作,input事件(鼠标键盘事件)怎么发生在正确的位置呢?启用合成器,只是加了一个中间过程,最终还是会把所有程序的图像合成显示出来,所以可以大胆设想,利用这个原本的input坐标系,简分为三类情况来处理:

这里用一个虚框来表示原本的显示效果,即原本不做任何操作,xserver会这样显示X程序,实框表示真正显示的效果,安卓会这样显示X程序。

image-20240411-034227.png
  1. 操作在前台的程序firefox,如果需要发送input事件,只需要把坐标转换成正确的坐标发送事件即可;即 android window mouse.x .y -> xserver screen.x .y

image-20240411-034236.pngimage-20240411-034316.png
  1. 操作未在前台的程序wps, 如果是fling或者hover操作,根据damage通知更新即加,如果有点击操作,就是在安卓窗口获取到焦点的时候将WPS在Xserver的位置也切换到前台,剩下的操作与第1点相同。

image-20240411-034359.png
  1. 平移和缩放窗口,平移是在安卓窗口上的操作,对X程序的显示其实不产生影响,甚至可以不做处理,因为重定向之后,即使有堆叠也可以拿完全显示的X程序图像。缩放在安卓上几乎不需要太多改动,但是在xserver却要重绘,resize等操作,是一个需要把操作指令转换成xserver函数的工作。

image-20240411-034413.png

Action 3:事件重定向的坐标转换,窗口ConfigreNotify

  1. 缩放时处理Randr;

  2. 坐标系转换;

  3. 快捷键捕获;

  4. 文本输入移植ibus?;

  5. ...

3. 安卓端实现

相较于Xserver的改动,安卓的实现简单,在VNC上的处理都可以直接移植,遗留有一个activity多栈未解决,也不是马上必须要解决的。

关于input,VNC与xorg-xserver对输入的处理是不是完全一致也是需要验证的。

关于output,所有的X程序的图像buffer都有两份,虽然合成器合成后的那个图像并不会做显示,但是也占用了内存,需要评估最终效果。

Action 4 :需要在安卓端研究的工作:

  1. 复杂输入事件需要验证,比如组合事件是否能直接生效;

  2. 显示数据使用翻倍,内存有没有问题;

  3. 资源配置文件的完善(字体,字符,键盘布局文件等);

  4. DecorcaptionView跟X程序操作按钮的替换;

  5. ...

窗口分离实验步骤

以上是原理方面的设想,当要做工程实现的时候,最好是在现有项目做一定的验证,而且只对关键部分做验证,显然显示重定向是决定方案是否可行的关键。但是之前要解决另一个问题,Android多SurfaceView渲染的问题。

Android多SurfaceView渲染

现在大部分安卓项目在使用opengl渲染的时候已经都学会自己配置EGL,而不是直接使用GlSurfaceView,好处是可以自己管理EGL环境,可以处理所有资源的申请和释放,不再跟view的生命周期绑定,不再使用原生的render,自己组织gl指令,满足各种特制化需求。

SurfaceView + EGL环境渲染程序的一般流程如下。

配置资源:

bool Renderer::initialize()
{
    const EGLint attribs[] = {
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_BLUE_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_RED_SIZE, 8,
            EGL_NONE
    };
    EGLDisplay display;
    EGLConfig config;
    EGLint numConfigs;
    EGLint format;
    EGLSurface surface;
    EGLSurface surface1;
    EGLContext context;
    EGLint width;
    EGLint height;
    GLfloat ratio;
​
    LOG_INFO("Initializing context");
​
    if ((display = eglGetDisplay(EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY) {
        LOG_ERROR("eglGetDisplay() returned error %d", eglGetError());
        return false;
    }
    if (!eglInitialize(display, 0, 0)) {
        LOG_ERROR("eglInitialize() returned error %d", eglGetError());
        return false;
    }
​
    if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs)) {
        LOG_ERROR("eglChooseConfig() returned error %d", eglGetError());
        destroy();
        return false;
    }
​
    if (!eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format)) {
        LOG_ERROR("eglGetConfigAttrib() returned error %d", eglGetError());
        destroy();
        return false;
    }
​
​
    if (!(surface = eglCreateWindowSurface(display, config, _window, 0))) {
        LOG_ERROR("eglCreateWindowSurface() returned error %d", eglGetError());
        destroy();
        return false;
    }
​
    const EGLint contextAttrs[] = {
            EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE
    };
​
    if (!(context = eglCreateContext(display, config, nullptr, contextAttrs))) {
        LOG_ERROR("eglCreateContext() returned error %d", eglGetError());
        destroy();
        return false;
    }
​
​
    if (!eglMakeCurrent(display, surface, surface, context)) {
        LOG_ERROR("eglMakeCurrent() returned error %d", eglGetError());
        destroy();
        return false;
    }
​
​
    if (!eglQuerySurface(display, surface, EGL_WIDTH, &width) ||
        !eglQuerySurface(display, surface, EGL_HEIGHT, &height)) {
        LOG_ERROR("eglQuerySurface() returned error %d", eglGetError());
        destroy();
        return false;
    }
​
    _display = display;
    _surface = surface;
    _context = context;
​
    initProgram(width, height);
    return true;
}

绘制图像:

if (!eglMakeCurrent(_display, _surface, _surface, _context)) {
    LOG_ERROR("eglMakeCurrent() returned error %d", eglGetError());
}
drawFrame();
if (!eglSwapBuffers(_display, _surface)) {
    LOG_ERROR("eglSwapBuffers() returned error %d", eglGetError());
}

具体的绘制逻辑在:

void Renderer::drawFrames() {
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
​
    glUseProgram(_programHandle);
​
    glVertexAttribPointer(_positionHandle, 2, GL_FLOAT, GL_FALSE, 0, gTriangleVertices);
    glEnableVertexAttribArray(_positionHandle);
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

尝试新增一个SurfaceView,依次绑定context,当然这个_surface1也是eglCreateWindowSurface生成的,验证两个SurfaceView能够在同个EGL环境中渲染。

           if (!eglMakeCurrent(_display, _surface, _surface, _context)) {
                LOG_ERROR("eglMakeCurrent() returned error %d", eglGetError());
            }
            drawFrame();
            if (!eglSwapBuffers(_display, _surface)) {
                LOG_ERROR("eglSwapBuffers() returned error %d", eglGetError());
            }
​
​
            if (!eglMakeCurrent(_display, _surface1, _surface1, _context)) {
                LOG_ERROR("eglMakeCurrent() returned error %d", eglGetError());
            }
            drawFrame();
            if (!eglSwapBuffers(_display, _surface1)) {
                LOG_ERROR("eglSwapBuffers() returned error %d", eglGetError());
            }

效果图:

image-20240411-034537.png

Termux-x11不同SurfaceView显示窗口buffer

验证成功后,开始使用多SurfaceView来渲染termux-x11上的多纹理buffer。

改动如下:

  • 传入多SurfaceView

Bool lorieChangeWindow(unused ClientPtr pClient, void *closure) {
    SurfaceRes *res = (SurfaceRes*) closure;
    jobject surface = res->surface;
    logh("lorieChangeWindow surface:%p id:%d", surface, res->id);
    if(res->id !=0){
        S2 = res;
        renderer_set_window(pvfb->env, S1->surface, pvfb->root.buffer);
        initAnotherSurface(pvfb->env, S2->surface);
        lorieSetCursor(NULL, NULL, CursorForDevice(GetMaster(lorieMouse, MASTER_POINTER)), -1, -1);
        logh("lorieChangeWindow buffer:%p", pvfb->root.buffer);
        if (pvfb->root.legacyDrawing) {
            renderer_update_root(pScreenPtr->width, pScreenPtr->height, ((PixmapPtr) pScreenPtr->devPrivate)->devPrivate.ptr, pvfb->root.flip);
            renderer_redraw(pvfb->env, pvfb->root.flip);
        }
        return TRUE;
    } else{
        S1 = res;
        return TRUE;
    }
}
  • 初始化surface

void initAnotherSurface(JNIEnv* env, jobject new_surface){
    EGLNativeWindowType w = ANativeWindow_fromSurface(env, new_surface);
    log("initAnotherSurface w:%p", w);
    EGLSurface eglSurface = eglCreateWindowSurface(egl_display, cfg, w, NULL);
    if (eglSurface == EGL_NO_SURFACE) {
        eglCheckError(__LINE__);
        return;
    } else {
        sfc1 = eglSurface;
    }
}
​
  • 在合成扩展回调中获取新窗口ID

pScreen->separateWindowPtr1 = pWin;
        modifyGlobalVariable();
void modifyGlobalVariable() {
    WindowPtr separatePtr = pScreenPtr->separateWindowPtr1;
    if (separatePtr) {
        tempPixmap = (*pScreenPtr->GetWindowPixmap)(separatePtr);
    }
​
    if (tempPixmap) {
        pScreenPtr->separatePixPtr1 = tempPixmap;
        log(ERROR, "modifyGlobalVariable 3 width:%d height:%d screex:%d screeny:%d",
            tempPixmap->drawable.width, tempPixmap->drawable.height,
            tempPixmap->screen_x, tempPixmap->screen_y);
        renderer_update_root_process1(tempPixmap->drawable.width, tempPixmap->drawable.height,
                                      ((PixmapPtr) pScreenPtr->separatePixPtr1)->devPrivate.ptr, 0);
    }
}
  • 绑定otherdisplay纹理

void renderer_update_root_process1(int w, int h, void* data, uint8_t flip) {
    if (eglGetCurrentContext() == EGL_NO_CONTEXT || !w || !h)
        return;
    log("renderer_update_root_process1 w:%d h:%d data:%p flip:%d display.width=%f display.height:%f",
        w, h, data, flip, otherDisplay.width, otherDisplay.height );
    if (otherDisplay.width != (float) w || otherDisplay.height != (float) h) {
        otherDisplay.width = (float) w;
        otherDisplay.height = (float) h;
        glBindTexture(GL_TEXTURE_2D, otherDisplay.id); checkGlError();
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); checkGlError();
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); checkGlError();
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); checkGlError();
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); checkGlError();
        glTexImage2D(GL_TEXTURE_2D, 0, flip ? GL_RGBA : GL_BGRA_EXT, w, h, 0, flip ? GL_RGBA : GL_BGRA_EXT, GL_UNSIGNED_BYTE, data); checkGlError();
    } else {
        glBindTexture(GL_TEXTURE_2D, otherDisplay.id); checkGlError();
​
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, flip ? GL_RGBA : GL_BGRA_EXT, GL_UNSIGNED_BYTE, data);
        checkGlError();
    }
}
  • 在demage回调的时候,把两个surface都渲染一遍

  if (eglMakeCurrent(egl_display, sfc, sfc, ctx) != EGL_TRUE) {
        log("Xlorie: eglMakeCurrent failed.\n");
        eglCheckError(__LINE__);
    }
​
    draw(display.id,  -1.f, -1.f, 1.f, 1.f, flip);
    draw_cursor();
    if (eglSwapBuffers(egl_display, sfc) != EGL_TRUE) {
        err = eglGetError();
        eglCheckError(__LINE__);
        if (err == EGL_BAD_NATIVE_WINDOW || err == EGL_BAD_SURFACE) {
            log("We've got %s so window is to be destroyed. "
                "Native window disconnected/abandoned, probably activity is destroyed or in background",
                eglErrorLabel(err));
            renderer_set_window(env, NULL, NULL);
            return FALSE;
        }
    }
​
    modifyGlobalVariable();
    if (!sfc1 || eglGetCurrentContext() == EGL_NO_CONTEXT || !otherDisplay.id) {
        return FALSE;
    }
    if (eglMakeCurrent(egl_display, sfc1, sfc1, ctx) != EGL_TRUE) {
        log("Xlorie: eglMakeCurrent failed.\n");
        eglCheckError(__LINE__);
    }
    log("renderer_redraw otherDisplayid:%d displayid:%d", otherDisplay.id, display.id);
​
    draw(otherDisplay.id,  -1.f, -1.f, 1.f, 1.f, flip);
    draw_cursor();
​
    if (eglSwapBuffers(egl_display, sfc1) != EGL_TRUE) {
        err = eglGetError();
        eglCheckError(__LINE__);
        if (err == EGL_BAD_NATIVE_WINDOW || err == EGL_BAD_SURFACE) {
            log("We've got %s so window is to be destroyed. "
                "Native window disconnected/abandoned, probably activity is destroyed or in background",
                eglErrorLabel(err));
            renderer_set_window(env, NULL, NULL);
            return FALSE;
        }
    }

调试效果如下,这里的弹窗口是选项弹窗,未选特定窗口,不如看视频

screen-20240321-150835.mp4image-20240411-034714.png

Action 5:探究另一种可能的实现方式:Xserver多screen

每个X程序独占一个screen,所有X程序的input,output不产生关系,唯一需要处理焦点切换。

未来需要做的事 ACTION

  1. 改造termux/termux-x11;

  2. 重定向后buffer的生命周期管理或者说X程序生命周期的管理;

  3. 事件重定向的坐标转换,窗口ConfigreNotify;

  4. 复杂输入事件需要验证,比如组合事件是否能直接生效;

  5. 探究另一种可能的实现方式:Xserver多screen。