Futureproof是基于Zig,Neovim和WebGPU构建的GPU着色器的实时编辑器

2021-01-01 08:53:58

该系统还使用FreeType进行字体光栅化,并使用GLFW进行窗口化,但是它们都比较成熟,因此我没有得到任何要点。

Neovim作为子进程(nvim --embed)运行,它使用msgpack-rpc格式在stdin和stdout上进行通信。

我从头开始在Zig中编写了msgpackand msgpack-rpclibraries,最终结果是< 1个KLOC。

侦听器线程监视子进程的标准输出,解码消息,然后将事件和响应传递到一对队列中。主循环在绘制每一帧之前声明并处理事件。

RPC调用处于阻塞状态:该调用已编码并传递到子进程的stdin,然后我们等待侦听器线程返回队列上的响应。

Neovim的抽象UI模型是具有特定宽度和高度的等距网格,网格中的每个单元格都有一个字符和属性ID;后者是对颜色,粗体,下划线等表格的查找。

网格和属性表是通过API消息(grid_line,grid_cursor_goto,hl_attr_define,mode_change等)修改的,因此我们的UI必须跟踪此状态。

这些API调用由主循环评估,并在CPU内存中处于编辑状态,但是我们希望渲染由GPU完成。事实证明,Zig和GLSL都可以导入C标头,因此Futureproof使用了一个有趣的技巧:我们定义了一个一组C结构,将它们填充到CPU中(在Zig中),然后将它们复制到GPU并直接使用(在GLSL中)。

元数据表(属性,字体图集等)位于统一缓冲区中,因为它们相对较小(总计11 KB)

字符网格较大(1 MB),因此作为存储缓冲区传递

字体被光栅化并打包到CPU端的纹理中。作为元数据的一部分,我们包括每个字符的位置和边界框,最终字体看起来像这样:

网格将每个单元格的字符和属性索引存储在uint32_t中,这将我们限制为65535个字符,但这是可以接受的。

在这种架构中,CPU端根本不考虑顶点位置,它只是将字符网格传递给顶点着色器,并告诉它绘制total_tiles * 6个元素。

然后,顶点着色器计算顶点位置,在字符网格中布置图块,从字体纹理中采样,依此类推。

我在macOS上开发了Futureproof,在某些情况下我需要使用本机API。

wgpu-native演示仅在main.c中编写了Objective-C,然后使用-x Objective-c进行编译,这意味着输入源文件是秘密的Objective-C,而不是纯C。

尽管我已尽力而为,但它与Zig并不完全兼容:直通C编译器不接受-x标志。相反,我们可以直接使用OS X启发的Objective-C运行时API Plain C中的应用程序。

实际上,这非常干净:我们没有定义我们自己的类,因此我们只使用sel_getUid,objc_lookUpClass和objc_msgSend。这是我们如何使用它从粘贴板获取字符串的方法。

Futureproof会将自身附加到Neovim缓冲区,因此只要缓冲区发生更改,它就会接收消息。这使我们可以在RPC屏障的Futureproof端保留文本的镜像。

如果文本在一定时间内没有更改(默认为200毫秒),则将其传递给shaderc并尝试将其从GLSL编译为SPIR-V。从返回值中解析出错误并将其传递回Neovim,显示在位置列表中,并在左列显示为标志。

成功编译着色器后,会将SPIR-V字节码传递到WebGPU中,并使用其余的GUI进行渲染。

一个意外的挑战是在将着色器渲染到同一GPU队列中的同时保持GUI性能。对于具有挑战性的着色器(如海景),着色器需要数百毫秒的渲染时间。

必须有一个很好的方法来处理此问题,但对于Futureproof,我使用了一个技巧:当着色器花费的时间太长时,预览图像会拆分为图块,每个图块在每个GUI框架中更新一次。存储在单独的纹理中,一旦所有图块都已渲染,就将其复制到主纹理中。

这有效地将着色器的速度降低了n ^ 2倍,其中n是每边的瓦片数。我们选择n来保持GUI响应,并使用中值过滤器来阻止单个慢帧将事情弄乱。

我很想听听使用WebGPU处理此问题的正确方法,但怀疑它需要支持多个队列,而这在标准中尚不支持。

我发现一个开发中的错误,没有得到任何反馈,因此是一个次要的警告。

最近,我发现了一个更严重的问题,该问题很容易使Futureproof陷入僵局。这虽然很令人担忧,但可能是我如何设计GUI和实时错误标记系统的一个问题;我正等着您的来信。 Neovim开发团队就此问题。

绝对不是!这是可以预期的:它是0.7.0,所以它不应该稳定或没有错误。

通用分配器,可以在程序退出时打印内存泄漏,就像一直在Valgrind中运行一样,并使无写泄漏的代码成为正常开发的一部分。

C库互操作性很棒:您可以本地导入头文件,然后调用C库,一切都可以!

传递显式分配器的哲学;尤其是如何将arenaallocator用于轻松的内存管理。

我想喜欢comptime评估,但是现在有点太不稳定了:我试图将其用于通用msgpack struct packer,并且经常混淆我的代码是否错误或编译器是否损坏。

更笼统地说,我担心comptime太强大了:没有某种概念或特征系统,它可能是万能的。例如,使用comptime类型变量进行通用化非常聪明,但这意味着生成文档会成为挑战:毕竟,它的功能足以根据类型返回完全不同的API!

(当然,对于C ++模板也是如此,但是它们很难用大多数人不会觉得太奇怪的方式来使用)

类似地,错误处理有点粗糙:使用语言级错误处理时,无法将数据(例如消息)附加到错误。

最后,我对字符串只是u8的数组有些疑惑。哲学。尽管保证静态字符串为UTF-8,但程序员有责任(而不是类型系统)来强制对来自其他来源的字符串进行正确编码。有关在哪里可以破坏字符串的详细信息,请参见讨论此帖子中的std :: fs :: metadata的内容

WebGPU很好,但是缺少文档。我不得不对示例中的许多行为进行逆向工程,读取源代码以及对现代图形API的既有知识。如上所述,缺少多队列渲染是唯一真正的沮丧。

我使用的是wgpu-native绑定,与wgpu-rs相比,它似乎并不受欢迎。我在5分钟内遇到了一个明显的错误,目前尚不清楚与它同步的频率wgpu-rs版本。

从文本到SPIR-V都需要shaderrc,这很令人沮丧,尤其是因为完整的下载将其解压缩到1.9GB(!!)。它看起来像是用nagaaims替换的,所以我对未来充满信心!

(很可能我会在Rust中构建一个类似的系统,下一次我想要一个用于半交互式图形编程的框架)

该代码位于Github上,欢迎使用fork;如果人们获得了关键的发展势头,我很乐意在此处链接它。

这是一个非常糟糕的康奈尔Box Raytracer,我把它放在一起,基本上是这样,我可以得到一个项目缩略图: