batch 是 soluna 在调用我们编写的 main.lua 时传入的参数。我们绘制时只需调用 batch:add, 这意味着,我们可以在任意地方调用,并不是说一定要在同一个地方调用。这有助于低耦合,高内聚。当我们需要绘制多个模块的 ui 时,每个 ui 可以独立传入 batch, 在内部实现排版和绘制。一般来说最佳实践是每个模块都具有一个 draw 函数和 update 函数。这样遍历多个模块时,聚合的函数只需要先 update 再 draw, 而不需要关注具体内部实现,各个模块也是独立实现的。 deepfuture 就是如此做。
在绘制 ui 时,我们可以有很多个 node,而不仅仅是一个 node。例如,bg 是一个 node, hud 也是一个 node。每个 node 都需要通过调用 calc 方法来更新结构。因此当整个界面的不同部位更新频率不同时,我们可以将其分开成多个 node,单独 calc 其变更,避免每帧都需要对整个 ui 重新计算,减轻压力。另外一个 node 中的 id 都是唯一的,而在多个 node 中 id 是可以重复的。这个 id 只是 yoga 用来计算使用,或者我们可以通过 id 来获取和设置 yoga 中的布局信息。这是我一开始误解的一点。我使用 soluna 编写一个 todo app 时,将整个页面都描绘成一个固定的 node ,结果在更新、计算时,遇到了动态添加的困难。
当我静下心来仔细阅读 deepfuture 代码时,发现了这一点。
当我静下心来仔细阅读 deepfuture 代码时,发现了这一点。
soluna 使用 facebook/yoga 作为排版工具。这个工具是 react native 的基础设施,仅仅是负责排版,不负责渲染。在 soluna 中,我们可以用 datalist 描述排版,然后将其载入到 yoga 中,生成一个 node, 接着根据 node 结构来生成渲染指令。这里的渲染指令是指传入 batch:add 的参数。因为实际渲染都是通过 batch 来实现的。关于 batch:add 函数的 api 的函数签名描述如下:
可以说是很简单了,把材质和坐标放进去就可以绘制。
但是正因为如此简单,如果我们要直接使用它来绘制整个画面,将会较为复杂。这就是为什么我们需要一个排版工具。这个排版工具实际上就是通过一系列抽象,使得我们在设计某些具体细节时,不需要关注全局。最后通过一次 calc 计算,替我们计算某个节点的全局坐标,这样我们就可以较为轻松的绘制。当然,如果有别的排版工具,也完全可以不使用 yoga 进行排版,这不是一个必要条件。
- batch:add(obj [, x [, y]])
- obj 可以是: 正整数 sprite id;材质模块返回的 userdata(例如 matquad.quad、mattext.block(…) 的结果);或已经打包好的二进制流。附带坐标则先平移后提交。
可以说是很简单了,把材质和坐标放进去就可以绘制。
但是正因为如此简单,如果我们要直接使用它来绘制整个画面,将会较为复杂。这就是为什么我们需要一个排版工具。这个排版工具实际上就是通过一系列抽象,使得我们在设计某些具体细节时,不需要关注全局。最后通过一次 calc 计算,替我们计算某个节点的全局坐标,这样我们就可以较为轻松的绘制。当然,如果有别的排版工具,也完全可以不使用 yoga 进行排版,这不是一个必要条件。
最近开始尝试学习如何使用 soluna,但是我太过急躁了,只是弄清了一点 BATCH (批渲染) API 的用法,就急不可耐地试图编写一个 TODO APP, 而且还是让 AI 写的。其结果就是代码质量失控,我完全不知道 AI 写了什么,任凭其忽悠。于是我变得急躁,失去耐心,失去谦虚。最后搞砸了一切,从头再来
service 内置一个 plugin.lua ,专门启动插件 service。例如,我们有一个 service 叫作 rss.lua ,在插件页面点击下载后,会携带插件代码,ltask.call plugin service 请求创建 rss service,定期检查 rss ,并注册一个路由用来展示 rss 功能页面。
感觉 ltask 天然适合 dalbox。每个插件就是一个 service。前面说的订阅,下载,AI 字幕都可以是单独的线程。 http 也是多个 service 负载均衡,数据库也是一个单独 service ,模板渲染也是单独 service
我大概明白了,是检查 LTASK_EXTERNAL_OPENLIBS 是否定义了。如果没有,就默认打开标准库。所以可以通过这个方式注入。不过 ant 的实现方式更彻底。直接替换掉 luaL_openlibs 。看来我的 lua binding 实现需要做一些调整,以便于支持这种操作
在为 ltask-go 编写 examples 时我陷入了困境,无法同时成功跑通三个平台的 bee.lua 。主要是无法跑通 windows 平台的。经过研究 ejoy/ant 的代码,我终于弄清楚了。嘿嘿,果然看不懂只是暂时的,迟早会弄懂。
https://github.com/ejoy/ant/blob/master/runtime/common/modules.c
ant 采用的方式是把所有的 clibs 的 luaopen 函数统一注入到自己的入口,这样 lua 代码就可以直接使用,而不需要寻找同名的 dylib
目前我还有一个疑惑,ltask 创建多个服务时,只会打开标准库。不知道是怎么注入的?难道是宏可以重写?
https://github.com/ejoy/ant/blob/master/runtime/common/modules.c
ant 采用的方式是把所有的 clibs 的 luaopen 函数统一注入到自己的入口,这样 lua 代码就可以直接使用,而不需要寻找同名的 dylib
目前我还有一个疑惑,ltask 创建多个服务时,只会打开标准库。不知道是怎么注入的?难道是宏可以重写?
rocknix 出场不支持中文,会导致中文字符全部显示为 ??? 即使在主题中安装了中文字体支持也没用。间接原因是每个 roms/*/gamtelist.xml 显示的游戏名字就是 ??? 因此可选的解决方法就是把 gamelist 里的显示名称改为中文字符,这样就可以在游戏机列表上显示为中文。这种事情可以写一个脚本来处理和生成
这个问题找到解决方案了: 之所以发生syscall frame 的错误,本质上还是因为 lua 在遇到错误的时候会进行 longjmp,即跳回安全点,这个过程对 goroutine 的栈造成了破坏。所以需要想办法避免 longjmp。在深入研究 lua 的源码后我发现 lua_error 会检查是否设置了安全位点setjmp来决定是否进行 longjmp 。如果没有设置,则检查用户是否设置了错误处理器,最后 fallback 到 panic 上。那么安全位点是什么时候设置的呢?其实很简单,就是调用 lua_pcall 的时候。这也是为什么该方法可以安全调用任何函数的原因。所以我们只要跳过该方法的调用,改为使用 call,同时设置错误处理器,就可以将不安全的错误用 go 接管。那么用 go 接管后,我们又该怎么返回错误呢?答案是进行简单的 panic 操作。然后在 call 的调用协程上 recover 就可以捕获这个错误。进一步讲,可以自己实现一个 lua_pcall,其底层实现是通过一个 lua_call 组合 panic 和 recover 来达成由 go 端接管 lua 错误处理的效果
lua binding 遇到一个问题,当 lua 取栈取到不存在的栈时,go 直接显示 exit syscall frame no longer availabe.这和 windows 环境下使用 yield 报错一样,不知道有没有什么关联
lua go binding 写得差不多了,有两个计划:1. 实现一个 ltask 。2. 打磨一下 binding 以支持多个版本: 所有版本共用一套封装,通过 tag 来标记版本,通过对比版本来决定是否注册到 purego
ginja 已经写好了第一个版本
go.yuchanns.xyz/ginja
但是性能很不理想,比起 go 标准库 template 慢了十几倍!
接下来想验证:
1. purego v.s. purego+libffi 性能
2. cgo v.s. purego 性能
如果确定是 libffi 拖后腿,我可能就需要考虑如何仅使用 purego 来实现了
go.yuchanns.xyz/ginja
但是性能很不理想,比起 go 标准库 template 慢了十几倍!
接下来想验证:
1. purego v.s. purego+libffi 性能
2. cgo v.s. purego 性能
如果确定是 libffi 拖后腿,我可能就需要考虑如何仅使用 purego 来实现了