wayland浅析之EGL、Opengles、GBM

news2024/7/6 17:54:00
  • 本文针对不同的compositor,浅析egl+opengles+gbm搭配使用情况;

文章目录

  • 1. 前言
    • 1.1 问题一:是不是调用eglSwapBuffers函数以后图像就直接显示到屏幕上了?
    • 1.2 问题二:EGL基本使用流程,EGL搭配GBM上屏显示基本流程?
    • 1.3 问题三: 什么是off-screen,如何进行off-screen?
    • 1.4 问题四:接着问题三、如何理解Texture、FBO、RenderBuffer在合成器中的作用;
  • 2. EGL
    • 2.1 获取Display的三种方式
    • 2.2 Config and Attribute
      • 2.2.1 EGL Config Attribute
      • 2.2.2 EGL Additional Attribute
        • 2.2.2.1 Context
        • 2.2.2.2 WindowSurface
        • 2.2.2.3 PbufferSurface
    • 2.3 EGLSurface
      • 2.3.1 Surface基本概念
      • 2.3.2 eglSwapBuffers作用
        • 2.3.2.1 Mesa中的eglSwapBuffers
        • 2.3.2.2 问题一的回答,eglSwapBuffers的在上层的作用?
        • 2.3.2.3 问题二的回答,总结wayland中使用EGL上屏显示流程
        • 2.3.2.4 补充wayland中的buffer协议抽象
  • 3.离屏渲染
    • 3.1 定义
    • 3.2 纹理
      • 3.2.1 再谈概念
      • 3.2.2 如何渲染到纹理
      • 3.2.3 引入FBO
    • 3.3 FBO
      • 3.3.1 概念
      • 3.3.2 附着`Texture Image`应用
      • 3.3.3 附着`RenderBuffer Image`应用
      • 3.3.4 回答:如何将一个纹理拷贝到另一个纹理?

AuthorDateVersionDescription
陈梓归2022-11-15V1.0第一个版本

1. 前言

1.1 问题一:是不是调用eglSwapBuffers函数以后图像就直接显示到屏幕上了?

1.2 问题二:EGL基本使用流程,EGL搭配GBM上屏显示基本流程?

1.3 问题三: 什么是off-screen,如何进行off-screen?

1.4 问题四:接着问题三、如何理解Texture、FBO、RenderBuffer在合成器中的作用;

2. EGL

  • EGL是Khronos渲染API(如OpenGL、OpenGL ES或OpenVG)与底层本地平台(窗口)系统之间的接口。
    • 简单理解:OpenGL是图形渲染的API,提供了一套统一的接口来达到跨平台的目的,但是光有OpenGL渲染的API还是不行,还需要有渲染环境的支持,而每个不同的操作系统(如windows、 linux、 android等)都有自己的一套窗口管理系统;
      • 假如对不同操作系统都适配一套接口,那就和OpenGL的跨平台初衷相违背了。
      • 所以EGL就是定义了一套标准,来构建不同平台的窗口系统的渲染环境。
    • EGL主要功能:处理图形上下文管理、Buffer管理和渲染同步
      在这里插入图片描述
  • 简介:

    • Display (EGLDisplay): 对实际显示设备/窗口系统的抽象;
    • Surface (EGLSurface): 存储图像的内存区域;
    • Context (EGLContext): 存储渲染API的状态信息;
  • 一套标准的EGL绘制流程简介:

    • 这里只考虑标准绘制情况,还不考虑扩展等特殊情况,特殊情况后面会详解;

在这里插入图片描述
在这里插入图片描述

1.  获取 EGL Display 对象:eglGetDisplay
2.  初始化与 EGLDisplay 之间的连接:eglInitialize
3.  获取 EGLConfig 对象:eglChooseConfig / eglGetconfigs
4.  创建 EGLContext 实例:eglCreateContext
5.  创建 EGLSurface 实例:eglCreatewindowSurface / eglCratePbufferSurface
6.  连接 EGLContext 和 EGLSurface 上下文: eglMakeCurrent 
7.  使用 OpenGL ES API 绘制图形:gl_*
8.  切换 front buffer 和 back buffer 显示:eglSwapBuffer
9.  断开并释放与 EGLSurface 关联的 EGLContext 对象:eglRelease
10. 删除 EGLSurface 对象
11. 删除 EGLContext 对象
12. 终止与 EGLDisplay 之间的连接

2.1 获取Display的三种方式

  • EGLDisplay eglGetDisplay(NativeDisplayType native_display)

  • EGLDisplay eglGetPlatformDisplay( EGLenum platform, void * native_display, const EGLAttrib * attrib_list);

  • EGLDisplay eglGetPlatformDisplayEXT( EGLenum platform, void *native_display, const EGLint *attrib_list);

  • 以上三种用法基本一致:

    • 特别是 EGLDisplay eglGetPlatformDisplayEGLDisplay eglGetPlatformDisplayEXT

      • 一个是在EGL 1.5以上版本引入,一个是1.4版本的扩展引入;
  • 它们与EGLDisplay eglGetDisplay(NativeDisplayType native_display);细微区别;

    • eglGetDisplay会根据现在的环境来决定默认的原生窗口系统,其他两个需要手动指定平台;
  • compositor运行相当于是裸机运行没有窗口环境,首先必须通过GBM或者EGL_PLATFORM_DEVICE_EXT扩展这两种方式来获取EGLDisplay;

    • 补充GBM概念: 基于GEM/TTM的驱动对外是没有提供统一的内存管理接口的,至少Buffer Object创建销毁等操作是需要自行提供设备相关的即口进行实现的。
      • 用户态没有统一的接口对缓冲区进行管理,这导致某些特定用户态程序的开发的困难,如wayland compositor。
      • 简单的说GBM就是为了实现DRM(gbm_device)作为EGL的本地平台,创建的句柄可以用来初始化EGL和创建渲染目标缓冲区
    • 以GBM平台为例:
    
    // get gdm_device
    // path = "/dev/dri/renderD128" / "dev/dri/card0"
    egl_gbm.render_fd = open(path, O_RDWR|O_CLOEXEC);
    assert(-1 != egl_gbm.render_fd);
    egl_gbm.gbm_device = gbm_create_device(egl_gbm.render_fd);
    assert(NULL != egl_gbm.gbm_device);
    
    // get display
    1. egl_gbm.display = eglGetDisplay((EGLNativeDisplayType)egl_gbm.gbm_device);
    
    2. egl_gbm.display = eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, egl_gbm.gbm_device, NULL);
    
    3. egl_gbm.display = eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_MESA, egl_gbm.gbm_device, NULL);
    
    // wlroots里面从严谨性来说,通过GBM获取EGL Display的时候,eglGetPlatformDisplayEXT后面的参数应该是EGL_PLATFORM_GBM_MESA而不是EGL_PLATFORM_GBM_KHR;
    
  • 相关EGL支持platform:

    • EGL_PLATFORM_DEVICE_EXT 0x313F

      • 通过/dev/dri/card0或者/dev/dri/renderD128作为平台设备扩展来申请EGL Display;
        • 这种方式还得依赖EGL_EXT_device_enumeration和EGL_EXT_device_query这两个特性,有兴趣可以了解一下;
    • EGL_PLATFORM_GBM_KHR / EGL_PLATFORM_GBM_MESA 0x31D7

      • To obtain an EGLDisplay from an GBM device, call eglGetPlatformDisplay with <platform> set to EGL_PLATFORM_GBM_KHR.
      • To obtain an EGLDisplay from an GBM device, call eglGetPlatformDisplayEXT with<platform> set to EGL_PLATFORM_GBM_MESA.
    • EGL_PLATFORM_WAYLAND_KHR / EGL_PLATFORM_WAYLAND_EXT 0x31D8

      • To obtain an EGLDisplay backed by a Wayland display, call eglGetPlatformDisplay with <platform> set to EGL_PLATFORM_WAYLAND_KHR.
      • To obtain an EGLDisplay backed by a Wayland display, call eglGetPlatformDisplayEXT with <platform> set to EGL_PLATFORM_WAYLAND_EXT.
    • EGL_PLATFORM_X11_KHR / EGL_PLATFORM_X11_EXT 0x31D5

      • To obtain an EGLDisplay backed by an X11 screen, call eglGetPlatformDisplay with <platform> set to EGL_PLATFORM_X11_KHR.
      • To obtain an EGLDisplay backed by an X11 screen, call eglGetPlatformDisplayEXT with <platform> set to EGL_PLATFORM_X11_EXT.
  • 补充:

    • KHR和EXT扩展,在使用上没有任何区别,只是EGL版本的历史兼容缘故;

2.2 Config and Attribute

2.2.1 EGL Config Attribute

  • Config属性一般只用于创建Surface和Context
Config AttributeDescribeDefault Value
EGL_BUFFER_SIZE颜色缓冲区中所有颜色分量的位数0
EGL_RED_SIZE颜色缓冲区中红色分量的位数0
EGL_GREEN_SIZE颜色缓冲区中绿色分量的位数0
EGL_BLUE_SIZE颜色缓冲区中蓝色分量的位数0
EGL_ALPHA_SIZE颜色缓冲区中Alpha值位数0
EGL_LUMINANCE_SIZE颜色缓冲区中亮度位数0
EGL_COLOR_BUFFER_TYPE颜色缓冲区类型:EGL_RGB_BUFFER / EGL_LUMINANCE_BUFFEREGL_RGB_BUFFER
EGL_CONFIG_ID唯一的EGLConfig标识符值EGL_DONT_CARE
EGL_DEPTH_SIZE深度缓冲区位数0
EGL_STENCIL_SIZE模版缓冲区位数0
EGL_SURFACE_TYPE支持的表面类型:EGL_WINDOW_BIT、EGL_PBUFFER_BIT、EGL_PIXMAP_BITEGL_WINDOW_BIT
EGL_RENDERABLE_TYPE支持的渲染接口:EGL_OPENGL_ES_BIT、EGL_OPENGL_ES2_BIT、EGL_OPENGL_ES3_BIT(KHR)、EGL_OPENGL_BITEGL_OPENGL_ES_BIT
  • 配置选择流程
    // 这是我们对配置的需求
    const EGLint config_attribs[] = {
        EGL_BUFFER_SIZE, 32, // color component bit 32
        EGL_DEPTH_SIZE, EGL_DONT_CARE,
        EGL_STENCIL_SIZE, EGL_DONT_CARE,
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_NONE,
    };
    EGLint max_num_configs, num_configs, config_index;
    // 1. 获取此Display支持的可用的configs组数量; 140
    if (!eglGetConfigs(display, NULL, 0, &max_num_configs)) {
        fake_log(ERROR, "Failed to get display configs");
        return false;
    }
    fake_log(INFO, "Display config max num = %d", max_num_configs);
    // 2. 获取匹配我们需求的configs配置组; 20 
    EGLConfig *configs = malloc(num_configs * sizeof(EGLConfig));
    if (!eglChooseConfig(display, config_attribs, configs, max_num_configs,
                         &num_configs)) {
        fake_log(ERROR, "Failed to choose specify configs");
        return false;
    }
    fake_log(INFO, "匹配 config_attribs Display choose config num = %d",
             num_configs);
    // 3. 在我们匹配的配置组里面,在匹配一下我们的格式需求,确定最终的配置;[20]
    config_index = match_config_to_visual(display, GBM_FORMAT_ARGB8888, configs,
                                          num_configs);

// ---- 
static int match_config_to_visual(EGLDisplay egl_display, EGLint visual_id,
                                  EGLConfig *configs, int count) {
    EGLint id;
    for (int i = 0; i < count; ++i) {
        if (!eglGetConfigAttrib(egl_display, configs[i], EGL_NATIVE_VISUAL_ID,
                                &id))
            continue;
        if (id == visual_id)
            return i;
    }
    return -1;
}

2.2.2 EGL Additional Attribute

  • 附加属性,是针对不同的egl函数所设置的需求属性;

2.2.2.1 Context

  • Egl Context上下文包含了操作所需的所有状态信息,OpenGL ES 必须有一个可用的上下文 EGLContext 才能进行绘图。
    • 如果没有 EGLContext ,OpenGL或者Opengles就没有执行的环境
标志描述默认
EGL_CONTEXT_CLIENT_VERSION指定所使用的OpenGLES版本相关的上下文类型1
/*
EGL_CONTEXT_CLIENT_VERSION, 3, //使用OpenGL ES 3.0 版本 API
EGL_CONTEXT_CLIENT_VERSION, 2, //使用OpenGL ES 2.0版本 API
EGL_CONTEXT_CLIENT_VERSION, 1, //使用OpenGL ES 1.0版本 API
*/
const ELGint attribList[] = {
    EGL_CONTEXT_CLIENT_VERSION, 2, //使用OpenGL ES 2.0 版本 API
    EGL_NONE
};
egl_gbm.context = eglCreateContext(egl_gbm.display, configs[config_index],
                                       EGL_NO_CONTEXT, attribs);

2.2.2.2 WindowSurface

标志描述默认
EGL_RENDER__BUFFER指定渲染所用的缓冲区
EGL_BACK_BUFFER
EGL_SINGLE_BUFFER
EGL_BACK_BUFFER
    // use surface specify config
    const EGLint attribList[] = {
        EGL_RENDER_BUFFER, EGL_BACK_BUFFER,
        EGL_NONE,
    };
// attribList
    egl_gbm.window_surface = egl_gbm.procs.eglCreatePlatformWindowSurfaceEXT(
        egl_gbm.display, configs[config_index], egl_gbm.gbm_surface,
        attribList);

2.2.2.3 PbufferSurface

标志描述默认
EGL_WIDTH指定Pbuffer的宽度0
EGL_HEIGHT指定Pbuffer的高度0
EGL_LARGEST_PBUFFER如果请求的大小不可用,则选择内部最大的可用pbuffer大小EGL_FALSE
EGL_TEXTURE_FORMAT如果pbuffer绑定到一个纹理贴图指定的纹理格式类型
EGL_TEXTURE_RGB
EGL_TEXTURE_RGBA
EGL_NO_TEXTURE
EGL_NO_TEXTURE
EGL_TEXTURE_TARGET指定Pbuffer作为纹理贴图时应该连接到的相关纹理目标
EGL_TEXTURE_2D
EGL_NO_TEXTURE
EGL_NO_TEXTURE

	EGLint pbuffer_attribs[] = {
		EGL_WIDTH, 512,
		EGL_HEIGHT, 512,
		EGL_LARGEST_BUFFER, EGL_TRUE,
		EGL_NONE
	};

egl_gbm.pbuffer_surface = eglCreatePbufferSurface(egl_gbm.display, pbuffer_configs, pbuffer_attribList);

2.3 EGLSurface

2.3.1 Surface基本概念

  • Surface是一个抽象的概念,可以理解成一个容器对象,里面附着有不同的buffer用于显示提交;

    • buffer: color buffer(颜色缓冲)、depth buffer(深度缓冲)、stencil buffer(模板缓冲)
  • EGL中一共有三种Surface

    • WindowSurface

      • 顾名思义WindowSurface是和窗口相关的,也就是在屏幕上的一块显示存储的封装,渲染后即显示在界面上。
      • eglCreateWindowSurface
        • EGLSurface eglCreateWindowSurface( EGLDisplay display, EGLConfig config, NativeWindowType native_window, EGLint const * attrib_list);
      • eglCreatePlatformWindowSurface
        • EGLSurface eglCreatePlatformWindowSurface( EGLDisplay display, EGLConfig config, void * native_window, EGLAttrib const * attrib_list);
      • eglCreatePlatformWindowSurfaceEXT
        • EGLSurface eglCreatePlatformWindowSurfaceEXT( EGLDisplay dpy, EGLConfig config, void *native_window, const EGLint *attrib_list);
        • EGL_EXT_platform_base
    • PbufferSurface

      • 在显存中开辟一个空间,将渲染后的数据(帧)存放在这里。
      • eglCreatePbufferSurface
        • EGLSurface eglCreatePbufferSurface( EGLDisplay display, EGLConfig config, EGLint const * attrib_list);
    • PixmapSurface

      • 以位图的形式存放在内存中,各平台的支持很差。
      • eglCreatePixmapSurface
        • EGLSurface eglCreatePixmapSurface( EGLDisplay display, EGLConfig config, NativePixmapType native_pixmap, EGLint const * attrib_list);
      • eglCreatePlatformPixmapSurface
        • EGLSurface eglCreatePlatformPixmapSurface( EGLDisplay display, EGLConfig config, void * native_pixmap, EGLint const * attrib_list);
类型绑定本地窗口句柄绑定本地类型缓冲区缓冲区备注
window surface多缓冲区包括front buffer and back buffer; 默认在backbuffer中渲染,需要通过eglSwapBuffer来把渲染的结果显示到屏幕。 也有EGL_RENDER_TYPE可设置为EGL_SINGLE_BUFFER但这个要看ES的实现。一般无效
PBuffer单缓冲区不绑定任何本地的东西。需要指定EGL_WIDTH,EGL_HEIGHT参数,来创建对应的大小。不可以被显示,调用eglSwapBuffer是无效的。这种缓冲区可以直接用作纹理数据;
pixmapbuffer是(NativePixmapType)单缓冲区一个Pixmap代表内存中的一张图片,如要上屏显示需转换成纹理
  • pixmapbuffer与pbuffers的不同之处在于,它们确实有一个相关的本地像素图和本地像素图类型,而且有可能使用客户端API以外的API对像素图进行渲染;
    • 使用客户端API以外的API:一个例子是,你想用GPU渲染一个图片,然后把它作为一个X11光标。在这种情况下,可以使用PixmapSurface,然后把东西绘制进去,再使用XCreateCursorFromPixmap把你的像素图变成一个光标(这是EGL规范中提到的 "本地API "的一个例子)。

GBM Platform平台设备只支持window surface,下面我们GBM Platform Device为例子来进行创建:

  • 简而言之:gbm_surface -> egl_window_surface
// create gbm device and gbm surface
egl_gbm.gbm_device = gbm_create_device(gbm_fd);
assert(NULL != egl_gbm.gbm_device);

egl_gbm.gbm_surface = gbm_surface_create(egl_gbm.gbm_device, egl_gbm.mode.hdisplay, egl_gbm.mode.vdisplay,
        GBM_FORMAT_XRGB8888, GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
 assert(NULL != egl_gbm.gbm_surface);

// get window_surface, 三者区别同Display不再叙述;
1. egl_gbm.window_surface = eglCreateWindowSurface(egl_gbm.display,configs[config_index], (EGLNativeWindowType)egl_gbm.gbm_surface,attribList);

2. egl_gbm.window_surface = eglCreatePlatformWindowSurface(egl_gbm.display, configs[config_index],egl_gbm.gbm_surface, (EGLAttrib *)attribList);

3. egl_gbm.window_surface = egl_gbm.procs.eglCreatePlatformWindowSurfaceEXT(egl_gbm.display, configs[config_index], egl_gbm.gbm_surface,attribList);

// create context
egl_gbm.context = eglCreateContext(egl_gbm.display, configs[config_index], EGL_NO_CONTEXT, attribs);

// draw_color_use_window_surface
eglMakeCurrent(egl_gbm.display, egl_gbm.window_surface,
               egl_gbm.window_surface, egl_gbm.context);
glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
eglSwapBuffers(egl_gbm.display, egl_gbm.window_surface);

// scan_output_surface_to_display
	drmModeSetCrtc
// read_output_surface_to_file
	glReadPixels

// read_output_surface_to_texture --- need opengl es 3.0
	glReadBuffer(GL_BACK)
	glCopyTexImage2D

2.3.2 eglSwapBuffers作用

  • 好,我们来回答问题一:是不是调用eglSwapBuffers函数以后图像就直接显示到屏幕上了?

    2.3.2.1 Mesa中的eglSwapBuffers

在这里插入图片描述

// color buffer就是我们用来渲染和送显的buffer
// 最大四个
#if defined(HAVE_WAYLAND_PLATFORM) || defined(HAVE_DRM_PLATFORM)
   struct {
#ifdef HAVE_WAYLAND_PLATFORM
      struct wl_buffer *wl_buffer;
      bool wl_release;
      __DRIimage *dri_image;
      /* for is_different_gpu case. NULL else */
      __DRIimage *linear_copy;
      /* for swrast */
      void *data;
      int data_size;
#endif
#ifdef HAVE_DRM_PLATFORM
      struct gbm_bo *bo;
#endif
      bool locked;
      int age;
   } color_buffers[4], *back, *current;
#endif

// GBM平台,eglSwapBuffers最终就调用dri2_drm_swap_buffers
// 第一次进来get_back_bo是小于0的,就会进去申请buffer;
static EGLBoolean
dri2_drm_swap_buffers(_EGLDisplay *disp, _EGLSurface *draw)
{
   struct dri2_egl_display *dri2_dpy = dri2_egl_display(disp);
   struct dri2_egl_surface *dri2_surf = dri2_egl_surface(draw);

   if (!dri2_dpy->flush) {
      dri2_dpy->core->swapBuffers(dri2_surf->dri_drawable);
      return EGL_TRUE;
   }

   if (dri2_surf->current)
      _eglError(EGL_BAD_SURFACE, "dri2_swap_buffers");
   for (unsigned i = 0; i < ARRAY_SIZE(dri2_surf->color_buffers); i++)
      if (dri2_surf->color_buffers[i].age > 0)
         dri2_surf->color_buffers[i].age++;

   /* Make sure we have a back buffer in case we're swapping without
    * ever rendering. */
   if (get_back_bo(dri2_surf) < 0)
      return _eglError(EGL_BAD_ALLOC, "dri2_swap_buffers");

   dri2_surf->current = dri2_surf->back;
   dri2_surf->current->age = 1;
   dri2_surf->back = NULL;

   dri2_flush_drawable_for_swapbuffers(disp, draw);
   dri2_dpy->flush->invalidate(dri2_surf->dri_drawable);

   return EGL_TRUE;
}


static int
get_back_bo(struct dri2_egl_surface *dri2_surf)
{
   struct dri2_egl_display *dri2_dpy =
      dri2_egl_display(dri2_surf->base.Resource.Display);
   struct gbm_dri_surface *surf = dri2_surf->gbm_surf;
   int age = 0;

   if (dri2_surf->back == NULL) {
      for (unsigned i = 0; i < ARRAY_SIZE(dri2_surf->color_buffers); i++) {
	 if (!dri2_surf->color_buffers[i].locked &&
	      dri2_surf->color_buffers[i].age >= age) {
	    dri2_surf->back = &dri2_surf->color_buffers[i];
	    age = dri2_surf->color_buffers[i].age;
	 }
      }
   }

   if (dri2_surf->back == NULL)
      return -1;
   if (dri2_surf->back->bo == NULL) {
      if (surf->base.v0.modifiers)
         dri2_surf->back->bo = gbm_bo_create_with_modifiers(&dri2_dpy->gbm_dri->base,
                                                            surf->base.v0.width,
                                                            surf->base.v0.height,
                                                            surf->base.v0.format,
                                                            surf->base.v0.modifiers,
                                                            surf->base.v0.count);
      else {
         unsigned flags = surf->base.v0.flags;
         if (dri2_surf->base.ProtectedContent)
            flags |= GBM_BO_USE_PROTECTED;
         dri2_surf->back->bo = gbm_bo_create(&dri2_dpy->gbm_dri->base,
                                             surf->base.v0.width,
                                             surf->base.v0.height,
                                             surf->base.v0.format,
                                             flags);
      }

   }
   if (dri2_surf->back->bo == NULL)
      return -1;

   return 0;
}

2.3.2.2 问题一的回答,eglSwapBuffers的在上层的作用?

  • 通过mesa代码可以看出,简单来说eglSwapBuffers = gbm_bo_create + get_back_bo(第一次拿到为NULL,就会调用bo_create去创建) + glFlush

  • 实际上eglSwapBuffers函数执行以后,只是提示上层(一般是compositor)有输出buffer可以用了,这个时候是把输出buffer显示到屏幕上还是输出到文件或者其他地方,由上层策略来决定;

    • 1. 如果显示到屏幕上这一种就是kwin和weston这两种compositor的底层送显方式;

      • scan_output_surface_to_display()

            eglMakeCurrent(egl_gbm.display, egl_gbm.window_surface,
               egl_gbm.window_surface, egl_gbm.context);
            egl_gbm.gbm_bo = gbm_surface_lock_front_buffer(egl_gbm.gbm_surface);
            egl_gbm.handle = gbm_bo_get_handle(egl_gbm.gbm_bo).u32;
            egl_gbm.pitch =
                gbm_bo_get_stride(egl_gbm.gbm_bo); // pitch = mode.hdisplay * 4
            // fake_log(ERROR, "handle = %d pitch = %d", egl_gbm.handle, egl_gbm.pitch);
            drmModeAddFB(egl_gbm.card_fd, egl_gbm.mode.hdisplay, egl_gbm.mode.vdisplay,
                         24, 32, egl_gbm.pitch, egl_gbm.handle, &egl_gbm.fb_id);
            drmModeSetCrtc(egl_gbm.card_fd, egl_gbm.crtc->crtc_id, egl_gbm.fb_id, 0, 0,
                           &egl_gbm.connector_id, 1, &egl_gbm.mode);
        
        
      • 这里补充一个点,关于之前提到的card节点即可渲染又可送显,render节点只能渲染,这里有很好的体现:

        fdcreate gbm bo / gbm surfacedrmModeSetCrtc
        cardGBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERINGyes
        cardGBM_BO_USE_RENDERING no
        renderGBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERINGno
        renderGBM_BO_USE_RENDERINGno
    • 2. 如果输出到其他地方,就是后面要说的off-screen;

      • 2.1 到内存,read_output_surface_to_file:

        eglMakeCurrent(egl_gbm.display, egl_gbm.window_surface,
               egl_gbm.window_surface, egl_gbm.context);
        static FILE *file = NULL;
        static GLbyte *pbits = NULL;  /* CPU memory to save image */
        static uint32_t frame_cnt = 0;
        uint32_t frame_size = 10 * 10 * 4;
        if (!file) {
           file = fopen("rgba.bin", "w+");
           assert(file);
           pbits = (GLbyte *)malloc(frame_size);
           assert(pbits);
        }
        glReadPixels(0, 0, 10, 10, GL_RGBA, GL_UNSIGNED_BYTE, pbits);
        size_t ret = fwrite(pbits, 1, frame_size, file);
        
      • 2.2 到纹理,window buffer到纹理需要OpenglES3.0才支持

         glReadBuffer(GL_BACK)
         glCopyTexImage2D
      

2.3.2.3 问题二的回答,总结wayland中使用EGL上屏显示流程

waylandGBMSurfacescanout buffer关联
Weston / Kwingbm_surfaceegl_surfaceeglSwapBuffer
Wlrootsgbm_bonoEglImage + glEGLImageTargetRenderbufferStorageOES(FBO)《— attach — 》gbm_bo
  • 通过以前的KMS讲解我们知道:
    • 上屏显示就是将我们想要输出屏幕的内容弄到一个Framebuffer中,然后调用KMS API进行送显;
    • 那么我们在Compositor怎么使用Opengl / Opengl Es 将内容渲染到一个scanout buffer中?
      • off-screen渲染中间过程,比如使用FBO,或者Pbuffer等,不再这里考虑中(后面会详解),这里只考虑Compositor最后的送显;

      • 1. weston/kwin中采用的方式:

        • GBM_Surface + Window_Surface + eglMakeCurrent(Surface和Context配置信息匹配的话,就关联上了scanout buffer) + eglSwapBuffer
      • 2. wlroots中采用的方式:

        • GBM_BO + EGL_KHR_no_config_context + EGL_KHR_surfaceless_context + EGLImageKHR
          在这里插入图片描述

2.3.2.4 补充wayland中的buffer协议抽象

GlobalGlobal CreateGlobal Bind
wl_shmcompositorclient/libEGL.so
wl_drm / mali_buffer_sharinglibEGL.solibEGL.so
zwp_linux_dmabuf_v1compositorclient/libEGL.so
  • 稍微补充:

    • libEGL.so如何创建和绑定global?
    • 创建:
      • wayland server调用BindWaylandDisplayWL -> libEGL_mesa.so / libEGL_mali.so -> 注册wl_drm / mali_buffer_sharing的global;
    • 绑定:
      • wayland client调用eglInitialize -> libEGL_mesa.so / libEGL_mali.so -> 绑定 wl_drm / mali_buffer_sharing的global;
  • 一共三种:

    • 第一种:wh_shm
      • 这种方式是通过共享内存的方式来实现客户端和Compositor之间的共享,通过这种方式分配的内存是物理不连续的,这种方式一般用于采用软件绘制的情况;而且当buffer在客户端绘制完成以后,通知Compositor开始合成,需要通过glTexImage2D()函数把buffer内容转成纹理上传到GPU中,这样的话性能是会受到影响的,因为纹理上传一般是比较耗时的操作。
    • 第二种:wl_drm
      • 这种方式通过EGL中的Window_Surface(Wayland Display)和eglSwapBuffer来保证客户端渲染buffer的创建和传递,然后客户端开始绘制,绘制完成以后,合成器拿到客户端输出buffer通过eglCreateImageKHR(EGL_WAYLAND_BUFFER_WL)接口创建EGLImage,这个EGLImage可以直接作为Compositor的输入纹理来使用,不需要额外的拷贝工作。
    • 第三种: zwp_linux_dmabuf_v1
      • 这种方式和第二种类似,区别在于合成器创建EGLImage后面的参数是eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT)
        • 需要通过zwp_linux_dmabuf_v1协议来协商底层gbm_bo(dma-buf)的格式,第二种和第三种就是我们说的Buffer Zero Copying
      • 关于协议使用可见Weston中dmabuf client使用分析
        • https://blog.csdn.net/u012839187/article/details/107535495
      • Buffer Zero Copying
  • 关于三者详解以后有时间会单独写一篇适配文档单独说明😄

3.离屏渲染

3.1 定义

  • 广义来说
    • 如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。
    • 如果有时因为面临一些限制,无法把渲染结果直接写入framebuffer,而是先暂存在另外的区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。
      在这里插入图片描述
  • 狭义来说off-screen就是先将内容渲染到纹理或者渲染缓冲区,然后再输出到scanout buffer进行送显;
    • 应用场景的区别在于内容渲染如何到纹理
      • 由于我们只会用到颜色缓冲区,所以纹理和渲染缓冲区没有区别
      • 当使用模板、深度缓冲区以及多重采样时,会有性能上的提升;(老外说的,待研究)

3.2 纹理

3.2.1 再谈概念

  • 纹理几个概念傻傻分不清楚:
    • 纹理目标:GL_TEXTURE_2D、TEXTURE_EXTERNAL_OES等;
    • 纹理对象:纹理对象只是一个容器对象,容器内开始并没有东西(类似于FBO概念),我们生成纹理数据保存进去;
    • 纹理单元:GL_TEXTURE0-32,++纹理单元可以理解成通道,将数据传递到shader的通道++;
      • 每一个纹理单元可以指定一个纹理目标:一般是GL_TEXTURE_1D, 2D, 3D or CUBE_MAP之一
  • 可以这样理解:
    1. 一个纹理单元就是一把左轮手枪
    2. 支持的几种纹理目标就是手枪的弹孔标号,当前选定的纹理目标就是手枪正对弹膛的单孔
    3. 纹理对象就是子弹(纹理对象ID就是子弹标号);
      • 子弹打出来就直接到采样器进行shader采样;
        在这里插入图片描述

3.2.2 如何渲染到纹理

  • 方式一:
    • 通过绘制到窗口系统提供的帧缓冲区(Window_Surface),然后将帧缓冲区的对应后缓冲区域复制到纹理来实现渲染到纹理;
      • API:glReadBuffer、glCopyTex(Sub)Image2D
  • 方式二:
    • 通过使用连接到纹理的pbuffer来实现渲染到纹理
      • API: glBindTexture、eglBindTexImage

3.2.3 引入FBO

  • 问题:
      1. 当我们使用方式一的时候,有一个弊端,只能在纹理尺寸小于等于窗口缓冲区尺寸才有效;
      1. 当我们使用方式二的时候,我们知道,窗口系统提供的表面必须连接到一个渲染上下文,使用pbuffer表面也需要一个渲染上下文,那么上下文频繁的切换会造成效率的低下;
      1. 再补充一个问题,如何将一个纹理拷贝到另一个纹理;

3.3 FBO

  • 为了解决上面的弊端,我们就需要引入FBO;

3.3.1 概念

  • FBO(Frame Buffer Object)即帧缓冲区对象,实际上是一个可添加缓冲区的容器,可以为其添加纹理或渲染缓冲区对象(RBO)。
    • FBO 本身不能用于渲染,只有添加了纹理或者渲染缓冲区之后才能作为渲染目标,它仅且提供了 3 个附着(Attachment),分别是颜色附着、深度附着和模板附着。
      在这里插入图片描述

3.3.2 附着Texture Image应用

static void draw_color_to_fbo_texture(){

    // Texture
    //off_screen_context
    static const EGLint context_attribs[] = {
        EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL_NONE
    };
    // 1. 可以看到我们创建的上下文是没有表面信息和配置信息的,不会造成切换的效率问题;
    egl_gbm.off_screen_context = eglCreateContext(egl_gbm.display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, context_attribs);
    eglMakeCurrent(egl_gbm.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
            egl_gbm.off_screen_context);

    // 激活纹理单元(通道)-激活手枪
    glActiveTexture(GL_TEXTURE0)
    // 生成纹理对象-生成子弹
    glGenTextures(1, &egl_gbm.texture_target_1);
    // 绑定纹理目标 - 子弹放入弹孔
    glBindTexture(GL_TEXTURE_2D, egl_gbm.texture_target_1);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, egl_gbm.mode.hdisplay, egl_gbm.mode.vdisplay, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);

    // Fbo
    glGenFramebuffers(1, &egl_gbm.fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, egl_gbm.fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, egl_gbm.texture_target_1, 0);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        fprintf(stderr, "FBO creation failed\n");
    }


    glClearColor(0.0f, 1.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glFlush();
    read_draw_to_file(EGL_NO_SURFACE, EGL_NO_SURFACE, egl_gbm.off_screen_context);


    glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    eglMakeCurrent(egl_gbm.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
            EGL_NO_CONTEXT);
}

3.3.3 附着RenderBuffer Image应用

static void draw_color_to_fbo_renderbuffer_display(){

    // dmabuf: create gbm_bo
    egl_gbm.gbm_rbo = gbm_bo_create(
            egl_gbm.gbm_device, egl_gbm.mode.hdisplay, egl_gbm.mode.vdisplay,
            GBM_FORMAT_XRGB8888, GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
    assert(NULL != egl_gbm.gbm_rbo);
    // RGB plane count = 1
    // YUY may be plane count = 3
    egl_gbm.plane_count = gbm_bo_get_plane_count(egl_gbm.gbm_rbo);
    egl_gbm.strides[0] = gbm_bo_get_stride(egl_gbm.gbm_rbo);
	egl_gbm.dmabuf_fds[0] = gbm_bo_get_fd(egl_gbm.gbm_rbo);
    egl_gbm.offsets[0] = gbm_bo_get_offset(egl_gbm.gbm_rbo,0);
    fake_log(ERROR, "plane_count = %d offset = %d strides = %d dmabuf_fds = %d", egl_gbm.plane_count, egl_gbm.offsets[0], egl_gbm.strides[0], egl_gbm.dmabuf_fds[0]);


   // egl_image create
    const EGLint attribs_test[] = {
        EGL_WIDTH, egl_gbm.mode.hdisplay,
        EGL_HEIGHT, egl_gbm.mode.vdisplay,
        EGL_LINUX_DRM_FOURCC_EXT, GBM_FORMAT_ARGB8888,
        EGL_DMA_BUF_PLANE0_FD_EXT, egl_gbm.dmabuf_fds[0],
        EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
        EGL_DMA_BUF_PLANE0_PITCH_EXT, egl_gbm.strides[0],
        EGL_NONE,
    };

    // EGL_KHR_image_base + EGL_EXT_image_dma_buf_import
    egl_gbm.egl_image = egl_gbm.procs.eglCreateImageKHR(egl_gbm.display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs_test);
    assert(EGL_NO_IMAGE_KHR != egl_gbm.egl_image);

    // Render Buffer
    //off_screen_context
    static const EGLint context_attribs[] = {
        EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL_NONE
    };
    egl_gbm.off_screen_context = eglCreateContext(egl_gbm.display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, context_attribs);
    eglMakeCurrent(egl_gbm.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
            egl_gbm.off_screen_context);

	glGenRenderbuffers(1, &egl_gbm.renderbuffer);
	glBindRenderbuffer(GL_RENDERBUFFER, egl_gbm.renderbuffer);
    // GL_OES_EGL_image
    gles_fake.procs.glEGLImageTargetRenderbufferStorageOES(GL_RENDERBUFFER, egl_gbm.egl_image);

    // Fbo
    glGenFramebuffers(1, &egl_gbm.fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, egl_gbm.fbo);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
            GL_RENDERBUFFER, egl_gbm.renderbuffer);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        fprintf(stderr, "FBO creation failed\n");
    }


    glClearColor(0.0f, 0.0f, 1.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glFlush();
    read_draw_to_file(EGL_NO_SURFACE, EGL_NO_SURFACE, egl_gbm.off_screen_context);
    egl_gbm.handle = gbm_bo_get_handle(egl_gbm.gbm_rbo).u32;
    egl_gbm.pitch =
        gbm_bo_get_stride(egl_gbm.gbm_rbo); // pitch = mode.hdisplay * 4
    fake_log(ERROR, "handle = %d pitch = %d", egl_gbm.handle, egl_gbm.pitch);
    drmModeAddFB(egl_gbm.card_fd, egl_gbm.mode.hdisplay, egl_gbm.mode.vdisplay,
            24, 32, egl_gbm.pitch, egl_gbm.handle, &egl_gbm.fb_id);
    drmModeSetCrtc(egl_gbm.card_fd, egl_gbm.crtc->crtc_id, egl_gbm.fb_id, 0, 0,
            &egl_gbm.connector_id, 1, &egl_gbm.mode);
    getchar();

    glBindRenderbuffer(GL_RENDERBUFFER, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    eglDestroyImage(egl_gbm.display, egl_gbm.egl_image);
    eglMakeCurrent(egl_gbm.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
            EGL_NO_CONTEXT);

}
  • 这一种方法就是wlroots中如何将scanout buffer与render buffer关联起来,并送显的方式!

3.3.4 回答:如何将一个纹理拷贝到另一个纹理?

    1. 添加目标纹理为 FBO 的颜色附着(颜色缓冲区) ,绑定源纹理渲染到目标纹理。
    1. 添加源纹理为 FBO 的颜色附着(颜色缓冲区) , 使用 glCopyTexImage2D 拷贝当前 FBO 的颜色缓冲区到目标纹理。
    1. 通过FBO的块传输glBlitFramebuffer来完成;

完整代码路径:https://github.com/fakeczg/egl_gbm_off_screen

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/622955.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaSE06_面向对象之封装

JavaSE-06 [面向对象OOP 封装] 第一章 面向对象思想 1.1 面向过程和面向对象 面向过程&#xff1a; 面向过程就是分析出解决问题所需要的步骤&#xff0c;然后用函数把这些步骤一步一步实现&#xff0c;使用的时候一个一个依次调用就可以了面向对象&#xff1a; 面向对象是…

Servlet、JSP

一、web服务器 1.1 常用服务器产品 Tomcat&#xff08;Apache开源&#xff0c;主流的web服务器之一&#xff0c;多用于javaweb项目开发&#xff09; jetty&#xff08;效率高于Tomcat&#xff09; WebLogic&#xff08;Oracl 收费&#xff09; WebSpera&#xff08;IBM&#xf…

cesium学习之旅1:cesium 基本介绍以及 cesium 的 hello world 程序

一&#xff1a;什么是Cesium Cesium 是一个跨平台、跨浏览器的展示三维地球和地图的 javascript 库。Cesium 使用WebGL 来进行硬件加速图形&#xff0c;使用时不需要任何插件支持&#xff0c;但是浏览器必须支持WebGL。Cesium是基于Apache2.0 许可的开源程序。它可以免费的用于…

代码随想录二刷 day16 | 二叉树之104.二叉树的最大深度 559.n叉树的最大深度 111.二叉树的最小深度 222.完全二叉树的节点个数

day16 104.二叉树的最大深度559.n叉树的最大深度111.二叉树的最小深度222.完全二叉树的节点个数 104.二叉树的最大深度 题目链接 解题思路&#xff1a;本题中根节点的高度就是最大深度 二叉树节点的深度&#xff1a; 指从根节点到该节点的最长简单路径边的条数或者节点数&…

TensorFlow项目练手——天气预测

项目介绍 通过以往的天气数据和实际天气温度&#xff0c;做一次回归预测&#xff0c;模型的输入是当前的所有特征值&#xff0c;而模型的输出是当天的实际天气温度 字段分析 目前已有的数据有348条svc数据&#xff0c;他们的字段分别代表 year&#xff1a;年month&#xff…

美客多卖家攻略:养号技巧分享

在跨境电商平台上成功运营并建立起具有竞争力的店铺并不容易。美客多作为一个颇具影响力的平台&#xff0c;更需要卖家们仔细研究和精心运营。在这里&#xff0c;我将分享一些秘诀&#xff0c;这些秘诀是在我自养号过程中总结出来的&#xff0c;有助于增加销量并提升店铺的排名…

高级数据分析师岗位的职责描述

高级数据分析师岗位的职责描述1 职责&#xff1a; 1.搭建和完善数据中心的数据指标体系与监控预测体系&#xff0c;并推动系统化实现; 2.负责对市场、行业、竞争对手、产品、客户、业务运营等方面数据的收集、分析&#xff0c;完成整理出分析报告、提供数据支持、分析建议; 3.对…

AI 写的高考作文,你打几分?

又是一年高考时&#xff0c;高考真的是人生的一件大事&#xff0c;毕业这么多年&#xff0c;每次看到高考相关信息&#xff0c;还是会不由自主的点进来&#xff0c;其中语文的作文是每年大伙津津乐道的话题。 树先生今天就收到了某条小秘书的【邀请函】&#xff0c;邀请参与「…

Elasticsearch:实用指南

我们将更多地讨论使用 Elasticsearch 的最佳实践。这些做法是一般性建议&#xff0c;可以应用于任何用例。 让我们开始吧。 Bulk Requests 批量 API 使得在单个 API 调用中执行许多索引/删除操作成为可能。 这可以大大增加索引速度。 每个子请求都是独立执行的&#xff0c;因此…

Elasticsearch8.6.0安装

Elasticsearch 8.5.0 安装 Elasticsearch 简介Elasticsearch 8.6.0 安装创建网络拉取镜像运行镜像设置密码修改kibana配置绑定ES代码绑定&#xff1a;手动绑定&#xff1a; 配置ik分词器扩展词词典停用词词典 Elasticsearch 简介 Elasticsearch&#xff08;ES&#xff09; 是一…

Redis搭建分片集群

一、什么是Redis分片集群 1、概念 Redis分片集群是用于将Redis的数据分布在多个Redis节点上的分布式系统。通过分片集群&#xff0c;可以将数据分成多个部分&#xff0c;并将每个部分存储在不同的节点上&#xff0c;以便实现Redis的高可用性和高性能。 2、Redis分片集群原理…

写字楼里的「连接」智慧,撬起万亿新赛道

【潮汐商业评论/原创】 对于新入职的Cherry来说&#xff0c;在新公司上班的体验也是全新的。 每天上班&#xff0c;尚不熟悉的她可以在互动屏的指导下精准找到目的地。办公室的温度、湿度和空气质量&#xff0c;会随着天气条件和人员的密集程度相应调整。休息时Cherry抬头就能…

nc/netcat使用

目录 一、前言1.netcat是什么2.netcat有什么用 二、netcat的使用1.程序文件2.作为HTTP客户端3.作为HTTP服务端4.文件传输 三、问题与思考四、小结 一、前言 1.netcat是什么 netcat是一个基于命令行的网络调试和开发工具。对于windows和linux操作系统中都有适配的程序包,程序文…

AcWing算法提高课-1.3.13机器分配

宣传一下算法提高课整理 <— CSDN个人主页&#xff1a;更好的阅读体验 <— 本题链接&#xff08;AcWing&#xff09; 点这里 题目描述 总公司拥有 M M M 台 相同 的高效设备&#xff0c;准备分给下属的 N N N 个分公司。 各分公司若获得这些设备&#xff0c;可以为…

Linux - 第24节 - Linux高级IO(三)

1.Reactor模式 1.1.Reactor模式的定义 Reactor反应器模式&#xff0c;也叫做分发者模式或通知者模式&#xff0c;是一种将就绪事件派发给对应服务处理程序的事件设计模式。 1.2.Reactor模式的角色构成 Reactor主要由以下五个角色构成&#xff1a; 角色解释Handle&#xff08;句…

LVS-DR负载群集的优势和部署实例(我们都会在各自喜欢的事情里变得可爱)

文章目录 一、DR模式数据包流向分析二、DR模式的特点三、DR模式中需要解决的问题问题1解决方式 问题2解决方式 四、LVS-DR部署实例1.配置NFS共享存储器2.配置节点web服务&#xff08;两台的配置相同&#xff09;3.配置LVS负载调度器 一、DR模式数据包流向分析 1.Client 客户端…

【Hello MySQL】数据库基础

目录 1. 什么是数据库 2. 主流数据库 3. MySQL的基本使用 3.1 MySQL安装 3.2 连接 MySQL 服务器 3.3 退出 MySQL 服务器 3.4 服务器&#xff0c;数据库&#xff0c;表关系 3.5 MySQL的配置 4. MySQL架构 5. SQL分类 6. 存储引擎 6.1 存储引擎 6.2 查看存储引擎 6.3 存储引擎对…

Vue.js 中的 $nextTick 方法是什么?有什么作用?

Vue.js 中的 $nextTick 方法是什么&#xff1f; 在 Vue.js 中&#xff0c;$nextTick 方法是一个非常有用的工具&#xff0c;它可以让我们在下一个 DOM 更新周期之前执行回调函数。这个方法可以用于很多场景&#xff0c;比如在 Vue 实例数据改变之后&#xff0c;立即获取更新后…

肠道重要菌属——Dorea菌,减肥过敏要重视它?

谷禾健康 认识 Dorea菌 Dorea菌属于厚壁菌门毛螺菌科&#xff0c;广泛存在于人体肠道内&#xff0c;谷禾数据显示该菌在人群的检出率超89%。该菌最早也是从人体粪便中分离出来。 “Dorea” 目前没有一个确定的译名&#xff0c;Dorea是以法国微生物学家 Joel Dor 的名字命名&…

进入流程化管理不再是奢望,开源快速开发框架助你梦想成真!

在数字化进程快速发展的今天&#xff0c;流程化管理是企业做强做大的重要一步。如何实现流程化管理&#xff1f;如何实现数字化发展目标&#xff1f;这些问题都是值得每一个企业深思的重要课题。开源快速开发框架是一种快速帮助企业提质增效的平台软件&#xff0c;可以让每一个…