Weston中shm window渲染
渲染流水线
一个Wayland client要将内存渲染到屏幕上,首先要申请一个graphic buffer,绘制完后传给Wayland compositor并通知其重绘。Wayland compositor收集所有Wayland client的请求,然后以一定周期把所有提交的graphic buffer进行合成。合成完后进行输出。本质上,client需要将窗口内容绘制到一个和compositor共享的buffer上。这个buffer可以是普通共享内存,也可以是DRM中的GBM或是gralloc提供的可供硬件(如GPU)操作的graphic buffer,当然你也可以直接调用ion的接口去创建buffer。在大多数移动平台上,没有专门的显存,因此它们最终都来自系统内存,区别在于图形加速硬件一般会要求物理连续且符合对齐要求的内存。如果是普通共享内存,一般是物理不连续的,多数情况用软件渲染。有些图形驱动也支持用物理不连续内存做硬件加速,但效率相对会低一些。根据buffer类型的不同,client可以选择自己绘制,或是通过Cairo,OpenGL绘制,或是更高层的如Qt,GTK+这些widget库等绘制。绘制完后client将buffer的handle传给server,以及需要重绘的区域。在server端,compositor将该buffer转为纹理,最后将其与其它的窗口内容进行合成。下面是抽象的流程图:
![](/assets/blank.gif)
shm类型的window渲染显示的流程
1.attach
根据client端的surface resource在server端分配buffer,给pending state作为pending中的buffer,下面的操作对象均是pending中的buffer。
2.damage
新旧damage区域融合得到新的damage区域。
3.commit
3.1 opengl渲染前的准备
根据shm buffer的类型、大小,设置对应的gl参数。保存到gl_surface_state结构体中。如果纹理和之前存在的不匹配,会手动生成一个合适的新纹理。
3.2 正式渲染显示
weston_output_repaint使用opengl渲染surface并将framebuffer写入drm设备。repaint_flush应用pending state。渲染步骤在repaint循环中。
buffer
weston-simple-shm::create_shm_buffer
用户空间:
struct buffer {
struct wl_buffer *buffer;
void *shm_data;
int busy;
};fd = os_create_anonymous_file(size); 用户空间创建size大小文件,取得fd。
data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);将fd用户空间buffer映射到进程内核空间。
pool = wl_shm_create_pool(display->shm, fd, size); 这个地方创建一个pool。这个pool可以很大,然后后续wl_shm_pool_create_buffer再从这个pool中分出来一部分用作绘画对应的server端操作: wayland/src/wayland-shm.c::shm_create_poolstruct wl_shm_pool *pool;pool->data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);[server]创建pool。映射同一个fd;此时client与weston达成共享内存。也通过server端的操作,为client端的wl_shm_pool赋值。并最终将pool传给clientwl_resource_set_implementation(pool->resource,&shm_pool_interface,pool, destroy_pool);需要明确,buffer是用户端创建的,用户端和server端通过传递fd实现buffer共享。用户空间:
buffer->buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, format);这个buffer->buffer对应的是wl_buffer结构体。从pool中取出一部分,套上一个wl_buffer对应的server端操作: wayland/src/wayalnd-shm.c::shm_pool_create_buffer
创建wl_shm_buffer结构体,并赋值,width/height/format/stride/offset;将之前创建的pool结构体付给buffer。buffer->pool = pool;通过pool获取wl_buffer,对用户空间的buffer进行赋值。注意是将wl_shm_buffer赋给wl_buffer
wl_resource_set_implementation(buffer->resource,&shm_buffer_interface,buffer, destroy_buffer);用户空间:
wl_shm_pool_destroy(pool);对应的server端操作: wayland/src/wayalnd-shm.c::shm_pool_destroy,随后调用wl_resource_destroy内部计数器-1,如果pool的计数为0[destroy pool且destroy buffer之后],则销毁pool的所有资源最终用户空间将mmap的地址给了buffer->shm_data //也就是对应weston:server的pool->data
buffer->shm_data = data;提供给client的绘画地址。
随即client会对buffer->shm_data空间执行绘画;
client和sever分别通过wl_buffer以及wl_shm_buffer对实际的绘画部分内容进行管理,他俩应该是等价的。
Server端是wl_shm_buffer->wl_shm_pool->data;
1.buffer本质上只有一块,是用户空间创建以后用来创建pool用到的buffer。
2.pool的本质是可以对这个buffer进行分割以及管理,分割通过offset的方式。
3.weston_buffer.release是由wl_buffer_send_release实现的通知。weston_buffer结构体本身有一个busy_count的计数器,在weston_buffer_reference或者其他函数调用的时候,如果发现busy_count为0,就会调用wl_buffer_send_release发送信号
surface_attach
attach函数拿到client端的buffer,并传给surface->pending state结构体。
static void
surface_attach(struct wl_client *client,struct wl_resource *resource,struct wl_resource *buffer_resource, int32_t sx, int32_t sy)
{struct weston_surface *surface = wl_resource_get_user_data(resource);struct weston_buffer *buffer = NULL;if (buffer_resource) {//核心功能:根据client端的resource在server分配bufferbuffer = weston_buffer_from_resource(buffer_resource);if (buffer == NULL) {wl_client_post_no_memory(client);return;}}//将分配的buffer设置为pending,待更新到currentweston_surface_state_set_buffer(&surface->pending, buffer);surface->pending.sx = sx;surface->pending.sy = sy;surface->pending.newly_attached = 1;
}
attach将wl_buffer设置为pending wl_buffer而不是currect。
本质上是需要window->surface与buffer->buffer 绑定;把用户空间的buffer管理起来,赋值到weston里面的weston_buffer结构体中,并且将weston_buffer与他的weston_surface->pending结构体关联起来。
在这里,client端的wl_buffer和server端的weston_buffer应该是等价的。
做完这一步,那么weston_surface->pending->buffer也就有了对应的weston-buffer
surface_damage
static void
surface_damage(struct wl_client *client,struct wl_resource *resource,int32_t x, int32_t y, int32_t width, int32_t height)
{struct weston_surface *surface = wl_resource_get_user_data(resource);if (width <= 0 || height <= 0)return;pixman_region32_union_rect(&surface->pending.damage_surface,&surface->pending.damage_surface,x, y, width, height);
}
pixman_region32_union_rect将原有的damage区域与新的damage区域进行组合,得到新的damage区域。
surface_commit
先对pending中的buffer进行缩放、旋转、长宽有效性等一系列验证,然后weston_surface_commit函数将surface提交给opengl渲染并合成显示。下面会详细讲解surface的渲染显示过程。
->surface_commit->1. weston_surface_is_pending_viewport_source_valid//对pending buffer进行缩放、旋转等数据校验操作->2. weston_surface_is_pending_viewport_dst_size_int//校验surface长宽的有效性->3. surface->pending.acquire_fence_fd//fence_fd有效->wl_shm_buffer_get(surface->pending.buffer->resource)//检查shm buffer->4.weston_surface_commit(surface)//commit surface->4.1 weston_surface_commit_state(surface, &surface->pending)//重点函数->4.2 weston_surface_commit_subsurface_order(surface)->4.3 weston_surface_schedule_repaint//重绘定时器->5. weston_subsurface_commit(surface)//subsurface commit
weston_surface_commit_state
先将pending中的buffer提取到surface中。attach的时候需要判断weston_buffer的类型,shm、egl、dmabuf三种类型对应了三种gl render attach函数。
weston_surface_commit_state(操作的是pending buffer)1.->weston_surface_attach(surface, state->buffer)weston_buffer_reference(&surface->buffer_ref, buffer)//关联weston_surface和weston_buffersurface->compositor->renderer->attach(surface, buffer)//使用合成器里的渲染器gl_renderer_attachgl_renderer_attach_shm//gl_renderer_attach_egl//创建对应surface buffer的egl_image,激活绑定纹理。gl_renderer_attach_dmabuf//使用dmabuf时的函数2.//渲染结束清理pending buffer3.-> weston_surface_build_buffer_matrix(surface, &surface->surface_to_buffer_matrix)//对surface进行旋转,裁剪,缩放等矩阵操作4.->weston_surface_update_size(surface)//设置surface大小5.->surface->committed(surface, state->sx, state->sy)//若desktop,则surface.c:: weston_desktop_surface_committed, 看上去是设置surface在屏幕上的位置,也就是显示位置,窗口管理一部分;主要是update了size,需要做对应的改动6.->apply_damage_buffer//dmage 区域合并7.->wl_surface.set_input_region8.->wl_surface.frame//插入frame_callbake_list链表9.->wl_signal_emit(&surface->commit_signal, surface)//!最后发出commit_signal,将output从pending list移除掉并添加进合成器的output_list
这里以shm为例进行深入分析。在repaint之前gl_renderer_attach_shm需要为渲染做一些准备,获取shm buffer,绑定到传入的surface buffer,根据shm buffer的参数设置gl相关的参数(像素格式、大小),纹理不匹配时ensure_textures生成新纹理,gl_surface_state表示这个surface的渲染状态。
static void
gl_renderer_attach_shm(struct weston_surface *es, struct weston_buffer *buffer,struct wl_shm_buffer *shm_buffer)
{struct weston_compositor *ec = es->compositor;struct gl_renderer *gr = get_renderer(ec);struct gl_surface_state *gs = get_surface_state(es);GLenum gl_format[3] = {0, 0, 0};GLenum gl_pixel_type;int pitch;int num_planes;buffer->shm_buffer = shm_buffer;buffer->width = wl_shm_buffer_get_width(shm_buffer);buffer->height = wl_shm_buffer_get_height(shm_buffer);num_planes = 1;gs->offset[0] = 0;gs->hsub[0] = 1;gs->vsub[0] = 1;switch (wl_shm_buffer_get_format(shm_buffer)) {case WL_SHM_FORMAT_XRGB8888:...case WL_SHM_FORMAT_ARGB8888:gs->shader_variant = SHADER_VARIANT_RGBA;pitch = wl_shm_buffer_get_stride(shm_buffer) / 4;gl_format[0] = GL_BGRA_EXT;gl_pixel_type = GL_UNSIGNED_BYTE;es->is_opaque = false;break;case WL_SHM_FORMAT_RGB565:...case WL_SHM_FORMAT_YUV420:...case WL_SHM_FORMAT_NV12:...case WL_SHM_FORMAT_YUYV:...default:weston_log("warning: unknown shm buffer format: %08x\n",wl_shm_buffer_get_format(shm_buffer));return;}//纹理不匹配的时候,生成新纹理if (pitch != gs->pitch ||buffer->height != gs->height ||gl_format[0] != gs->gl_format[0] ||gl_format[1] != gs->gl_format[1] ||gl_format[2] != gs->gl_format[2] ||gl_pixel_type != gs->gl_pixel_type ||gs->buffer_type != BUFFER_TYPE_SHM) {......ensure_textures(gs, GL_TEXTURE_2D, num_planes);}
}
weston_surface_schedule_repaint(surface)
渲染前的准备工作完成之后,下面就是渲染并显示这个surface,这里才开始真正调用到opengl和drm。
weston_output_schedule_repaint->idle_repaint->start_repaint_loop->drm_output_start_repaint_loop//libweston/backend-drm/drm.cdrmVBlank vbl = {.request.type = DRM_VBLANK_RELATIVE,.request.sequence = 0,.request.signal = 0,};
//设置用于输出的vblank同步的类型,传入crtc的pipeline,返回当前的显示器是主显示器,还是
//副显示器
vbl.request.type |= drm_waitvblank_pipe(output->crtc);
//等待vblank事件触发,成功则返回0
ret = drmWaitVBlank(backend->drm.fd, &vbl);/* Error ret or zero timestamp means failure to get valid timestamp */
if ((ret == 0) && (vbl.reply.tval_sec > 0 || vbl.reply.tval_usec > 0)) {//屏幕刷新率计算weston_compositor_read_presentation_clock(backend->compositor,&tnow);drm_output_update_msc(output, vbl.reply.sequence);weston_output_finish_frame(output_base, &ts,WP_PRESENTATION_FEEDBACK_INVALID);
}
//第一次启动不会走到这里,在weston_output_finish_frame中启动repaint循环,repaint得到数据后才会走到这里。
pending_state = drm_pending_state_alloc(backend);
//复制pending state到current state
drm_output_state_duplicate(output->state_cur, pending_state,DRM_OUTPUT_STATE_PRESERVE_PLANES);
//在pending状态的时候,更新所有drm output的状态
ret = drm_pending_state_apply(pending_state);
指定了drmvblank的类型为DRM_VBLANK_RELATIVE,设置sequence为当前的vblank计数。
DRM_VBLANK_ABSOLUTE:request.sequence是过去某个时间点以来的 vblank 计数,例如系统启动。
DRM_VBLANK_RELATIVE:request.sequence是当前值的 vblank 计数。例如 1 指定下一个 vblank。该值可以与这些值的任意组合进行按位或运算:
DRM_VBLANK_SECONDARY: 使用第二显示器的 vblank。
DRM_VBLANK_EVENT: 立即返回并触发事件回调而不是等待指定的 vblank。
weston_output_finish_frame
output_repaint_timer_handler函数最后会开启一轮新的repaint定时,repaint从而一直循环下去。repaint_begin创建一个合成器的drm_pending_state repaint_data,每一次repaint都要分配一个新的drm_pending_state给drm使用。
weston_output_repaint使用opengl渲染surface并将framebuffer写入drm设备。repaint_flush应用pending state。
weston_output_finish_frame//计算帧率->output_repaint_timer_arm//启动repaint定时器->output_repaint_timer_handler//定时器溢出处理函数,最后再次启动repaint定时器,从而实现repaint循环。
output_repaint_timer_handler//创建一个合成器的drm_pending_state repaint_data,每一次repaint都要分配一个//新的drm_pending_state给drm使用。->repaint_data =compositor->backend->repaint_begin//数据绘制,渲染完的数据保存在output->weston_output_maybe_repaint(output, &now, repaint_data)-->weston_output_repaint(output, repaint_data);//送显->compositor->backend->repaint_flush(compositor,repaint_data);
weston_output_repaint
weston_compositor_build_view_list(ec);
if (output->assign_planes && !output->disable_planes) {//assign planes分配planesoutput->assign_planes(output, repaint_data);} else {wl_list_for_each(ev, &ec->view_list, link) {weston_view_move_to_plane(ev, &ec->primary_plane);}
//计算damage区域
output_accumulate_damage(output);pixman_region32_init(&output_damage);
pixman_region32_intersect(&output_damage,&ec->primary_plane.damage, &output->region);
pixman_region32_subtract(&output_damage,&output_damage, &ec->primary_plane.clip);
//repaint渲染出结果
output->repaint(output, &output_damage, repaint_data);
pixman_region32_fini(&output_damage);
//向client端发送done消息
wl_list_for_each_safe(cb, cnext, &frame_callback_list, link) {wl_callback_send_done(cb->resource, frame_time_msec);wl_resource_destroy(cb->resource);}
drm_assign_planes
drm_pending_state *pending_state = repaint_data;
*primary = &output_base->compositor->primary_plane//primary走GPU渲染
drm_output_propose_state_mode mode = DRM_OUTPUT_PROPOSE_STATE_PLANES_ONLY//默认使用overlay模式state = drm_output_propose_state(output_base, pending_state, mode);
//为每个output中的view分配对应的plane,并且分配plane对应的合成器,默认是GPU合成。
//如果这里overlay失败,就会尝试使用mix模式;再失败,就是尝试GPU-ONLY的合成模式
weston_view_move_to_plane(ev, primary);//如果没有assign_planes,直接全部交给primary_plane
drm_output_repaint
output->repaint最终指向的是drm_output_render_gl函数,走到gl_renderer_repaint_output这里,我们终于来到了最核心的opengl渲染部分。通过访问drm compositor的drm_fd来访问drm设备。在opengl画完数据后,drm_fb_get_from_bo将fb写入drm设备(最重要的函数)。
drm_output_render(state, damage)->drm_output_render_gl(state, damage)//第一步 gl渲染->gl_renderer_repaint_output->repaint_views->draw_view //opengl渲染过程//第二步 拿到gbm buffer->bo = gbm_surface_lock_front_buffer(output->gbm_surface);//opengl画好以后从gbm_surface中拿到gbm_buffer,gbm buffer object//第三步,将buffer写入drm设备。//************送显最最核心的函数************//通过gbm_bo拿到drm frame buffer//ret作为返回值,是drm_fb类型,drm_fb_get_from_bo设置了drm_fb中的drm_fd,以及 buffer的实际数据与大小。//其中drm_fb_addfb函数通过drm api(drmModeAddFB2WithModifiers, //drmModeAddFB2,drmModeAddFB)将framebuffer写入进drm设备。->ret = drm_fb_get_from_bo(bo, b, true, BUFFER_GBM_SURFACE);//返回给scanout_state,为了后续尝试计算新damage区域,如果失败就是0,那么也就是全图->ret->gbm_surface = output->gbm_surface;->drmModeCreatePropertyBlob(b->drm.fd, rects, sizeof(*rects) * n_rects, &scanout_state->damage_blob_id);
gl_renderer_repaint_output
gl_renderer_repaint_output中仍然有缩放、旋转操作,设置视图边界的操作。
wl_list_for_each_reverse(view, &compositor->view_list, link) {if (view->plane == &compositor->primary_plane) {struct gl_surface_state *gs =get_surface_state(view->surface);gs->used_in_output_repaint = false;}}
get_surface_state->gl_renderer_create_surface(surface)->//将buffer引用和gl surface中的buffer引用关联起来weston_buffer_reference(&gs->buffer_ref, buffer);weston_buffer_release_reference(&gs->buffer_release_ref,es->buffer_release_ref.buffer_release); if (surface->buffer_ref.buffer) {//attach到buffer_ref.buffer//其中gl_renderer_attach_shm将共享缓存中的内容读到surface中,这里涉及大量的gl参数//有gl_renderer_attach_egl、gl_renderer_attach_dmabufgl_renderer_attach(surface, surface->buffer_ref.buffer);//将buffer_ref.buffer//刷新damage区域gl_renderer_flush_damage(surface);}//gr->create_sync(gr->egl_display, EGL_SYNC_NATIVE_FENCE_ANDROID,attribs)//调用EGL的"eglCreateSyncKHR"创建一个sync对象
go->begin_render_sync = create_render_sync(gr);
//渲染damage区域
repaint_views(output, &total_damage);wl_signal_emit(&output->frame_signal, output_damage);
//
go->end_render_sync = create_render_sync(gr);
//swapSwapBuffers的作用
ret = eglSwapBuffers(gr->egl_display, go->egl_surface);
//在swap buffer之后必须提交render sync 对象,只有在flush之后, render sync
//对象才真正拥有一个有效的sync file fd
timeline_submit_render_sync(gr, output, go->begin_render_sync,TIMELINE_RENDER_POINT_TYPE_BEGIN);
timeline_submit_render_sync(gr, output, go->end_render_sync,TIMELINE_RENDER_POINT_TYPE_END);update_buffer_release_fences(compositor, output);gl_renderer_garbage_collect_programs(gr);
repaint_views
static void
repaint_views(struct weston_output *output, pixman_region32_t *damage)
{struct weston_compositor *compositor = output->compositor;struct weston_view *view;wl_list_for_each_reverse(view, &compositor->view_list, link)if (view->plane == &compositor->primary_plane)//!draw_view(view, output, damage);->ensure_surface_buffer_is_ready//等待EGLSyncKHR object->attribs[1] = dup(surface->acquire_fence_fd);//复制文件描述符->sync = gr->create_sync(gr->egl_display,EGL_SYNC_NATIVE_FENCE_ANDROID,attribs);//根据复制的fd创建sync对象->wait_ret = gr->wait_sync(gr->egl_display, sync, 0);//阻塞等待egl完成fence->destroy_ret = gr->destroy_sync(gr->egl_display, sync);//完成后摧毁sync对象->gl_shader_config_init_for_view//配置输入纹理的着色器,surface的旋转矩阵、边缘滤波器种类(linear/nearest)//blend之后,opengl进行渲染-> if (pixman_region32_not_empty(&surface_blend)) {glEnable(GL_BLEND);//GL渲染repaint_region(gr, ev, &repaint, &surface_blend, &sconf);gs->used_in_output_repaint = true;}
}
实际渲染过程repaint_region,使能顶点着色器/片段着色器,打开shader program,glDrawArrays绘制三角形(一个四边形,由两个三角形组成,六个顶点数据)。
drm_repaint_flush
应用pending buffer
drm_pending_state_apply->drm_pending_state_apply_atomic->drm_output_apply_state_atomic//注意是以plane_list遍历->drmModeAtomicCommit //这个就是commit,没什么好讲的;
将pending state切到current后,终于完成了渲染显示整个流程。
drm_output_enable
drm函数的初始化过程。
static int
drm_output_enable(struct weston_output *base)
{struct drm_output *output = to_drm_output(base);struct drm_backend *b = to_drm_backend(base->compositor);int ret;assert(!output->virtual);ret = drm_output_attach_crtc(output);if (ret < 0)return -1;ret = drm_output_init_planes(output);if (ret < 0)goto err_crtc;if (drm_output_init_gamma_size(output) < 0)goto err_planes;if (b->pageflip_timeout)drm_output_pageflip_timer_create(output);if (b->use_pixman) {if (drm_output_init_pixman(output, b) < 0) {weston_log("Failed to init output pixman state\n");goto err_planes;}} else if (drm_output_init_egl(output, b) < 0) {weston_log("Failed to init output gl state\n");goto err_planes;}drm_output_init_backlight(output);output->base.start_repaint_loop = drm_output_start_repaint_loop;output->base.repaint = drm_output_repaint;output->base.assign_planes = drm_assign_planes;output->base.set_dpms = drm_set_dpms;output->base.switch_mode = drm_output_switch_mode;output->base.set_gamma = drm_output_set_gamma;weston_log("Output %s (crtc %d) video modes:\n",output->base.name, output->crtc->crtc_id);drm_output_print_modes(output);return 0;err_planes:drm_output_deinit_planes(output);err_crtc:drm_output_detach_crtc(output);return -1;
}
Weston中shm window渲染相关推荐
- react把表格渲染好ui_在React中实现条件渲染的7种方法
借助react,我们可以构建动态且高度交互的单页应用程序,充分利用这种交互性的一种方法是通过条件渲染. 条件渲染一词描述了根据某些条件渲染不同UI标签的能力.在react文档中,这是一种根据条件渲染不 ...
- 解决WPF中重载Window.OnRender函数失效问题
原文:解决WPF中重载Window.OnRender函数失效问题 今天实验一个绘图算法的时候,偶然发现重载Window.OnRender的方法是没有效果的. public partial class ...
- echart 实例显示位置_技术分享:如何在Unity中使用实例化渲染?
编者按 在日常开发中,通常说到优化.提高帧率时,总是会提到批量渲染.之前简单总结了静态合批(点此查看全文)以及动态合批(点此查看全文),这次作者将和大家聊聊实例化渲染. 作者:枸杞忧天 (本文内容由公 ...
- U3D中物体的渲染顺序
U3D中物体的渲染顺序 1,由SHADER中渲染队列及队列中的值决定 2,在同一队列中,若材质相同 2.1 对于UI,按其在场景层级中的先后顺序绘制 2.2 对于3D不透明物体,按其离相机的距离,由近 ...
- JavaScript中的window.close在FireFox和Chrome上不能正常动作的解决方法
原文:JavaScript中的window.close在FireFox和Chrome上不能正常动作的解决方法 JS中关闭窗口的方法window.close()在IE上能够正常动作,而在FireFox和 ...
- IOS 5 中@synthesize window = _window是什么意思呢
IOS 5 中@synthesize window = _window是什么意思呢 @synthesize window = _window;//指明实例变量的名称,这个名称用来存储指针.(程序内部最 ...
- Window Server 2008中开启Window Media Player功能
Window Server 2008中开启Window Media Player功能 服务器管理器 功能 优质Windows音频视频体验 打勾 安装 其它功能有: 桌面休验 等.... the end ...
- JavaScript 中的 window onload 应该什么时候写
JavaScript 中的 window onload 应该什么时候写 1. 页内式 JS 代码 1.1 页内式 JS 代码写在 head 内部 如果 script 标签写在 head 标签内部,则位 ...
- [译] 绘制路径:Android 中矢量图渲染
原文地址:Draw a Path: Rendering Android VectorDrawables 原文作者:Nick Butcher 译文出自:掘金翻译计划 本文永久链接:github.com/ ...
最新文章
- 机器学习典型步骤以及训练集、验证集和测试集概念
- 怎样评价推荐系统的结果质量?
- 推荐算法炼丹笔记:电商搜索推荐业务词汇表
- 「MacOS」Mac快捷键
- maven项目部署打包
- 大数据如何应用在企业人力资源管理
- CKEditor 5 在线编辑 PDF
- 双向循环链表(图文讲解)
- Unit 3-Lecture 5: The Pigeonhole Principle and Inclusion-Exclusion
- 如何用js对url做urlencoding处理?
- 可信认证之九阴真经一
- Android 实战 - 天气(有缺陷)APP
- QUIC不可靠的数据报扩展(An Unreliable Datagram Extension to QUIC)
- 【已解决】将CentOS7系统安装至U盘(一):系统安装与使用
- 金庸武功之““兰花拂穴手””--elk5.5安装
- “QQ显示iPhone在线”背后的虚荣与焦虑
- 详细安装指南-Ubuntu16.04,CUDA8.0,Caffe,OpenCV3.1,Theano,Tensorflow,纯属转载,等待自己修改
- 华为--OSPF抓包实验分析邻接关系的七个状态,单区域ospf配置
- CRC32原理及实现学习
- Linux使用笔记:Oracle数据库安装配置(命令行安装)