antvis / g-webgl-compute

A GPGPU implementation based on WebGL.
MIT License
144 stars 15 forks source link

DSL -> GLSL Compiler 设计 #17

Open xiaoiver opened 4 years ago

xiaoiver commented 4 years ago

问题背景

目前已有的 GPGPU 实践从使用者角度可以分成两类:

  1. API 组合调用
  2. DSL 转译成 GLSL

Tensorflow.js 使用了第一种方式,开发者不需要关心 GLSL 语法:

截屏2020-05-06 上午9 35 10

类似的还有 P4、P5 这样的图表库,开发者在使用 data transform 时也不需要关心这些操作实际上是在 GPU 端执行的:

截屏2020-05-06 上午9 36 58

但有些类型的计算任务只能通过用户编程式描述,无法通过 API 组合完成,例如:

此时就需要提供一种开发者更容易接受的 DSL,GPGPU 库在运行时完成转译。

DSL 选择

GPU.js 选择 JS/TS 作为 DSL,在运行时传入 function.toString() 通过 acorn 生成 AST,最终根据目标平台生成 WebGL 1/2 的 Shader 代码。 image

这种做法的优点很明显,前端开发者很容易上手。但局限性同样存在:

因此我们需要选择一种强类型的 DSL。Stardust 选择了在 TS 的语法基础上进行扩展。 image

另外,Stardust 也解决了和渲染结合(sanddance 效果)的问题: image

我们的实现

我们优先解决布局计算的问题,在这个场景下并不涉及和渲染结合的问题,即只考虑 Compute Shader 的实现。

DSL 定义

数据类型

https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)

标量:

let a: int = 10; // -> int a = 10;

let a: uint = 10; // -> uint a = 10;

let a = true; // 类型自动推导 bool a = true;


向量:

// 默认自动推导 let b = [1, 1, 1]; // -> vec3 b = vec3(1.0, 1.0, 1.0);

let b = [true, true]; // -> bvec2 b = bvec2(true, true);

// 报错 let b = [true, 1];

// 显式声明类型 let b: ivec4 = [1, 1, 1, 1]; // -> ivec4 b = ivec4(1, 1, 1, 1);


swizzling,GLSL 特有语法糖,不需要转译。

let b = [1, 2, 2]; let c = b.xxy; let d = b.rgb; // -> vec3 b = vec3(1.0, 2.0, 2.0); vec3 c = b.xxy; vec3 d = b.rgb;


矩阵:

let b: mat4; // -> mat4 b;


全局常量:

const SPEED_DIVISOR = 800.0;

define SPEED_DIVISOR 800.0


结构体:

interface Light { eyePosOrDir: vec3; isDirectional: bool; } const variableName: Light;

struct Light { vec3 eyePosOrDir; bool isDirectional; } variableName;


由于我们暂时只考虑 Compute Shader 的转译,`sampler` 类型不考虑。

### main 函数

区分其他函数,通过 `export default` 声明 `main` 函数:

function a() {}

export default compute() {}

// -> function a() {}

void main() {}


### threadID

在数据并行中,所有线程执行相同的核函数处理不同的数据,因此需要在函数中获取当前的线程 ID。考虑到 WebGPU 和 WebGL 不同的实现方式:

// -> WebGPU uint threadID = gl_GlobalInvocationID.x;

// -> WebGL uniform float u_TexSize; varying vec2 v_TexCoord; uint threadID = uint(floor(v_TexCoord.s * u_TexSize + 0.5));


### 数据读写

通过 threadID 就可以获取当前线程需要处理的数据。同样考虑到 GLSL 4.5 和 1.0 的差异,用户需要调用我们封装好的数据读写方法:

import { getData, setData } from 'g-webgpu';

export default function compute(threadID: uint) { let node = getData(threadID); setData(threadID, [1, 1, 1, 1]); }

// -> WebGPU GLSL 4.5 function getData(i) { return u_Data[i]; } function setData(i, data) { u_Data[i] = data; } void main() { uint threadID = gl_GlobalInvocationID.x; vec4 node = getData(threadID); setData(threadID, vec4(1.0)); }

// -> WebGL GLSL 1.0 function getData(i) { return texture2D(u_Data, vec2((float(i) + 0.5) / u_TexSize , 1)); } void main() { uint threadID = uint(floor(v_TexCoord.s * u_TexSize + 0.5)); vec4 node = getData(threadID); gl_FragColor = vec4(1.0); }


### Uniform

区别于全局常量(全大写生成 `define`),配合 interface 生成结构体:

interface Params { float k; float k2; } const params: Params;

// -> WebGPU GLSL 4.5 layout(std140, set = 0, binding = 1) uniform Params { float k; float k2; } params;

// -> WebGL GLSL 1.0 struct Params { float k; float k2; } params; uniform Params params;


### 内置数学函数

GPU.js 会转译 `Math` 方法到 GLSL 原生方法:
https://github.com/gpujs/gpu.js#supported-math-functions

Math.sin() // -> sin()


###  限制

#### 固定长度的循环

GLSL 中的循环长度必须是固定的:

for (int j = 0; j < PARTICLE_NUM; j++) {


#### 其他 JS 原生对象

`Date` `RegExp` `String` 这些显然不能使用。

## Parser 生成

关于 Parser 的生成考虑两个方案:
* Antlr。定义 g4 格式的 Lexer/Parser,可以在已有 [TS 语法文件](https://github.com/antlr/grammars-v4/blob/master/typescript/TypeScriptLexer.g4)基础上进行修改。通过 Visitor/Listener 实现 AST 生成。
* Pegjs。更加轻量,可以将 AST 生成代码(JS 描述)写在语法文件中。

我们希望生成的 Parser 本身是用 TS 编写的,这一点 antlr4ts 和 ts-pegjs 都可以做到。

## AST

参考  ESTree 规范对于节点的定义:
* https://github.com/estree/estree/
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/typescript-estree/src/ts-estree/ts-estree.ts#L1677

例如变量声明:
```typescript
export interface VariableDeclaration extends BaseNode {
  type: AST_NODE_TYPES.VariableDeclaration;
  declarations: VariableDeclarator[];
  kind: 'let' | 'const' | 'var';
  declare?: boolean;
}