跳转到内容
查看源码

Kernel 与 .ent 语言

.ent 文件是 Kernel 库:它包含若干 kernel 以及它们共享的声明(struct、type、fn)。.ent 与 .esn 共用同一套语法、词法和字面量:// 注释、; 结束语句、{ } 块。诊断格式为 文件:行:列: 信息。语言的精确定义在前端源码(crates/frontend/language)及其测试中。

struct Particle { position: Vec2; velocity: Vec2; } // 文档级 struct:下面所有 Kernel 可见
type Swarm = Rows<Particle>; // 端口类型别名,同样共享
kernel image.grayscale@1 {
requires { field.read; field.write; } // 需要的能力
input source: Image; // 输入端口
param amount: Float = 1.0; // 参数与默认值
output result: Image; // 输出端口
struct Hit { t: Float; id: Int; } // 只属于这个 Kernel 的 struct
fn luma(c: Vec4) -> Float { return c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722; }
compute { // 执行主体(这里是 map 模式)
let c = sample(source);
let g = luma(c);
return mix(c, vec4(g, g, g, c.a), amount);
}
}
  • 身份:名字@版本 就是 Kernel 的身份。图绑定到这个 id,永远不绑定到文件。
  • 能力:requires { … } 列出 Kernel 需要的能力,不声明就不能调用相应的内置函数。见能力与安全。
  • 端口:input、output,以及 scan 模式的 memory。每个端口携带一个 Field。
  • 主体:compute、scan、scatter、prefix、group N 之一,决定执行模式。
  • 预算:budget N; 声明一个元素最多可以运行的循环迭代次数(默认 16777216)。
//! 预设图或包清单的文档:文档开头的几行。
use "kernels/";
/// Kernel 的文档:紧挨在它上面的几行。
kernel tally.twice@1 {
requires { field.read; field.write; }
/// 端口或参数的文档,紧挨在它上面。
input counts: Rows<Int>;
output result: Rows<Int>;
compute { return sample(counts) * 2; }
}

/// 放在 kernel、input、param、output、memory 正上方;空行或 // 行会结束它。对语法来说它们只是注释,但会随 Kernel 源码一起经过 publish、kernel 和 snapshot,修改它们也会重新发布。

compute 主体由 let 绑定和一个结果组成。

类别 写法与规则
绑定 let n = …;;let { x, y } = window(); 按名字解构;let { x: width, y: height } = window(); 重命名;_ 不绑定;expr; 等价于 let _ = expr;
算术 + - * /、%(截断余数);Int 与 Float 严格分开,但 vec2/3/4 接受 Int 分量;Float 与向量运算时会广播
分量 v.xy、v.zyx、c.rgb;v.x 取单个分量
位运算 & | ^、<< >> >>>(移位计数取模 32,>> 算术、>>> 逻辑);0xFF 十六进制,超过 8 位或带 l 则为 Long
逻辑 &&、|| 短路;==、!= 比较标量和整个向量,< 等只比较标量
if if c { a } else { b } 是表达式;if c { return a; }(无 else)守卫后续语句;只含语句的 if hit { emit(p); } 执行后继续
match 对 Int:match i { 0 => a, 1 | 2 => …, 8..16 => c, 16..=31 => d, _ => b };范围最多 256 个值,各分支同类型
枚举 type Mode = enum { Idle, Walk, Attack = 10, Guard };——命名的 Int,Mode.Guard 是 11;列全所有变体的 match 不需要 _
loop loop n with x = init until done { next } 是表达式;次数可以是字面量(上限 65536)或任意 Int 表达式;if done { break x; } 提前结束最内层循环
结构体 Name { a: …, b: … } 构造;Name { ..base, a: … } 复制并替换字段
嵌套 表达式最多嵌套 256 层,超过时文档被拒绝并指出位置

[T; N] 保存 N 个同类型值:[Int; 16]、[Vec3; 4]、[[Int; 4]; 4]。数组可以做结构体字段、函数参数与返回值、循环携带值,也可以是 Field 的元素。

let a = [1, 2, 3];
let zeros = [0.0; 8];
let w = [each 32 { unit(weights, x, index()) }]; // 逐元素构造
let b = [each 4 { if index() == k { v } else { a[index()] } }]; // 替换第 k 个元素

字面量下标在编译时检查,计算出的 Int 下标会被夹到范围内。数组长度 1–4096;任何值最多 65536 个字,结构体和数组最多嵌套 64 层。

参数可以是 Kernel 持有的任何值:

param lens: Mat4 = mat4(1.0);
param cell: IVec2 = ivec2(3, 4);
param edges: [IVec2; 12] = [ … ];
param style: Style = Style { ground: vec3(0.1, 0.1, 0.1), width: 1.0 };

默认值必须是字面量。值按布局匹配而不是按写法:从外部(场景、回放的 param、协议)给值时,按顺序排列的数组可以填充任意形状,例如 [[1, 0], [0, 1]] 是一个 Mat2。

  • 文档顶层的 struct、type、fn 被该文档中所有 Kernel 共享,也被所有 use 它的文档共享。.ent 可以 use 其他 .ent 文件或目录。
  • 名字的查找顺序:Kernel 自己的声明 → 所在文档自己的 → 它 use 的文档中唯一的那个。两个被使用的包对同一名字给出不同答案时必须限定:cast_fx::clear(p)、Rows<ui_panel::Mark>。
  • 函数之间可以任意顺序调用,但不能递归(调用链回到自己会被拒绝并指出环);重复用 loop 表达。
  • 函数可以接收 Field 参数:fn cast(voxels: Volume<Int>, eye: Vec3) -> Hit,调用时传入输入端口名,每个端口实例化一次。
  • 泛型:fn twice<T>(v: T) -> T,每组类型编译一个实例。
  • 函数可以用数学内置函数的名字(如 step、mix),此时在本文档内代替它;Field、memory、上下文和构造类内置函数的名字不能被占用。
  • 整数失败:+ - * 溢出、除零或对零取余、对最小整数取负,在所有后端上都会使该元素失败,报告按行优先顺序第一个出错的元素;本次求值什么都不提交。add_wrap、sub_wrap、mul_wrap 是回绕版本。
  • 浮点:正数除以零是无穷,零除以零是 NaN,NaN 不等于也不排序于任何值。GPU 上算术产生的 NaN 在驱动的快速数学下可能变成数字。
  • 预算:一个元素所有循环加起来的迭代次数上限,默认 16777216,可用 budget N; 声明(1 到 2^30)。一次运行所有元素合计的上限由 Host 决定(默认四十亿,--work <M> 以百万为单位设置)。超出即失败,运行中的系统保留上一次状态。

Double、Long 及其向量与矩阵(DVec2..4、LVec2/3、DMat2..4)。double(x)、long(x) 转换,float(x)、int(x) 转回。Double 与 Float、Long 与 Int 不会隐式相遇,混用会被拒绝并提示写哪个转换。字面量在遇到宽度之前保持精确:0.1d、8l 显式指定宽度。GPU 在 map、group、归约和部分 scatter 中计算 Long,但从不计算 Double——含 Double 的节点在 GPU 会话中会改在 JIT 上运行。

端口可以用 as 以更窄的格式存储标量:Grid<Vec4 as unorm8> 每像素 4 字节,Rows<Int as u8> 每行 1 字节。Kernel 仍以 Float / Int 计算,读时加宽、写时变窄。可选 f16、unorm8、unorm16、snorm8、u8、i8、u16、i16。窄 Field 与宽 Field 是不同布局,不能直接相连;只有 map 或 scan 的步骤能写窄输出;64 位标量不能变窄。