From feeede1726a4c710d90f2d74163aa9f0baf84d41 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Thu, 11 Sep 2025 12:55:11 +0800 Subject: [PATCH 1/8] feat: implement functionality on linux --- src/entry.c | 8 ++++ src/font_system.c | 88 ++++++++++++++++++++++++++++++++++++++++++ src/gamepad.c | 64 ++++++++++++++++++++++++++++++ src/lualib/fontmgr.lua | 15 ++++++- src/lualib/soluna.lua | 2 +- 5 files changed, 175 insertions(+), 2 deletions(-) diff --git a/src/entry.c b/src/entry.c index 9ae34b7..78d3d25 100644 --- a/src/entry.c +++ b/src/entry.c @@ -8,6 +8,10 @@ #define SOKOL_METAL +#elif defined(__linux__) + +#define SOKOL_GLCORE + #else #error Unsupport platform @@ -40,6 +44,10 @@ #define PLATFORM "macos" +#elif defined(__linux__) + +#define PLATFORM "linux" + #else #define PLATFORM "unknown" diff --git a/src/font_system.c b/src/font_system.c index ca8b5cc..9808b75 100644 --- a/src/font_system.c +++ b/src/font_system.c @@ -178,6 +178,94 @@ lttfdata(lua_State *L) { return 1; } +#elif defined(__linux__) + +#include +#include +#include + +static void * +free_data(void *ud, void *ptr, size_t oszie, size_t nsize) { + free(ptr); + return NULL; +} + +static int +lttfdata(lua_State *L) { + const char *familyName = luaL_checkstring(L, 1); + + if (!FcInit()) { + return luaL_error(L, "Failed to initialize fontconfig"); + } + + FcPattern *pattern = FcNameParse((const FcChar8*)familyName); + if (!pattern) { + FcFini(); + return luaL_error(L, "Failed to parse font name: %s", familyName); + } + + FcConfigSubstitute(NULL, pattern, FcMatchPattern); + FcDefaultSubstitute(pattern); + + FcResult result; + FcPattern *match = FcFontMatch(NULL, pattern, &result); + FcPatternDestroy(pattern); + + if (!match || result != FcResultMatch) { + if (match) FcPatternDestroy(match); + FcFini(); + return luaL_error(L, "Font not found: %s", familyName); + } + + FcChar8 *filename; + if (FcPatternGetString(match, FC_FILE, 0, &filename) != FcResultMatch) { + FcPatternDestroy(match); + FcFini(); + return luaL_error(L, "Failed to get font file path for: %s", familyName); + } + + FILE *file = fopen((const char*)filename, "rb"); + if (!file) { + FcPatternDestroy(match); + FcFini(); + return luaL_error(L, "Failed to open font file: %s", filename); + } + + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + if (fileSize <= 0) { + fclose(file); + FcPatternDestroy(match); + FcFini(); + return luaL_error(L, "Invalid font file size: %s", filename); + } + + char *buf = malloc(fileSize + 1); + if (!buf) { + fclose(file); + FcPatternDestroy(match); + FcFini(); + return luaL_error(L, "Out of memory : sysfont"); + } + + size_t bytesRead = fread(buf, 1, fileSize, file); + fclose(file); + FcPatternDestroy(match); + FcFini(); + + if (bytesRead != fileSize) { + free(buf); + return luaL_error(L, "Failed to read font file: %s", filename); + } + + buf[fileSize] = 0; + + lua_pushexternalstring(L, buf, fileSize, free_data, NULL); + return 1; +} + #else static int diff --git a/src/gamepad.c b/src/gamepad.c index b1b4eab..bd1bf28 100644 --- a/src/gamepad.c +++ b/src/gamepad.c @@ -182,6 +182,70 @@ gamepad_getstate(int index, struct gamepad_state *state) { return 0; } +#elif defined(__linux__) + +#include +#include +#include +#include + +static int +gamepad_getstate(int index, struct gamepad_state *state) { + char device_path[32]; + snprintf(device_path, sizeof(device_path), "/dev/input/js%d", index); + + int fd = open(device_path, O_RDONLY | O_NONBLOCK); + if (fd < 0) { + return 1; + } + + static struct { + uint16_t buttons; + uint8_t lt, rt; + int16_t ls_x, ls_y, rs_x, rs_y; + uint32_t packet; + } cache = {0}; + + struct js_event event; + + while (read(fd, &event, sizeof(event)) == sizeof(event)) { + cache.packet++; + + if (event.type & JS_EVENT_BUTTON) { + if (event.number < 16) { + if (event.value) { + cache.buttons |= (1 << event.number); + } else { + cache.buttons &= ~(1 << event.number); + } + } + } else if (event.type & JS_EVENT_AXIS) { + int16_t value = event.value; + switch (event.number) { + case 0: cache.ls_x = value; break; + case 1: cache.ls_y = -value; break; + case 2: cache.lt = (value + 32768) >> 8; break; + case 3: cache.rs_x = value; break; + case 4: cache.rs_y = -value; break; + case 5: cache.rt = (value + 32768) >> 8; break; + } + } + } + + close(fd); + + state->packet = cache.packet; + state->buttons = cache.buttons; + state->lt = cache.lt; + state->rt = cache.rt; + state->ls_x = cache.ls_x; + state->ls_y = cache.ls_y; + state->rs_x = cache.rs_x; + state->rs_y = cache.rs_y; + + return 0; +} + #else // todo : linux and mac support diff --git a/src/lualib/fontmgr.lua b/src/lualib/fontmgr.lua index 51a8c22..7cab242 100644 --- a/src/lualib/fontmgr.lua +++ b/src/lualib/fontmgr.lua @@ -39,7 +39,20 @@ local ids = { UNICODE_2_0_BMP = 3, UNICODE_2_0_FULL = 4, }, - lang = { default = 0 }, + lang = { + default = 0, + ENGLISH = 0, + CHINESE = 1, + FRENCH = 2, + GERMAN = 3, + JAPANESE = 4, + KOREAN = 5, + SPANISH = 6, + ITALIAN = 7, + DUTCH = 8, + SWEDISH = 9, + RUSSIAN = 10, + }, }, MICROSOFT = { id = 3, diff --git a/src/lualib/soluna.lua b/src/lualib/soluna.lua index 2141f4e..455eceb 100644 --- a/src/lualib/soluna.lua +++ b/src/lualib/soluna.lua @@ -49,7 +49,7 @@ function soluna.gamedir(name) dir = dir .. "\\" .. name lfs.mkdir(dir) return dir .. "\\" - elseif soluna.platform == "macos" then + elseif soluna.platform == "macos" or soluna.platform == "linux" then local lfs = require "soluna.lfs" local dir = lfs.personaldir() .. "/.local/share" lfs.mkdir(dir) From cf2da60af33ad06ba2e6a135d67e6b46b9787e58 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Thu, 11 Sep 2025 13:08:45 +0800 Subject: [PATCH 2/8] build: luamake work on Linux --- clibs/sokol/make.lua | 3 ++- clibs/soluna/make.lua | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/clibs/sokol/make.lua b/clibs/sokol/make.lua index 732e0b4..70d27b2 100644 --- a/clibs/sokol/make.lua +++ b/clibs/sokol/make.lua @@ -22,7 +22,8 @@ end for path in fs.pairs("src") do local lang = lm.os == "windows" and "hlsl4" or - lm.os == "macos" and "metal_macos" or "unknown" + lm.os == "macos" and "metal_macos" or + lm.os == "linux" and "glsl410" or "unknown" if path:extension() == ".glsl" then local base = path:stem():string() compile_shader(path:string(), base .. ".glsl.h", lang) diff --git a/clibs/soluna/make.lua b/clibs/soluna/make.lua index 6dfcb71..75ab0a2 100644 --- a/clibs/soluna/make.lua +++ b/clibs/soluna/make.lua @@ -27,6 +27,20 @@ lm:source_set "soluna_src" { "-x objective-c", }, }, + linux = { + links = { + "pthread", + "dl", + "GL", + "X11", + "Xrandr", + "Xi", + "Xxf86vm", + "Xcursor", + "GLU", + "asound", + }, + }, msvc = { ldflags = { "-SUBSYSTEM:WINDOWS", From c21290d8be855b78f06f720b3a9ab1cf40af03b0 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Fri, 12 Sep 2025 11:51:17 +0800 Subject: [PATCH 3/8] fix(build/linux): avoid SSE alignment crash in material_text initialization --- src/material_text.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/material_text.c b/src/material_text.c index 22dcb67..e221040 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -188,11 +188,12 @@ lnew_material_text_normal(lua_State *L) { m->font = lua_touserdata(L, -1); lua_pop(L, 1); - m->fs_uniform = (fs_params_t) { - .edge_mask = font_manager_sdf_mask(m->font), - .dist_multiplier = 1.0f, - .color= 0xff000000, - }; + fs_params_t temp = { + .edge_mask = font_manager_sdf_mask(m->font), + .dist_multiplier = 1.0f, + .color = 0xff000000, + }; + memcpy(&m->fs_uniform, &temp, sizeof(fs_params_t)); if (luaL_newmetatable(L, "SOLUNA_MATERIAL_TEXT")) { luaL_Reg l[] = { From 807f2c5f8bfab6b28376a855a98373bce85280fe Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Fri, 12 Sep 2025 12:19:46 +0800 Subject: [PATCH 4/8] fix(service/render): make opengl context runs on render thread --- src/entry.c | 18 ++++++++++++++++++ src/lualib/main.lua | 2 ++ src/service/render.lua | 3 +++ 3 files changed, 23 insertions(+) diff --git a/src/entry.c b/src/entry.c index 78d3d25..a44bcfc 100644 --- a/src/entry.c +++ b/src/entry.c @@ -176,10 +176,28 @@ lmqueue(lua_State *L) { return 1; } +static int +lcontext_acquire(lua_State *L) { +#if defined(__linux__) + _sapp_glx_make_current(); +#endif + return 0; +} + +static int +lcontext_release(lua_State *L) { +#if defined(__linux__) + _sapp.glx.MakeCurrent(_sapp.x11.display, None, NULL); +#endif + return 0; +} + int luaopen_soluna_app(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { + { "context_acquire", lcontext_acquire }, + { "context_release", lcontext_release }, { "mqueue", lmqueue }, { "unpackmessage", lmessage_unpack }, { "sendmessage", lmessage_send }, diff --git a/src/lualib/main.lua b/src/lualib/main.lua index 9cf9066..a74fc59 100644 --- a/src/lualib/main.lua +++ b/src/lualib/main.lua @@ -124,8 +124,10 @@ local function start(config) if v then dispatch_appmsg(v) end + soluna_app.context_release() send_message("frame", count) frame_barrier:wait() + soluna_app.context_acquire() end, event = function(ev) send_message(unpackevent(ev)) diff --git a/src/service/render.lua b/src/service/render.lua index b9f7982..d293513 100644 --- a/src/service/render.lua +++ b/src/service/render.lua @@ -7,6 +7,7 @@ local defmat = require "soluna.material.default" local textmat = require "soluna.material.text" local quadmat = require "soluna.material.quad" local maskmat = require "soluna.material.mask" +local soluna_app = require "soluna.app" global require, assert, pairs, pcall, ipairs @@ -154,6 +155,7 @@ local function frame(count) -- todo: do not wait all batch commits local batch_n = #batch batch.wait() + soluna_app.context_acquire() if update_image then update_image() end STATE.drawmgr:reset() STATE.bindings:voffset(0, 0) @@ -180,6 +182,7 @@ local function frame(count) obj.draw(ptr, n, tex) end STATE.pass:finish() + soluna_app.context_release() end function S.frame(count) From a665e13d5cd3647703482e195f371b1bfc518c8c Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Fri, 12 Sep 2025 14:43:48 +0800 Subject: [PATCH 5/8] fix(build/sokol): use glsl430 on Linux --- clibs/sokol/make.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clibs/sokol/make.lua b/clibs/sokol/make.lua index 70d27b2..b017ff6 100644 --- a/clibs/sokol/make.lua +++ b/clibs/sokol/make.lua @@ -23,7 +23,7 @@ end for path in fs.pairs("src") do local lang = lm.os == "windows" and "hlsl4" or lm.os == "macos" and "metal_macos" or - lm.os == "linux" and "glsl410" or "unknown" + lm.os == "linux" and "glsl430" or "unknown" if path:extension() == ".glsl" then local base = path:stem():string() compile_shader(path:string(), base .. ".glsl.h", lang) From 4e302669589e8f28e70301637803e7efb5e3cf65 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Sun, 14 Sep 2025 23:37:34 +0800 Subject: [PATCH 6/8] debug: fprintf on pipeline create failed --- make.lua | 3 +++ src/material_default.c | 6 ++++++ src/material_mask.c | 6 ++++++ src/material_quad.c | 6 ++++++ src/material_text.c | 6 ++++++ 5 files changed, 27 insertions(+) diff --git a/make.lua b/make.lua index af54e2b..feec4c2 100644 --- a/make.lua +++ b/make.lua @@ -62,6 +62,9 @@ lm:conf({ lm.os ~= "windows" and "fontconfig", }, }, + defines = { + -- "SOKOL_DEBUG", + } }) lm:import "clibs/lua/make.lua" diff --git a/src/material_default.c b/src/material_default.c index 1d7a33a..6800cb0 100644 --- a/src/material_default.c +++ b/src/material_default.c @@ -95,6 +95,9 @@ lmaterial_default_draw(lua_State *L) { static void init_pipeline(struct material_default *p) { sg_shader shd = sg_make_shader(texquad_shader_desc(sg_query_backend())); + if (sg_query_shader_state(shd) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "failed to create shader for default material\n"); + } p->pip = sg_make_pipeline(&(sg_pipeline_desc) { .layout = { @@ -117,6 +120,9 @@ init_pipeline(struct material_default *p) { .primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP, .label = "default-pipeline" }); + if (sg_query_pipeline_state(p->pip) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "failed to create pipeline for default material\n"); + } } static int diff --git a/src/material_mask.c b/src/material_mask.c index e7fde31..cd47720 100644 --- a/src/material_mask.c +++ b/src/material_mask.c @@ -109,6 +109,9 @@ lmaterial_mask_draw(lua_State *L) { static void init_pipeline(struct material_mask *p) { sg_shader shd = sg_make_shader(maskquad_shader_desc(sg_query_backend())); + if (sg_query_shader_state(shd) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "Failed to create shader for mask material!\n"); + } p->pip = sg_make_pipeline(&(sg_pipeline_desc) { .layout = { @@ -132,6 +135,9 @@ init_pipeline(struct material_mask *p) { .primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP, .label = "mask-pipeline" }); + if (sg_query_pipeline_state(p->pip) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "Failed to create pipeline for mask material!\n"); + } } static int diff --git a/src/material_quad.c b/src/material_quad.c index c7e9a83..3416823 100644 --- a/src/material_quad.c +++ b/src/material_quad.c @@ -103,6 +103,9 @@ lmateraial_quad_draw(lua_State *L) { static void init_pipeline(struct material_quad *m) { sg_shader shd = sg_make_shader(colorquad_shader_desc(sg_query_backend())); + if (sg_query_shader_state(shd) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "Failed to create shader for quad material!\n"); + } m->pip = sg_make_pipeline(&(sg_pipeline_desc) { .layout = { @@ -124,6 +127,9 @@ init_pipeline(struct material_quad *m) { .primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP, .label = "colorquad-pipeline" }); + if (sg_query_pipeline_state(m->pip) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "failed to create pipeline for colorquad\n"); + } } static int diff --git a/src/material_text.c b/src/material_text.c index e221040..9e30101 100644 --- a/src/material_text.c +++ b/src/material_text.c @@ -148,6 +148,9 @@ lmateraial_text_draw(lua_State *L) { static void init_pipeline(struct material_text *m) { sg_shader shd = sg_make_shader(texquad_shader_desc(sg_query_backend())); + if (sg_query_shader_state(shd) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "failed to create shader for text material\n"); + } m->pip = sg_make_pipeline(&(sg_pipeline_desc) { .layout = { @@ -170,6 +173,9 @@ init_pipeline(struct material_text *m) { .primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP, .label = "text-pipeline" }); + if (sg_query_pipeline_state(m->pip) != SG_RESOURCESTATE_VALID) { + fprintf(stderr, "Failed to create pipeline for text material!\n"); + } } static int From 573903fe537395fdeb90d928ce87c49a457f0b38 Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Mon, 15 Sep 2025 09:51:48 +0800 Subject: [PATCH 7/8] fix(service/render): manipulate context on render init --- src/service/render.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/service/render.lua b/src/service/render.lua index d293513..15b9875 100644 --- a/src/service/render.lua +++ b/src/service/render.lua @@ -234,6 +234,7 @@ function S.load_sprites(name) end function S.init(arg) + soluna_app.context_acquire() font.init() local texture_size = setting.texture_size @@ -363,6 +364,7 @@ function S.init(arg) uniform = STATE.uniform, sr_buffer = STATE.srbuffer_mem, } + soluna_app.context_release() end function S.resize(w, h) From 4ca2c4694a9c9e7649ecc1c80787d2c4a7834b4e Mon Sep 17 00:00:00 2001 From: Hanchin Hsieh Date: Mon, 15 Sep 2025 10:46:24 +0800 Subject: [PATCH 8/8] ci(nightly): support Linux --- .github/workflows/nightly.yml | 20 ++++++++++++++++++-- src/entry.c | 4 ++-- src/lualib/fontmgr.lua | 6 +++--- src/lualib/main.lua | 4 ++-- src/service/render.lua | 8 ++++---- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2e7bc46..ad2efe5 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -69,7 +69,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, macos-latest] + os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v5 with: @@ -102,11 +102,20 @@ jobs: - uses: yuchanns/actions-luamake@v1.0.0 with: luamake-version: "5bedfce66f075a9f68b1475747738b81b3b41c25" + - name: Install Dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential \ + libgl1-mesa-dev libglu1-mesa-dev libx11-dev \ + libxrandr-dev libxi-dev libxxf86vm-dev libxcursor-dev \ + libasound2-dev libfontconfig1-dev - name: Build (Windows) if: runner.os == 'Windows' shell: powershell id: build-windows run: | + luamake clean luamake precompile luamake soluna $SOLUNA_BINARY = "soluna.exe" @@ -122,8 +131,15 @@ jobs: run: | luamake precompile luamake soluna + if [[ "$RUNNER_OS" == "Linux" ]]; then + RENAME_BINARY="soluna-linux-amd64" + elif [[ "$RUNNER_OS" == "macOS" ]]; then + RENAME_BINARY="soluna-macos-arm64" + else + echo "Unsupported OS: $RUNNER_OS" + exit 1 + fi SOLUNA_BINARY="soluna" - RENAME_BINARY="soluna-macos-arm64" SOLUNA_PATH=$(find bin -name $SOLUNA_BINARY | head -n 1) cp "$SOLUNA_PATH" "bin/$RENAME_BINARY" echo "SOLUNA_PATH=bin/$RENAME_BINARY" >> $GITHUB_OUTPUT diff --git a/src/entry.c b/src/entry.c index a44bcfc..6d533fc 100644 --- a/src/entry.c +++ b/src/entry.c @@ -196,8 +196,8 @@ int luaopen_soluna_app(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { - { "context_acquire", lcontext_acquire }, - { "context_release", lcontext_release }, + { "context_acquire", lcontext_acquire }, + { "context_release", lcontext_release }, { "mqueue", lmqueue }, { "unpackmessage", lmessage_unpack }, { "sendmessage", lmessage_send }, diff --git a/src/lualib/fontmgr.lua b/src/lualib/fontmgr.lua index 7cab242..54001a6 100644 --- a/src/lualib/fontmgr.lua +++ b/src/lualib/fontmgr.lua @@ -40,8 +40,8 @@ local ids = { UNICODE_2_0_FULL = 4, }, lang = { - default = 0, - ENGLISH = 0, + default = 0, + ENGLISH = 0, CHINESE = 1, FRENCH = 2, GERMAN = 3, @@ -52,7 +52,7 @@ local ids = { DUTCH = 8, SWEDISH = 9, RUSSIAN = 10, - }, + }, }, MICROSOFT = { id = 3, diff --git a/src/lualib/main.lua b/src/lualib/main.lua index a74fc59..cb5063a 100644 --- a/src/lualib/main.lua +++ b/src/lualib/main.lua @@ -124,10 +124,10 @@ local function start(config) if v then dispatch_appmsg(v) end - soluna_app.context_release() + soluna_app.context_release() send_message("frame", count) frame_barrier:wait() - soluna_app.context_acquire() + soluna_app.context_acquire() end, event = function(ev) send_message(unpackevent(ev)) diff --git a/src/service/render.lua b/src/service/render.lua index 15b9875..77d8e78 100644 --- a/src/service/render.lua +++ b/src/service/render.lua @@ -155,7 +155,7 @@ local function frame(count) -- todo: do not wait all batch commits local batch_n = #batch batch.wait() - soluna_app.context_acquire() + soluna_app.context_acquire() if update_image then update_image() end STATE.drawmgr:reset() STATE.bindings:voffset(0, 0) @@ -182,7 +182,7 @@ local function frame(count) obj.draw(ptr, n, tex) end STATE.pass:finish() - soluna_app.context_release() + soluna_app.context_release() end function S.frame(count) @@ -234,7 +234,7 @@ function S.load_sprites(name) end function S.init(arg) - soluna_app.context_acquire() + soluna_app.context_acquire() font.init() local texture_size = setting.texture_size @@ -364,7 +364,7 @@ function S.init(arg) uniform = STATE.uniform, sr_buffer = STATE.srbuffer_mem, } - soluna_app.context_release() + soluna_app.context_release() end function S.resize(w, h)