Skip to content

Commit 0ce5890

Browse files
committed
feat(多线程模式): update article.md
1 parent a29870c commit 0ce5890

File tree

1 file changed

+93
-7
lines changed

1 file changed

+93
-7
lines changed

packages/多线程模式/article.md

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919

2020
TODO tu
2121

22-
WorldForNoWorker是引擎的门户,封装了引擎的API
22+
WorldForNoWorker是门户,封装了API
2323

2424
PipeManager负责管理管道
2525

26-
NoWorkerPipeline是引擎注册的管道模块
26+
NoWorkerPipeline是注册的管道模块
2727

2828
Init Pipeline是初始化管道,包括初始化相关的Job,用来实现初始化的逻辑
2929

@@ -63,8 +63,8 @@ TODO tu
6363

6464

6565
主循环的一帧中,首先运行了Update Pipeline管道,执行下面的Job逻辑:
66-
首先进行物理计算,更新位置
67-
最后更新模型矩阵
66+
首先进行物理计算,更新所有TransformComponent组件的位置
67+
最后更新所有TransformComponent组件的模型矩阵
6868

6969
然后运行了Render Pipeline管道,执行下面的Job逻辑:
7070
首先发送相机数据;
@@ -498,21 +498,107 @@ export let render = (state: state): Promise<state> => {
498498
TODO tu
499499

500500

501-
TODO 总体来看,分为Render、针对每个运行环境的渲染、渲染的步骤这三个部分
501+
总体来看,分为Main Worker、Physics Worker、Render Worker这三个部分
502+
503+
Main Worker包括了运行在主线程的模块,Physics Worker包括了运行在物理线程的模块,Render Worker包括了运行在渲染线程的模块
504+
505+
这三个部分的模块结构跟之前一样,都是有一个用户模块,它调用了一个门户模块;
506+
门户模块调用一个PipelineManager模块来管理管道;
507+
门户模块调用了Manager+Component+GameObject来创建场景;
508+
门户模块包括一个管道模块;
509+
管道模块包括几个管道,每个管道包括多个Job;
510+
Job调用了Manager+Component+GameObject来获得场景数据
511+
512+
513+
514+
515+
我们看下这三个部分对应的三个线程之间的数据传送:
516+
主要有两种方式来实现数据传送:
517+
- 拷贝
518+
- 共享SharedArrayBuffer
519+
520+
这里介绍下共享SharedArrayBuffer:
521+
两种组件的两个Buffer是SharedArrayBuffer,由主线程创建,并将其共享给渲染线程和物理线程,使他们能够从中读场景数据;
522+
主线程创建了RenderWorkderData的Buffer,用来保存了场景中所有的transformComponent和basicMaterialComponent。主线程将其共享给渲染线程,使渲染线程能够获得它们,从而通过它们获得场景数据(如位置);
523+
主线程创建了PhysicsWorkderData的Buffer,用来保存了场景中所有的transformComponent的位置。主线程将其共享给物理线程,使物理线程能够将计算后的位置写进去
524+
525+
526+
为什么物理线程不直接将计算后的位置写到共享的TransformComponent组件的Buffer中呢?
527+
这就涉及到同步的问题,我们等下再来讨论
528+
529+
530+
531+
532+
533+
534+
535+
我们来看下流程图
536+
<!-- ,图中的虚线是指线程之间在时间上的对应关系 -->
537+
538+
首先是初始化流程图:
539+
TODO tu
540+
541+
542+
543+
这里并行运行了三个线程的Init Pipeline
544+
具体的运行顺序如下;
545+
1.主线程创建了渲染线程和物理线程的worker。创建后,这两个线程的Init Pipeline开始运行,等待主线程发送数据;
546+
2.主线程开始三条并行的Job线:一条创建了RenderWorkderData的Buffer和PhysicsWorkerData的Buffer,发送了渲染数据和物理数据;另外两条等待渲染线程和物理线程发送结束初始化的标志
547+
3.渲染线程在获得主线程发送的渲染数据后,开始初始化渲染,依次执行这些Job逻辑:初始化TransformComponent和BasicMaterialComponent、创建RenderWorkerData的Buffer的视图、创建WebGL上下文、初始化材质、发送结束初始化的标志
548+
4.物理线程在获得主线程发送的物理数据后,开始初始化物理,依次执行这些Job逻辑:初始化TransformComponent和BasicMaterialComponent、创建PhysicsWorkerData的Buffer的视图、发送结束初始化的标志
502549

503550

504551

505552

506-
TODO 延迟一帧
507553

508-
TODO 同步
554+
555+
556+
557+
然后来看下主循环的一帧流程图:
558+
TODO tu
559+
560+
<!-- 图中的虚线是指线程之间在时间上的对应关系 -->
561+
562+
563+
这里首先并行运行了主线程的Update Pipeline、渲染线程的Render Pipeline、物理线程的Update Pipeline;
564+
然后运行了主线程的Sync Pipeline
565+
566+
具体的运行顺序如下;
567+
568+
1.主线程运行Update Pipeline,因为场景可能有变化,所以更新了RenderWorkderData的Buffer中的组件数据;
569+
2.主线程发送开始主循环的标志,此时渲染线程和物理线程接收到了该标志,开始分别运行Render Pipeline和Update Pipeline。
570+
其中物理线程在Update Pipeline中依次执行这些Job逻辑:物理计算、发送结束标志;
571+
渲染线程在Render Pipeline中依次执行这些Job逻辑:获得主线程发送的渲染数据、发送相机数据、渲染、发送结束标志
572+
3.主线程在发送开始主循环的标志后就结束了Update Pipeline,并运行Sync Pipeline,等待另外两个线程发送结束标志
573+
4.主线程获得两个线程发送结束标志后,依次执行这些Job逻辑:使用物理线程中经过物理计算得到的值来更新所有TransformComponent组件的位置、更新所有TransformComponent组件的模型矩阵
574+
575+
576+
577+
578+
这里回答之前提到的“同步”的问题:
579+
因为主线程和物理线程的Update Pipeline是在同一时间并行运行的,如果物理线程在管道的物理计算的Job中将计算后的位置写到TransformComponent组件的Buffer,那么此后主线程从中读取位置时可能就获得修改后的值而不是原始值,从而造成冲突
580+
所以首先物理线程的物理计算的Job将计算后的位置写到PhysicsWorkderData的Buffer中,然后在主线程的Sync Pipeline管道中再将其写到TransformComponent组件的Buffer中
581+
582+
583+
584+
585+
值得注意的是,渲染线程和物理线程相比主线程是延迟了一帧的。
586+
这是因为在主线程的Sync Pipeline中的“Update Transform”Job中会更新模型矩阵,而这更新后的值只能由下一帧的渲染线程和物理线程使用
509587

510588

511589

512590
## 结合UML图,描述如何具体地解决问题?
591+
592+
- 因为把渲染和物理计算的逻辑分别移到两个线程中,与主线程并行运行,从而提高了FPS
593+
594+
595+
513596
## 给出代码?
514597

515598

599+
Client代码:
600+
TODO continue
601+
516602

517603
<!-- # 设计意图
518604

0 commit comments

Comments
 (0)