Skip to content

feat(extlua): support external material#99

Merged
cloudwu merged 5 commits intocloudwu:masterfrom
yuchanns:feat/ext-material
Apr 27, 2026
Merged

feat(extlua): support external material#99
cloudwu merged 5 commits intocloudwu:masterfrom
yuchanns:feat/ext-material

Conversation

@yuchanns
Copy link
Copy Markdown
Contributor

@yuchanns yuchanns commented Apr 26, 2026

鉴于 sokol 的 shader 机制,这可能是最合适的外挂 shader (材质) 的方式。因为 sokol 想把额外的 shader 放在数据文件中极不方便。

不过,要做外挂材质,还需要把必要的 sokol api 也导出。

Originally posted by @cloudwu in #81 (comment)

尝试为 extlua 实现 shader 外挂机制.

  1. 为 extlua 扩展导出了 sokol_api (sokol_gfx) 和 soluna_api (material). 其中 soluna api 我觉得应该避免导出 struct 细节, 所以只给出接收 void* 的函数.
  2. 把内置材质从 render service 里拆到 material/mat*.lua 下, render service 通过 load chunk 的方式加载和执行材质注册.
  3. 外部材质也走同样的加载逻辑.
  4. 材质 id 由 render service 统一分配, 内部材质从0~255, 外挂材质从256开始.
  5. id 分配时内部材质通过 embed 时 REG_MATERIAL 确保稳定的排序. 外挂材质则根据 extlua_material 的配置排序.
  6. 把 perspective_quad 从内置材质移除, 移动到 extlua_sample 作为测试和示例.

通过 extlua 进行材质扩展时, 编译需要包含 extlua/{extlua,sokolapi,solunaapi}.c 以及 extlua/solunaapi.h, 然后使用 sokol api 和 soluna api 进行材质编写:

#include <lua.h>
#include <lauxlib.h>
#include <stdint.h>
#include <stddef.h>

#include "sokol/sokol_gfx.h"
#include "xx.glsl.h"
#include "solunaapi.h"

LUA_API void luaapi_init(lua_State *L);
void sokolapi_init(lua_State *L);

#if defined(_WIN32)
#define EXTLUA_EXPORT __declspec(dllexport)
#else
#define EXTLUA_EXPORT __attribute__((visibility("default")))
#endif

EXTLUA_EXPORT int
extlua_init(lua_State *L) {
	luaapi_init(L);
	sokolapi_init(L);
	solunaapi_init(L);
	luaL_Reg l[] = {
		{ "ext.foobar", luaopen_foobar },
		{ "ext.matxx", luaopen_ext_matxx },
		{ NULL, NULL },
	};
	luaL_newlib(L, l);
	return 1;
}

并在 lua 侧进行注册几个指定的方法: submit, drawreset

local render = require "soluna.render"
local mat = require "ext.matxx"

local ctx = ...
local state = ctx.state

local inst_buffer = render.buffer {
	--
}

local bindings = render.bindings()
bindings:vbuffer(0, inst_buffer)
bindings:sampler(0, state.default_sampler)

local cobj = mat.new {
	inst_buffer = inst_buffer,
	bindings = bindings,
	uniform = state.uniform,
	sprite_bank = ctx.arg.bank_ptr,
	tmp_buffer = ctx.tmp_buffer,
}

local material = {}

function material.reset()
	bindings:base(0)
end

function material.submit(ptr, n)
	cobj:submit(ptr, n)
end

function material.draw(ptr, n, tex)
	bindings:view(1, state.views[tex + 1])
	cobj:draw(ptr, n, tex)
end

return material

然后在 *.game 文件里指定加载路径和材质:

entry : extlua.lua
extlua_entry : extlua_init
extlua_preload : sample
extlua_material : matxx
extlua_material_path : material/?.lua

@yuchanns yuchanns marked this pull request as draft April 27, 2026 00:16
@yuchanns yuchanns force-pushed the feat/ext-material branch from c1e8362 to 8b91af8 Compare April 27, 2026 01:50
@yuchanns yuchanns marked this pull request as ready for review April 27, 2026 01:53
@yuchanns yuchanns force-pushed the feat/ext-material branch from 8b91af8 to bc154bd Compare April 27, 2026 04:22
@yuchanns yuchanns force-pushed the feat/ext-material branch 8 times, most recently from ea71c4d to 502e0f0 Compare April 27, 2026 05:54
@yuchanns yuchanns force-pushed the feat/ext-material branch from 502e0f0 to 5e550cf Compare April 27, 2026 06:47
@cloudwu
Copy link
Copy Markdown
Owner

cloudwu commented Apr 27, 2026

我大致看了一下,有这样的改进意见:

目前 extlua 导出了三组 api ,分别是 lua sokol soluna ,这没有问题。其中 lua 和 sokol 是完全独立的,它们的 api 都是由第三方项目设计过的,所以我认为完全没有问题。

需要推敲的是 soluna api 。

它实际上和 sokol api 无关,但其依赖了 lua ,即参数里有 lua_State *L 。我认为可以把这层依赖去掉,让三者完全解耦。

从 sample https://github.com/yuchanns/soluna/blob/502e0f004d0069a5b9c6b6d7f219f182f509e783/extlua/extlua_sample.c 看,其实引入的 L 主要是为了调用 luaL_error 处理错误分支。我觉得可以把这些 api 改成错误值返回,在封装层把错误返回导向 luaL_error ,这样在写扩展材质的时候就不需要调用 lua 的错误处理了。

因为这里有一组回调函数风格的 api ,是由写 ext shader 者调用 soluna api 传入 callback ,而 callback 再调用 soluna api 。当 soluna api 用错误返回的形式,就需要 callback 中检查这些 api 可能的 error 返回值。这一点让此处设计变复杂。

我审查了一下现有代码。似乎大部分错误返回只是用来检查参数错误。似乎改成 C assert 也没什么问题。即使用者应该保证传入正确的参数。这一点还需要仔细审核一下,看有没有例外。

另外,现在的实现中使用了一个 struct stream_guard 用来自动在错误产生后释放内存。一旦上面的模式改成错误返回而不是 lua error ,似乎也不需要了。

@cloudwu
Copy link
Copy Markdown
Owner

cloudwu commented Apr 27, 2026

以 sample 为例:

lmaterial_perspective_quad_submit 调用了 soluna_material_submit 并传入 callback submit 。而 submit 里又调用了 soluna_material_stream_read

姑且认为 soluna_material_stream_read 是有可能出错的,目前出错后,就调用 luaL_error ,直接跳出整个调用链。如果不传入 L ,那么 soluna_material_stream_read 就无法做到这点。

所以,我们应该看 soluna_material_stream_read 是否需要出错即可跳出的机制。有两种可能:

  1. 按上面的想法,或许只是参数出错,这样 C assert 打断即可。
  2. 一定需要处理运行时错误。如果让调用者检查错误返回并通过错误返回值传递回去,使用负担比较大。或许可以改成静默处理:一旦处于错误状态,后面的操作无效且无害即可。框架自己在调用完毕后知道这个错误状态就可以正确处理了。因为这里已经通过一个 void * 透明递传了一个用户不需要了解的状态指针,所以这种错误状态也是可以被正确传递的,而不需要额外通过一个 L 。

1 也可以用 2 来实现。即检查出错误参数后,让后续 api 无效且无害。frame 执行完整个 callback 调用后,再用 luaL_error 抛出有意义的错误信息。

@yuchanns
Copy link
Copy Markdown
Contributor Author

已经去掉对 lua_State 的依赖.
错误使用 material_stream_context 传递

@yuchanns yuchanns force-pushed the feat/ext-material branch from a7606e0 to 6fa0613 Compare April 27, 2026 09:37
@cloudwu
Copy link
Copy Markdown
Owner

cloudwu commented Apr 27, 2026

  1. 为 extlua 扩展导出了 sokol_api (sokol_gfx) 和 soluna_api (material). 其中 soluna api 我觉得应该避免导出 struct 细节, 所以只给出接收 void* 的函数.

这里说的是后面那个 context 吗?就是用于在 callback 间传递的,使用者的确不关心它是什么。

不过我觉得比起用 void * ,给个类型可能会更好一些,可以避免错误:

struct material_stream_context {
  void * ctx;
};

然后使用 struct material_stream_context 而不是 void * 传递 context 。

@yuchanns
Copy link
Copy Markdown
Contributor Author

已经把 material stream context, binding, sprite bank 都改成 typed handle 了

@cloudwu
Copy link
Copy Markdown
Owner

cloudwu commented Apr 27, 2026

我的意思是这样:

把 void * 换成 soluna_render_bindings * 并不能阻止 C 编译器的隐式转换。C 的规则是可以将 void * 转换为其它指针类型。而

因为是 opaque 数据,只是确定是一个指针的话,最好用

struct material_stream_context {
  void * ctx;
};

而不是 struct material_stream_context * ,这样使用者写错类型就无法通过编译。

在 sokol 里也用的类似技巧,只不过它用的 int 而不是 void *

typedef struct sg_buffer        { uint32_t id; } sg_buffer;
typedef struct sg_image         { uint32_t id; } sg_image;
typedef struct sg_sampler       { uint32_t id; } sg_sampler;
typedef struct sg_shader        { uint32_t id; } sg_shader;
typedef struct sg_pipeline      { uint32_t id; } sg_pipeline;
typedef struct sg_view          { uint32_t id; } sg_view;

另外,我觉得这里 typedef 是多余的。其实要求使用者多注明 struct 更明确。(如果和 sokol 风格保持一致,多加一个 typedef 也行)

所以

sg_bindings soluna_material_bindings(soluna_render_bindings *bindings);

应该是

sg_bindings soluna_material_bindings(struct soluna_render_bindings bindings);

这样参数和返回值都是 opaque 类型,而不是指针。

@yuchanns yuchanns force-pushed the feat/ext-material branch from 9925320 to 376c4d0 Compare April 27, 2026 10:39
@yuchanns
Copy link
Copy Markdown
Contributor Author

已调整. 技巧知识+1. 之前用 Rust 写 C Bindings 时生成的好像就是这样的 opaque type.

@cloudwu cloudwu merged commit 59f5d0d into cloudwu:master Apr 27, 2026
5 checks passed
@yuchanns yuchanns deleted the feat/ext-material branch April 27, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants