Direct Rendering Manager - 基本概念
Direct Rendering Manager 基本概念
- 1 概述
- 2 DRM
- 2.1 libdrm
- 2.2 KMS(Kernel Mode Setting)
- 2.3 GEM(Graphics Execution Manager)
- 2.3.1 Fence
- 2.3.2 CMA(Contiguous Memory Allocator)
- 2.3.3 DMA-BUF
- 3 DRM代码结构
- 3.1 drm文件列表
- 3.2 drm设备操作API
在以前对于Linux图形子系统接触中只涉及了FB架构,FBDEV向app提供/dev/fbx设备节点来访问display controller和帧缓存,通常来说是由用户来填充mmap映射过来的显存,然后拿去送显。这种方式比较简单,操作起来并不复杂,随着内核更替,衍生出一种新的显示框架-DRM,DRM较FB,内容更加丰富,功能更加齐全(支持多层合成、Vsync、dma-buf、异步更新、fence机制等等),能够应对复杂多变的显示应用场景。尽管FB退出历史舞台,在DRM中也并未将其遗弃,而是集合到DRM中,供部分嵌入式设备使用。出于对DRM架构的兴趣及想了解GPU与DRM联系,这里进行一个简要记录。对于DRM架构介绍,大牛很多,写的已经很全面了,这里只是在他们基础上做一个简要总结,方便学习过程中的查找。
以 The DRM/KMS subsystem from a newbie’s point of view的说明,在Linux中用于显示的子系统存在以下几种:
① FBDEV: Framebuffer Device
② DRM/KMS: Direct Rendering Manager / Kernel Mode Setting
③ V4L2: Video For Linux 2
选择DRM在于:维护积极、使用广泛、功能齐全且高级;FBDEV维护不积极、功能欠缺;V4L2较适合于视频输出,不适于复杂显示
相关大神文章链接:
何小龙:DRM (Direct Rendering Manager)
蜗窝科技:图形子系统
Younix脏羊:Linux DRM
揭开Wayland的面纱(一):X Window的前生今世
揭开Wayland的面纱(二):Wayland应运而生
1 概述
Linux子系统众多,而其中的图形子系统作为与用户息息相关的子系统:对下,它要管理形态各异的、性能各异的显示相关的器件;对上,它要向应用程序提供易用的、友好的、功能强大的图形用户界面(GUI)。在Linux graphic subsytem(1)_概述与Linux graphic subsystem(2)_DRI介绍中对于图形子系统的描述非常清晰,这里对一些主要概念进行概括:
Windows System:X window、Wayland Compositior、Android SurfaceFlinger
① 遵从client-server架构,server即display server/window server/compositor,管理输入设备、输出设备
② client绘图请求,交给display server,以一定规则混合、叠加
③ client与display server之间以某种协议交互,如Binder
例:X Windows System:只提供实现GUI环境的基本框架
窗口管理器(window manager):负责控制应用程序窗口的布局和外观,使每个应用程序窗口尽量以统一、一致的方式呈现给用户
GUI工具集(GUI Toolkits):Windowing system之上的进一步的封装
桌面环境(desktop environment):应用程序级别的封装,通过提供一系列界面一致、操作方式一致的应用程序,使系统以更为友好的方式向用户提供服务
DRI(Direct Render Infrastructure):在Application<---->Service<---->Driver<---->Hardware软件架构下,APP无法直接访问硬件导致游戏等3D场景达不到性能最优,DRI为3D Rendering提供直接访问硬件的框架,使以X server为中心的设计转向以Kernel及组件为中心的设计。Linux为DRI开辟了两条路径:DRM与KMS(Kernel Mode Setting),分别实现Rendering和送显。
DRM(Direct Rendering Manager):libdrm+kms(ctrc、encoder、connector、plane、fb、vblank、property)+gem(dumb、prime、fence)
① 统一管理、调度多个应用程序向显卡发送的命令请求,可以类比为管理CPU资源的进程管理(process management)模块
② 统一管理显示有关的memory(memory可以是GPU专用的,也可以是system ram划给GPU的,后一种方法在嵌入式系统比较常用),该功能由GEM(Graphics Execution Manager)模块实现
KMS(Kernel Mode Setting):也称为Atomic KMS
① 显示模式(display mode)的设置,包括屏幕分辨率(resolution)、颜色深的(color depth)、屏幕刷新率(refresh rate)等等
② 一般来说,是通过控制display controller的来实现上述功能的
GEM(Graphic Execution Manager):负责显示buffer的分配和释放,也是GPU唯一用到DRM的地方,设计dma-buf
2 DRM
初步了解DRM,可以参考The DRM/KMS subsystem from a newbie’s point of view
DRM总体分为三个模块:libdrm、KMS、GEM,后两者是DRM中最重要模块,贯穿整个过程的核心。
2.1 libdrm
硬件相关接口:如内存映射、DMA操作、fence管理等。
2.2 KMS(Kernel Mode Setting)
涉及到的元素有:CRTC,ENCODER,CONNECTOR,PLANE,FB,VBLANK,property
FB:DRM Framebuffer
用于存储显示数据的内存区域,使用GEM or TTM管理。TTM(Translation Table Maps)出于GEM之前,设计用于管理GPU访问不同类型的内存,如Video RAM、GART-Graphics Address Remapping Table、CPU不可直接访问的显存,此外还用于维护内存一致性,最重要的概念就是fences,来确保CPU-GPU内存一致性。由于TTM将独显与集显于一体化管理,导致其复杂度高,在现在系统中,已经使用更简单、API更好的GEM用以替换TTM,但考虑TTM对涉及独显、IOMMU场合适配程度更好,目前并未舍弃TTM,而是将其融入GEM的API下。
kernel使用struct drm_framebuffer表示Framebuffer:
// include/drm/drm_framebuffer.h
struct drm_framebuffer {struct drm_device *dev;struct list_head head;struct drm_mode_object base;const struct drm_format_info *format; // drm格式信息const struct drm_framebuffer_funcs *funcs;unsigned int pitches[4]; // Line stride per bufferunsigned int offsets[4]; // Offset from buffer start to the actual pixel data in bytes, per buffer.uint64_t modifier; // Data layout modifierunsigned int width;unsigned int height;int flags;int hot_x;int hot_y;struct list_head filp_head;struct drm_gem_object *obj[4];
};
struct drm_framebuffer主要元素的展示如下图所示(来自brezillon-drm-kms):
内存缓冲区组织,采取FOURCC格式代码
// include/drm/drm_fourcc.h
struct drm_format_info {u32 format;u8 depth;union {u8 cpp[3];u8 char_per_block[3];};u8 block_w[3];u8 block_h[3];u8 hsub;u8 vsub;bool has_alpha;bool is_yuv;
};
CRTC:阴极摄像管上下文
可以通过CRT/LCD/VGA Information and Timing了解下LCD成像原理。CRTC对内连接FrameBuffer,对外连接Encoder,完成对buffer扫描、产生时序。上述功能的主要通过struct drm_crtc_funcs和struct drm_crtc_helper_funcs这两个描述符实现。
struct drm_crtc_funcs {...int (*set_config)(struct drm_mode_set *set,struct drm_modeset_acquire_ctx *ctx); // 更新待送显数据int (*page_flip)(struct drm_crtc *crtc,struct drm_framebuffer *fb,struct drm_pending_vblank_event *event,uint32_t flags,struct drm_modeset_acquire_ctx *ctx); // 乒乓缓存...
};
Planes:硬件图层
简单说就是图层,每一次显示可能传入多个图层,通过Blending操作最终显示到屏幕。这种好处在于可以控制某一个图层处于特定模式,如给 Video 刷新提供了高速通道,使 Video 单独为一个图层。
Encoder:编码器
它的作用就是将内存的 pixel 像素编码(转换)为显示器所需要的信号,将CRTC输出的timing时序转换为外部设备所需的信号模块。例如 DVID、VGA、YPbPr、CVBS、Mipi、eDP 等。
Connector:
连接物理显示设备的连接器,如HDMI、DisplayPort、DSI总线,通常和Encoder驱动绑定在一起。
显示过程其实就是通过CRTC扫描FrameBuffer与Planes的内容,通过Encoder转换为外部设备能够识别的信号,再通过Connector连接器到处到显示屏上,完成显示。
2.3 GEM(Graphics Execution Manager)
涉及到的元素有:DUMB、PRIME、Fence
这几种概念强烈建议看一下龙哥的关于 DRM 中 DUMB 和 PRIME 名字的由来
DUMB:只支持连续物理内存,基于kernel中通用CMA API实现,多用于小分辨率简单场景
PRIME:连续、非连续物理内存都支持,基于DMA-BUF机制,可以实现buffer共享,多用于大内存复杂场景。dma-buf建议看龙哥的dma-buf系列
Fence:buffer同步机制,基于内核dma_fence机制实现,用于防止显示内容出现异步问题。注意一点,GPU中处理的buffer的fence是由display来创建的
2.3.1 Fence
这里暂时只解释一下“GPU中处理的buffer的fence是由display来创建的”的意思,以Android显示中的triple buffer为例解释,在display占用bufferC时,SurfaceFlinger Acquire BufferA,在commit时,display为BufferC创建releasefence,在一次Vsync信号到来时,释放releasefence,致使GPU可以立即拿到BufferC进行渲染操作。
推荐:简图记录-android fence机制
2.3.2 CMA(Contiguous Memory Allocator)
CMA是内存管理子系统的一个模块,负责物理地址连续的内存分配,处于需要连续内存的其他内核模块和内存管理模块之间的一个中间层模块,专用于分配物理连续的大块内存,以满足大内存需求设备(Display、Camera)。
Linux中使用4K作为page size,对于huge page的处理,是通过MMU将连续的所需大小的huge page的虚拟地址mapping到连续的物理地址去,相当于将huge page拆分成连续的4K page frame。对于驱动而言,大内存数据交互需要用到DMA,同样驱动分配的DMA-Buffer也必须是物理连续的(DMA-Buffer与huge page的差别在于huge page需要物理地址首地址地址对齐)。有了DMA-Buffer,为何又引入CMA呢?CMA模块学习笔记:① 应用启动分配的DMA-Buffer容易造成内存资源浪费;② 设备驱动分配的DMA-Buffer在内存碎片化下变得不可靠;CMA的出现能够使分配的内存可以被其他模块使用,驱动分配CMA后,其他模块需要吐出来。
驱动通过DMA mapping framework间接使用CMA服务:
2.3.3 DMA-BUF
mmap知识推荐:DRM 驱动 mmap 详解
推荐:dma-buf 由浅入深
3 DRM代码结构
3.1 drm文件列表
// ls drivers/gpu/drm/
amd drm_blend.c drm_dp_aux_dev.c drm_fourcc.c drm_lock.c drm_prime.c drm_vm.c meson savage vc4
arc drm_bridge.c drm_dp_dual_mode_helper.c drm_framebuffer.c drm_memory.c drm_print.c etnaviv mga selftests vgem
arm drm_bufs.c drm_dp_helper.c drm_gem.c drm_mipi_dsi.c drm_probe_helper.c exynos mgag200 shmobile via
armada drm_cache.c drm_dp_mst_topology.c drm_gem_cma_helper.c drm_mm.c drm_property.c fsl-dcu msm sis virtio
ast drm_color_mgmt.c drm_drv.c drm_gem_framebuffer_helper.c drm_mode_config.c drm_rect.c gma500 mxsfb sprd vmwgfx
ati_pcigart.c drm_connector.c drm_dumb_buffers.c drm_global.c drm_mode_object.c drm_scatter.c hisilicon nouveau sti zte
atmel-hlcdc drm_context.c drm_edid.c drm_hashtab.c drm_modes.c drm_scdc_helper.c i2c omapdrm stm
bochs drm_crtc.c drm_edid_load.c drm_info.c drm_modeset_helper.c drm_simple_kms_helper.c i810 panel sun4i
bridge drm_crtc_helper.c drm_encoder.c drm_internal.h drm_modeset_lock.c drm_syncobj.c i915 pl111 tdfx
cirrus drm_crtc_helper_internal.h drm_encoder_slave.c drm_ioc32.c drm_of.c drm_sysfs.c imx qxl tegra
drm_agpsupport.c drm_crtc_internal.h drm_fb_cma_helper.c drm_ioctl.c drm_panel.c drm_trace.h Kconfig r128 tilcdc
drm_atomic.c drm_debugfs.c drm_fb_helper.c drm_irq.c drm_pci.c drm_trace_points.c lib radeon tinydrm
drm_atomic_helper.c drm_debugfs_crc.c drm_file.c drm_kms_helper_common.c drm_plane.c drm_vblank.c Makefile rcar-du ttm
drm_auth.c drm_dma.c drm_flip_work.c drm_legacy.h drm_plane_helper.c drm_vma_manager.c mediatek rockchip udl
3.2 drm设备操作API
Open设备
fd = open(DRM_DEVICE, O_RDWR, S_IRWXU);if (fd < 0) {ALOGE("open drm device failed fd=%d.", fd);return -1;}
设置用户支持的能力
drm_public int drmSetClientCap(int fd, uint64_t capability, uint64_t value)
{struct drm_set_client_cap cap;...return drmIoctl(fd, DRM_IOCTL_SET_CLIENT_CAP, &cap);
}
检索Resource
drm_public drmModeResPtr drmModeGetResources(int fd)
{struct drm_mode_card_res res, counts;...if (drmIoctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res))return 0;counts = res;if (res.count_fbs) {res.fb_id_ptr = VOID2U64(drmMalloc(res.count_fbs*sizeof(uint32_t)));if (!res.fb_id_ptr)goto err_allocs;}if (res.count_crtcs) {res.crtc_id_ptr = VOID2U64(drmMalloc(res.count_crtcs*sizeof(uint32_t)));if (!res.crtc_id_ptr)goto err_allocs;}if (res.count_connectors) {res.connector_id_ptr = VOID2U64(drmMalloc(res.count_connectors*sizeof(uint32_t)));if (!res.connector_id_ptr)goto err_allocs;}if (res.count_encoders) {res.encoder_id_ptr = VOID2U64(drmMalloc(res.count_encoders*sizeof(uint32_t)));if (!res.encoder_id_ptr)goto err_allocs;}if (drmIoctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res))goto err_allocs;/* The number of available connectors and etc may have changed with a* hotplug event in between the ioctls, in which case the field is* silently ignored by the kernel.*/if (counts.count_fbs < res.count_fbs ||counts.count_crtcs < res.count_crtcs ||counts.count_connectors < res.count_connectors ||counts.count_encoders < res.count_encoders){drmFree(U642VOID(res.fb_id_ptr));drmFree(U642VOID(res.crtc_id_ptr));drmFree(U642VOID(res.connector_id_ptr));drmFree(U642VOID(res.encoder_id_ptr));goto retry;}...
}
获取Connector
static drmModeConnectorPtr
_drmModeGetConnector(int fd, uint32_t connector_id, int probe)
{struct drm_mode_get_connector conn, counts;drmModeConnectorPtr r = NULL;struct drm_mode_modeinfo stack_mode;...if (drmIoctl(fd, DRM_IOCTL_MODE_GETCONNECTOR, &conn))return 0;retry:counts = conn;if (conn.count_props) {conn.props_ptr = VOID2U64(drmMalloc(conn.count_props*sizeof(uint32_t)));if (!conn.props_ptr)goto err_allocs;conn.prop_values_ptr = VOID2U64(drmMalloc(conn.count_props*sizeof(uint64_t)));if (!conn.prop_values_ptr)goto err_allocs;}if (conn.count_modes) {conn.modes_ptr = VOID2U64(drmMalloc(conn.count_modes*sizeof(struct drm_mode_modeinfo)));if (!conn.modes_ptr)goto err_allocs;} else {conn.count_modes = 1;conn.modes_ptr = VOID2U64(&stack_mode);}if (conn.count_encoders) {conn.encoders_ptr = VOID2U64(drmMalloc(conn.count_encoders*sizeof(uint32_t)));if (!conn.encoders_ptr)goto err_allocs;}if (drmIoctl(fd, DRM_IOCTL_MODE_GETCONNECTOR, &conn))goto err_allocs;/* The number of available connectors and etc may have changed with a* hotplug event in between the ioctls, in which case the field is* silently ignored by the kernel.*/if (counts.count_props < conn.count_props ||counts.count_modes < conn.count_modes ||counts.count_encoders < conn.count_encoders) {drmFree(U642VOID(conn.props_ptr));drmFree(U642VOID(conn.prop_values_ptr));if (U642VOID(conn.modes_ptr) != &stack_mode)drmFree(U642VOID(conn.modes_ptr));drmFree(U642VOID(conn.encoders_ptr));goto retry;}...
}
获取 Encoder
drm_public drmModeEncoderPtr drmModeGetEncoder(int fd, uint32_t encoder_id)
{struct drm_mode_get_encoder enc;drmModeEncoderPtr r = NULL;...if (drmIoctl(fd, DRM_IOCTL_MODE_GETENCODER, &enc))return 0;...
}
设置CRTC与获取CRTC
drm_public int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,uint32_t x, uint32_t y, uint32_t *connectors, int count,drmModeModeInfoPtr mode)
{struct drm_mode_crtc crtc;...return DRM_IOCTL(fd, DRM_IOCTL_MODE_SETCRTC, &crtc);
}drm_public drmModeCrtcPtr drmModeGetCrtc(int fd, uint32_t crtcId)
{struct drm_mode_crtc crtc;drmModeCrtcPtr r;...if (drmIoctl(fd, DRM_IOCTL_MODE_GETCRTC, &crtc))return 0;...
}
FrameBuffer
static int modeset_create_fb(int fd, struct modeset_dev *dev)
{struct drm_mode_create_dumb creq;struct drm_mode_destroy_dumb dreq;struct drm_mode_map_dumb mreq;...ret = drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq); // 创建DUMB Buffer.../* create framebuffer object for the dumb-buffer */ret = drmModeAddFB(fd, dev->width, dev->height, 24, 32, dev->stride,dev->handle, &dev->fb); // 添加FB.../* prepare buffer for memory mapping */memset(&mreq, 0, sizeof(mreq)); // 准备mapmreq.handle = dev->handle;ret = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq);.../* perform actual memory mapping */dev->map = mmap(0, dev->size, PROT_READ | PROT_WRITE, MAP_SHARED,fd, mreq.offset); // 做map操作.../* clear the framebuffer to 0 */memset(dev->map, 0, dev->size);...
}
从上面大致看出,基本都是ioctl,DRM 驱动程序开发(VKMS)总结了这些IOCTL含义,大致如下:
IOCTL | API | Desc |
---|---|---|
DRM_IOCTL_VERSION | drmGetVersion | 查询驱动版本 |
DRM_IOCTL_GET_UNIQUE | drmGetBusid | 获取设备总线ID |
DRM_IOCTL_GET_MAGIC | drmGetMagic | 获取 Magic Number,用于 GEM ioctl 权限检查 |
DRM_IOCTL_IRQ_BUSID | drmGetInterruptFromBusID | 从总线ID获取IRQ |
DRM_IOCTL_GET_MAP | drmGetMap | 获取mapping后内存 |
DRM_IOCTL_GET_CLIENT | drmGetClient | 获取当前 DRM 设备上的所有 client 进程 |
DRM_IOCTL_GET_CAP | drmGetCap | 获取当前 DRM 设备所支持的能力 |
DRM_IOCTL_SET_CLIENT_CAP | drmSetClientCap | 告诉 DRM 驱动当前用户进程所支持的能力 |
DRM_IOCTL_CONTROL | drmCtlInstHandler | 安装IRQ处理程序 |
DRM_IOCTL_ADD_MAP | drmAddMap | 内存映射相关 |
DRM_IOCTL_SET_MASTER | drmSetMaster | 获取 DRM-Master 访问权限 |
DRM_IOCTL_ADD_CTX | drmCreateContext | 创建上下文 |
DRM_IOCTL_DMA | drmDMA | 保留DMA缓冲区 |
DRM_IOCTL_LOCK | drmGetLock | 获取重量级锁 |
DRM_IOCTL_PRIME_HANDLE_TO_FD | drmPrimeHandleToFD | 将fd与handle绑定 |
DRM_IOCTL_AGP_ACQUIRE | drmAgpAcquire | 获取AGP设备 |
DRM_IOCTL_WAIT_VBLANK | drmWaitVBlank | 等待VBLANK |
DRM_IOCTL_MODE_GETRESOURCES | drmModeGetResources | 检索Resource |
DRM_IOCTL_MODE_GETCRTC | drmModeGetCrtc | 检索CRTC |
DRM_IOCTL_MODE_CURSOR | drmModeSetCursor | 光标操作 |
DRM_IOCTL_MODE_GETENCODER | drmModeGetEncoder | 检索Encoder |
DRM_IOCTL_MODE_GETPROPERTY | drmModeGetProperty | 检索属性 |
DRM_IOCTL_MODE_GETFB | drmModeGetFB | 获取指定 ID 的 framebuffer object |
DRM_IOCTL_MODE_PAGE_FLIP | drmModePageFlip | 基于 VSYNC 同步机制的显示刷新 |
DRM_IOCTL_MODE_GETPLANERESOURCES | drmModeGetPlaneResources | 获取 Plane 资源列表 |
DRM_IOCTL_MODE_OBJ_GETPROPERTIES | drmModeObjectGetProperties | 获取该 object 所拥有的所有 Property |
DRM_IOCTL_MODE_CREATEPROPBLOB | drmModeCreatePropertyBlob | 创建1个 Property Blob 对象 |
DRM_IOCTL_SYNCOBJ_CREATE | drmSyncobjCreate | 同步对象创建 |
Direct Rendering Manager - 基本概念相关推荐
- DRM(Direct Rendering Manager)学习简介
学习DRM一年多了,由于该架构较为复杂,代码量较多,且国内参考文献较少,初学者学习起来较为困难.因此决定将自己学习的经验总结分享给大家,希望对正在学习DRM的同学有所帮助,同时交流经验. 由于本人工作 ...
- DRM (Direct Rendering Manager) 的发展历史
!!!声明!!! 本文章转自:何小龙 链接:https://blog.csdn.net/hexiaolong2009/article/details/88075520 转载只是为了学习备份. 前言 了 ...
- DRM(Direct Rendering Manager)
DRM DRM是Linux目前主流的图形显示框架,相比FB架构,DRM更能适应当前日益更新的显示硬件.比如FB原生不支持多层合成,不支持VSYNC,不支持DMA-BUF,不支持异步更新,不支持fenc ...
- SCCM 2012系列之一 Operations Manager 关键概念
Operations Manager 关键概念 1. Operations Manager 的用途 大小型企业通常依赖于其计算环境提供的服务和应用程序. IT 部门负责确保这些关键服务和应用程序的性能 ...
- Direct Draw 01-基本概念
一.位深度 计算机中,一个字节(Byte)是由8个位(Bit)组成的.位深度指的是用来描述某状态值所使用的计算机位的个数.在DirectDraw中,通常用位深度来代表位图中的颜色值所使用的位个数,从另 ...
- DRM——学习篇0:概念认识
刚开始学习,记忆不是很好,容易忘,边学边记,阅读的速度会比较慢,看的会比较仔细. 这边主要参考以下博客,前辈们水平很高,写的很详细,详细的知识学习可查看以下链接. 详细请看: 蜗窝科技:http: ...
- 嵌入式的一些概念 FMC/GPIO/MIO与EMIO/GTP、GTX、GTH和GTZ
xilinux zcu102 硬件连接 USB UART USB JTAG Ethernet cable 安装驱动(x USB UART)和调试工具(Tera Term) faq FMC连接器(FMC ...
- linux内核(4.17.10)配置项详解(x86)
64-bit kernel--支持64位 General setup--通用设置 Cross-compiler tool prefix--交叉编译工具前缀 Local version-append t ...
- 4.x版本内核中platform_device的生成
一.Display Server X Windows 和 X Server The X Window System (X11, or shortened to simply X) is a windo ...
最新文章
- GIS最新热点以及未来发展热门
- 智能调温需要哪些计算机知识,一种智能调温的计算机用鼠标的制作方法
- asin c语言中 返回值范围_asin()_C语言asin()详解:反正弦函数,求反正弦值
- 选择器Selector
- 基本 Python 词汇
- python如何运行程序_01
- jenkind + git + mave + shell + tomcat
- Cesium:向地图中添加线的方法
- SVN删除或新建(添加)文件
- nested exception is java.lang.NumberFormatException: For input string: “swagger-ui.html“]
- 向量叉积和点积混合运算_向量点积叉积及其几何意义
- ORA-39194: Table mode jobs require the tables to be comma separated.
- ForkJoinPool线程池
- 开课通知 | 《AISHELL-3语音合成实战》课程
- python提取cad中的文字_[python]提取PPT中的文字(包括图片中的文字)
- 算法与价值观如何平衡?凤凰新闻客户端新版给出了答案
- c语言xp与pow的差别,关于c ++:调用pow()时舍入结果的差异
- 以太坊数据结构MPT 1
- 新消费主义下的共享经济发展趋势
- 树莓派更换源后update/upgrade更新后无法启动