diff --git a/.github/actions/soluna/action.yml b/.github/actions/soluna/action.yml index 3ddacc1..43203b4 100644 --- a/.github/actions/soluna/action.yml +++ b/.github/actions/soluna/action.yml @@ -6,6 +6,10 @@ inputs: description: 'The path to the Soluna repository. Defaults to .' required: false default: '.' + debug: + description: 'Whether to build Soluna in debug mode. Defaults to false.' + required: false + default: 'false' outputs: SOLUNA_BINARY: @@ -20,6 +24,9 @@ outputs: SOLUNA_JS_PATH: description: 'The path to the Soluna JavaScript glue code for WebAssembly.' value: ${{ steps.set-output.outputs.SOLUNA_JS_PATH }} + SOLUNA_WASM_MAP_PATH: + description: 'The path to the built Soluna WebAssembly map if debug is true.' + value: ${{ steps.set-output.outputs.SOLUNA_WASM_MAP_PATH }} runs: using: "composite" @@ -35,7 +42,7 @@ runs: id: cache with: path: ${{ inputs.soluna_path }}/bin - key: ${{ runner.os }}-soluna-build-${{ steps.refs.outputs.commit }} + key: ${{ runner.os }}-soluna-build-${{ steps.refs.outputs.commit }}-${{ inputs.debug }} - name: Checkout all submodules if: steps.cache.outputs.cache-hit != 'true' working-directory: ${{ inputs.soluna_path }} @@ -60,13 +67,23 @@ runs: shell: powershell working-directory: ${{ inputs.soluna_path }} run: | - luamake soluna + if (${{ inputs.debug }} -eq 'true') { + $env:LUAMAKE_BUILD_TYPE = 'debug' + } else { + $env:LUAMAKE_BUILD_TYPE = 'release' + } + luamake -mode $env:LUAMAKE_BUILD_TYPE soluna - name: Build (Unix) if: runner.os != 'Windows' && steps.cache.outputs.cache-hit != 'true' shell: bash working-directory: ${{ inputs.soluna_path }} run: | - luamake soluna + if [ "${{ inputs.debug }}" == "true" ]; then + export LUAMAKE_BUILD_TYPE=debug + else + export LUAMAKE_BUILD_TYPE=release + fi + luamake -mode $LUAMAKE_BUILD_TYPE soluna - uses: mymindstorm/setup-emsdk@v14 if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true' with: @@ -77,7 +94,12 @@ runs: working-directory: ${{ inputs.soluna_path }} shell: bash run: | - luamake -compiler emcc + if [ "${{ inputs.debug }}" == "true" ]; then + export LUAMAKE_BUILD_TYPE=debug + else + export LUAMAKE_BUILD_TYPE=release + fi + luamake -mode $LUAMAKE_BUILD_TYPE -compiler emcc SOLUNA_JS="soluna.js" SOLUNA_JS_PATH=$(find bin -name $SOLUNA_JS | head -n 1) sed -i 's/setBindGroup(groupIndex,group,(growMemViews(),HEAPU32),dynamicOffsetsPtr>>2,dynamicOffsetCount)/setBindGroup(groupIndex,group,(growMemViews(),HEAPU32).subarray(dynamicOffsetsPtr>>2,(dynamicOffsetsPtr>>2)+dynamicOffsetCount))/g' "$SOLUNA_JS_PATH" @@ -104,5 +126,9 @@ runs: SOLUNA_JS_PATH=$(find $bin_dir -name "soluna.js" | head -n 1) echo "SOLUNA_WASM_PATH=$SOLUNA_WASM_PATH" >> $GITHUB_OUTPUT echo "SOLUNA_JS_PATH=$SOLUNA_JS_PATH" >> $GITHUB_OUTPUT + if [ "${{ inputs.debug }}" == "true" ]; then + SOLUNA_WASM_MAP_PATH=$(find $bin_dir -name "soluna.wasm.map" | head -n 1) + echo "SOLUNA_WASM_MAP_PATH=$SOLUNA_WASM_MAP_PATH" >> $GITHUB_OUTPUT + fi fi shell: bash diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 22ae0fe..4f96124 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -9,6 +9,11 @@ on: branches: - master workflow_dispatch: + inputs: + debug: + description: 'Whether to build in debug mode' + required: false + default: 'false' jobs: build: @@ -31,12 +36,13 @@ jobs: id: build with: soluna_path: "." + debug: ${{ github.event.inputs.debug }} - - name: Build WASM side module (sample) - uses: ./.github/actions/sample - id: sample - with: - soluna_path: "." + # - name: Build WASM side module (sample) + # uses: ./.github/actions/sample + # id: sample + # with: + # soluna_path: "." - name: Install web build tools run: | @@ -50,10 +56,13 @@ jobs: --site web \ --wasm "${{ steps.build.outputs.SOLUNA_WASM_PATH }}" \ --js "${{ steps.build.outputs.SOLUNA_JS_PATH }}" + if [ -f "${{ steps.build.outputs.SOLUNA_WASM_MAP_PATH }}" ]; then + cp "${{ steps.build.outputs.SOLUNA_WASM_MAP_PATH }}" web/static/runtime/soluna.wasm.map + fi - - name: Copy WASM side module into runtime - run: | - cp "${{ steps.sample.outputs.SAMPLE_WASM_PATH }}" web/static/runtime/sample.wasm + # - name: Copy WASM side module into runtime + # run: | + # cp "${{ steps.sample.outputs.SAMPLE_WASM_PATH }}" web/static/runtime/sample.wasm - name: Setup Hugo uses: peaceiris/actions-hugo@v3 diff --git a/make.lua b/make.lua index 84d97c8..538d4d7 100644 --- a/make.lua +++ b/make.lua @@ -85,8 +85,8 @@ lm:conf { flags = { "-Wall", "-pthread", - "-fPIC", "--use-port=emdawnwebgpu", + "-fwasm-exceptions", }, links = { "idbfs.js", @@ -101,6 +101,12 @@ lm:conf { "-s USE_PTHREADS=1", "-s PTHREAD_POOL_SIZE='Math.max(2,navigator.hardwareConcurrency)'", "-s PTHREAD_POOL_SIZE_STRICT=2", + "-s AUDIO_WORKLET=1", + "-s WASM_WORKERS=1", + "-s JSPI", + "-fwasm-exceptions", + lm.mode == "debug" and "-gsource-map", + lm.mode == "debug" and "-s EXCEPTION_STACK_TRACES=1", lm.mode == "debug" and "-s ASSERTIONS=2", -- lm.mode == "debug" and "-s SAFE_HEAP=1", lm.mode == "debug" and "-s STACK_OVERFLOW_CHECK=1", @@ -109,6 +115,7 @@ lm:conf { defines = { "_POSIX_C_SOURCE=200809L", "_GNU_SOURCE", + "MA_ENABLE_AUDIO_WORKLETS", }, }, defines = { @@ -132,13 +139,6 @@ lm:import "clibs/soluna/make.lua" lm:exe "soluna" { deps = deps, - emcc = { - ldflags = { - "-s MAIN_MODULE=1", - "-Wl,-u,emscripten_builtin_memalign", - "-Wl,--export=emscripten_builtin_memalign", - }, - }, } lm:dll "sample" { diff --git a/script/build_web.lua b/script/build_web.lua index e990953..fd5f397 100644 --- a/script/build_web.lua +++ b/script/build_web.lua @@ -13,13 +13,6 @@ local function run(cmd) end end -local function read_file(path) - local f = assert(io.open(path, "rb")) - local data = f:read("*a") - f:close() - return data -end - local function write_file(path, data) local f = assert(io.open(path, "wb")) f:write(data) @@ -116,13 +109,6 @@ local function shortcode_quote(value) return value:gsub("\\", "\\\\"):gsub("\"", "\\\"") end -local function html_escape(value) - return value - :gsub("&", "&") - :gsub("<", "<") - :gsub(">", ">") -end - local function write_front_matter(lines, fields) lines[#lines + 1] = "---" for _, field in ipairs(fields) do @@ -324,7 +310,7 @@ local example_paths = exec_lines("find " .. shell_quote(soluna_dir .. "/test") . table.sort(example_paths) for _, path in ipairs(example_paths) do local name = path:match("([^/]+)%.lua$") - if name then + if name and name ~= "extlua" then examples[#examples + 1] = { id = name, title = titleize(name), diff --git a/src/audio.c b/src/audio.c index 88d5cee..fb9263c 100644 --- a/src/audio.c +++ b/src/audio.c @@ -3,6 +3,10 @@ #include "zipreader.h" +#ifdef __EMSCRIPTEN__ +#include +#endif + #define MA_NO_WIN32_FILEIO #define MA_NO_MP3 #define MA_NO_FLAC @@ -54,6 +58,47 @@ struct custom_engine { struct custom_vfs vfs; }; +#if defined(__EMSCRIPTEN__) +EM_JS(void, soluna_webaudio_resume_on_gesture, (int audio_context), { + if (typeof document === "undefined") return; + try { + const ctx = emscriptenGetAudioObject(audio_context); + if (!ctx || typeof ctx.resume !== "function") return; + const resume = () => { + if (ctx.state === "running") return; + const p = ctx.resume(); + if (p && typeof p.catch === "function") { + p.catch((err) => console.error("Failed to resume AudioContext", err)); + } + }; + ["pointerdown", "touchstart", "touchend", "keydown", "click"].forEach((event_type) => { + document.addEventListener(event_type, resume, { once: true, capture: true }); + }); + } catch (err) { + console.error("Failed to install WebAudio resume handler", err); + } +}); + +static void +inject_webaudio_resume(struct ma_engine *engine) { + ma_device *device; + if (engine == NULL) { + return; + } + device = ma_engine_get_device(engine); + if (device == NULL || device->pContext == NULL) { + return; + } + if (device->pContext->backend != ma_backend_webaudio) { + return; + } + if (device->webaudio.audioContext == 0) { + return; + } + soluna_webaudio_resume_on_gesture(device->webaudio.audioContext); +} +#endif + static ma_result zr_open(ma_vfs* pVFS, const char* pFilePath, ma_uint32 openMode, ma_vfs_file* pFile) { struct custom_vfs *vfs = (struct custom_vfs *)pVFS; @@ -165,6 +210,9 @@ laudio_init(lua_State *L) { if (r != MA_SUCCESS) { return luaL_error(L, "ma_engine_init() error : %s", ma_result_description(r)); } +#if defined(__EMSCRIPTEN__) + inject_webaudio_resume(&e->engine); +#endif lua_pushlightuserdata(L, (void *)e); return 2; diff --git a/test/audio.lua b/test/audio.lua index b2ba0ff..6d68d1d 100644 --- a/test/audio.lua +++ b/test/audio.lua @@ -1,14 +1,126 @@ local soluna = require "soluna" +local matquad = require "soluna.material.quad" +local mattext = require "soluna.material.text" +local font = require "soluna.font" +local file = require "soluna.file" soluna.load_sounds "asset/sounds.dl" soluna.set_window_title "Soluna sound sample" +local args = ... +local batch = args.batch +local screen_w = args.width +local screen_h = args.height + +local BUTTON_W = 180 +local BUTTON_H = 64 +local SHADOW_Y = 6 + +local pointer_x = screen_w // 2 +local pointer_y = screen_h // 2 +local pressed = false + +local function load_font(data, name) + if not data then + return + end + font.import(data) + return font.name(name or "") +end + +local function font_init() + if soluna.platform == "wasm" then + local fontid = load_font(file.load "asset/font/SourceHanSansSC-Regular.ttf", "Source Han Sans SC Regular") + if fontid then + return fontid + end + end + + local sysfont = require "soluna.font.system" + for _, name in ipairs { + "WenQuanYi Micro Hei", + "Microsoft YaHei", + "Yuanti SC", + "Source Han Sans SC Regular", + } do + local ok, data = pcall(sysfont.ttfdata, name) + local fontid = ok and load_font(data, name) + if fontid then + return fontid + end + end + error "No available system font for audio sample" +end + +local play_label = mattext.block(font.cobj(), font_init(), 28, 0xff28435c, "C")("Play", BUTTON_W, BUTTON_H) + +local function update_pointer(x, y) + pointer_x = x + pointer_y = y +end + +local function inside_button(x, y) + local bx = (screen_w - BUTTON_W) // 2 + local by = (screen_h - BUTTON_H) // 2 + return x >= bx and x <= bx + BUTTON_W and y >= by and y <= by + BUTTON_H +end + local callback = {} -soluna.play_sound "bloop" +function callback.window_resize(w, h) + screen_w = w + screen_h = h +end -function callback.frame(count) +function callback.mouse_move(x, y) + update_pointer(x, y) end -return callback +function callback.mouse_button(button, key_state) + if button ~= 0 then + return + end + if key_state == 1 then + pressed = inside_button(pointer_x, pointer_y) + return + end + if pressed and inside_button(pointer_x, pointer_y) then + soluna.play_sound "bloop" + end + pressed = false +end + +function callback.touch_begin(x, y) + update_pointer(x, y) + pressed = inside_button(x, y) +end +function callback.touch_moved(x, y) + update_pointer(x, y) + pressed = inside_button(x, y) +end + +function callback.touch_end(x, y) + update_pointer(x, y) + if pressed and inside_button(x, y) then + soluna.play_sound "bloop" + end + pressed = false +end + +function callback.touch_cancelled() + pressed = false +end + +function callback.frame() + local bx = (screen_w - BUTTON_W) // 2 + local by = (screen_h - BUTTON_H) // 2 + local hovered = inside_button(pointer_x, pointer_y) + local face_y = by + (pressed and 4 or 0) + local color = pressed and 0xffcfd8e4 or hovered and 0xfffbfdff or 0xffeef3f8 + batch:add(matquad.quad(BUTTON_W, BUTTON_H, 0xff7389a3), bx, by + SHADOW_Y) + batch:add(matquad.quad(BUTTON_W, BUTTON_H, color), bx, face_y) + batch:add(play_label, bx, face_y) +end + +return callback diff --git a/web/assets/playframe.js b/web/assets/playframe.js index dd32850..042b076 100644 --- a/web/assets/playframe.js +++ b/web/assets/playframe.js @@ -261,14 +261,14 @@ return; } - let sampleWasmBuffer = null; - try { - sampleWasmBuffer = await loadBuffer(`${base}/runtime/sample.wasm`); - } catch (err) { - setStatus("Failed to load external module sample.wasm."); - setNote(err.message); - return; - } + // let sampleWasmBuffer = null; + // try { + // sampleWasmBuffer = await loadBuffer(`${base}/runtime/sample.wasm`); + // } catch (err) { + // setStatus("Failed to load external module sample.wasm."); + // setNote(err.message); + // return; + // } setStatus("Preparing fonts..."); let fontZip; @@ -280,7 +280,7 @@ return; } - const mainGame = "entry : main.lua\nhigh_dpi : true\ntext_sampler :\n min_filter : linear\n mag_filter : linear\nextlua_entry : extlua_init\nextlua_preload : sample\n"; + const mainGame = "entry : main.lua\nhigh_dpi : true\ntext_sampler :\n min_filter : linear\n mag_filter : linear\n"; const mainLuaBytes = new TextEncoder().encode(sourceText); const mainGameBytes = new TextEncoder().encode(mainGame); const mainZip = createZip([ @@ -295,6 +295,7 @@ "zipfile=/data/main.zip:/data/asset.zip:/data/font.zip", "cpath=/data/?.wasm", ], + mainScriptUrlOrBlob: `${base}/runtime/soluna.js`, canvas, print(text) { appendConsole(String(text || ""), false); @@ -303,10 +304,7 @@ appendConsole(String(text || ""), true); }, locateFile(path) { - if (path === "soluna.wasm") { - return `${base}/runtime/soluna.wasm`; - } - if (path.endsWith(".wasm")) { + if (path.endsWith(".wasm") || path.endsWith(".js")) { return `${base}/runtime/${path}`; } return path; @@ -317,15 +315,15 @@ Module.addRunDependency("asset-zip"); Module.addRunDependency("main-zip"); Module.addRunDependency("font-zip"); - Module.addRunDependency("sample-wasm"); + // Module.addRunDependency("sample-wasm"); Module.FS.writeFile("/data/asset.zip", new Uint8Array(assetBuffer), { canOwn: true }); Module.FS.writeFile("/data/main.zip", mainZip, { canOwn: true }); Module.FS.writeFile("/data/font.zip", fontZip, { canOwn: true }); - Module.FS.writeFile("/data/sample.wasm", new Uint8Array(sampleWasmBuffer), { canOwn: true }); + // Module.FS.writeFile("/data/sample.wasm", new Uint8Array(sampleWasmBuffer), { canOwn: true }); Module.removeRunDependency("asset-zip"); Module.removeRunDependency("main-zip"); Module.removeRunDependency("font-zip"); - Module.removeRunDependency("sample-wasm"); + // Module.removeRunDependency("sample-wasm"); }, ], onAbort(reason) {