1. 渲染流水线

一个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转为纹理,最后将其与其它的窗口内容进行合成。下面是抽象的流程图:

  1. 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循环中。

  1. 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发送信号

  1. 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

  1. 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区域。

  1. 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渲染相关推荐

  1. react把表格渲染好ui_在React中实现条件渲染的7种方法

    借助react,我们可以构建动态且高度交互的单页应用程序,充分利用这种交互性的一种方法是通过条件渲染. 条件渲染一词描述了根据某些条件渲染不同UI标签的能力.在react文档中,这是一种根据条件渲染不 ...

  2. 解决WPF中重载Window.OnRender函数失效问题

    原文:解决WPF中重载Window.OnRender函数失效问题 今天实验一个绘图算法的时候,偶然发现重载Window.OnRender的方法是没有效果的. public partial class ...

  3. echart 实例显示位置_技术分享:如何在Unity中使用实例化渲染?

    编者按 在日常开发中,通常说到优化.提高帧率时,总是会提到批量渲染.之前简单总结了静态合批(点此查看全文)以及动态合批(点此查看全文),这次作者将和大家聊聊实例化渲染. 作者:枸杞忧天 (本文内容由公 ...

  4. U3D中物体的渲染顺序

    U3D中物体的渲染顺序 1,由SHADER中渲染队列及队列中的值决定 2,在同一队列中,若材质相同 2.1 对于UI,按其在场景层级中的先后顺序绘制 2.2 对于3D不透明物体,按其离相机的距离,由近 ...

  5. JavaScript中的window.close在FireFox和Chrome上不能正常动作的解决方法

    原文:JavaScript中的window.close在FireFox和Chrome上不能正常动作的解决方法 JS中关闭窗口的方法window.close()在IE上能够正常动作,而在FireFox和 ...

  6. IOS 5 中@synthesize window = _window是什么意思呢

    IOS 5 中@synthesize window = _window是什么意思呢 @synthesize window = _window;//指明实例变量的名称,这个名称用来存储指针.(程序内部最 ...

  7. Window Server 2008中开启Window Media Player功能

    Window Server 2008中开启Window Media Player功能 服务器管理器 功能 优质Windows音频视频体验 打勾 安装 其它功能有: 桌面休验 等.... the end ...

  8. JavaScript 中的 window onload 应该什么时候写

    JavaScript 中的 window onload 应该什么时候写 1. 页内式 JS 代码 1.1 页内式 JS 代码写在 head 内部 如果 script 标签写在 head 标签内部,则位 ...

  9. [译] 绘制路径:Android 中矢量图渲染

    原文地址:Draw a Path: Rendering Android VectorDrawables 原文作者:Nick Butcher 译文出自:掘金翻译计划 本文永久链接:github.com/ ...

最新文章

  1. 机器学习典型步骤以及训练集、验证集和测试集概念
  2. 怎样评价推荐系统的结果质量?
  3. 推荐算法炼丹笔记:电商搜索推荐业务词汇表
  4. 「MacOS」Mac快捷键
  5. maven项目部署打包
  6. 大数据如何应用在企业人力资源管理
  7. CKEditor 5 在线编辑 PDF
  8. 双向循环链表(图文讲解)
  9. Unit 3-Lecture 5: The Pigeonhole Principle and Inclusion-Exclusion
  10. 如何用js对url做urlencoding处理?
  11. 可信认证之九阴真经一
  12. Android 实战 - 天气(有缺陷)APP
  13. QUIC不可靠的数据报扩展(An Unreliable Datagram Extension to QUIC)
  14. 【已解决】将CentOS7系统安装至U盘(一):系统安装与使用
  15. 金庸武功之““兰花拂穴手””--elk5.5安装
  16. “QQ显示iPhone在线”背后的虚荣与焦虑
  17. 详细安装指南-Ubuntu16.04,CUDA8.0,Caffe,OpenCV3.1,Theano,Tensorflow,纯属转载,等待自己修改
  18. 华为--OSPF抓包实验分析邻接关系的七个状态,单区域ospf配置
  19. CRC32原理及实现学习
  20. Linux使用笔记:Oracle数据库安装配置(命令行安装)

热门文章

  1. 翻译原版教材的一些技巧和心得
  2. LINUX手动查看和修改MTU值的方法
  3. 光环Python培训班
  4. matlab小波变换特侦提取,如何利用小波变换的多分辨分析特性提取微弱的生命信号...
  5. 区块链圈频现百万元年薪招聘 现金+股票仍难觅良才
  6. InDesign CS3 常用的字体
  7. Access-Control-Allow-Origin VUE本地调试跨域问题使用nginx解决
  8. java打印Excel表格
  9. TBox、ECall、BCall、ICall简介及之间的关系
  10. shape context-形状上下文