Repository: cloudwu/soluna Branch: master Commit: 59f5d0dc69a7 Files: 218 Total size: 673.5 KB Directory structure: gitextract_o983gujm/ ├── .editorconfig ├── .github/ │ ├── actions/ │ │ ├── sample/ │ │ │ └── action.yml │ │ └── soluna/ │ │ └── action.yml │ └── workflows/ │ ├── nightly.yml │ └── pages.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── asset/ │ ├── sounds.dl │ └── sprites.dl ├── clibs/ │ ├── datalist/ │ │ └── make.lua │ ├── ltask/ │ │ └── make.lua │ ├── lua/ │ │ └── make.lua │ ├── sample/ │ │ └── make.lua │ ├── soluna/ │ │ ├── compile_lua.lua │ │ ├── compile_shader.lua │ │ ├── make.lua │ │ ├── runlua.lua │ │ └── shader2c.lua │ ├── yoga/ │ │ └── make.lua │ └── zip/ │ └── make.lua ├── docs/ │ ├── app.lua │ ├── args.lua │ ├── callback.lua │ ├── coroutine.lua │ ├── crypt.lua │ ├── datalist.lua │ ├── file.lua │ ├── font.lua │ ├── font_system.lua │ ├── image.lua │ ├── layout.lua │ ├── lfs.lua │ ├── material_mask.lua │ ├── material_quad.lua │ ├── material_text.lua │ ├── soluna.lua │ ├── text.lua │ ├── url.lua │ └── zip.lua ├── extlua/ │ ├── extlua.c │ ├── extlua.temp.c │ ├── extlua_impl.c │ ├── extlua_impl.temp.c │ ├── extlua_sample.c │ ├── gen.lua │ ├── gen_sokol.lua │ ├── gen_soluna.lua │ ├── perspective_quad.glsl │ ├── sokolapi.c │ ├── sokolapi.temp.c │ ├── sokolapi_impl.c │ ├── sokolapi_impl.temp.c │ ├── solunaapi.c │ ├── solunaapi.h │ ├── solunaapi.h.temp │ ├── solunaapi.temp.c │ ├── solunaapi_impl.c │ └── solunaapi_impl.temp.c ├── make.lua ├── script/ │ ├── act.lua │ ├── act_targets.lua │ ├── compile_commands.lua │ ├── datalist2c.lua │ ├── hashversion.lua │ └── lua2c.lua ├── src/ │ ├── appevent.h │ ├── audio.c │ ├── batch.c │ ├── batch.h │ ├── blit.glsl │ ├── colorquad.glsl │ ├── data/ │ │ └── settingdefault.dl │ ├── drawmgr.c │ ├── embedlua.c │ ├── entry.c │ ├── extapi.c │ ├── extapi_types.h │ ├── extapi_types.temp.h │ ├── external.c │ ├── file.c │ ├── font.c │ ├── font_define.h │ ├── font_manager.c │ ├── font_manager.h │ ├── font_system.c │ ├── gamepad.c │ ├── image.c │ ├── ime_char_filter.h │ ├── ime_state.h │ ├── lcrypt.c │ ├── lfs.c │ ├── loginfo.h │ ├── lsha1.c │ ├── luabuffer.h │ ├── lualib/ │ │ ├── coroutine.lua │ │ ├── fontmgr.lua │ │ ├── icon.lua │ │ ├── initsetting.lua │ │ ├── layout.lua │ │ ├── main.lua │ │ ├── packageloader.lua │ │ ├── print_r.lua │ │ ├── soluna.lua │ │ ├── spritebundle.lua │ │ ├── text.lua │ │ └── util.lua │ ├── luamods.c │ ├── luayoga.c │ ├── luazip.c │ ├── maskquad.glsl │ ├── material/ │ │ ├── matdefault.lua │ │ ├── matmask.lua │ │ ├── matquad.lua │ │ └── mattext.lua │ ├── material_blit.c │ ├── material_default.c │ ├── material_mask.c │ ├── material_quad.c │ ├── material_text.c │ ├── material_util.c │ ├── material_util.h │ ├── mutex.h │ ├── openlibs.c │ ├── openurl.c │ ├── platform/ │ │ ├── linux/ │ │ │ ├── soluna_linux_ime.c │ │ │ └── soluna_linux_ime.h │ │ ├── macos/ │ │ │ ├── soluna_macos_ime.h │ │ │ └── soluna_macos_ime.m │ │ ├── wasm/ │ │ │ ├── soluna_ime.js │ │ │ ├── soluna_openurl.js │ │ │ └── soluna_wasm_ime.h │ │ └── windows/ │ │ ├── soluna_windows_ime.c │ │ └── soluna_windows_ime.h │ ├── render.c │ ├── render_bindings.c │ ├── render_bindings.h │ ├── render_uniform.c │ ├── sdfimage.c │ ├── sdftext.glsl │ ├── service/ │ │ ├── audio.lua │ │ ├── gamepad.lua │ │ ├── loader.lua │ │ ├── log.lua │ │ ├── render.lua │ │ ├── settings.lua │ │ └── start.lua │ ├── sprite_submit.h │ ├── spritemgr.c │ ├── spritemgr.h │ ├── srbuffer.c │ ├── srbuffer.h │ ├── texquad.glsl │ ├── tmpbuffer.h │ ├── transform.c │ ├── transform.h │ ├── truetype.c │ ├── truetype.h │ ├── version.h │ ├── winfile.c │ ├── writelog.c │ ├── yogaone.cpp │ └── zipreader.h ├── test/ │ ├── audio.game │ ├── audio.lua │ ├── bundle.lua │ ├── extlua/ │ │ └── material/ │ │ └── perspective_quad.lua │ ├── extlua.game │ ├── extlua.lua │ ├── file.lua │ ├── hello.game │ ├── hello.lua │ ├── icon.lua │ ├── image.lua │ ├── ime.lua │ ├── intersect.lua │ ├── layout.lua │ ├── mask.lua │ ├── mtex.game │ ├── mtex.lua │ ├── setting.lua │ ├── sprite.lua │ ├── spritepack.lua │ ├── test.lua │ ├── text.lua │ ├── version.lua │ ├── window.game │ └── window.lua └── website/ ├── README.md ├── README.zh-CN.md ├── astro.config.mjs ├── eslint.config.mjs ├── package.json ├── packages/ │ └── astro-theme-soluna/ │ ├── package.json │ └── src/ │ ├── client/ │ │ └── play.ts │ ├── components/ │ │ ├── Footer.astro │ │ ├── Hero.astro │ │ ├── Menubar.astro │ │ ├── Nav.astro │ │ ├── PlainList.astro │ │ ├── Section.astro │ │ ├── docs/ │ │ │ └── DocsPage.astro │ │ └── examples/ │ │ ├── ExampleListPage.astro │ │ └── ExamplePlayPage.astro │ ├── layouts/ │ │ └── BaseLayout.astro │ └── styles/ │ └── theme.css ├── pnpm-workspace.yaml ├── scripts/ │ └── prepare-runtime.mjs ├── src/ │ ├── content.config.ts │ ├── lib/ │ │ ├── content.ts │ │ └── readme.ts │ └── pages/ │ ├── docs/ │ │ └── index.astro │ ├── examples/ │ │ ├── [id].astro │ │ └── index.astro │ └── index.astro └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # see https://github.com/CppCXY/EmmyLuaCodeStyle [*.lua] max_line_length = 120 end_of_line = lf indent_style = tab indent_size = 4 quote_style = double call_arg_parentheses = remove auto_collapse_lines = false ================================================ FILE: .github/actions/sample/action.yml ================================================ name: Build Sample WASM description: Build sample.wasm side module for Soluna web runtime inputs: soluna_path: description: "The path to the Soluna repository. Defaults to ." required: false default: "." luamake_version: description: "LuaMake commit SHA used to build." required: false default: "5bedfce66f075a9f68b1475747738b81b3b41c25" emsdk_version: description: "Emscripten SDK version." required: false default: "4.0.17" outputs: SAMPLE_WASM_PATH: description: "The path to built sample.wasm side module." value: ${{ steps.set-output.outputs.SAMPLE_WASM_PATH }} runs: using: "composite" steps: - name: Get the soluna commit id: refs working-directory: ${{ inputs.soluna_path }} shell: bash run: | echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Cache sample wasm build uses: actions/cache@v4 id: cache with: path: | ${{ inputs.soluna_path }}/bin ${{ inputs.soluna_path }}/build key: ${{ runner.os }}-sample-wasm-${{ steps.refs.outputs.commit }}-${{ inputs.luamake_version }}-${{ inputs.emsdk_version }} - name: Checkout all submodules if: steps.cache.outputs.cache-hit != 'true' working-directory: ${{ inputs.soluna_path }} shell: bash run: | git submodule update --init --recursive - name: Setup LuaMake if: steps.cache.outputs.cache-hit != 'true' uses: yuchanns/actions-luamake@v1 with: luamake-version: ${{ inputs.luamake_version }} - name: Setup Emscripten if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true' uses: mymindstorm/setup-emsdk@v14 with: version: ${{ inputs.emsdk_version }} actions-cache-folder: 'emsdk-cache' - name: Build sample side module if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true' shell: bash working-directory: ${{ inputs.soluna_path }} run: | luamake -compiler emcc sample - name: Set output id: set-output shell: bash working-directory: ${{ inputs.soluna_path }} run: | soluna_root=$(pwd -P) SAMPLE_WASM_PATH=$(find "$soluna_root/bin" -name "sample.wasm" | head -n 1) if [ -z "$SAMPLE_WASM_PATH" ]; then echo "sample.wasm not found" >&2 exit 1 fi echo "SAMPLE_WASM_PATH=$SAMPLE_WASM_PATH" >> "$GITHUB_OUTPUT" ================================================ FILE: .github/actions/soluna/action.yml ================================================ name: Build Soluna description: Build Soluna for different operating systems inputs: soluna_path: 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: description: 'The name of the built Soluna binary.' value: ${{ steps.set-output.outputs.SOLUNA_BINARY }} SOLUNA_PATH: description: 'The path to the built Soluna binary.' value: ${{ steps.set-output.outputs.SOLUNA_PATH }} SOLUNA_WASM_PATH: description: 'The path to the built Soluna WebAssembly binary.' value: ${{ steps.set-output.outputs.SOLUNA_WASM_PATH }} 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" steps: - name: Get the soluna commit id: refs working-directory: ${{ inputs.soluna_path }} run: | echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT shell: bash - name: Cache soluna build uses: actions/cache@v4 id: cache with: path: ${{ inputs.soluna_path }}/bin 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 }} run: | git submodule update --init --recursive shell: bash - uses: yuchanns/actions-luamake@v1 if: steps.cache.outputs.cache-hit != 'true' with: luamake-version: "5bedfce66f075a9f68b1475747738b81b3b41c25" - name: Install Dependencies (Linux) shell: bash if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true' 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' && steps.cache.outputs.cache-hit != 'true' shell: powershell working-directory: ${{ inputs.soluna_path }} run: | 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: | 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: version: 4.0.17 actions-cache-folder: 'emsdk-cache' - name: Build Emscripten if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true' working-directory: ${{ inputs.soluna_path }} shell: bash run: | 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" 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" perl -0pi -e 's/var group=WebGPU\.getJsObject\(groupPtr\);if\(dynamicOffsetCount==0\)\{pass\.setBindGroup\(groupIndex,group\)\}else\{pass\.setBindGroup\(groupIndex,group,\(growMemViews\(\),HEAPU32\)\.subarray\(dynamicOffsetsPtr>>2,\(dynamicOffsetsPtr>>2\)\+dynamicOffsetCount\)\)\}/var group=WebGPU.getJsObject(groupPtr);if(!group){return}if(dynamicOffsetCount==0){pass.setBindGroup(groupIndex,group)}else{pass.setBindGroup(groupIndex,group,(growMemViews(),HEAPU32).subarray(dynamicOffsetsPtr>>2,(dynamicOffsetsPtr>>2)+dynamicOffsetCount))}/g' "$SOLUNA_JS_PATH" perl -0pi -e 's/var group = WebGPU\.getJsObject\(groupPtr\);\n if \(dynamicOffsetCount == 0\) \{\n pass\.setBindGroup\(groupIndex, group\);\n \} else \{\n pass\.setBindGroup\(groupIndex, group, \(growMemViews\(\), HEAPU32\)\.subarray\(\(\(dynamicOffsetsPtr\) >> 2\), \(\(dynamicOffsetsPtr\) >> 2\) \+ dynamicOffsetCount\)\);\n \}/var group = WebGPU.getJsObject(groupPtr);\n if (!group) {\n return;\n }\n if (dynamicOffsetCount == 0) {\n pass.setBindGroup(groupIndex, group);\n } else {\n pass.setBindGroup(groupIndex, group, (growMemViews(), HEAPU32).subarray(((dynamicOffsetsPtr) >> 2), ((dynamicOffsetsPtr) >> 2) + dynamicOffsetCount));\n }/g' "$SOLUNA_JS_PATH" if grep -Fq 'setBindGroup(groupIndex,group,(growMemViews(),HEAPU32),dynamicOffsetsPtr>>2,dynamicOffsetCount)' "$SOLUNA_JS_PATH" || \ grep -Fq 'setBindGroup(groupIndex, group, (growMemViews(), HEAPU32), ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount)' "$SOLUNA_JS_PATH"; then echo "Unpatched WebGPU setBindGroup glue remains in $SOLUNA_JS_PATH" >&2 exit 1 fi - name: Set Output Build Path id: set-output run: | soluna_root=$(cd "${{ inputs.soluna_path }}" && pwd -P) if [ "${{ runner.os }}" == "Windows" ]; then soluna_root_output=$(cd "${{ inputs.soluna_path }}" && pwd -W | sed 's#\\#/#g') else soluna_root_output="$soluna_root" fi bin_dir="$soluna_root/bin" bin_dir_output="$soluna_root_output/bin" if [ "${{ runner.os }}" == "Windows" ]; then SOLUNA_BINARY="soluna.exe" RENAME_BINARY="soluna-windows-amd64.exe" elif [ "${{ runner.os }}" == "macOS" ]; then SOLUNA_BINARY="soluna" RENAME_BINARY="soluna-macos-arm64" else SOLUNA_BINARY="soluna" RENAME_BINARY="soluna-linux-amd64" fi SOLUNA_PATH=$(find "$bin_dir" -name "$SOLUNA_BINARY" | head -n 1) cp "$SOLUNA_PATH" "$bin_dir/$RENAME_BINARY" echo "SOLUNA_PATH=$bin_dir_output/$RENAME_BINARY" >> $GITHUB_OUTPUT echo "SOLUNA_BINARY=$RENAME_BINARY" >> $GITHUB_OUTPUT if [ "${{ runner.os }}" == "Linux" ]; then SOLUNA_WASM_PATH=$(find "$bin_dir" -name "soluna.wasm" | head -n 1) 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 ================================================ FILE: .github/workflows/nightly.yml ================================================ name: Nightly Build on: push: branches: - master pull_request: branches: - master workflow_dispatch: schedule: - cron: '0 0 * * *' # Runs every day at midnight UTC jobs: check: name: Determine Build Necessity runs-on: ubuntu-latest outputs: proceed: ${{ steps.check.outputs.proceed }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Get the last nightly commit id: last_nightly uses: actions/github-script@v7 with: script: | const releases = await github.rest.repos.listReleases({ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }); const nightlyRelease = releases.data.find(release => release.tag_name.includes('nightly') || release.name.includes('Nightly')); if (nightlyRelease) { const tagName = nightlyRelease.tag_name; const tag = await github.rest.git.getRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `tags/${tagName}` }); const commitSha = tag.data.object.sha; return commitSha; } else { return null; } - name: Check the proceed condition id: check run: | LAST_NIGHTLY_COMMIT="${{ steps.last_nightly.outputs.result }}" CURRENT_COMMIT="${GITHUB_SHA}" echo "Last nightly commit: $LAST_NIGHTLY_COMMIT" echo "Current commit: $CURRENT_COMMIT" if [ -z "$LAST_NIGHTLY_COMMIT" ]; then echo "No previous nightly release found. Proceeding with build." echo "proceed=true" >> $GITHUB_OUTPUT elif [ "$LAST_NIGHTLY_COMMIT" != "$CURRENT_COMMIT" ]; then echo "New commits found since last nightly release. Proceeding with build." echo "proceed=true" >> $GITHUB_OUTPUT else echo "No new commits since last nightly release. Skipping build." echo "proceed=false" >> $GITHUB_OUTPUT fi build: name: Nightly Build on ${{ matrix.os }} needs: check if: needs.check.outputs.proceed == 'true' runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v6 with: submodules: recursive - uses: ./.github/actions/soluna name: Build id: build with: soluna_path: "." - uses: actions/upload-artifact@v4 name: Upload if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' with: name: "soluna-${{ runner.os }}-${{ steps.build.outputs.SOLUNA_BINARY }}" if-no-files-found: "error" path: "${{ steps.build.outputs.SOLUNA_PATH }}" overwrite: "true" - uses: actions/upload-artifact@v4 name: Upload Emscripten if: (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && runner.os == 'Linux' with: name: "soluna-emscripten" if-no-files-found: "error" overwrite: "true" path: | ${{ steps.build.outputs.SOLUNA_WASM_PATH }} ${{ steps.build.outputs.SOLUNA_JS_PATH }} release: name: Create Nightly Release needs: [check, build] runs-on: ubuntu-latest if: (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && github.actor != 'nektos/act' permissions: contents: write steps: - uses: actions/checkout@v6 - name: Download all artifacts uses: actions/download-artifact@v5 with: path: artifacts - name: Prepare release assets run: | mkdir -p release-assets find artifacts -type f -name "soluna*" -exec cp {} release-assets/ \; ls -la release-assets/ - name: Delete existing nightly releases uses: actions/github-script@v7 with: script: | const releases = await github.rest.repos.listReleases({ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }); for (const release of releases.data) { if (release.tag_name.includes('nightly') || release.name.includes('Nightly')) { console.log(`Deleting release: ${release.tag_name}`); try { await github.rest.repos.deleteRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: release.id }); try { await github.rest.git.deleteRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `tags/${release.tag_name}` }); } catch (error) { console.log(`Tag ${release.tag_name} might not exist or already deleted`); } } catch (error) { console.log(`Failed to delete release ${release.tag_name}: ${error.message}`); } } } - name: Create nightly release id: create-release uses: actions/github-script@v7 with: script: | const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); const commitSha = context.sha.substring(0, 7); const tagName = 'nightly'; const releaseNotes = `🌙 **Nightly Build** **Build Information:** - **Commit:** \`${context.sha}\` - **Branch:** \`${context.ref.replace('refs/heads/', '')}\` - **Build Time:** \`${new Date().toISOString()}\` - **Workflow:** [${context.runId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) > ⚠️ **Note:** This is an automated nightly build. It may contain unstable features and bugs. > > 📦 **Previous nightly releases are automatically removed to keep the repository clean.**`; const { data } = await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, tag_name: tagName, name: `Nightly Build (${timestamp})`, body: releaseNotes, prerelease: true, make_latest: 'true', draft: false, }); console.log(`Created release: ${data.html_url}`); return data.id; - name: Upload release assets uses: actions/github-script@v7 with: script: | const fs = require('fs'); const path = require('path'); const releaseId = ${{ steps.create-release.outputs.result }}; const assetsDir = 'release-assets'; const files = fs.readdirSync(assetsDir); for (const file of files) { const filePath = path.join(assetsDir, file); const stats = fs.statSync(filePath); if (stats.isFile()) { console.log(`Uploading ${file}...`); const content = fs.readFileSync(filePath); await github.rest.repos.uploadReleaseAsset({ owner: context.repo.owner, repo: context.repo.repo, release_id: releaseId, name: file, data: content }); console.log(`Uploaded ${file}`); } } ================================================ FILE: .github/workflows/pages.yml ================================================ name: GitHub Pages on: workflow_run: workflows: - Nightly Build types: - completed branches: - master workflow_dispatch: inputs: debug: description: 'Whether to build in debug mode' required: false default: 'false' jobs: build: name: Build Web Site if: | github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }} permissions: contents: read pages: write id-token: write steps: - uses: actions/checkout@v6 with: submodules: recursive fetch-depth: 0 ref: ${{ github.event.workflow_run.head_sha || github.sha }} - name: Build Soluna uses: ./.github/actions/soluna id: build with: soluna_path: "." debug: ${{ github.event.inputs.debug }} - name: Build Sample uses: ./.github/actions/sample id: sample with: soluna_path: "." - name: Build Astro uses: withastro/action@v6 env: SITE_BASE: /soluna/ SOLUNA_JS_PATH: ${{ steps.build.outputs.SOLUNA_JS_PATH }} SOLUNA_WASM_PATH: ${{ steps.build.outputs.SOLUNA_WASM_PATH }} SOLUNA_WASM_MAP_PATH: ${{ steps.build.outputs.SOLUNA_WASM_MAP_PATH }} SAMPLE_WASM_PATH: ${{ steps.sample.outputs.SAMPLE_WASM_PATH }} with: path: website node-version: 24 package-manager: pnpm@10.28.2 build-cmd: pnpm run build out-dir: dist deploy: name: Deploy to GitHub Pages if: | (github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')) && github.actor != 'nektos/act' needs: [build] runs-on: ubuntu-latest permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .gitignore ================================================ bin build compile_commands.json .cache node_modules .pnpm-store pnpm-debug.log* website/node_modules website/packages/*/node_modules website/dist website/.astro website/public/runtime/ ================================================ FILE: .gitmodules ================================================ [submodule "3rd/ltask"] path = 3rd/ltask url = https://github.com/cloudwu/ltask.git [submodule "3rd/sokol"] path = 3rd/sokol url = https://github.com/floooh/sokol.git [submodule "3rd/lua"] path = 3rd/lua url = https://github.com/lua/lua.git [submodule "3rd/stb"] path = 3rd/stb url = https://github.com/nothings/stb.git [submodule "3rd/datalist"] path = 3rd/datalist url = https://github.com/cloudwu/datalist.git [submodule "3rd/yoga"] path = 3rd/yoga url = https://github.com/facebook/yoga.git [submodule "3rd/zlib"] path = 3rd/zlib url = https://github.com/madler/zlib.git [submodule "bin/sokol-tools-bin"] path = bin/sokol-tools-bin url = https://github.com/floooh/sokol-tools-bin.git [submodule "3rd/miniaudio"] path = 3rd/miniaudio url = https://github.com/mackron/miniaudio.git ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2025 codingnow.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY : all clean shader extlua_sample BUILD=build BIN=bin APPNAME=soluna.exe CC?=gcc # msvc support #CC=cl LD=$(CC) LUA_EXE=$(BUILD)/lua.exe SHDC=$(BIN)/sokol-tools-bin/bin/win32/sokol-shdc.exe VERSION=$(shell git rev-parse HEAD) #for msvc ifeq ($(CC),cl) CCPP=cl CFLAGS=-utf-8 -W3 -O2 OUTPUT_O=-c -Fo: OUTPUT_EXE=-Fe: STDC=-std:c11 -experimental:c11atomics STDCPP=-std:c++20 SUBSYSTEM=-LINK -SUBSYSTEM:WINDOWS -ENTRY:"mainCRTStartup" LDFLAGS=$(SUBSYSTEM) xinput.lib Ws2_32.lib ntdll.lib Imm32.lib SHARED=-LD else CCPP=g++ CFLAGS=-Wall -O2 OUTPUT_O=-c -o OUTPUT_EXE=-o STDC=-std=c99 -lm STDCPP=-std=c++20 SUBSYSTEM=-Wl,-subsystem,windows LDFLAGS=-lkernel32 -luser32 -lshell32 -lgdi32 -ldxgi -ld3d11 -lwinmm -lws2_32 -lntdll -lxinput -limm32 -lstdc++ $(SUBSYSTEM) SHARED=--shared endif all : $(BIN)/$(APPNAME) 3RDINC=-I3rd YOGAINC=-I3rd/yoga MINIAUDIOINC=-I3rd/miniaudio LUAINC=-I3rd/lua LUASRC:=$(wildcard 3rd/lua/*.c 3rd/lua/*.h) WINFILE:=src/winfile.c $(LUA_EXE) : $(LUASRC) $(WINFILE) $(CC) $(CFLAGS) -o $@ 3rd/lua/onelua.c $(WINFILE) -DMAKE_LUA -Dfopen=fopen_utf8 COMPILE_C=$(CC) $(CFLAGS) $(STDC) $(OUTPUT_O) $@ $< COMPILE_LUA=$(LUA_EXE) script/lua2c.lua $< $@ COMPILE_DATALIST=$(LUA_EXE) script/datalist2c.lua $< $@ LUA_O=$(BUILD)/onelua.o $(LUA_O) : $(LUASRC) $(CC) $(CFLAGS) $(OUTPUT_O) $@ 3rd/lua/onelua.c -DMAKE_LIB -Dfopen=fopen_utf8 SHADER_SRC=$(wildcard src/*.glsl) SHADER_O=$(patsubst src/%.glsl,$(BUILD)/%.glsl.h,$(SHADER_SRC)) EXTLUA_SHADER_SRC=$(wildcard extlua/*.glsl) EXTLUA_SHADER_O=$(patsubst extlua/%.glsl,$(BUILD)/%.glsl.h,$(EXTLUA_SHADER_SRC)) SHADERINC=-I$(BUILD) $(BUILD)/%.glsl.h : src/%.glsl $(SHDC) --input $< --output $@ --slang hlsl4 --format sokol $(BUILD)/%.glsl.h : extlua/%.glsl $(SHDC) --input $< --output $@ --slang hlsl4 --format sokol shader : $(SHADER_O) $(EXTLUA_SHADER_O) MAIN_FULL=$(wildcard src/*.c) PLATFORM_FULL=$(wildcard src/platform/windows/*.c) MAIN_C=$(notdir $(MAIN_FULL)) MAIN_O=$(patsubst %.c,$(BUILD)/soluna_%.o,$(MAIN_C)) PLATFORM_C=$(notdir $(PLATFORM_FULL)) PLATFORM_O=$(patsubst %.c,$(BUILD)/platform_%.o,$(PLATFORM_C)) EXTLUA_O=$(BUILD)/extlua_impl.o $(BUILD)/sokolapi_impl.o $(BUILD)/solunaapi_impl.o $(MAIN_O) : $(SHADER_O) LTASK_FULL=$(wildcard 3rd/ltask/src/*.c) LTASK_C=$(notdir $(LTASK_FULL)) LTASK_O=$(patsubst %.c,$(BUILD)/ltask_%.o,$(LTASK_C)) LTASK_LUASRC=\ 3rd/ltask/service/root.lua\ 3rd/ltask/service/timer.lua\ $(wildcard 3rd/ltask/lualib/*.lua src/lualib/*.lua src/service/*.lua src/material/*.lua) LTASK_LUACODE=$(patsubst %.lua, $(BUILD)/%.lua.h, $(notdir $(LTASK_LUASRC))) DATALIST_SRC=$(wildcard src/data/*.dl) DATALIST_CODE=$(patsubst %.dl, $(BUILD)/%.dl.h, $(notdir $(DATALIST_SRC))) ZLIBINC=-I3rd/zlib ZLIB_FULL=$(wildcard 3rd/zlib/*.c) ZLIB_C = $(notdir $(ZLIB_FULL)) ZLIB_O = $(patsubst %.c,$(BUILD)/zlib_%.o,$(ZLIB_C)) MINIZIP_FULL=\ 3rd\zlib\contrib/minizip/ioapi.c\ 3rd\zlib\contrib/minizip/unzip.c\ 3rd\zlib\contrib/minizip/zip.c\ 3rd\zlib\contrib/minizip/iowin32.c MINIZIP_C = $(notdir $(MINIZIP_FULL)) MINIZIP_O = $(patsubst %.c,$(BUILD)/minizip_%.o,$(MINIZIP_C)) $(LTASK_LUACODE) $(DATALIST_CODE) : | $(LUA_EXE) $(BUILD)/%.lua.h : 3rd/ltask/service/%.lua $(COMPILE_LUA) $(BUILD)/%.lua.h : 3rd/ltask/lualib/%.lua $(COMPILE_LUA) $(BUILD)/%.lua.h : src/lualib/%.lua $(COMPILE_LUA) $(BUILD)/%.lua.h : src/service/%.lua $(COMPILE_LUA) $(BUILD)/%.lua.h : src/material/%.lua $(COMPILE_LUA) $(BUILD)/%.dl.h : src/data/%.dl $(COMPILE_DATALIST) $(BUILD)/soluna_embedlua.o : src/embedlua.c $(LTASK_LUACODE) $(DATALIST_CODE) $(COMPILE_C) -I$(BUILD) $(LUAINC) $(BUILD)/soluna_entry.o : src/entry.c src/version.h $(COMPILE_C) $(LUAINC) $(3RDINC) -DSOLUNA_HASH_VERSION=\"$(VERSION)\" $(BUILD)/soluna_%.o : src/%.c $(COMPILE_C) $(LUAINC) $(3RDINC) $(SHADERINC) $(YOGAINC) $(ZLIBINC) $(MINIAUDIOINC) $(BUILD)/platform_%.o : src/platform/windows/%.c $(COMPILE_C) $(LUAINC) $(3RDINC) $(SHADERINC) $(YOGAINC) $(ZLIBINC) -Isrc $(BUILD)/ltask_%.o : 3rd/ltask/src/%.c $(COMPILE_C) $(LUAINC) -D_WIN32_WINNT=0x0601 -DLTASK_EXTERNAL_OPENLIBS=soluna_openlibs DATALIST_O=$(BUILD)/datalist.o $(DATALIST_O) : 3rd/datalist/datalist.c $(COMPILE_C) $(LUAINC) YOGASRC:=$(wildcard 3rd/yoga/yoga/*.cpp $(addsuffix *.cpp,$(wildcard 3rd/yoga/yoga/*/))) $(BUILD)/yoga.o : src/yogaone.cpp $(YOGASRC) $(CCPP) $(STDCPP) $(OUTPUT_O) $@ $< $(YOGAINC) $(CFLAGS) $(BUILD)/zlib_%.o : 3rd/zlib/%.c $(COMPILE_C) $(ZLIBINC) $(BUILD)/minizip_%.o : 3rd/zlib/contrib/minizip/%.c $(COMPILE_C) $(ZLIBINC) $(BUILD)/extlua_impl.o : extlua/extlua_impl.c $(COMPILE_C) $(LUAINC) $(BUILD)/sokolapi_impl.o : extlua/sokolapi_impl.c $(COMPILE_C) $(3RDINC) $(BUILD)/solunaapi_impl.o : extlua/solunaapi_impl.c $(COMPILE_C) $(LUAINC) $(3RDINC) $(BIN)/$(APPNAME): $(MAIN_O) $(PLATFORM_O) $(EXTLUA_O) $(LTASK_O) $(LUA_O) $(DATALIST_O) $(BUILD)/yoga.o $(ZLIB_O) $(MINIZIP_O) $(LD) $(OUTPUT_EXE) $@ $^ $(LDFLAGS) $(BIN)/sample.dll : extlua/extlua.c extlua/sokolapi.c extlua/solunaapi.c extlua/extlua_sample.c | $(EXTLUA_SHADER_O) $(CC) $(CFLAGS) $(SHARED) $(OUTPUT_EXE) $@ $^ $(LUAINC) $(3RDINC) $(SHADERINC) -Iextlua extlua_sample: $(BIN)/sample.dll clean : rm -f $(BIN)/*.exe $(BIN)/*.dll $(BUILD)/*.o $(BUILD)/*.h ================================================ FILE: README.md ================================================
Sokol + Lua = Soluna
# Soluna [Live Examples / 在线示例](https://cloudwu.github.io/soluna/) Soluna is a 2D game framework for Lua. It is built on top of [sokol](https://github.com/floooh/sokol), integrates ltask for multithreading, and runs on Windows, Linux, macOS, and modern browsers through WebAssembly. Soluna 是一个 Lua 2D 游戏框架。它基于 [sokol](https://github.com/floooh/sokol),整合 ltask 作为多线程框架,可运行在 Windows、Linux、macOS 以及通过 WebAssembly 支持的现代浏览器中。 [![Nightly](/../../actions/workflows/nightly.yml/badge.svg)](/../../actions/workflows/nightly.yml) ## Documentation / 文档 - [API Reference / API 参考](./docs) - [Examples / 示例](./test) - [Wiki](https://github.com/cloudwu/soluna/wiki) ## Precompiled Binaries / 预编译二进制文件 Precompiled binaries for Windows, Linux, macOS, and WebAssembly are available from [Nightly Releases](/../../releases/tag/nightly). Windows、Linux、macOS 和 WebAssembly 的预编译二进制文件可从 [Nightly Releases](/../../releases/tag/nightly) 下载。 ## Building from Source / 从源码构建 Soluna can be built with `make` on Windows and with `luamake` on all supported platforms. The GitHub Action in [`.github/actions/soluna`](./.github/actions/soluna) shows the exact CI build flow. Soluna 可在 Windows 上通过 `make` 构建,也可在所有支持平台上通过 `luamake` 构建。[`.github/actions/soluna`](./.github/actions/soluna) 展示了 CI 使用的完整构建流程。 ### GitHub Actions Integration / GitHub Actions 集成 ```yaml - uses: actions/checkout@v6 with: repository: cloudwu/soluna ref: path: soluna submodules: recursive - uses: ./soluna/.github/actions/soluna id: soluna with: soluna_path: soluna - run: | echo "Soluna binary is at ${{ steps.soluna.outputs.SOLUNA_PATH }}" echo "Soluna WASM binary is at ${{ steps.soluna.outputs.SOLUNA_WASM_PATH }}" echo "Soluna js glue is at ${{ steps.soluna.outputs.SOLUNA_JS_PATH }}" ``` ## Local Website Build / 本地构建与运行网站 The website is an Astro app in `website/`. It renders the homepage from this README, generates API pages from `docs/`, and builds live example pages from `test/`. 网站是位于 `website/` 目录的 Astro 应用。它使用本 README 生成首页,从 `docs/` 生成 API 页面,并从 `test/` 生成在线示例页面。 Build the WebAssembly runtime from the repository root first: 先在仓库根目录构建 WebAssembly runtime: ```bash luamake -compiler emcc luamake -compiler emcc sample ``` Then install dependencies and start the local dev server: 然后安装依赖并启动本地开发服务器: ```bash cd website pnpm install pnpm run dev ``` ## Projects Made with Soluna / 使用 Soluna 制作的项目 - [Deep Future](https://github.com/cloudwu/deepfuture), a digital version of the board game Deep Future. / 电子版桌游《深远未来》。 ## License / 许可证 Soluna is licensed under the MIT License. See [LICENSE](./LICENSE) for details. Soluna 使用 MIT 许可证。详情见 [LICENSE](./LICENSE)。 ================================================ FILE: asset/sounds.dl ================================================ -- name : bloop filename : asset/sounds/bloop_x.wav -- name : bloop_loop filename : asset/sounds/bloop_x.wav group : music volume : 0.6 pitch : 0.8 loop : true stream : true ================================================ FILE: asset/sprites.dl ================================================ -- name : avatar filename : avatar.png x : -0.5 y : -1 -- name : avatar2 filename : avatar.png x : -0.5 y : -0.5 ================================================ FILE: clibs/datalist/make.lua ================================================ local lm = require "luamake" lm.rootdir = lm.basedir .. "/3rd/datalist" lm:source_set "datalist_src" { sources = { "datalist.c", }, includes = { lm.basedir .. "/3rd/lua", }, } ================================================ FILE: clibs/ltask/make.lua ================================================ local lm = require "luamake" lm.rootdir = lm.basedir .. "/3rd/ltask" lm:source_set "ltask_src" { sources = { "src/*.c", }, includes = { lm.basedir .. "/3rd/lua", }, defines = { "LTASK_EXTERNAL_OPENLIBS=soluna_openlibs", }, } ================================================ FILE: clibs/lua/make.lua ================================================ local lm = require "luamake" lm.rootdir = lm.basedir .. "/3rd/lua" if lm.os == "windows" then lm:source_set "winfile" { sources = { lm.basedir .. "/src/winfile.c", }, } end lm:source_set "lua_src" { sources = { "onelua.c", }, defines = { "MAKE_LIB", lm.os == "windows" and "LUA_DL_DLL" or "LUA_USE_DLOPEN", }, } lm:exe "lua" { deps = { lm.os == "windows" and "winfile", }, sources = { "onelua.c", }, defines = { "MAKE_LUA", }, windows = { defines = { "fopen=fopen_utf8", }, }, } ================================================ FILE: clibs/sample/make.lua ================================================ local lm = require "luamake" local platform = require "bee.platform" lm.rootdir = lm.basedir local function shdc_plat() if lm.os == "windows" then return "win32" end if lm.os == "linux" then return "linux" end if lm.os == "macos" then return platform.Arch == "arm64" and "osx_arm64" or "osx" end return "unknown" end local paths = { windows = "$PATH/$NAME.exe", macos = "$PATH/$NAME", linux = "$PATH/$NAME", } local shdc = assert(paths[lm.os]):gsub("%$(%u+)", { PATH = tostring(lm.basedir / "bin/sokol-tools-bin/bin" / shdc_plat()), NAME = "sokol-shdc", }) local function shader_lang() local plat = lm.platform if plat == "msvc" or plat == "clang-cl" or plat == "mingw" then return "hlsl4" end if plat == "macos" then return "metal_macos" end if plat == "emcc" then return "wgsl" end if plat == "linux" then return "glsl430" end return "unknown" end local function compile_shader(src, name) local dep = name .. "_shader" local target = lm.builddir .. "/" .. name lm:runlua(dep) { script = lm.basedir .. "/clibs/soluna/shader2c.lua", inputs = lm.basedir .. "/" .. src, outputs = lm.basedir .. "/" .. target, args = { shdc, "$in", "$out", shader_lang(), }, } return dep end local sample_shader = compile_shader("extlua/perspective_quad.glsl", "perspective_quad.glsl.h") lm:dll "sample" { sources = { "extlua/extlua.c", "extlua/sokolapi.c", "extlua/solunaapi.c", "extlua/extlua_sample.c", }, objdeps = { sample_shader, }, includes = { "3rd/lua", "3rd", "build", "extlua", }, } ================================================ FILE: clibs/soluna/compile_lua.lua ================================================ local lm = require "luamake" local fs = require "bee.filesystem" local function compile_lua_code(script, src, name) local dep = name .. "_lua_code" local target = lm.builddir .. "/" .. name local bindir = lm.bindir if lm.platform == "emcc" then bindir = lm.osbindir end lm:runlua(dep) { script = lm.basedir .. "/clibs/soluna/runlua.lua", deps = { "lua", }, inputs = lm.basedir .. "/" .. src, outputs = lm.basedir .. "/" .. target, args = { bindir, lm.basedir .. "/" .. script, "$in", "$out", }, } return dep end local lua_code_src = { "3rd/ltask/service", "3rd/ltask/lualib", "src/service", "src/lualib", "src/material", } return function(objdeps) for _, dir in ipairs(lua_code_src) do for path in fs.pairs(lm.basedir .. "/" .. dir) do if path:extension() == ".lua" then local base = path:stem():string() local dep = compile_lua_code("script/lua2c.lua", path:string(), base .. ".lua.h") objdeps[#objdeps + 1] = dep end end end for path in fs.pairs "src/data" do if path:extension() == ".dl" then local base = path:stem():string() local dep = compile_lua_code("script/datalist2c.lua", path:string(), base .. ".dl.h") objdeps[#objdeps + 1] = dep end end end ================================================ FILE: clibs/soluna/compile_shader.lua ================================================ local lm = require "luamake" local fs = require "bee.filesystem" local platform = require "bee.platform" local function shdc_plat() if lm.os == "windows" then return "win32" end if lm.os == "linux" then return "linux" end if lm.os == "macos" then return platform.Arch == "arm64" and "osx_arm64" or "osx" end return "unknown" end local paths = { windows = "$PATH/$NAME.exe", macos = "$PATH/$NAME", linux = "$PATH/$NAME", } local shdc = assert(paths[lm.os]):gsub("%$(%u+)", { PATH = tostring(lm.basedir / "bin/sokol-tools-bin/bin" / shdc_plat()), NAME = "sokol-shdc", }) local function compile_shader(src, name, lang) local dep = name .. "_shader" local target = lm.builddir .. "/" .. name lm:runlua(dep) { script = lm.basedir .. "/clibs/soluna/shader2c.lua", inputs = lm.basedir .. "/" .. src, outputs = lm.basedir .. "/" .. target, args = { shdc, "$in", "$out", lang, }, } return dep end local function shader_lang() local plat = lm.platform if plat == "msvc" or plat == "clang-cl" or plat == "mingw" then return "hlsl4" end if plat == "macos" then return "metal_macos" end if plat == "emcc" then return "wgsl" end if plat == "linux" then return "glsl430" end return "unknown" end return function(objdeps) for path in fs.pairs "src" do local lang = shader_lang() if path:extension() == ".glsl" then local base = path:stem():string() local dep = compile_shader(path:string(), base .. ".glsl.h", lang) objdeps[#objdeps + 1] = dep end end end ================================================ FILE: clibs/soluna/make.lua ================================================ local lm = require "luamake" local subprocess = require "bee.subprocess" local compile_lua = require "compile_lua" local compile_shader = require "compile_shader" lm.rootdir = lm.basedir local ok, process, errMsg = pcall(subprocess.spawn, { lm.os ~= "windows" and "git" or "C:\\Program Files\\Git\\cmd\\git.exe", "rev-parse", "HEAD", stdout = true, }) local commit if ok then if errMsg then print("Failed to start git process: " .. errMsg) else local output = process.stdout:read "a" commit = output:match "^%s*(.-)%s*$" process:wait() end end local objdeps = {} compile_lua(objdeps) compile_shader(objdeps) lm:source_set "soluna_src" { sources = { "src/*.c", "extlua/extlua_impl.c", "extlua/sokolapi_impl.c", "extlua/solunaapi_impl.c", }, objdeps = objdeps, defines = { commit and string.format('SOLUNA_HASH_VERSION=\\"%s\\"', commit), }, includes = { "build", "src", "3rd", "3rd/lua", "3rd/yoga", "3rd/zlib", "3rd/miniaudio", }, clang = { sources = lm.os == "macos" and { "src/platform/macos/*.m", }, flags = lm.os == "macos" and { "-x objective-c", }, frameworks = lm.os == "macos" and { "AudioToolbox", "IOKit", "CoreText", "CoreFoundation", "Foundation", "Cocoa", "Metal", "MetalKit", "QuartzCore", }, }, windows = { sources = { "src/platform/windows/*.c", }, includes = { "3rd/zlib/contrib/minizip", } }, gcc = { sources = lm.os == "linux" and { "src/platform/linux/*.c", } or nil, links = lm.os == "linux" and { "pthread", "dl", "GL", "X11", "Xrandr", "Xi", "Xxf86vm", "Xcursor", "GLU", "asound", }, }, msvc = { ldflags = { "-SUBSYSTEM:WINDOWS", "xinput.lib", "Ws2_32.lib", "ntdll.lib", "Imm32.lib", }, }, mingw = { links = { "kernel32", "user32", "shell32", "gdi32", "dxgi", "d3d11", "winmm", "ws2_32", "ntdll", "xinput", "imm32", }, flags = { "-Wl,subsystem,windows", }, }, } ================================================ FILE: clibs/soluna/runlua.lua ================================================ local subprocess = require "bee.subprocess" local platform = require "bee.platform" local bindir, script, src, target = ... local luaexe = platform.os == "windows" and bindir .. "/lua.exe" or bindir .. "/lua" local process = assert(subprocess.spawn { luaexe, script, src, target, }) local code = process:wait() if code ~= 0 then os.exit(code, true) end ================================================ FILE: clibs/soluna/shader2c.lua ================================================ local subprocess = require "bee.subprocess" local shdcexe, src, target, lang = ... local process = assert(subprocess.spawn { shdcexe, "--input", src, "--output", target, "--slang", lang, "--format", "sokol", }) local code = process:wait() if code ~= 0 then os.exit(code, true) end ================================================ FILE: clibs/yoga/make.lua ================================================ local lm = require "luamake" lm.rootdir = lm.basedir .. "/3rd/yoga" lm:source_set "yoga_src" { sources = { "yoga/*.cpp", "yoga/*/*.cpp", }, includes = { lm.rootdir, } } ================================================ FILE: clibs/zip/make.lua ================================================ local lm = require "luamake" lm.rootdir = lm.basedir .. "/3rd/zlib" lm:source_set "minizip" { sources = { "contrib/minizip/ioapi.c", "contrib/minizip/unzip.c", "contrib/minizip/zip.c", }, windows = { sources = { "contrib/minizip/iowin32.c", }, includes = { "contrib/minizip", }, }, includes = { lm.rootdir, }, } lm:source_set "zlib" { sources = { "*.c", "!gz*.c", }, } lm:source_set "zip_src" { deps = { "minizip", "zlib", } } ================================================ FILE: docs/app.lua ================================================ ---@meta soluna.app ---输入法候选窗口矩形 ---IME candidate window rectangle. ---@class soluna.app.ImeRect ---@field x number 左上角 X / Left coordinate ---@field y number 左上角 Y / Top coordinate ---@field width number 宽度 / Width ---@field height number 高度 / Height ---@field text_color? integer 文本 ARGB 颜色;alpha 为 0 时补为 0xff / Text color in ARGB; alpha 0 is promoted to 0xff ---应用控制模块 ---Application control module. ---@class soluna.app local app = {} ---请求应用优雅退出 ---Requests graceful application quit. function app.quit() end ---设置输入法字体 ---Sets the IME font face and pixel size. ---@overload fun() ---@overload fun(font_size: number) ---@param font_name? string 字体名;nil 表示平台默认字体 / Font face; nil uses platform default ---@param font_size number 字体像素大小 / Font size in pixels function app.set_ime_font(font_name, font_size) end ---设置输入法候选窗口矩形 ---Sets the IME candidate window rectangle. ---@param rect? soluna.app.ImeRect nil 会清除矩形 / nil clears the rectangle function app.set_ime_rect(rect) end return app ================================================ FILE: docs/args.lua ================================================ ---@meta ---可提交给 batch 的绘制对象 ---Drawable object accepted by `Batch:add`. ---@alias soluna.Drawable integer|string|userdata ---绘制批次 ---Render batch object. ---@class Batch local batch = {} ---向批次添加 sprite、material 对象或 packed stream ---Adds a sprite id, material userdata, or packed command stream. ---@param sprite soluna.Drawable sprite ID、material userdata 或 packed string / Sprite id, material userdata, or packed string ---@param x? number X 坐标,默认 0 / X position, default 0 ---@param y? number Y 坐标,默认 0 / Y position, default 0 function batch:add(sprite, x, y) end ---打开或关闭变换层 ---Opens or closes a transform layer. ---@overload fun(self: Batch) ---@overload fun(self: Batch, rotation: number) ---@overload fun(self: Batch, x: number, y: number) ---@overload fun(self: Batch, scale: number, x: number, y: number) ---@param scale number 缩放倍率,不能为 0 / Scale factor, cannot be 0 ---@param rotation number 旋转弧度 / Rotation in radians ---@param x number X 平移 / X translation ---@param y number Y 平移 / Y translation function batch:layer(scale, rotation, x, y) end ---把屏幕点转换到当前 layer 坐标 ---Transforms a screen point into the current layer space. ---@param x number 屏幕 X / Screen X ---@param y number 屏幕 Y / Screen Y ---@return number x 转换后的 X / Transformed X ---@return number y 转换后的 Y / Transformed Y function batch:point(x, y) end ---入口参数表 ---Entry argument table passed to the game script. ---@class Args ---@field width integer 当前窗口宽度 / Current window width ---@field height integer 当前窗口高度 / Current window height ---@field batch Batch 绘制批次 / Render batch ---@field [integer] string 启动参数 / Startup argument local args = {} return args ================================================ FILE: docs/callback.lua ================================================ ---@meta ---游戏入口返回的 callback 表 ---Callback table returned by the game entry script. ---@class Callback local callback = {} ---每帧调用 ---Called once per frame. ---@param count integer frame 计数 / Frame counter function callback.frame(count) end ---键盘事件 ---Keyboard event. ---@param keycode integer Sokol key code / Sokol key code ---@param state integer 1 为按下,0 为释放 / 1 for key down, 0 for key up function callback.key(keycode, state) end ---字符输入事件 ---Text input event. ---@param codepoint integer Unicode codepoint / Unicode codepoint function callback.char(codepoint) end ---鼠标按钮事件 ---Mouse button event. ---@param button integer 0 左键,1 右键,2 中键 / 0 left, 1 right, 2 middle ---@param state integer 1 为按下,0 为释放 / 1 for down, 0 for up function callback.mouse_button(button, state) end ---鼠标移动事件 ---Mouse move event. ---@param x integer 逻辑像素 X / Logical pixel X ---@param y integer 逻辑像素 Y / Logical pixel Y function callback.mouse_move(x, y) end ---鼠标滚轮事件 ---Mouse scroll event. ---@param y integer 垂直滚动量 / Vertical scroll delta ---@param x integer 水平滚动量 / Horizontal scroll delta function callback.mouse_scroll(y, x) end ---其它鼠标事件 ---Other mouse event. ---@param event_type integer Sokol event type / Sokol event type function callback.mouse(event_type) end ---触摸开始 ---Touch begin event. ---@param x integer 逻辑像素 X / Logical pixel X ---@param y integer 逻辑像素 Y / Logical pixel Y function callback.touch_begin(x, y) end ---触摸移动 ---Touch move event. ---@param x integer 逻辑像素 X / Logical pixel X ---@param y integer 逻辑像素 Y / Logical pixel Y function callback.touch_moved(x, y) end ---触摸结束 ---Touch end event. ---@param x integer 逻辑像素 X / Logical pixel X ---@param y integer 逻辑像素 Y / Logical pixel Y function callback.touch_end(x, y) end ---触摸取消 ---Touch cancelled event. ---@param x integer 逻辑像素 X / Logical pixel X ---@param y integer 逻辑像素 Y / Logical pixel Y function callback.touch_cancelled(x, y) end ---窗口尺寸变化 ---Window resize event. ---@param width integer 新窗口宽度 / New window width ---@param height integer 新窗口高度 / New window height function callback.window_resize(width, height) end return callback ================================================ FILE: docs/coroutine.lua ================================================ ---@meta soluna.coroutine ---ltask 兼容 coroutine 模块 ---ltask-compatible coroutine module. ---@class soluna.coroutine local coroutine = {} ---创建受 ltask 跟踪的 coroutine ---Creates a coroutine tracked by the ltask bridge. ---@param f function coroutine 函数 / Coroutine function ---@return thread co coroutine 线程 / Coroutine thread function coroutine.create(f) end ---恢复 coroutine ---Resumes a tracked coroutine. ---@param co thread coroutine 线程 / Coroutine thread ---@param ... any 传入参数 / Arguments ---@return boolean ok 是否成功 / Whether resume succeeded ---@return any ... 返回值或错误 / Return values or error function coroutine.resume(co, ...) end ---挂起当前 coroutine ---Yields from the current coroutine. ---@param ... any 返回给 resume 的值 / Values returned to resume ---@return any ... 下次 resume 传入的值 / Values passed by the next resume function coroutine.yield(...) end return coroutine ================================================ FILE: docs/crypt.lua ================================================ ---@meta soluna.crypt ---密码辅助模块 ---Cryptography helper module. ---@class soluna.crypt local crypt = {} ---编码为小写十六进制字符串 ---Encodes binary data as lower-case hex. ---@param data string 二进制数据 / Binary data ---@return string hex 十六进制字符串 / Hex string function crypt.hexencode(data) end ---计算 SHA-1 摘要 ---Calculates SHA-1 digest. ---@param data string 输入数据 / Input data ---@return string hash 20 字节摘要 / 20-byte digest function crypt.sha1(data) end return crypt ================================================ FILE: docs/datalist.lua ================================================ ---@meta soluna.datalist ---datalist 解析模块 ---Datalist parser module. ---@class soluna.datalist local datalist = {} ---解析 datalist 文本 ---Parses datalist text. ---@param data string datalist 文本 / Datalist text ---@return table parsed 解析结果 / Parsed result function datalist.parse(data) end ---为 datalist 格式引用字符串 ---Quotes a string for datalist syntax. ---@param str string 原始字符串 / Raw string ---@return string quoted quoted 字符串 / Quoted string function datalist.quote(str) end return datalist ================================================ FILE: docs/file.lua ================================================ ---@meta soluna.file ---文件属性表 ---File attribute table. ---@class soluna.file.Attributes ---@field mode "file"|"directory"|"link"|"socket"|"named pipe"|"char device"|"block device"|"other" 文件类型 / File type ---@field dev integer 设备号 / Device id ---@field ino integer inode / Inode ---@field nlink integer 硬链接数 / Hard link count ---@field uid integer owner user id / Owner user id ---@field gid integer owner group id / Owner group id ---@field rdev integer special file device id / Special file device id ---@field access integer 最后访问时间 / Last access time ---@field modification integer 最后修改时间 / Last modification time ---@field change integer 最后状态变化时间 / Last status change time ---@field size integer 文件大小 / File size ---@field permissions string 权限字符串 / Permission string ---文件加载模块 ---File loading module. ---@class soluna.file local file = {} ---加载文件内容 ---Loads file contents. ---@param filename string 文件路径 / File path ---@param mode? string 本地文件打开模式,默认 `"rb"` / Local file open mode, default `"rb"` ---@return string? content 文件内容;失败返回 nil / File contents, nil on failure function file.load(filename, mode) end ---获取文件属性 ---Gets file attributes. ---@param filename string 文件路径 / File path ---@return soluna.file.Attributes|string? attributes 本地文件返回属性表,zip 文件可返回 `"file"` 或 `"directory"` / Local files return attributes; zip files may return `"file"` or `"directory"` function file.attributes(filename) end ---判断文件是否存在 ---Checks whether a file exists. ---@param filename string 文件路径 / File path ---@return boolean? exists 存在时为 true,否则为 nil / true when found, nil otherwise function file.exist(filename) end ---判断本地文件是否存在 ---Checks whether a local file exists. ---@param filename string 文件路径 / File path ---@return boolean? exists 存在时为 true,否则为 nil / true when found, nil otherwise function file.local_exist(filename) end ---加载本地文件内容 ---Loads local file contents. ---@param filename string 文件路径 / File path ---@param mode? string 打开模式,默认 `"rb"` / Open mode, default `"rb"` ---@return string? content 文件内容;失败返回 nil / File contents, nil on failure function file.local_load(filename, mode) end ---遍历目录条目 ---Iterates directory entries. ---@param path string 目录路径 / Directory path ---@return fun(): string? iterator 迭代器 / Iterator ---@return userdata? state 本地目录句柄 / Local directory handle function file.dir(path) end return file ================================================ FILE: docs/font.lua ================================================ ---@meta soluna.font ---字体模块 ---Font module. ---@class soluna.font local font = {} ---导入 TrueType 字体数据 ---Imports TrueType font data. ---@param data string TTF/TTC 字体数据 / TTF/TTC font data function font.import(data) end ---按字体族名获取 font id ---Gets a font id by family name. ---@param name string 字体族名 / Font family name ---@return integer? fontid 字体 id;找不到时为 nil / Font id, nil when not found function font.name(name) end ---返回字体管理器 C 指针 ---Returns the native font manager pointer. ---@return lightuserdata fontcobj 字体管理器指针 / Font manager pointer function font.cobj() end return font ================================================ FILE: docs/font_system.lua ================================================ ---@meta soluna.font.system ---系统字体模块 ---System font module. ---@class soluna.font.system local font_system = {} ---按字体族名读取系统 TTF/TTC 数据 ---Reads system TTF/TTC data by family name. ---@param name string 字体族名 / Font family name ---@return string? data 字体数据;wasm 或失败时可能为 nil / Font data, nil on wasm or failure function font_system.ttfdata(name) end return font_system ================================================ FILE: docs/image.lua ================================================ ---@meta soluna.image ---图片模块 ---Image module. ---@class soluna.image local image = {} ---从 PNG 数据加载 RGBA 图片 ---Loads RGBA image data from PNG bytes. ---@param data string PNG 数据 / PNG bytes ---@return string? data RGBA 像素数据,失败时为 nil / RGBA pixels, nil on failure ---@return integer|string width_or_error 成功时为宽度,失败时为错误信息 / Width on success, error message on failure ---@return integer? height 高度 / Height function image.load(data) end ---按比例缩放 RGBA 或灰度图片 ---Resizes RGBA or grayscale image data by scale factors. ---@param data string RGBA 或灰度像素数据 / RGBA or grayscale pixels ---@param width integer 原始宽度 / Source width ---@param height integer 原始高度 / Source height ---@param scale_x number X 缩放倍率 / X scale factor ---@param scale_y? number Y 缩放倍率,默认等于 `scale_x` / Y scale factor, default is `scale_x` ---@return string data 缩放后的像素数据 / Resized pixels ---@return integer width 新宽度 / New width ---@return integer height 新高度 / New height function image.resize(data, width, height, scale_x, scale_y) end return image ================================================ FILE: docs/layout.lua ================================================ ---@meta soluna.layout ---layout 元素对象 ---Layout element object. ---@class soluna.layout.Element local element = {} ---更新元素 Yoga 属性 ---Updates Yoga attributes on the element. ---@param attr table 属性表 / Attribute table function element:update(attr) end ---读取元素布局结果 ---Reads calculated element layout. ---@return number x X 坐标 / X coordinate ---@return number y Y 坐标 / Y coordinate ---@return number w 宽度 / Width ---@return number h 高度 / Height function element:get() end ---返回元素属性表 ---Returns the element attribute table. ---@return table attrs 属性表 / Attribute table function element:attribs() end ---layout 文档对象 ---Layout document object. ---@class soluna.layout.Document ---@field [string] soluna.layout.Element 按 id 访问元素 / Element access by id ---layout 绘制条目 ---Calculated drawable layout item. ---@class soluna.layout.Item ---@field x number X 坐标 / X coordinate ---@field y number Y 坐标 / Y coordinate ---@field w number 宽度 / Width ---@field h number 高度 / Height ---@field [string] any datalist 属性 / Datalist attributes ---layout 计算结果 ---Calculated layout item list. ---@class soluna.layout.Result ---@field [integer] soluna.layout.Item 绘制条目 / Drawable item ---@field width number 根节点宽度 / Root width ---@field height number 根节点高度 / Root height ---Yoga layout 模块 ---Yoga layout module. ---@class soluna.layout local layout = {} ---加载 layout 定义 ---Loads a layout definition. ---@param filename_or_list string|table layout 文件路径或已解析 datalist / Layout file path or parsed datalist ---@param scripts? fun(name: string): table children 动态 children resolver / Dynamic children resolver ---@return soluna.layout.Document document layout 文档 / Layout document function layout.load(filename_or_list, scripts) end ---计算 layout 并返回绘制条目 ---Calculates layout and returns drawable items. ---@param document soluna.layout.Document layout 文档 / Layout document ---@return soluna.layout.Result items 绘制条目列表 / Drawable item list function layout.calc(document) end return layout ================================================ FILE: docs/lfs.lua ================================================ ---@meta soluna.lfs ---本地文件属性表 ---Local file attribute table. ---@class soluna.lfs.Attributes ---@field mode "file"|"directory"|"link"|"socket"|"named pipe"|"char device"|"block device"|"other" 文件类型 / File type ---@field size integer 文件大小 / File size ---@field access integer 最后访问时间 / Last access time ---@field modification integer 最后修改时间 / Last modification time ---@field change integer 最后状态变化时间 / Last status change time ---@field permissions string 权限字符串 / Permission string ---本地文件系统模块 ---Local filesystem module. ---@class soluna.lfs local lfs = {} ---获取文件属性 ---Gets file attributes. ---@param filename string 文件路径 / File path ---@param member? string 可选属性名 / Optional attribute name ---@return soluna.lfs.Attributes|integer|string|nil attributes 属性表或指定属性 / Attribute table or selected attribute ---@return string? err 错误信息 / Error message ---@return integer? errno 系统错误码 / System errno function lfs.attributes(filename, member) end ---遍历目录条目 ---Iterates directory entries. ---@param path string 目录路径 / Directory path ---@return fun(): string? iterator 迭代器 / Iterator ---@return userdata state 目录句柄 / Directory handle function lfs.dir(path) end return lfs ================================================ FILE: docs/material_mask.lua ================================================ ---@meta soluna.material.mask ---mask material 模块 ---Mask material module. ---@class soluna.material.mask local matmask = {} ---创建带颜色遮罩的 sprite command stream ---Creates a colored mask sprite command stream. ---@param sprite integer 1-based sprite id / 1-based sprite id ---@param color integer ARGB 颜色,alpha 为 0 时补为 0xff / ARGB color; alpha 0 is promoted to 0xff ---@return string stream 可传给 `batch:add` 的 packed stream / Packed stream for `batch:add` function matmask.mask(sprite, color) end return matmask ================================================ FILE: docs/material_quad.lua ================================================ ---@meta soluna.material.quad ---quad material 模块 ---Quad material module. ---@class soluna.material.quad local matquad = {} ---创建纯色矩形 command stream ---Creates a solid rectangle command stream. ---@param width integer 宽度 / Width ---@param height integer 高度 / Height ---@param color integer ARGB 颜色,alpha 为 0 时补为 0xff / ARGB color; alpha 0 is promoted to 0xff ---@return string stream 可传给 `batch:add` 的 packed stream / Packed stream for `batch:add` function matquad.quad(width, height, color) end return matquad ================================================ FILE: docs/material_text.lua ================================================ ---@meta soluna.material.text ---文本块创建函数 ---Text block builder function. ---@alias soluna.material.text.Block fun(text: string, width?: integer, height?: integer): string, integer ---光标位置查询函数 ---Text cursor query function. ---@alias soluna.material.text.Cursor fun(text: string, position: integer, width?: integer, height?: integer): integer, integer, integer, integer, integer, integer ---text material 模块 ---Text material module. ---@class soluna.material.text local mattext = {} ---创建文本块和光标查询函数 ---Creates text block and cursor query functions. ---@param fontcobj lightuserdata `font.cobj()` 返回的字体管理器指针 / Font manager pointer returned by `font.cobj()` ---@param fontid integer `font.name()` 返回的字体 id / Font id returned by `font.name()` ---@param size? integer 字体像素大小,默认 16 / Font pixel size, default 16 ---@param color? integer ARGB 颜色,默认 `0xff000000` / ARGB color, default `0xff000000` ---@param alignment? string 对齐代码,如 `"LT"`、`"CV"`、`"RB"` / Alignment code such as `"LT"`, `"CV"`, `"RB"` ---@return soluna.material.text.Block block 创建 packed text stream / Creates packed text stream ---@return soluna.material.text.Cursor cursor 查询光标矩形 / Queries cursor rectangle function mattext.block(fontcobj, fontid, size, color, alignment) end return mattext ================================================ FILE: docs/soluna.lua ================================================ ---@meta ---单个 sprite id 或动画帧 id 列表 ---Single sprite id or animation frame id list. ---@alias Sprite integer|integer[] ---sprite bundle 名称到 id 的映射 ---Sprite bundle name-to-id mapping. ---@alias SpriteBundle table ---窗口图标图片描述 ---Window icon image descriptor. ---@class soluna.IconImage ---@field data string|userdata|lightuserdata RGBA 像素数据 / RGBA pixel buffer ---@field w? integer 宽度;也可使用 `width` / Width; `width` is also accepted ---@field h? integer 高度;也可使用 `height` / Height; `height` is also accepted ---@field width? integer 宽度;`w` 的别名 / Width alias for `w` ---@field height? integer 高度;`h` 的别名 / Height alias for `h` ---@field stride? integer 每行字节数,默认 `width * 4` / Row stride in bytes, default `width * 4` ---@field size? integer `lightuserdata` 数据大小 / Buffer size for `lightuserdata` ---运行时预加载 sprite 图片 ---Runtime preloaded sprite image. ---@class soluna.PreloadSprite ---@field filename string 虚拟文件名 / Virtual filename ---@field content string RGBA 像素数据 / RGBA pixel data ---@field w integer 宽度 / Width ---@field h integer 高度 / Height ---音频播放选项 ---Audio playback options. ---@class soluna.AudioPlayOptions ---@field group? string audio bus 名称 / Audio bus name ---@field volume? number 线性音量倍率 / Linear volume multiplier ---@field pan? number 声像,范围通常为 `[-1.0, 1.0]` / Stereo pan, usually in `[-1.0, 1.0]` ---@field pitch? number pitch 倍率 / Pitch multiplier ---@field loop? boolean 是否循环播放 / Whether playback loops ---@field stream? boolean 是否流式播放 / Whether to stream instead of preloading ---音频播放实例 ---Audio playback instance. ---@class soluna.AudioVoice local AudioVoice = {} ---停止播放 ---Stops playback. ---@param fade_seconds? number fade out 秒数 / Fade-out seconds ---@return boolean ok voice 有效且请求成功时为 true / true when the voice is valid and the request succeeds function AudioVoice:stop(fade_seconds) end ---返回是否仍在播放 ---Returns whether the voice is still playing. ---@return boolean playing 是否播放中 / Whether it is playing function AudioVoice:playing() end ---设置 voice 音量 ---Sets voice volume. ---@param volume number 线性音量倍率 / Linear volume multiplier ---@return boolean ok voice 有效时为 true / true when the voice is valid function AudioVoice:set_volume(volume) end ---设置 voice 声像 ---Sets voice pan. ---@param pan number 声像 / Stereo pan ---@return boolean ok voice 有效时为 true / true when the voice is valid function AudioVoice:set_pan(pan) end ---设置 voice pitch ---Sets voice pitch. ---@param pitch number pitch 倍率 / Pitch multiplier ---@return boolean ok voice 有效时为 true / true when the voice is valid function AudioVoice:set_pitch(pitch) end ---设置 voice 是否循环 ---Sets whether the voice loops. ---@param loop boolean 是否循环 / Whether to loop ---@return boolean ok voice 有效时为 true / true when the voice is valid function AudioVoice:set_loop(loop) end ---跳转播放位置 ---Seeks to a playback position. ---@param seconds number 目标秒数 / Target seconds ---@return boolean ok voice 有效且 seek 成功时为 true / true when valid and seek succeeds function AudioVoice:seek(seconds) end ---返回当前播放位置 ---Returns the current playback position. ---@return number? seconds 当前秒数 / Current seconds ---@return string? err 错误信息 / Error message function AudioVoice:tell() end ---音频 bus 句柄 ---Audio bus handle. ---@class soluna.AudioBus local AudioBus = {} ---设置 bus 音量 ---Sets bus volume. ---@param volume number 线性音量倍率 / Linear volume multiplier ---@return boolean ok bus 存在时为 true / true when the bus exists function AudioBus:set_volume(volume) end ---Soluna 主模块 ---Soluna root module. ---@class soluna ---@field platform "windows"|"macos"|"linux"|"wasm" 当前平台 / Current platform ---@field version string 运行时版本字符串 / Runtime version string ---@field version_api integer API 版本号 / API version number local soluna = {} ---返回 `.game` 设置表 ---Returns the `.game` settings table. ---@return table settings 游戏设置 / Game settings function soluna.settings() end ---设置窗口标题 ---Sets the window title. ---@param text string 标题文字 / Window title function soluna.set_window_title(text) end ---设置窗口图标 ---Sets one icon image or an icon image list. ---@param data soluna.IconImage|soluna.IconImage[] 图标数据 / Icon data function soluna.set_icon(data) end ---返回并创建游戏数据目录 ---Returns and creates the game data directory. ---@param name? string 项目名,默认来自 `settings.project` / Project name, default from `settings.project` ---@return string path 绝对路径,结尾带 `/` / Absolute path ending with `/` function soluna.gamedir(name) end ---加载 sprite bundle ---Loads a sprite bundle. ---@param filename string|table `.dl` 文件路径或已解析 bundle 表 / `.dl` path or parsed bundle table ---@return SpriteBundle sprites sprite 名称映射 / Sprite name mapping function soluna.load_sprites(filename) end ---预加载运行时生成的 RGBA sprite 图片 ---Preloads runtime-generated RGBA sprite images. ---@param sprites soluna.PreloadSprite|soluna.PreloadSprite[] 单个 sprite 或列表 / One sprite or a list function soluna.preload(sprites) end ---加载音频定义 bundle ---Loads an audio definition bundle. ---@param filename string `sounds.dl` 文件路径 / `sounds.dl` path function soluna.load_sounds(filename) end ---播放音频并返回 voice ---Plays a sound and returns a voice handle. ---@param name string `sounds.dl` 中的音频名 / Sound name from `sounds.dl` ---@param opts? soluna.AudioPlayOptions 播放选项覆盖 / Playback option overrides ---@return soluna.AudioVoice? voice voice 句柄 / Voice handle ---@return string? err 错误信息 / Error message function soluna.play_sound(name, opts) end ---返回 audio bus 句柄 ---Returns an audio bus handle. ---@param name string bus 名称 / Bus name ---@return soluna.AudioBus? bus bus 句柄 / Bus handle ---@return string? err 错误信息 / Error message function soluna.audio_bus(name) end return soluna ================================================ FILE: docs/text.lua ================================================ ---@meta soluna.text ---文本预处理模块 ---Text preprocessing module. ---@class soluna.text ---@field convert table 文本转换缓存表 / Text conversion cache table local text = {} ---初始化内嵌 icon bundle ---Initializes the embedded icon bundle. ---@param bundle_file string icon bundle `.dl` 文件路径 / Icon bundle `.dl` path function text.init(bundle_file) end return text ================================================ FILE: docs/url.lua ================================================ ---@meta soluna.url ---URL 模块 ---URL module. ---@class soluna.url local url = {} ---用系统默认浏览器打开 URL ---Opens a URL with the system default browser. ---@param link string URL / URL function url.open(link) end return url ================================================ FILE: docs/zip.lua ================================================ ---@meta soluna.zip ---ZIP 模块 ---ZIP module. ---@class soluna.zip local zip = {} ---打开 ZIP 文件 ---Opens a ZIP archive. ---@param filename string ZIP 文件路径 / ZIP file path ---@param mode "r"|"w"|"a" 打开模式:读、写、追加 / Open mode: read, write, append ---@return userdata? zipfile ZIP 句柄;失败时为 nil / ZIP handle, nil on failure function zip.open(filename, mode) end return zip ================================================ FILE: extlua/extlua.c ================================================ // AUTO GENERATED by extlua.temp.c, DONT EDIT #include #include #include #include #include struct lua_api { int version; lua_State * (*lua_newstate) (lua_Alloc f, void *ud, unsigned seed); void (*lua_close) (lua_State *L); lua_State * (*lua_newthread) (lua_State *L); int (*lua_closethread) (lua_State *L, lua_State *from); lua_CFunction (*lua_atpanic) (lua_State *L, lua_CFunction panicf); lua_Number (*lua_version) (lua_State *L); int (*lua_absindex) (lua_State *L, int idx); int (*lua_gettop) (lua_State *L); void (*lua_settop) (lua_State *L, int idx); void (*lua_pushvalue) (lua_State *L, int idx); void (*lua_rotate) (lua_State *L, int idx, int n); void (*lua_copy) (lua_State *L, int fromidx, int toidx); int (*lua_checkstack) (lua_State *L, int n); void (*lua_xmove) (lua_State *from, lua_State *to, int n); int (*lua_isnumber) (lua_State *L, int idx); int (*lua_isstring) (lua_State *L, int idx); int (*lua_iscfunction) (lua_State *L, int idx); int (*lua_isinteger) (lua_State *L, int idx); int (*lua_isuserdata) (lua_State *L, int idx); int (*lua_type) (lua_State *L, int idx); const char * (*lua_typename) (lua_State *L, int tp); lua_Number (*lua_tonumberx) (lua_State *L, int idx, int *isnum); lua_Integer (*lua_tointegerx) (lua_State *L, int idx, int *isnum); int (*lua_toboolean) (lua_State *L, int idx); const char * (*lua_tolstring) (lua_State *L, int idx, size_t *len); lua_Unsigned (*lua_rawlen) (lua_State *L, int idx); lua_CFunction (*lua_tocfunction) (lua_State *L, int idx); void * (*lua_touserdata) (lua_State *L, int idx); lua_State * (*lua_tothread) (lua_State *L, int idx); const void * (*lua_topointer) (lua_State *L, int idx); void (*lua_arith) (lua_State *L, int op); int (*lua_rawequal) (lua_State *L, int idx1, int idx2); int (*lua_compare) (lua_State *L, int idx1, int idx2, int op); void (*lua_pushnil) (lua_State *L); void (*lua_pushnumber) (lua_State *L, lua_Number n); void (*lua_pushinteger) (lua_State *L, lua_Integer n); const char * (*lua_pushlstring) (lua_State *L, const char *s, size_t len); const char * (*lua_pushexternalstring) (lua_State *L, const char *s, size_t len, lua_Alloc falloc, void *ud); const char * (*lua_pushstring) (lua_State *L, const char *s); const char * (*lua_pushvfstring) (lua_State *L, const char *fmt, va_list argp); void (*lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); void (*lua_pushboolean) (lua_State *L, int b); void (*lua_pushlightuserdata) (lua_State *L, void *p); int (*lua_pushthread) (lua_State *L); int (*lua_getglobal) (lua_State *L, const char *name); int (*lua_gettable) (lua_State *L, int idx); int (*lua_getfield) (lua_State *L, int idx, const char *k); int (*lua_geti) (lua_State *L, int idx, lua_Integer n); int (*lua_rawget) (lua_State *L, int idx); int (*lua_rawgeti) (lua_State *L, int idx, lua_Integer n); int (*lua_rawgetp) (lua_State *L, int idx, const void *p); void (*lua_createtable) (lua_State *L, int narr, int nrec); void * (*lua_newuserdatauv) (lua_State *L, size_t sz, int nuvalue); int (*lua_getmetatable) (lua_State *L, int objindex); int (*lua_getiuservalue) (lua_State *L, int idx, int n); void (*lua_setglobal) (lua_State *L, const char *name); void (*lua_settable) (lua_State *L, int idx); void (*lua_setfield) (lua_State *L, int idx, const char *k); void (*lua_seti) (lua_State *L, int idx, lua_Integer n); void (*lua_rawset) (lua_State *L, int idx); void (*lua_rawseti) (lua_State *L, int idx, lua_Integer n); void (*lua_rawsetp) (lua_State *L, int idx, const void *p); int (*lua_setmetatable) (lua_State *L, int objindex); int (*lua_setiuservalue) (lua_State *L, int idx, int n); void (*lua_callk) (lua_State *L, int nargs, int nresults, lua_KContext ctx, lua_KFunction k); int (*lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc, lua_KContext ctx, lua_KFunction k); int (*lua_load) (lua_State *L, lua_Reader reader, void *dt, const char *chunkname, const char *mode); int (*lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip); int (*lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k); int (*lua_resume) (lua_State *L, lua_State *from, int narg, int *nres); int (*lua_status) (lua_State *L); int (*lua_isyieldable) (lua_State *L); void (*lua_setwarnf) (lua_State *L, lua_WarnFunction f, void *ud); void (*lua_warning) (lua_State *L, const char *msg, int tocont); int (*lua_error) (lua_State *L); int (*lua_next) (lua_State *L, int idx); void (*lua_concat) (lua_State *L, int n); void (*lua_len) (lua_State *L, int idx); unsigned (*lua_numbertocstring) (lua_State *L, int idx, char *buff); size_t (*lua_stringtonumber) (lua_State *L, const char *s); lua_Alloc (*lua_getallocf) (lua_State *L, void **ud); void (*lua_setallocf) (lua_State *L, lua_Alloc f, void *ud); void (*lua_toclose) (lua_State *L, int idx); void (*lua_closeslot) (lua_State *L, int idx); int (*lua_getstack) (lua_State *L, int level, lua_Debug *ar); int (*lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar); const char * (*lua_getlocal) (lua_State *L, const lua_Debug *ar, int n); const char * (*lua_setlocal) (lua_State *L, const lua_Debug *ar, int n); const char * (*lua_getupvalue) (lua_State *L, int funcindex, int n); const char * (*lua_setupvalue) (lua_State *L, int funcindex, int n); void * (*lua_upvalueid) (lua_State *L, int fidx, int n); void (*lua_upvaluejoin) (lua_State *L, int fidx1, int n1, int fidx2, int n2); void (*lua_sethook) (lua_State *L, lua_Hook func, int mask, int count); lua_Hook (*lua_gethook) (lua_State *L); int (*lua_gethookmask) (lua_State *L); int (*lua_gethookcount) (lua_State *L); void (*luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz); int (*luaL_getmetafield) (lua_State *L, int obj, const char *e); int (*luaL_callmeta) (lua_State *L, int obj, const char *e); const char * (*luaL_tolstring) (lua_State *L, int idx, size_t *len); int (*luaL_argerror) (lua_State *L, int arg, const char *extramsg); int (*luaL_typeerror) (lua_State *L, int arg, const char *tname); const char * (*luaL_checklstring) (lua_State *L, int arg, size_t *l); const char * (*luaL_optlstring) (lua_State *L, int arg, const char *def, size_t *l); lua_Number (*luaL_checknumber) (lua_State *L, int arg); lua_Number (*luaL_optnumber) (lua_State *L, int arg, lua_Number def); lua_Integer (*luaL_checkinteger) (lua_State *L, int arg); lua_Integer (*luaL_optinteger) (lua_State *L, int arg, lua_Integer def); void (*luaL_checkstack) (lua_State *L, int sz, const char *msg); void (*luaL_checktype) (lua_State *L, int arg, int t); void (*luaL_checkany) (lua_State *L, int arg); int (*luaL_newmetatable) (lua_State *L, const char *tname); void (*luaL_setmetatable) (lua_State *L, const char *tname); void * (*luaL_testudata) (lua_State *L, int ud, const char *tname); void * (*luaL_checkudata) (lua_State *L, int ud, const char *tname); void (*luaL_where) (lua_State *L, int lvl); int (*luaL_checkoption) (lua_State *L, int arg, const char *def, const char *const lst[]); int (*luaL_fileresult) (lua_State *L, int stat, const char *fname); int (*luaL_execresult) (lua_State *L, int stat); void * (*luaL_alloc) (void *ud, void *ptr, size_t osize, size_t nsize); int (*luaL_ref) (lua_State *L, int t); void (*luaL_unref) (lua_State *L, int t, int ref); int (*luaL_loadfilex) (lua_State *L, const char *filename, const char *mode); int (*luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz, const char *name, const char *mode); int (*luaL_loadstring) (lua_State *L, const char *s); lua_State * (*luaL_newstate) (void); unsigned (*luaL_makeseed) (lua_State *L); lua_Integer (*luaL_len) (lua_State *L, int idx); void (*luaL_addgsub) (luaL_Buffer *b, const char *s, const char *p, const char *r); const char * (*luaL_gsub) (lua_State *L, const char *s, const char *p, const char *r); void (*luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup); int (*luaL_getsubtable) (lua_State *L, int idx, const char *fname); void (*luaL_traceback) (lua_State *L, lua_State *L1, const char *msg, int level); void (*luaL_requiref) (lua_State *L, const char *modname, lua_CFunction openf, int glb); void (*luaL_buffinit) (lua_State *L, luaL_Buffer *B); char * (*luaL_prepbuffsize) (luaL_Buffer *B, size_t sz); void (*luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); void (*luaL_addstring) (luaL_Buffer *B, const char *s); void (*luaL_addvalue) (luaL_Buffer *B); void (*luaL_pushresult) (luaL_Buffer *B); void (*luaL_pushresultsize) (luaL_Buffer *B, size_t sz); char * (*luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz); int (*lua_gc) (lua_State *L, int what, ...); }; static struct lua_api API; LUA_API lua_State * lua_newstate(lua_Alloc f, void *ud, unsigned seed) { return API.lua_newstate(f,ud,seed); } LUA_API void lua_close(lua_State *L) { API.lua_close(L); } LUA_API lua_State * lua_newthread(lua_State *L) { return API.lua_newthread(L); } LUA_API int lua_closethread(lua_State *L, lua_State *from) { return API.lua_closethread(L,from); } LUA_API lua_CFunction lua_atpanic(lua_State *L, lua_CFunction panicf) { return API.lua_atpanic(L,panicf); } LUA_API lua_Number lua_version(lua_State *L) { return API.lua_version(L); } LUA_API int lua_absindex(lua_State *L, int idx) { return API.lua_absindex(L,idx); } LUA_API int lua_gettop(lua_State *L) { return API.lua_gettop(L); } LUA_API void lua_settop(lua_State *L, int idx) { API.lua_settop(L,idx); } LUA_API void lua_pushvalue(lua_State *L, int idx) { API.lua_pushvalue(L,idx); } LUA_API void lua_rotate(lua_State *L, int idx, int n) { API.lua_rotate(L,idx,n); } LUA_API void lua_copy(lua_State *L, int fromidx, int toidx) { API.lua_copy(L,fromidx,toidx); } LUA_API int lua_checkstack(lua_State *L, int n) { return API.lua_checkstack(L,n); } LUA_API void lua_xmove(lua_State *from, lua_State *to, int n) { API.lua_xmove(from,to,n); } LUA_API int lua_isnumber(lua_State *L, int idx) { return API.lua_isnumber(L,idx); } LUA_API int lua_isstring(lua_State *L, int idx) { return API.lua_isstring(L,idx); } LUA_API int lua_iscfunction(lua_State *L, int idx) { return API.lua_iscfunction(L,idx); } LUA_API int lua_isinteger(lua_State *L, int idx) { return API.lua_isinteger(L,idx); } LUA_API int lua_isuserdata(lua_State *L, int idx) { return API.lua_isuserdata(L,idx); } LUA_API int lua_type(lua_State *L, int idx) { return API.lua_type(L,idx); } LUA_API const char * lua_typename(lua_State *L, int tp) { return API.lua_typename(L,tp); } LUA_API lua_Number lua_tonumberx(lua_State *L, int idx, int *isnum) { return API.lua_tonumberx(L,idx,isnum); } LUA_API lua_Integer lua_tointegerx(lua_State *L, int idx, int *isnum) { return API.lua_tointegerx(L,idx,isnum); } LUA_API int lua_toboolean(lua_State *L, int idx) { return API.lua_toboolean(L,idx); } LUA_API const char * lua_tolstring(lua_State *L, int idx, size_t *len) { return API.lua_tolstring(L,idx,len); } LUA_API lua_Unsigned lua_rawlen(lua_State *L, int idx) { return API.lua_rawlen(L,idx); } LUA_API lua_CFunction lua_tocfunction(lua_State *L, int idx) { return API.lua_tocfunction(L,idx); } LUA_API void * lua_touserdata(lua_State *L, int idx) { return API.lua_touserdata(L,idx); } LUA_API lua_State * lua_tothread(lua_State *L, int idx) { return API.lua_tothread(L,idx); } LUA_API const void * lua_topointer(lua_State *L, int idx) { return API.lua_topointer(L,idx); } LUA_API void lua_arith(lua_State *L, int op) { API.lua_arith(L,op); } LUA_API int lua_rawequal(lua_State *L, int idx1, int idx2) { return API.lua_rawequal(L,idx1,idx2); } LUA_API int lua_compare(lua_State *L, int idx1, int idx2, int op) { return API.lua_compare(L,idx1,idx2,op); } LUA_API void lua_pushnil(lua_State *L) { API.lua_pushnil(L); } LUA_API void lua_pushnumber(lua_State *L, lua_Number n) { API.lua_pushnumber(L,n); } LUA_API void lua_pushinteger(lua_State *L, lua_Integer n) { API.lua_pushinteger(L,n); } LUA_API const char * lua_pushlstring(lua_State *L, const char *s, size_t len) { return API.lua_pushlstring(L,s,len); } LUA_API const char * lua_pushexternalstring(lua_State *L, const char *s, size_t len, lua_Alloc falloc, void *ud) { return API.lua_pushexternalstring(L,s,len,falloc,ud); } LUA_API const char * lua_pushstring(lua_State *L, const char *s) { return API.lua_pushstring(L,s); } LUA_API const char * lua_pushvfstring(lua_State *L, const char *fmt, va_list argp) { return API.lua_pushvfstring(L,fmt,argp); } LUA_API void lua_pushcclosure(lua_State *L, lua_CFunction fn, int n) { API.lua_pushcclosure(L,fn,n); } LUA_API void lua_pushboolean(lua_State *L, int b) { API.lua_pushboolean(L,b); } LUA_API void lua_pushlightuserdata(lua_State *L, void *p) { API.lua_pushlightuserdata(L,p); } LUA_API int lua_pushthread(lua_State *L) { return API.lua_pushthread(L); } LUA_API int lua_getglobal(lua_State *L, const char *name) { return API.lua_getglobal(L,name); } LUA_API int lua_gettable(lua_State *L, int idx) { return API.lua_gettable(L,idx); } LUA_API int lua_getfield(lua_State *L, int idx, const char *k) { return API.lua_getfield(L,idx,k); } LUA_API int lua_geti(lua_State *L, int idx, lua_Integer n) { return API.lua_geti(L,idx,n); } LUA_API int lua_rawget(lua_State *L, int idx) { return API.lua_rawget(L,idx); } LUA_API int lua_rawgeti(lua_State *L, int idx, lua_Integer n) { return API.lua_rawgeti(L,idx,n); } LUA_API int lua_rawgetp(lua_State *L, int idx, const void *p) { return API.lua_rawgetp(L,idx,p); } LUA_API void lua_createtable(lua_State *L, int narr, int nrec) { API.lua_createtable(L,narr,nrec); } LUA_API void * lua_newuserdatauv(lua_State *L, size_t sz, int nuvalue) { return API.lua_newuserdatauv(L,sz,nuvalue); } LUA_API int lua_getmetatable(lua_State *L, int objindex) { return API.lua_getmetatable(L,objindex); } LUA_API int lua_getiuservalue(lua_State *L, int idx, int n) { return API.lua_getiuservalue(L,idx,n); } LUA_API void lua_setglobal(lua_State *L, const char *name) { API.lua_setglobal(L,name); } LUA_API void lua_settable(lua_State *L, int idx) { API.lua_settable(L,idx); } LUA_API void lua_setfield(lua_State *L, int idx, const char *k) { API.lua_setfield(L,idx,k); } LUA_API void lua_seti(lua_State *L, int idx, lua_Integer n) { API.lua_seti(L,idx,n); } LUA_API void lua_rawset(lua_State *L, int idx) { API.lua_rawset(L,idx); } LUA_API void lua_rawseti(lua_State *L, int idx, lua_Integer n) { API.lua_rawseti(L,idx,n); } LUA_API void lua_rawsetp(lua_State *L, int idx, const void *p) { API.lua_rawsetp(L,idx,p); } LUA_API int lua_setmetatable(lua_State *L, int objindex) { return API.lua_setmetatable(L,objindex); } LUA_API int lua_setiuservalue(lua_State *L, int idx, int n) { return API.lua_setiuservalue(L,idx,n); } LUA_API void lua_callk(lua_State *L, int nargs, int nresults, lua_KContext ctx, lua_KFunction k) { API.lua_callk(L,nargs,nresults,ctx,k); } LUA_API int lua_pcallk(lua_State *L, int nargs, int nresults, int errfunc, lua_KContext ctx, lua_KFunction k) { return API.lua_pcallk(L,nargs,nresults,errfunc,ctx,k); } LUA_API int lua_load(lua_State *L, lua_Reader reader, void *dt, const char *chunkname, const char *mode) { return API.lua_load(L,reader,dt,chunkname,mode); } LUA_API int lua_dump(lua_State *L, lua_Writer writer, void *data, int strip) { return API.lua_dump(L,writer,data,strip); } LUA_API int lua_yieldk(lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k) { return API.lua_yieldk(L,nresults,ctx,k); } LUA_API int lua_resume(lua_State *L, lua_State *from, int narg, int *nres) { return API.lua_resume(L,from,narg,nres); } LUA_API int lua_status(lua_State *L) { return API.lua_status(L); } LUA_API int lua_isyieldable(lua_State *L) { return API.lua_isyieldable(L); } LUA_API void lua_setwarnf(lua_State *L, lua_WarnFunction f, void *ud) { API.lua_setwarnf(L,f,ud); } LUA_API void lua_warning(lua_State *L, const char *msg, int tocont) { API.lua_warning(L,msg,tocont); } LUA_API int lua_error(lua_State *L) { return API.lua_error(L); } LUA_API int lua_next(lua_State *L, int idx) { return API.lua_next(L,idx); } LUA_API void lua_concat(lua_State *L, int n) { API.lua_concat(L,n); } LUA_API void lua_len(lua_State *L, int idx) { API.lua_len(L,idx); } LUA_API unsigned lua_numbertocstring(lua_State *L, int idx, char *buff) { return API.lua_numbertocstring(L,idx,buff); } LUA_API size_t lua_stringtonumber(lua_State *L, const char *s) { return API.lua_stringtonumber(L,s); } LUA_API lua_Alloc lua_getallocf(lua_State *L, void **ud) { return API.lua_getallocf(L,ud); } LUA_API void lua_setallocf(lua_State *L, lua_Alloc f, void *ud) { API.lua_setallocf(L,f,ud); } LUA_API void lua_toclose(lua_State *L, int idx) { API.lua_toclose(L,idx); } LUA_API void lua_closeslot(lua_State *L, int idx) { API.lua_closeslot(L,idx); } LUA_API int lua_getstack(lua_State *L, int level, lua_Debug *ar) { return API.lua_getstack(L,level,ar); } LUA_API int lua_getinfo(lua_State *L, const char *what, lua_Debug *ar) { return API.lua_getinfo(L,what,ar); } LUA_API const char * lua_getlocal(lua_State *L, const lua_Debug *ar, int n) { return API.lua_getlocal(L,ar,n); } LUA_API const char * lua_setlocal(lua_State *L, const lua_Debug *ar, int n) { return API.lua_setlocal(L,ar,n); } LUA_API const char * lua_getupvalue(lua_State *L, int funcindex, int n) { return API.lua_getupvalue(L,funcindex,n); } LUA_API const char * lua_setupvalue(lua_State *L, int funcindex, int n) { return API.lua_setupvalue(L,funcindex,n); } LUA_API void * lua_upvalueid(lua_State *L, int fidx, int n) { return API.lua_upvalueid(L,fidx,n); } LUA_API void lua_upvaluejoin(lua_State *L, int fidx1, int n1, int fidx2, int n2) { API.lua_upvaluejoin(L,fidx1,n1,fidx2,n2); } LUA_API void lua_sethook(lua_State *L, lua_Hook func, int mask, int count) { API.lua_sethook(L,func,mask,count); } LUA_API lua_Hook lua_gethook(lua_State *L) { return API.lua_gethook(L); } LUA_API int lua_gethookmask(lua_State *L) { return API.lua_gethookmask(L); } LUA_API int lua_gethookcount(lua_State *L) { return API.lua_gethookcount(L); } LUA_API void luaL_checkversion_(lua_State *L, lua_Number ver, size_t sz) { API.luaL_checkversion_(L,ver,sz); } LUA_API int luaL_getmetafield(lua_State *L, int obj, const char *e) { return API.luaL_getmetafield(L,obj,e); } LUA_API int luaL_callmeta(lua_State *L, int obj, const char *e) { return API.luaL_callmeta(L,obj,e); } LUA_API const char * luaL_tolstring(lua_State *L, int idx, size_t *len) { return API.luaL_tolstring(L,idx,len); } LUA_API int luaL_argerror(lua_State *L, int arg, const char *extramsg) { return API.luaL_argerror(L,arg,extramsg); } LUA_API int luaL_typeerror(lua_State *L, int arg, const char *tname) { return API.luaL_typeerror(L,arg,tname); } LUA_API const char * luaL_checklstring(lua_State *L, int arg, size_t *l) { return API.luaL_checklstring(L,arg,l); } LUA_API const char * luaL_optlstring(lua_State *L, int arg, const char *def, size_t *l) { return API.luaL_optlstring(L,arg,def,l); } LUA_API lua_Number luaL_checknumber(lua_State *L, int arg) { return API.luaL_checknumber(L,arg); } LUA_API lua_Number luaL_optnumber(lua_State *L, int arg, lua_Number def) { return API.luaL_optnumber(L,arg,def); } LUA_API lua_Integer luaL_checkinteger(lua_State *L, int arg) { return API.luaL_checkinteger(L,arg); } LUA_API lua_Integer luaL_optinteger(lua_State *L, int arg, lua_Integer def) { return API.luaL_optinteger(L,arg,def); } LUA_API void luaL_checkstack(lua_State *L, int sz, const char *msg) { API.luaL_checkstack(L,sz,msg); } LUA_API void luaL_checktype(lua_State *L, int arg, int t) { API.luaL_checktype(L,arg,t); } LUA_API void luaL_checkany(lua_State *L, int arg) { API.luaL_checkany(L,arg); } LUA_API int luaL_newmetatable(lua_State *L, const char *tname) { return API.luaL_newmetatable(L,tname); } LUA_API void luaL_setmetatable(lua_State *L, const char *tname) { API.luaL_setmetatable(L,tname); } LUA_API void * luaL_testudata(lua_State *L, int ud, const char *tname) { return API.luaL_testudata(L,ud,tname); } LUA_API void * luaL_checkudata(lua_State *L, int ud, const char *tname) { return API.luaL_checkudata(L,ud,tname); } LUA_API void luaL_where(lua_State *L, int lvl) { API.luaL_where(L,lvl); } LUA_API int luaL_checkoption(lua_State *L, int arg, const char *def, const char *const lst[]) { return API.luaL_checkoption(L,arg,def,lst); } LUA_API int luaL_fileresult(lua_State *L, int stat, const char *fname) { return API.luaL_fileresult(L,stat,fname); } LUA_API int luaL_execresult(lua_State *L, int stat) { return API.luaL_execresult(L,stat); } LUA_API void * luaL_alloc(void *ud, void *ptr, size_t osize, size_t nsize) { return API.luaL_alloc(ud,ptr,osize,nsize); } LUA_API int luaL_ref(lua_State *L, int t) { return API.luaL_ref(L,t); } LUA_API void luaL_unref(lua_State *L, int t, int ref) { API.luaL_unref(L,t,ref); } LUA_API int luaL_loadfilex(lua_State *L, const char *filename, const char *mode) { return API.luaL_loadfilex(L,filename,mode); } LUA_API int luaL_loadbufferx(lua_State *L, const char *buff, size_t sz, const char *name, const char *mode) { return API.luaL_loadbufferx(L,buff,sz,name,mode); } LUA_API int luaL_loadstring(lua_State *L, const char *s) { return API.luaL_loadstring(L,s); } LUA_API lua_State * luaL_newstate(void) { return API.luaL_newstate(); } LUA_API unsigned luaL_makeseed(lua_State *L) { return API.luaL_makeseed(L); } LUA_API lua_Integer luaL_len(lua_State *L, int idx) { return API.luaL_len(L,idx); } LUA_API void luaL_addgsub(luaL_Buffer *b, const char *s, const char *p, const char *r) { API.luaL_addgsub(b,s,p,r); } LUA_API const char * luaL_gsub(lua_State *L, const char *s, const char *p, const char *r) { return API.luaL_gsub(L,s,p,r); } LUA_API void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup) { API.luaL_setfuncs(L,l,nup); } LUA_API int luaL_getsubtable(lua_State *L, int idx, const char *fname) { return API.luaL_getsubtable(L,idx,fname); } LUA_API void luaL_traceback(lua_State *L, lua_State *L1, const char *msg, int level) { API.luaL_traceback(L,L1,msg,level); } LUA_API void luaL_requiref(lua_State *L, const char *modname, lua_CFunction openf, int glb) { API.luaL_requiref(L,modname,openf,glb); } LUA_API void luaL_buffinit(lua_State *L, luaL_Buffer *B) { API.luaL_buffinit(L,B); } LUA_API char * luaL_prepbuffsize(luaL_Buffer *B, size_t sz) { return API.luaL_prepbuffsize(B,sz); } LUA_API void luaL_addlstring(luaL_Buffer *B, const char *s, size_t l) { API.luaL_addlstring(B,s,l); } LUA_API void luaL_addstring(luaL_Buffer *B, const char *s) { API.luaL_addstring(B,s); } LUA_API void luaL_addvalue(luaL_Buffer *B) { API.luaL_addvalue(B); } LUA_API void luaL_pushresult(luaL_Buffer *B) { API.luaL_pushresult(B); } LUA_API void luaL_pushresultsize(luaL_Buffer *B, size_t sz) { API.luaL_pushresultsize(B,sz); } LUA_API char * luaL_buffinitsize(lua_State *L, luaL_Buffer *B, size_t sz) { return API.luaL_buffinitsize(L,B,sz); } LUA_API const char *lua_pushfstring (lua_State *L, const char *fmt, ...) { const char *ret; va_list argp; va_start(argp, fmt); ret = API.lua_pushvfstring(L, fmt, argp); va_end(argp); return ret; } LUA_API int lua_gc (lua_State *L, int what, ...) { va_list argp; va_start(argp, what); int p1 = va_arg(argp, int); int p2 = va_arg(argp, int); int p3 = va_arg(argp, int); va_end(argp); return API.lua_gc(L, what, p1, p2, p3); } LUA_API int luaL_error(lua_State *L, const char *fmt, ...) { va_list argp; va_start(argp, fmt); luaL_where(L, 1); lua_pushvfstring(L, fmt, argp); va_end(argp); lua_concat(L, 2); return lua_error(L); } static void stub_luaL_checkversion_ (lua_State *L, lua_Number ver, size_t sz) { } static void stub_lua_createtable (lua_State *L, int narr, int nrec) { } static void stub_luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { } struct sokol_api; struct soluna_api; struct extlua_apis { struct lua_api * lua; struct sokol_api * sokol; struct soluna_api * soluna; }; LUA_API void luaapi_init(lua_State *L) { struct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L); struct lua_api * api = apis->lua; if (api->version == LUA_VERSION_NUM) { API = *api; return; } // stub for luaL_newlib API.luaL_checkversion_ = stub_luaL_checkversion_; API.lua_createtable = stub_lua_createtable; API.luaL_setfuncs = stub_luaL_setfuncs; } ================================================ FILE: extlua/extlua.temp.c ================================================ #include #include #include #include #include struct lua_api { int version; $API_DECL$ }; static struct lua_api API; $API_IMPL$ LUA_API const char *lua_pushfstring (lua_State *L, const char *fmt, ...) { const char *ret; va_list argp; va_start(argp, fmt); ret = API.lua_pushvfstring(L, fmt, argp); va_end(argp); return ret; } LUA_API int lua_gc (lua_State *L, int what, ...) { va_list argp; va_start(argp, what); int p1 = va_arg(argp, int); int p2 = va_arg(argp, int); int p3 = va_arg(argp, int); va_end(argp); return API.lua_gc(L, what, p1, p2, p3); } LUA_API int luaL_error(lua_State *L, const char *fmt, ...) { va_list argp; va_start(argp, fmt); luaL_where(L, 1); lua_pushvfstring(L, fmt, argp); va_end(argp); lua_concat(L, 2); return lua_error(L); } static void stub_luaL_checkversion_ (lua_State *L, lua_Number ver, size_t sz) { } static void stub_lua_createtable (lua_State *L, int narr, int nrec) { } static void stub_luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { } struct sokol_api; struct soluna_api; struct extlua_apis { struct lua_api * lua; struct sokol_api * sokol; struct soluna_api * soluna; }; LUA_API void luaapi_init(lua_State *L) { struct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L); struct lua_api * api = apis->lua; if (api->version == LUA_VERSION_NUM) { API = *api; return; } // stub for luaL_newlib API.luaL_checkversion_ = stub_luaL_checkversion_; API.lua_createtable = stub_lua_createtable; API.luaL_setfuncs = stub_luaL_setfuncs; } ================================================ FILE: extlua/extlua_impl.c ================================================ // AUTO GENERATED by extlua_impl.temp.c, DONT EDIT #include #include struct lua_api { int version; lua_State * (*lua_newstate) (lua_Alloc f, void *ud, unsigned seed); void (*lua_close) (lua_State *L); lua_State * (*lua_newthread) (lua_State *L); int (*lua_closethread) (lua_State *L, lua_State *from); lua_CFunction (*lua_atpanic) (lua_State *L, lua_CFunction panicf); lua_Number (*lua_version) (lua_State *L); int (*lua_absindex) (lua_State *L, int idx); int (*lua_gettop) (lua_State *L); void (*lua_settop) (lua_State *L, int idx); void (*lua_pushvalue) (lua_State *L, int idx); void (*lua_rotate) (lua_State *L, int idx, int n); void (*lua_copy) (lua_State *L, int fromidx, int toidx); int (*lua_checkstack) (lua_State *L, int n); void (*lua_xmove) (lua_State *from, lua_State *to, int n); int (*lua_isnumber) (lua_State *L, int idx); int (*lua_isstring) (lua_State *L, int idx); int (*lua_iscfunction) (lua_State *L, int idx); int (*lua_isinteger) (lua_State *L, int idx); int (*lua_isuserdata) (lua_State *L, int idx); int (*lua_type) (lua_State *L, int idx); const char * (*lua_typename) (lua_State *L, int tp); lua_Number (*lua_tonumberx) (lua_State *L, int idx, int *isnum); lua_Integer (*lua_tointegerx) (lua_State *L, int idx, int *isnum); int (*lua_toboolean) (lua_State *L, int idx); const char * (*lua_tolstring) (lua_State *L, int idx, size_t *len); lua_Unsigned (*lua_rawlen) (lua_State *L, int idx); lua_CFunction (*lua_tocfunction) (lua_State *L, int idx); void * (*lua_touserdata) (lua_State *L, int idx); lua_State * (*lua_tothread) (lua_State *L, int idx); const void * (*lua_topointer) (lua_State *L, int idx); void (*lua_arith) (lua_State *L, int op); int (*lua_rawequal) (lua_State *L, int idx1, int idx2); int (*lua_compare) (lua_State *L, int idx1, int idx2, int op); void (*lua_pushnil) (lua_State *L); void (*lua_pushnumber) (lua_State *L, lua_Number n); void (*lua_pushinteger) (lua_State *L, lua_Integer n); const char * (*lua_pushlstring) (lua_State *L, const char *s, size_t len); const char * (*lua_pushexternalstring) (lua_State *L, const char *s, size_t len, lua_Alloc falloc, void *ud); const char * (*lua_pushstring) (lua_State *L, const char *s); const char * (*lua_pushvfstring) (lua_State *L, const char *fmt, va_list argp); void (*lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); void (*lua_pushboolean) (lua_State *L, int b); void (*lua_pushlightuserdata) (lua_State *L, void *p); int (*lua_pushthread) (lua_State *L); int (*lua_getglobal) (lua_State *L, const char *name); int (*lua_gettable) (lua_State *L, int idx); int (*lua_getfield) (lua_State *L, int idx, const char *k); int (*lua_geti) (lua_State *L, int idx, lua_Integer n); int (*lua_rawget) (lua_State *L, int idx); int (*lua_rawgeti) (lua_State *L, int idx, lua_Integer n); int (*lua_rawgetp) (lua_State *L, int idx, const void *p); void (*lua_createtable) (lua_State *L, int narr, int nrec); void * (*lua_newuserdatauv) (lua_State *L, size_t sz, int nuvalue); int (*lua_getmetatable) (lua_State *L, int objindex); int (*lua_getiuservalue) (lua_State *L, int idx, int n); void (*lua_setglobal) (lua_State *L, const char *name); void (*lua_settable) (lua_State *L, int idx); void (*lua_setfield) (lua_State *L, int idx, const char *k); void (*lua_seti) (lua_State *L, int idx, lua_Integer n); void (*lua_rawset) (lua_State *L, int idx); void (*lua_rawseti) (lua_State *L, int idx, lua_Integer n); void (*lua_rawsetp) (lua_State *L, int idx, const void *p); int (*lua_setmetatable) (lua_State *L, int objindex); int (*lua_setiuservalue) (lua_State *L, int idx, int n); void (*lua_callk) (lua_State *L, int nargs, int nresults, lua_KContext ctx, lua_KFunction k); int (*lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc, lua_KContext ctx, lua_KFunction k); int (*lua_load) (lua_State *L, lua_Reader reader, void *dt, const char *chunkname, const char *mode); int (*lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip); int (*lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k); int (*lua_resume) (lua_State *L, lua_State *from, int narg, int *nres); int (*lua_status) (lua_State *L); int (*lua_isyieldable) (lua_State *L); void (*lua_setwarnf) (lua_State *L, lua_WarnFunction f, void *ud); void (*lua_warning) (lua_State *L, const char *msg, int tocont); int (*lua_error) (lua_State *L); int (*lua_next) (lua_State *L, int idx); void (*lua_concat) (lua_State *L, int n); void (*lua_len) (lua_State *L, int idx); unsigned (*lua_numbertocstring) (lua_State *L, int idx, char *buff); size_t (*lua_stringtonumber) (lua_State *L, const char *s); lua_Alloc (*lua_getallocf) (lua_State *L, void **ud); void (*lua_setallocf) (lua_State *L, lua_Alloc f, void *ud); void (*lua_toclose) (lua_State *L, int idx); void (*lua_closeslot) (lua_State *L, int idx); int (*lua_getstack) (lua_State *L, int level, lua_Debug *ar); int (*lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar); const char * (*lua_getlocal) (lua_State *L, const lua_Debug *ar, int n); const char * (*lua_setlocal) (lua_State *L, const lua_Debug *ar, int n); const char * (*lua_getupvalue) (lua_State *L, int funcindex, int n); const char * (*lua_setupvalue) (lua_State *L, int funcindex, int n); void * (*lua_upvalueid) (lua_State *L, int fidx, int n); void (*lua_upvaluejoin) (lua_State *L, int fidx1, int n1, int fidx2, int n2); void (*lua_sethook) (lua_State *L, lua_Hook func, int mask, int count); lua_Hook (*lua_gethook) (lua_State *L); int (*lua_gethookmask) (lua_State *L); int (*lua_gethookcount) (lua_State *L); void (*luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz); int (*luaL_getmetafield) (lua_State *L, int obj, const char *e); int (*luaL_callmeta) (lua_State *L, int obj, const char *e); const char * (*luaL_tolstring) (lua_State *L, int idx, size_t *len); int (*luaL_argerror) (lua_State *L, int arg, const char *extramsg); int (*luaL_typeerror) (lua_State *L, int arg, const char *tname); const char * (*luaL_checklstring) (lua_State *L, int arg, size_t *l); const char * (*luaL_optlstring) (lua_State *L, int arg, const char *def, size_t *l); lua_Number (*luaL_checknumber) (lua_State *L, int arg); lua_Number (*luaL_optnumber) (lua_State *L, int arg, lua_Number def); lua_Integer (*luaL_checkinteger) (lua_State *L, int arg); lua_Integer (*luaL_optinteger) (lua_State *L, int arg, lua_Integer def); void (*luaL_checkstack) (lua_State *L, int sz, const char *msg); void (*luaL_checktype) (lua_State *L, int arg, int t); void (*luaL_checkany) (lua_State *L, int arg); int (*luaL_newmetatable) (lua_State *L, const char *tname); void (*luaL_setmetatable) (lua_State *L, const char *tname); void * (*luaL_testudata) (lua_State *L, int ud, const char *tname); void * (*luaL_checkudata) (lua_State *L, int ud, const char *tname); void (*luaL_where) (lua_State *L, int lvl); int (*luaL_checkoption) (lua_State *L, int arg, const char *def, const char *const lst[]); int (*luaL_fileresult) (lua_State *L, int stat, const char *fname); int (*luaL_execresult) (lua_State *L, int stat); void * (*luaL_alloc) (void *ud, void *ptr, size_t osize, size_t nsize); int (*luaL_ref) (lua_State *L, int t); void (*luaL_unref) (lua_State *L, int t, int ref); int (*luaL_loadfilex) (lua_State *L, const char *filename, const char *mode); int (*luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz, const char *name, const char *mode); int (*luaL_loadstring) (lua_State *L, const char *s); lua_State * (*luaL_newstate) (void); unsigned (*luaL_makeseed) (lua_State *L); lua_Integer (*luaL_len) (lua_State *L, int idx); void (*luaL_addgsub) (luaL_Buffer *b, const char *s, const char *p, const char *r); const char * (*luaL_gsub) (lua_State *L, const char *s, const char *p, const char *r); void (*luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup); int (*luaL_getsubtable) (lua_State *L, int idx, const char *fname); void (*luaL_traceback) (lua_State *L, lua_State *L1, const char *msg, int level); void (*luaL_requiref) (lua_State *L, const char *modname, lua_CFunction openf, int glb); void (*luaL_buffinit) (lua_State *L, luaL_Buffer *B); char * (*luaL_prepbuffsize) (luaL_Buffer *B, size_t sz); void (*luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); void (*luaL_addstring) (luaL_Buffer *B, const char *s); void (*luaL_addvalue) (luaL_Buffer *B); void (*luaL_pushresult) (luaL_Buffer *B); void (*luaL_pushresultsize) (luaL_Buffer *B, size_t sz); char * (*luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz); int (*lua_gc) (lua_State *L, int what, ...); }; struct lua_api * extlua_api() { static struct lua_api api = { LUA_VERSION_NUM, lua_newstate, lua_close, lua_newthread, lua_closethread, lua_atpanic, lua_version, lua_absindex, lua_gettop, lua_settop, lua_pushvalue, lua_rotate, lua_copy, lua_checkstack, lua_xmove, lua_isnumber, lua_isstring, lua_iscfunction, lua_isinteger, lua_isuserdata, lua_type, lua_typename, lua_tonumberx, lua_tointegerx, lua_toboolean, lua_tolstring, lua_rawlen, lua_tocfunction, lua_touserdata, lua_tothread, lua_topointer, lua_arith, lua_rawequal, lua_compare, lua_pushnil, lua_pushnumber, lua_pushinteger, lua_pushlstring, lua_pushexternalstring, lua_pushstring, lua_pushvfstring, lua_pushcclosure, lua_pushboolean, lua_pushlightuserdata, lua_pushthread, lua_getglobal, lua_gettable, lua_getfield, lua_geti, lua_rawget, lua_rawgeti, lua_rawgetp, lua_createtable, lua_newuserdatauv, lua_getmetatable, lua_getiuservalue, lua_setglobal, lua_settable, lua_setfield, lua_seti, lua_rawset, lua_rawseti, lua_rawsetp, lua_setmetatable, lua_setiuservalue, lua_callk, lua_pcallk, lua_load, lua_dump, lua_yieldk, lua_resume, lua_status, lua_isyieldable, lua_setwarnf, lua_warning, lua_error, lua_next, lua_concat, lua_len, lua_numbertocstring, lua_stringtonumber, lua_getallocf, lua_setallocf, lua_toclose, lua_closeslot, lua_getstack, lua_getinfo, lua_getlocal, lua_setlocal, lua_getupvalue, lua_setupvalue, lua_upvalueid, lua_upvaluejoin, lua_sethook, lua_gethook, lua_gethookmask, lua_gethookcount, luaL_checkversion_, luaL_getmetafield, luaL_callmeta, luaL_tolstring, luaL_argerror, luaL_typeerror, luaL_checklstring, luaL_optlstring, luaL_checknumber, luaL_optnumber, luaL_checkinteger, luaL_optinteger, luaL_checkstack, luaL_checktype, luaL_checkany, luaL_newmetatable, luaL_setmetatable, luaL_testudata, luaL_checkudata, luaL_where, luaL_checkoption, luaL_fileresult, luaL_execresult, luaL_alloc, luaL_ref, luaL_unref, luaL_loadfilex, luaL_loadbufferx, luaL_loadstring, luaL_newstate, luaL_makeseed, luaL_len, luaL_addgsub, luaL_gsub, luaL_setfuncs, luaL_getsubtable, luaL_traceback, luaL_requiref, luaL_buffinit, luaL_prepbuffsize, luaL_addlstring, luaL_addstring, luaL_addvalue, luaL_pushresult, luaL_pushresultsize, luaL_buffinitsize, lua_gc, }; return &api; } ================================================ FILE: extlua/extlua_impl.temp.c ================================================ #include #include struct lua_api { int version; $API_DECL$ }; struct lua_api * extlua_api() { static struct lua_api api = { LUA_VERSION_NUM, $API_STRUCT$ }; return &api; } ================================================ FILE: extlua/extlua_sample.c ================================================ #include #include #include #include #include "sokol/sokol_gfx.h" #include "perspective_quad.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 #define PQUAD_CORNER_N 4 #define PQUAD_INFO_CORNER_MASK 0x3u #define PQUAD_INFO_USE_SPRITE_RECT 0x4u #define PQUAD_EPSILON 0.000001f struct color { unsigned char channel[4]; }; struct pquad_payload { uint32_t info; float q; struct color color; }; struct pquad_inst { float pos_h0[3]; float pos_h1[3]; float pos_h2[3]; float uv_rect[4]; float q[4]; struct color color; }; struct material_perspective_quad { sg_pipeline pip; sg_buffer inst; struct soluna_render_bindings bind; int base; vs_params_t *uniform; struct soluna_sprite_bank bank; void *tmp_ptr; size_t tmp_size; }; struct sprite_rect_basis { float scale_x; float scale_y; float shear_x; float shear_y; float tx; float ty; }; static int material_id = 0; static void * free_material_stream(void *ud, void *ptr, size_t osize, size_t nsize) { (void)ud; (void)osize; if (nsize == 0) { soluna_material_stream_free(ptr); } return NULL; } static sg_pipeline make_pipeline(sg_pipeline_desc *desc) { sg_shader shd = sg_make_shader(perspective_quad_shader_desc(sg_query_backend())); desc->shader = shd; desc->primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP; desc->label = "extlua-perspective-quad-pipeline"; desc->layout.buffers[0].step_func = SG_VERTEXSTEP_PER_INSTANCE; desc->colors[0].blend = (sg_blend_state) { .enabled = true, .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA, .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, .src_factor_alpha = SG_BLENDFACTOR_ONE, .dst_factor_alpha = SG_BLENDFACTOR_ZERO, }; return sg_make_pipeline(desc); } static void set_position_homography(const float pos[PQUAD_CORNER_N][2], struct pquad_inst *inst) { const float x0 = pos[0][0], y0 = pos[0][1]; const float x1 = pos[1][0], y1 = pos[1][1]; const float x2 = pos[2][0], y2 = pos[2][1]; const float x3 = pos[3][0], y3 = pos[3][1]; const float sx = x0 - x1 + x3 - x2; const float sy = y0 - y1 + y3 - y2; const float dx1 = x1 - x3; const float dx2 = x2 - x3; const float dy1 = y1 - y3; const float dy2 = y2 - y3; const float det = dx1 * dy2 - dx2 * dy1; float m31 = 0.0f; float m32 = 0.0f; float abs_det = det < 0.0f ? -det : det; if (abs_det > PQUAD_EPSILON) { m31 = (sx * dy2 - sy * dx2) / det; m32 = (sy * dx1 - sx * dy1) / det; } const float m11 = x1 - x0 + m31 * x1; const float m12 = x2 - x0 + m32 * x2; const float m13 = x0; const float m21 = y1 - y0 + m31 * y1; const float m22 = y2 - y0 + m32 * y2; const float m23 = y0; inst->pos_h0[0] = m11; inst->pos_h0[1] = m21; inst->pos_h0[2] = m31; inst->pos_h1[0] = m12; inst->pos_h1[1] = m22; inst->pos_h1[2] = m32; inst->pos_h2[0] = m13; inst->pos_h2[1] = m23; inst->pos_h2[2] = 1.0f; } static inline soluna_material_error perspective_quad_count(int prim_n, int *out) { if (prim_n % PQUAD_CORNER_N != 0) { return "Invalid perspective quad primitive count"; } *out = prim_n / PQUAD_CORNER_N; return NULL; } static inline void decode_sprite_rect_basis(struct sprite_rect_basis *basis, uint32_t corner, const struct soluna_material_stream_data *item) { float x = item->x; float y = item->y; switch (corner) { case 0: basis->scale_x = x; basis->scale_y = y; break; case 1: basis->shear_x = x; basis->shear_y = y; break; case 2: basis->tx = x; basis->ty = y; break; } } static inline void build_quad_from_rect(const struct sprite_rect_basis *basis, const struct soluna_sprite_rect *rect, float pos[PQUAD_CORNER_N][2]) { float scale_x = basis->scale_x - basis->tx; float scale_y = basis->scale_y - basis->ty; float shear_x = basis->shear_x - basis->tx; float shear_y = basis->shear_y - basis->ty; int corner; for (corner=0; cornerw - rect->ox) : -rect->ox; float y = (corner >> 1) ? (rect->h - rect->oy) : -rect->oy; pos[corner][0] = x * scale_x + y * shear_x + basis->tx; pos[corner][1] = x * shear_y + y * scale_y + basis->ty; } } static void submit(void *m_, struct soluna_material_stream_context ctx, int n) { struct material_perspective_quad *m = (struct material_perspective_quad *)m_; struct pquad_inst *tmp = (struct pquad_inst *)m->tmp_ptr; int out_n; soluna_material_error err = perspective_quad_count(n, &out_n); if (err != NULL) { soluna_material_stream_error(ctx, err); return; } int i; for (i=0; icolor = payload.color; } else if (item.sprite != sprite || flags != stream_flags) { soluna_material_stream_error(ctx, "Invalid perspective quad stream"); return; } uint32_t corner = payload.info & PQUAD_INFO_CORNER_MASK; if (corner >= PQUAD_CORNER_N) { soluna_material_stream_error(ctx, "Invalid perspective quad corner"); return; } if (stream_flags & PQUAD_INFO_USE_SPRITE_RECT) { decode_sprite_rect_basis(&sprite_rect_basis, corner, &item); } else { pos[corner][0] = item.x; pos[corner][1] = item.y; } float q = payload.q; if (q <= PQUAD_EPSILON) { q = PQUAD_EPSILON; } inst->q[corner] = q; } struct soluna_sprite_rect sprite_rect; if (!soluna_material_sprite_rect(m->bank, sprite, &sprite_rect)) { soluna_material_stream_error(ctx, "Invalid perspective quad sprite"); return; } if (stream_flags & PQUAD_INFO_USE_SPRITE_RECT) { build_quad_from_rect(&sprite_rect_basis, &sprite_rect, pos); } set_position_homography(pos, inst); inst->uv_rect[0] = sprite_rect.u; inst->uv_rect[1] = sprite_rect.v; inst->uv_rect[2] = sprite_rect.w; inst->uv_rect[3] = sprite_rect.h; } sg_append_buffer(m->inst, &(sg_range) { tmp, out_n * sizeof(tmp[0]) }); } static int lmaterial_perspective_quad_submit(lua_State *L) { struct material_perspective_quad *m = (struct material_perspective_quad *)luaL_checkudata(L, 1, "EXTLUA_MATERIAL_PERSPECTIVE_QUAD"); int inst_batch_n = (int)(m->tmp_size / sizeof(struct pquad_inst)); if (inst_batch_n < 1) { return luaL_error(L, "Perspective quad tmp buffer is too small"); } const void *stream = lua_touserdata(L, 2); int prim_n = luaL_checkinteger(L, 3); soluna_material_error err = soluna_material_submit(stream, prim_n, material_id, inst_batch_n * PQUAD_CORNER_N, m, submit); if (err != NULL) { return luaL_error(L, "%s", err); } return 0; } static int lmaterial_perspective_quad_draw(lua_State *L) { struct material_perspective_quad *m = (struct material_perspective_quad *)luaL_checkudata(L, 1, "EXTLUA_MATERIAL_PERSPECTIVE_QUAD"); int prim_n = luaL_checkinteger(L, 3); if (prim_n <= 0) { return 0; } int quad_n; soluna_material_error err = perspective_quad_count(prim_n, &quad_n); if (err != NULL) { return luaL_error(L, "%s", err); } sg_apply_pipeline(m->pip); sg_apply_uniforms(UB_vs_params, &(sg_range) { m->uniform, sizeof(vs_params_t) }); sg_bindings bindings = soluna_material_bindings(m->bind); bindings.vertex_buffer_offsets[0] += (size_t)m->base * sizeof(struct pquad_inst); sg_apply_bindings(&bindings); sg_draw(0, 4, quad_n); m->base += quad_n; return 0; } static int lmaterial_perspective_quad_reset(lua_State *L) { struct material_perspective_quad *m = (struct material_perspective_quad *)luaL_checkudata(L, 1, "EXTLUA_MATERIAL_PERSPECTIVE_QUAD"); m->base = 0; return 0; } static int lset_material_id(lua_State *L) { int id = luaL_checkinteger(L, 1); if (id <= 0) { return luaL_error(L, "Invalid perspective quad material id %d", id); } material_id = id; return 0; } static void init_pipeline(struct material_perspective_quad *p) { sg_pipeline_desc desc = { .layout.attrs = { [ATTR_perspective_quad_pos_h0].format = SG_VERTEXFORMAT_FLOAT3, [ATTR_perspective_quad_pos_h1].format = SG_VERTEXFORMAT_FLOAT3, [ATTR_perspective_quad_pos_h2].format = SG_VERTEXFORMAT_FLOAT3, [ATTR_perspective_quad_uv_rect].format = SG_VERTEXFORMAT_FLOAT4, [ATTR_perspective_quad_q].format = SG_VERTEXFORMAT_FLOAT4, [ATTR_perspective_quad_color].format = SG_VERTEXFORMAT_UBYTE4N, }, }; p->pip = make_pipeline(&desc); } static int lnew_material_perspective_quad(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct material_perspective_quad *m = (struct material_perspective_quad *)lua_newuserdatauv(L, sizeof(*m), 4); int material_index = lua_gettop(L); init_pipeline(m); m->base = 0; if (lua_getfield(L, 1, "inst_buffer") != LUA_TUSERDATA) { return luaL_error(L, "Invalid key .inst_buffer"); } luaL_checkudata(L, -1, "SOKOL_BUFFER"); lua_pushvalue(L, -1); lua_setiuservalue(L, material_index, 1); lua_pushlightuserdata(L, &m->inst); lua_call(L, 1, 0); if (lua_getfield(L, 1, "bindings") != LUA_TUSERDATA) { return luaL_error(L, "Invalid key .bindings"); } m->bind = (struct soluna_render_bindings) { .ctx = luaL_checkudata(L, -1, "SOKOL_BINDINGS"), }; lua_pushvalue(L, -1); lua_setiuservalue(L, material_index, 2); lua_pop(L, 1); if (lua_getfield(L, 1, "uniform") != LUA_TUSERDATA) { return luaL_error(L, "Invalid key .uniform"); } m->uniform = (vs_params_t *)luaL_checkudata(L, -1, "SOKOL_UNIFORM"); lua_pushvalue(L, -1); lua_setiuservalue(L, material_index, 3); lua_pop(L, 1); if (lua_getfield(L, 1, "sprite_bank") != LUA_TLIGHTUSERDATA) { return luaL_error(L, "Invalid key .sprite_bank"); } m->bank = (struct soluna_sprite_bank) { .ctx = lua_touserdata(L, -1), }; lua_pop(L, 1); if (lua_getfield(L, 1, "tmp_buffer") != LUA_TUSERDATA) { return luaL_error(L, "Invalid key .tmp_buffer"); } if (lua_getmetatable(L, -1)) { return luaL_error(L, "Not an userdata without metatable"); } m->tmp_ptr = lua_touserdata(L, -1); m->tmp_size = lua_rawlen(L, -1); lua_setiuservalue(L, material_index, 4); if (luaL_newmetatable(L, "EXTLUA_MATERIAL_PERSPECTIVE_QUAD")) { luaL_Reg l[] = { { "__index", NULL }, { "reset", lmaterial_perspective_quad_reset }, { "submit", lmaterial_perspective_quad_submit }, { "draw", lmaterial_perspective_quad_draw }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } static inline float get_number_field(lua_State *L, int index, const char *field, float defv) { float v; lua_getfield(L, index, field); v = luaL_optnumber(L, -1, defv); lua_pop(L, 1); return v; } static int get_quad(lua_State *L, int index, float quad[8]) { if (lua_getfield(L, index, "quad") == LUA_TTABLE) { int i; for (i=0; i<8; i++) { lua_geti(L, -1, i + 1); quad[i] = luaL_checknumber(L, -1); lua_pop(L, 1); } lua_pop(L, 1); return 0; } lua_pop(L, 1); return 1; } static void get_q(lua_State *L, int index, float q[4]) { int i; if (lua_getfield(L, index, "q") != LUA_TTABLE) { for (i=0; i<4; i++) { q[i] = 1.0f; } lua_pop(L, 1); return; } for (i=0; i<4; i++) { lua_geti(L, -1, i + 1); q[i] = luaL_optnumber(L, -1, 1.0f); lua_pop(L, 1); } lua_pop(L, 1); } static struct color get_color(lua_State *L, int index) { uint32_t color; struct color c; lua_getfield(L, index, "color"); color = (uint32_t)luaL_optinteger(L, -1, 0xffffffff); if (!(color & 0xff000000)) { color |= 0xff000000; } c.channel[0] = (color >> 16) & 0xff; c.channel[1] = (color >> 8) & 0xff; c.channel[2] = color & 0xff; c.channel[3] = (color >> 24) & 0xff; lua_pop(L, 1); return c; } struct pquad_stream_context { int sprite; int use_sprite_rect; float scale_x; float scale_y; float shear_x; float shear_y; float quad[8]; float q[4]; struct color color; struct pquad_payload payload[PQUAD_CORNER_N]; }; static void write_perspective_quad_stream(void *ud, int index, struct soluna_material_stream_item *item) { struct pquad_stream_context *ctx = (struct pquad_stream_context *)ud; item->sprite = ctx->sprite; if (ctx->use_sprite_rect) { switch (index) { case 0: item->x = ctx->scale_x; item->y = ctx->scale_y; break; case 1: item->x = ctx->shear_x; item->y = ctx->shear_y; break; default: item->x = 0.0f; item->y = 0.0f; break; } } else { float x = ctx->quad[index * 2]; float y = ctx->quad[index * 2 + 1]; item->x = x * ctx->scale_x + y * ctx->shear_x; item->y = x * ctx->shear_y + y * ctx->scale_y; } struct pquad_payload *payload = &ctx->payload[index]; uint32_t info = (uint32_t)index; if (ctx->use_sprite_rect) { info |= PQUAD_INFO_USE_SPRITE_RECT; } payload->info = info; payload->q = ctx->q[index]; payload->color = ctx->color; item->payload = payload; } static int lperspective_quad_sprite(lua_State *L) { if (material_id <= 0) { return luaL_error(L, "Perspective quad material is not registered"); } luaL_checktype(L, 2, LUA_TTABLE); struct pquad_stream_context ctx; ctx.sprite = luaL_checkinteger(L, 1) - 1; ctx.use_sprite_rect = get_quad(L, 2, ctx.quad); ctx.scale_x = get_number_field(L, 2, "scale_x", 1.0f); ctx.scale_y = get_number_field(L, 2, "scale_y", 1.0f); ctx.shear_x = get_number_field(L, 2, "shear_x", 0.0f); ctx.shear_y = get_number_field(L, 2, "shear_y", 0.0f); get_q(L, 2, ctx.q); ctx.color = get_color(L, 2); struct soluna_material_stream stream; soluna_material_error err = soluna_material_push_stream(material_id, PQUAD_CORNER_N, sizeof(struct pquad_payload), write_perspective_quad_stream, &ctx, &stream); if (err != NULL) { return luaL_error(L, "%s", err); } lua_pushexternalstring(L, stream.data, stream.size, free_material_stream, NULL); return 1; } static int luaopen_ext_material_perspective_quad(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "set_material_id", lset_material_id }, { "new", lnew_material_perspective_quad }, { "sprite", lperspective_quad_sprite }, { "instance_size", NULL }, { NULL, NULL }, }; luaL_newlib(L, l); lua_pushinteger(L, sizeof(struct pquad_inst)); lua_setfield(L, -2, "instance_size"); return 1; } static int lhello(lua_State *L) { lua_pushstring(L, "Hello World From Sample"); return 1; } static int luaopen_foobar(lua_State *L) { luaL_Reg l[] = { { "hello", lhello }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } 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.material.perspective_quad", luaopen_ext_material_perspective_quad }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: extlua/gen.lua ================================================ -- extlua.h[/.c] generator local luapath = "../3rd/lua/" local function parse_params(params) local p = {} if params == "(void)" then return p end local pn = 1 for t, n in params:gmatch ("(.-)([%w_.%[%]]+)[,)]", 2) do local is_array if n:sub(-2) == "[]" then is_array = true n = n:sub(1, -3) end p[pn] = { type = t, name = n, is_array = is_array } pn = pn + 1 end return p end local function get_apis(result, filename, prefix) local f = assert(io.open(luapath .. filename)) local src = f:read "*a" f:close() local n = #result + 1 for line in src:gmatch( prefix .. "%s*(.-;)%s*\n" ) do if not line:find("...", 1, true) then local retv, funcname, params = line:match "([%s%w_*]+)%(([%s%w_]+)%)%s*(%b());$" if retv == nil then if line:match "%S" then error("Invalid function " .. line) end end retv = retv:gsub("%s+$", "") if retv:match "%s*void%s*$" then retv = "void" end result[n] = { ret = retv, name = funcname, params = parse_params(params), } n = n + 1 end end end local function decls(params) if #params == 0 then return "void" end local r = {} for idx, p in ipairs(params) do local d = p.type .. p.name if p.is_array then d = d .. "[]" end r[idx] = d end return table.concat(r, ",") end local function gen_decl(apis) local d = {} for idx, item in ipairs(apis) do d[idx] = ("\t%s (*%s) (%s);"):format(item.ret, item.name, decls(item.params)) end d[#d+1] = "\tint (*lua_gc) (lua_State *L, int what, ...);" return table.concat(d, "\n") end local function gen_struct(apis) local d = {} for idx, item in ipairs(apis) do d[idx] = ("\t\t%s,"):format(item.name) end d[#d+1] = "\t\tlua_gc," return table.concat(d, "\n") end local function args(params) local args = {} for idx, p in ipairs(params) do args[idx] = p.name end return table.concat(args, ",") end local impl_fmt = [[ LUA_API %s %s(%s) { %sAPI.%s(%s); } ]] local function gen_impl(apis) local d = {} local n = 1 for idx, item in ipairs(apis) do if item.name ~= "lua_gc" then local ret if item.ret == "void" then ret = "" else ret = "return " end local impl = impl_fmt:format(item.ret, item.name, decls(item.params), ret, item.name, args(item.params)) d[n] = impl; n = n + 1 end end return table.concat(d) end local function readfile(filename) local f = assert(io.open(filename)) local content = f:read "a" f:close() return content end local function genfile(filename, temp) local t = readfile(filename) local output_filename = filename:gsub("%.temp", "") local output_f = assert(io.open(output_filename, "w")) output_f:write("// AUTO GENERATED by " .. filename .. ", DONT EDIT\n\n") output_f:write((t:gsub("%$([%w_]+)%$", temp))) output_f:close() end local apis = {} get_apis(apis, "lua.h", "LUA_API") get_apis(apis, "lauxlib.h", "LUALIB_API") local convert = { API_DECL = gen_decl(apis), API_STRUCT = gen_struct(apis), API_IMPL = gen_impl(apis), } genfile("extlua.temp.c", convert) genfile("extlua_impl.temp.c", convert) ================================================ FILE: extlua/gen_sokol.lua ================================================ -- sokolapi.c generator local header = "../3rd/sokol/sokol_gfx.h" local allowlist = { sg_make_buffer = true, sg_destroy_buffer = true, sg_update_buffer = true, sg_append_buffer = true, sg_make_shader = true, sg_destroy_shader = true, sg_make_pipeline = true, sg_destroy_pipeline = true, sg_apply_pipeline = true, sg_apply_bindings = true, sg_apply_uniforms = true, sg_draw = true, sg_draw_ex = true, sg_query_backend = true, sg_query_buffer_state = true, sg_query_shader_state = true, sg_query_pipeline_state = true, } local function parse_params(params) local p = {} if params == "(void)" then return p end local body = params:sub(2, -2) local n = 1 for decl in body:gmatch "[^,]+" do local t, name = decl:match "^(.-)([%w_]+)%s*$" if not t then error("Invalid param " .. decl) end p[n] = { type = t, name = name } n = n + 1 end return p end local function readfile(filename) local f = assert(io.open(filename)) local content = f:read "a" f:close() return content end local function get_apis() local src = readfile(header) local apis = {} for line in src:gmatch "SOKOL_GFX_API_DECL%s*(.-;)%s*\n" do local retv, name, params = line:match "([%s%w_*]+)%s+(sg_[%w_]+)%s*(%b());$" if name and allowlist[name] then apis[#apis + 1] = { ret = retv:gsub("%s+$", ""), name = name, params = parse_params(params), } end end return apis end local function decls(params) if #params == 0 then return "void" end local r = {} for i, p in ipairs(params) do r[i] = p.type .. p.name end return table.concat(r, ", ") end local function args(params) local r = {} for i, p in ipairs(params) do r[i] = p.name end return table.concat(r, ", ") end local function gen_decl(apis) local r = {} for i, api in ipairs(apis) do r[i] = ("\t%s (*%s) (%s);"):format(api.ret, api.name, decls(api.params)) end return table.concat(r, "\n") end local function gen_struct(apis) local r = {} for i, api in ipairs(apis) do r[i] = ("\t\t%s,"):format(api.name) end return table.concat(r, "\n") end local function gen_impl(apis) local r = {} for i, api in ipairs(apis) do local ret = api.ret == "void" and "" or "return " r[i] = ("SOKOL_GFX_API_DECL %s\n%s(%s) {\n\t%sAPI.%s(%s);\n}\n\n"):format( api.ret, api.name, decls(api.params), ret, api.name, args(api.params) ) end return table.concat(r) end local function genfile(filename, temp) local t = readfile(filename) local output = filename:gsub("%.temp", "") local f = assert(io.open(output, "w")) f:write("// AUTO GENERATED by " .. filename .. ", DONT EDIT\n\n") f:write((t:gsub("%$([%w_]+)%$", temp))) f:close() end local apis = get_apis() local convert = { API_DECL = gen_decl(apis), API_STRUCT = gen_struct(apis), API_IMPL = gen_impl(apis), } genfile("sokolapi.temp.c", convert) genfile("sokolapi_impl.temp.c", convert) ================================================ FILE: extlua/gen_soluna.lua ================================================ -- solunaapi.c generator local apis = { { ret = "soluna_material_error", name = "soluna_material_submit", params = { { type = "const void *", name = "stream" }, { type = "int ", name = "prim_n" }, { type = "int ", name = "material_id" }, { type = "int ", name = "batch_n" }, { type = "void *", name = "ud" }, { type = "soluna_material_submit_func ", name = "submit" }, }, }, { ret = "int", name = "soluna_material_sprite_rect", params = { { type = "struct soluna_sprite_bank ", name = "bank" }, { type = "int ", name = "sprite" }, { type = "struct soluna_sprite_rect *", name = "out" }, }, }, { ret = "sg_bindings", name = "soluna_material_bindings", params = { { type = "struct soluna_render_bindings ", name = "bindings" }, }, }, { ret = "soluna_material_error", name = "soluna_material_push_stream", params = { { type = "int ", name = "material_id" }, { type = "int ", name = "count" }, { type = "size_t ", name = "payload_size" }, { type = "soluna_material_stream_write_func ", name = "write" }, { type = "void *", name = "ud" }, { type = "struct soluna_material_stream *", name = "out" }, }, }, { ret = "void", name = "soluna_material_stream_free", params = { { type = "void *", name = "ptr" }, }, }, { ret = "int", name = "soluna_material_stream_read", params = { { type = "struct soluna_material_stream_context ", name = "ctx" }, { type = "int ", name = "index" }, { type = "size_t ", name = "payload_size" }, { type = "void *", name = "payload" }, { type = "struct soluna_material_stream_data *", name = "out" }, }, }, { ret = "void", name = "soluna_material_stream_error", params = { { type = "struct soluna_material_stream_context ", name = "ctx" }, { type = "const char *", name = "error" }, }, }, { ret = "int", name = "soluna_material_stream_failed", params = { { type = "struct soluna_material_stream_context ", name = "ctx" }, }, }, } local type_decl = [[ #define SOLUNA_EXT_API_VERSION 1 struct soluna_sprite_rect { int texture; float u; float v; float w; float h; float ox; float oy; }; struct soluna_material_stream_item { float x; float y; int sprite; const void *payload; }; struct soluna_material_stream_data { float x; float y; int sprite; }; struct soluna_material_stream { char *data; size_t size; }; typedef const char *soluna_material_error; struct soluna_material_stream_context { void *ctx; }; struct soluna_render_bindings { void *ctx; }; struct soluna_sprite_bank { void *ctx; }; typedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n); typedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item); ]] local host_type_decl = [[ #include #include "sokol/sokol_gfx.h" ]] .. type_decl local function readfile(filename) local f = assert(io.open(filename)) local content = f:read "a" f:close() return content end local function genfile(filename, temp) local t = readfile(filename) local output = filename:gsub("%.temp", "") local f = assert(io.open(output, "w")) f:write("// AUTO GENERATED by " .. filename .. ", DONT EDIT\n\n") f:write((t:gsub("%$([%w_]+)%$", temp))) f:close() end local function decls(params) if #params == 0 then return "void" end local r = {} for i, p in ipairs(params) do r[i] = p.type .. p.name end return table.concat(r, ", ") end local function args(params) local r = {} for i, p in ipairs(params) do r[i] = p.name end return table.concat(r, ", ") end local function field_name(api) return api.field or api.name:gsub("^soluna_", "") end local function impl_name(api) return api.impl or field_name(api) end local function gen_header_decl() local r = { "void solunaapi_init(lua_State *L);" } for _, api in ipairs(apis) do r[#r + 1] = ("%s %s(%s);"):format(api.ret, api.name, decls(api.params)) end return table.concat(r, "\n") end local function gen_api_decl() local r = {} for i, api in ipairs(apis) do r[i] = ("\t%s (*%s) (%s);"):format(api.ret, field_name(api), decls(api.params)) end return table.concat(r, "\n") end local function gen_api_struct() local r = {} for i, api in ipairs(apis) do r[i] = ("\t\t%s,"):format(impl_name(api)) end return table.concat(r, "\n") end local function gen_api_extern() local r = {} for i, api in ipairs(apis) do r[i] = ("extern %s %s(%s);"):format(api.ret, impl_name(api), decls(api.params)) end return table.concat(r, "\n") end local function gen_api_impl() local r = {} for i, api in ipairs(apis) do local ret = api.ret == "void" and "" or "return " r[i] = ("%s\n%s(%s) {\n\t%sAPI.%s(%s);\n}\n\n"):format( api.ret, api.name, decls(api.params), ret, field_name(api), args(api.params) ) end return table.concat(r) end local convert = { TYPE_DECL = type_decl, HEADER_DECL = gen_header_decl(), API_DECL = gen_api_decl(), API_STRUCT = gen_api_struct(), API_EXTERN = gen_api_extern(), API_IMPL = gen_api_impl(), HOST_TYPE_DECL = host_type_decl, } genfile("solunaapi.h.temp", convert) genfile("solunaapi.temp.c", convert) genfile("solunaapi_impl.temp.c", convert) genfile("../src/extapi_types.temp.h", convert) ================================================ FILE: extlua/perspective_quad.glsl ================================================ @vs vs layout(binding=0) uniform vs_params { vec2 framesize; float texsize; }; in vec3 pos_h0; in vec3 pos_h1; in vec3 pos_h2; in vec4 uv_rect; in vec4 q; in vec4 color; out vec3 uvq; out vec4 frag_color; out flat float tex_scale; void main() { vec2 corner = vec2(float(gl_VertexIndex & 1), float(gl_VertexIndex >> 1)); mat3 pos_h = mat3(pos_h0, pos_h1, pos_h2); vec3 pos_hv = pos_h * vec3(corner, 1.0); float pos_w = max(pos_hv.z, 1e-6); vec2 pos = pos_hv.xy / pos_w; vec2 uv = uv_rect.xy + uv_rect.zw * corner; float qx0 = mix(q.x, q.y, corner.x); float qx1 = mix(q.z, q.w, corner.x); float qv = max(mix(qx0, qx1, corner.y), 1e-6); uvq = vec3(uv * qv, qv); vec2 clip = pos * framesize; gl_Position = vec4(clip.x - 1.0, clip.y + 1.0, 0.0, 1.0); frag_color = color; tex_scale = texsize; } @end @fs fs layout(binding=1) uniform texture2D tex; layout(binding=0) uniform sampler smp; in vec3 uvq; in vec4 frag_color; in flat float tex_scale; out vec4 out_color; void main() { vec3 proj_uv = vec3(uvq.xy * tex_scale, uvq.z); out_color = textureProj(sampler2D(tex, smp), proj_uv) * frag_color; } @end @program perspective_quad vs fs ================================================ FILE: extlua/sokolapi.c ================================================ // AUTO GENERATED by sokolapi.temp.c, DONT EDIT #include #include "sokol/sokol_gfx.h" struct sokol_api { int version; sg_buffer (*sg_make_buffer) (const sg_buffer_desc* desc); sg_shader (*sg_make_shader) (const sg_shader_desc* desc); sg_pipeline (*sg_make_pipeline) (const sg_pipeline_desc* desc); void (*sg_destroy_buffer) (sg_buffer buf); void (*sg_destroy_shader) (sg_shader shd); void (*sg_destroy_pipeline) (sg_pipeline pip); void (*sg_update_buffer) (sg_buffer buf, const sg_range* data); int (*sg_append_buffer) (sg_buffer buf, const sg_range* data); void (*sg_apply_pipeline) (sg_pipeline pip); void (*sg_apply_bindings) (const sg_bindings* bindings); void (*sg_apply_uniforms) (int ub_slot, const sg_range* data); void (*sg_draw) (int base_element, int num_elements, int num_instances); void (*sg_draw_ex) (int base_element, int num_elements, int num_instances, int base_vertex, int base_instance); sg_backend (*sg_query_backend) (void); sg_resource_state (*sg_query_buffer_state) (sg_buffer buf); sg_resource_state (*sg_query_shader_state) (sg_shader shd); sg_resource_state (*sg_query_pipeline_state) (sg_pipeline pip); }; static struct sokol_api API; SOKOL_GFX_API_DECL sg_buffer sg_make_buffer(const sg_buffer_desc* desc) { return API.sg_make_buffer(desc); } SOKOL_GFX_API_DECL sg_shader sg_make_shader(const sg_shader_desc* desc) { return API.sg_make_shader(desc); } SOKOL_GFX_API_DECL sg_pipeline sg_make_pipeline(const sg_pipeline_desc* desc) { return API.sg_make_pipeline(desc); } SOKOL_GFX_API_DECL void sg_destroy_buffer(sg_buffer buf) { API.sg_destroy_buffer(buf); } SOKOL_GFX_API_DECL void sg_destroy_shader(sg_shader shd) { API.sg_destroy_shader(shd); } SOKOL_GFX_API_DECL void sg_destroy_pipeline(sg_pipeline pip) { API.sg_destroy_pipeline(pip); } SOKOL_GFX_API_DECL void sg_update_buffer(sg_buffer buf, const sg_range* data) { API.sg_update_buffer(buf, data); } SOKOL_GFX_API_DECL int sg_append_buffer(sg_buffer buf, const sg_range* data) { return API.sg_append_buffer(buf, data); } SOKOL_GFX_API_DECL void sg_apply_pipeline(sg_pipeline pip) { API.sg_apply_pipeline(pip); } SOKOL_GFX_API_DECL void sg_apply_bindings(const sg_bindings* bindings) { API.sg_apply_bindings(bindings); } SOKOL_GFX_API_DECL void sg_apply_uniforms(int ub_slot, const sg_range* data) { API.sg_apply_uniforms(ub_slot, data); } SOKOL_GFX_API_DECL void sg_draw(int base_element, int num_elements, int num_instances) { API.sg_draw(base_element, num_elements, num_instances); } SOKOL_GFX_API_DECL void sg_draw_ex(int base_element, int num_elements, int num_instances, int base_vertex, int base_instance) { API.sg_draw_ex(base_element, num_elements, num_instances, base_vertex, base_instance); } SOKOL_GFX_API_DECL sg_backend sg_query_backend(void) { return API.sg_query_backend(); } SOKOL_GFX_API_DECL sg_resource_state sg_query_buffer_state(sg_buffer buf) { return API.sg_query_buffer_state(buf); } SOKOL_GFX_API_DECL sg_resource_state sg_query_shader_state(sg_shader shd) { return API.sg_query_shader_state(shd); } SOKOL_GFX_API_DECL sg_resource_state sg_query_pipeline_state(sg_pipeline pip) { return API.sg_query_pipeline_state(pip); } struct lua_api; struct soluna_api; struct extlua_apis { struct lua_api * lua; struct sokol_api * sokol; struct soluna_api * soluna; }; void sokolapi_init(lua_State *L) { struct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L); API = *apis->sokol; } ================================================ FILE: extlua/sokolapi.temp.c ================================================ #include #include "sokol/sokol_gfx.h" struct sokol_api { int version; $API_DECL$ }; static struct sokol_api API; $API_IMPL$ struct lua_api; struct soluna_api; struct extlua_apis { struct lua_api * lua; struct sokol_api * sokol; struct soluna_api * soluna; }; void sokolapi_init(lua_State *L) { struct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L); API = *apis->sokol; } ================================================ FILE: extlua/sokolapi_impl.c ================================================ // AUTO GENERATED by sokolapi_impl.temp.c, DONT EDIT #include "sokol/sokol_gfx.h" struct sokol_api { int version; sg_buffer (*sg_make_buffer) (const sg_buffer_desc* desc); sg_shader (*sg_make_shader) (const sg_shader_desc* desc); sg_pipeline (*sg_make_pipeline) (const sg_pipeline_desc* desc); void (*sg_destroy_buffer) (sg_buffer buf); void (*sg_destroy_shader) (sg_shader shd); void (*sg_destroy_pipeline) (sg_pipeline pip); void (*sg_update_buffer) (sg_buffer buf, const sg_range* data); int (*sg_append_buffer) (sg_buffer buf, const sg_range* data); void (*sg_apply_pipeline) (sg_pipeline pip); void (*sg_apply_bindings) (const sg_bindings* bindings); void (*sg_apply_uniforms) (int ub_slot, const sg_range* data); void (*sg_draw) (int base_element, int num_elements, int num_instances); void (*sg_draw_ex) (int base_element, int num_elements, int num_instances, int base_vertex, int base_instance); sg_backend (*sg_query_backend) (void); sg_resource_state (*sg_query_buffer_state) (sg_buffer buf); sg_resource_state (*sg_query_shader_state) (sg_shader shd); sg_resource_state (*sg_query_pipeline_state) (sg_pipeline pip); }; struct sokol_api * extlua_sokol_api() { static struct sokol_api api = { SOKOL_GFX_INCLUDED, sg_make_buffer, sg_make_shader, sg_make_pipeline, sg_destroy_buffer, sg_destroy_shader, sg_destroy_pipeline, sg_update_buffer, sg_append_buffer, sg_apply_pipeline, sg_apply_bindings, sg_apply_uniforms, sg_draw, sg_draw_ex, sg_query_backend, sg_query_buffer_state, sg_query_shader_state, sg_query_pipeline_state, }; return &api; } ================================================ FILE: extlua/sokolapi_impl.temp.c ================================================ #include "sokol/sokol_gfx.h" struct sokol_api { int version; $API_DECL$ }; struct sokol_api * extlua_sokol_api() { static struct sokol_api api = { SOKOL_GFX_INCLUDED, $API_STRUCT$ }; return &api; } ================================================ FILE: extlua/solunaapi.c ================================================ // AUTO GENERATED by solunaapi.temp.c, DONT EDIT #include "solunaapi.h" #include struct soluna_api { int version; soluna_material_error (*material_submit) (const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit); int (*material_sprite_rect) (struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out); sg_bindings (*material_bindings) (struct soluna_render_bindings bindings); soluna_material_error (*material_push_stream) (int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out); void (*material_stream_free) (void *ptr); int (*material_stream_read) (struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out); void (*material_stream_error) (struct soluna_material_stream_context ctx, const char *error); int (*material_stream_failed) (struct soluna_material_stream_context ctx); }; static struct soluna_api API; soluna_material_error soluna_material_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit) { return API.material_submit(stream, prim_n, material_id, batch_n, ud, submit); } int soluna_material_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out) { return API.material_sprite_rect(bank, sprite, out); } sg_bindings soluna_material_bindings(struct soluna_render_bindings bindings) { return API.material_bindings(bindings); } soluna_material_error soluna_material_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out) { return API.material_push_stream(material_id, count, payload_size, write, ud, out); } void soluna_material_stream_free(void *ptr) { API.material_stream_free(ptr); } int soluna_material_stream_read(struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out) { return API.material_stream_read(ctx, index, payload_size, payload, out); } void soluna_material_stream_error(struct soluna_material_stream_context ctx, const char *error) { API.material_stream_error(ctx, error); } int soluna_material_stream_failed(struct soluna_material_stream_context ctx) { return API.material_stream_failed(ctx); } struct lua_api; struct sokol_api; struct extlua_apis { struct lua_api * lua; struct sokol_api * sokol; struct soluna_api * soluna; }; void solunaapi_init(lua_State *L) { struct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L); if (apis == NULL || apis->soluna == NULL || apis->soluna->version != SOLUNA_EXT_API_VERSION) { int version = (apis != NULL && apis->soluna != NULL) ? apis->soluna->version : 0; luaL_error(L, "soluna ext api version mismatch, expected %d got %d", SOLUNA_EXT_API_VERSION, version); } API = *apis->soluna; } ================================================ FILE: extlua/solunaapi.h ================================================ // AUTO GENERATED by solunaapi.h.temp, DONT EDIT #ifndef SOLUNAAPI_H #define SOLUNAAPI_H #include #include #include "sokol/sokol_gfx.h" #define SOLUNA_EXT_API_VERSION 1 struct soluna_sprite_rect { int texture; float u; float v; float w; float h; float ox; float oy; }; struct soluna_material_stream_item { float x; float y; int sprite; const void *payload; }; struct soluna_material_stream_data { float x; float y; int sprite; }; struct soluna_material_stream { char *data; size_t size; }; typedef const char *soluna_material_error; struct soluna_material_stream_context { void *ctx; }; struct soluna_render_bindings { void *ctx; }; struct soluna_sprite_bank { void *ctx; }; typedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n); typedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item); void solunaapi_init(lua_State *L); soluna_material_error soluna_material_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit); int soluna_material_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out); sg_bindings soluna_material_bindings(struct soluna_render_bindings bindings); soluna_material_error soluna_material_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out); void soluna_material_stream_free(void *ptr); int soluna_material_stream_read(struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out); void soluna_material_stream_error(struct soluna_material_stream_context ctx, const char *error); int soluna_material_stream_failed(struct soluna_material_stream_context ctx); #endif ================================================ FILE: extlua/solunaapi.h.temp ================================================ #ifndef SOLUNAAPI_H #define SOLUNAAPI_H #include #include #include "sokol/sokol_gfx.h" $TYPE_DECL$ $HEADER_DECL$ #endif ================================================ FILE: extlua/solunaapi.temp.c ================================================ #include "solunaapi.h" #include struct soluna_api { int version; $API_DECL$ }; static struct soluna_api API; $API_IMPL$ struct lua_api; struct sokol_api; struct extlua_apis { struct lua_api * lua; struct sokol_api * sokol; struct soluna_api * soluna; }; void solunaapi_init(lua_State *L) { struct extlua_apis *apis = *(struct extlua_apis **)lua_getextraspace(L); if (apis == NULL || apis->soluna == NULL || apis->soluna->version != SOLUNA_EXT_API_VERSION) { int version = (apis != NULL && apis->soluna != NULL) ? apis->soluna->version : 0; luaL_error(L, "soluna ext api version mismatch, expected %d got %d", SOLUNA_EXT_API_VERSION, version); } API = *apis->soluna; } ================================================ FILE: extlua/solunaapi_impl.c ================================================ // AUTO GENERATED by solunaapi_impl.temp.c, DONT EDIT #include #include "sokol/sokol_gfx.h" #define SOLUNA_EXT_API_VERSION 1 struct soluna_sprite_rect { int texture; float u; float v; float w; float h; float ox; float oy; }; struct soluna_material_stream_item { float x; float y; int sprite; const void *payload; }; struct soluna_material_stream_data { float x; float y; int sprite; }; struct soluna_material_stream { char *data; size_t size; }; typedef const char *soluna_material_error; struct soluna_material_stream_context { void *ctx; }; struct soluna_render_bindings { void *ctx; }; struct soluna_sprite_bank { void *ctx; }; typedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n); typedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item); extern soluna_material_error material_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit); extern int material_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out); extern sg_bindings material_bindings(struct soluna_render_bindings bindings); extern soluna_material_error material_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out); extern void material_stream_free(void *ptr); extern int material_stream_read(struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out); extern void material_stream_error(struct soluna_material_stream_context ctx, const char *error); extern int material_stream_failed(struct soluna_material_stream_context ctx); struct soluna_api { int version; soluna_material_error (*material_submit) (const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit); int (*material_sprite_rect) (struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out); sg_bindings (*material_bindings) (struct soluna_render_bindings bindings); soluna_material_error (*material_push_stream) (int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out); void (*material_stream_free) (void *ptr); int (*material_stream_read) (struct soluna_material_stream_context ctx, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out); void (*material_stream_error) (struct soluna_material_stream_context ctx, const char *error); int (*material_stream_failed) (struct soluna_material_stream_context ctx); }; struct soluna_api * extlua_soluna_api() { static struct soluna_api api = { SOLUNA_EXT_API_VERSION, material_submit, material_sprite_rect, material_bindings, material_push_stream, material_stream_free, material_stream_read, material_stream_error, material_stream_failed, }; return &api; } ================================================ FILE: extlua/solunaapi_impl.temp.c ================================================ $HOST_TYPE_DECL$ $API_EXTERN$ struct soluna_api { int version; $API_DECL$ }; struct soluna_api * extlua_soluna_api() { static struct soluna_api api = { SOLUNA_EXT_API_VERSION, $API_STRUCT$ }; return &api; } ================================================ FILE: make.lua ================================================ local lm = require "luamake" local fs = require "bee.filesystem" local function detect_emcc() if lm.compiler == "emcc" then return true end if type(lm.cc) == "string" and lm.cc:find("emcc", 1, true) then return true end return false end local osplat = (function() if lm.os == "windows" then if lm.compiler == "gcc" then return "mingw" end if lm.cc == "clang-cl" then return "clang-cl" end return "msvc" end return lm.os end)() local plat = (function() if detect_emcc() then return "emcc" end return osplat end)() lm.platform = plat lm.basedir = lm:path "." lm.bindir = ("bin/%s/%s"):format(plat, lm.mode) lm.osbindir = ("bin/%s/%s"):format(osplat, lm.mode) lm:conf { cxx = "c++20", clang = { c = "c11", }, flags = { lm.mode ~= "debug" and "-O2", }, msvc = { c = "c11", flags = { "-W3", "-utf-8", "-experimental:c11atomics", "/wd4244", "/wd4267", "/wd4305", "/wd4996", "/wd4018", "/wd4113", }, defines = { "_CRT_SECURE_NO_WARNINGS", "_CRT_NONSTDC_NO_DEPRECATE", "_CRT_SECURE_NO_DEPRECATE" }, }, mingw = { c = "c99", }, gcc = { c = "c11", flags = { "-Wall", }, defines = { "_POSIX_C_SOURCE=199309L", "_GNU_SOURCE", }, links = { "m", (lm.os ~= "windows" and lm.platform ~= "emcc") and "fontconfig", }, }, emcc = { c = "gnu11", flags = { "-Wall", "-pthread", "-fPIC", "--use-port=emdawnwebgpu", "-fwasm-exceptions", }, links = { "idbfs.js", }, ldflags = { "--use-port=emdawnwebgpu", "-s ALLOW_MEMORY_GROWTH", "-s FORCE_FILESYSTEM=1", "-s USE_PTHREADS=1", "-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", lm.mode == "debug" and "-s PTHREADS_DEBUG=1", }, defines = { "_POSIX_C_SOURCE=200809L", "_GNU_SOURCE", }, }, defines = { -- lm.mode == "debug" and "DEBUGLOG", lm.mode == "debug" and "SOKOL_DEBUG", } } local deps = { "soluna_src" } for path in fs.pairs(lm.basedir .. "/clibs") do local name = path:stem():string() if name ~= "soluna" and name ~= "sample" and fs.exists(path / "make.lua") then local makefile = ("clibs/%s/make.lua"):format(name) lm:import(makefile) deps[#deps + 1] = name .. "_src" end end lm:import "clibs/soluna/make.lua" lm:import "clibs/sample/make.lua" lm:exe "soluna" { deps = deps, emcc = { ldflags = { "--js-library=src/platform/wasm/soluna_ime.js", "--js-library=src/platform/wasm/soluna_openurl.js", "-s MODULARIZE=1", "-s EXPORT_ES6=1", "-s EXPORT_NAME=createApp", '-s EXPORTED_FUNCTIONS=\'["_main"]\'', '-s EXPORTED_RUNTIME_METHODS=\'["FS","FS_createPath","FS_createDataFile","IDBFS"]\'', "-s PTHREAD_POOL_SIZE='Math.max(2,navigator.hardwareConcurrency)'", "-s PTHREAD_POOL_SIZE_STRICT=2", "-s MAIN_MODULE=2", "-Wl,-u,emscripten_builtin_memalign", "-Wl,--export=emscripten_builtin_memalign", }, }, } lm:import "script/act_targets.lua" lm:runlua "cc" { script = "script/compile_commands.lua", args = { "build/build.ninja", "compile_commands.json", }, inputs = { "build/build.ninja", "script/compile_commands.lua", }, outputs = { "compile_commands.json", }, } lm:default "soluna" ================================================ FILE: script/act.lua ================================================ local platform = require "bee.platform" local fs = require "bee.filesystem" local subprocess = require "bee.subprocess" local is_windows = platform.os == "windows" local function quote_ps(value) return "'" .. value:gsub("'", "''") .. "'" end local function cmdline(args) local out = {} for i = 1, #args do local v = args[i] if v:find("[%s\"]") then out[#out + 1] = '"' .. v:gsub('"', '\\"') .. '"' else out[#out + 1] = v end end return table.concat(out, " ") end local function run(args, option) option = option or {} print("> " .. cmdline(args)) local process, errmsg = subprocess.spawn { args, searchPath = true, stdout = option.stdout ~= nil and option.stdout or io.stdout, stderr = option.stderr ~= nil and option.stderr or "stdout", } assert(process, errmsg) local code = process:wait() process:detach() if code ~= 0 then error(("command failed (%d): %s"):format(code, cmdline(args))) end end local function run_code(args) local process = assert(subprocess.spawn { args, searchPath = true, stdout = true, stderr = true, }) local code = process:wait() process:detach() return code end local function exists(path) return fs.exists(path) end local function getenv(name, default) local value = os.getenv(name) if value == nil or value == "" then return default end return value end local function reset_dir(path) pcall(fs.remove_all, path) if not exists(path) then fs.create_directories(path) end end local function get_home() if is_windows then local userprofile = os.getenv("USERPROFILE") if userprofile and userprofile ~= "" then return fs.path(userprofile) end local home_drive = os.getenv("HOMEDRIVE") or "" local home_path = os.getenv("HOMEPATH") or "" local combined = home_drive .. home_path if combined ~= "" then return fs.path(combined) end return nil end local home = os.getenv("HOME") if home and home ~= "" then return fs.path(home) end return nil end local function detect_python() if is_windows then if run_code { "where", "py" } == 0 then return { "py", "-3" } end if run_code { "where", "python" } == 0 then return { "python" } end else if run_code { "which", "python3" } == 0 then return { "python3" } end if run_code { "which", "python" } == 0 then return { "python" } end end error "python interpreter not found" end local function find_file(root, filename) for path, status in fs.pairs_r(root) do if status and status:is_regular_file() and path:filename():string() == filename then return path end end return nil end local function collect_files(root) local files = {} if not exists(root) then return files end for path, status in fs.pairs_r(root) do if status and status:is_regular_file() then files[#files + 1] = path end end table.sort(files, function(a, b) return a:string() < b:string() end) return files end local workflow = (arg[1] or "pages"):lower() local workflow_file = ({ pages = ".github/workflows/pages.yml", nightly = ".github/workflows/nightly.yml", })[workflow] if not workflow_file then error(("unknown workflow: %s (expected: pages or nightly)"):format(workflow)) end local function parse_options(argv) local options = { port = tonumber(getenv("PORT", "8080")) or 8080, host_os = platform.os, } for i = 2, #argv do local value = argv[i] local port_num = tonumber(value) local kv_key, kv_value = value:match("^([%w_]+)=(.+)$") if kv_key == "port" then local p = tonumber(kv_value) if p then options.port = p end elseif kv_key == "host_os" then options.host_os = kv_value:lower() elseif port_num then options.port = port_num end end return options end local options = parse_options(arg) local port = options.port local home = assert(get_home(), "home directory is not set") local root = fs.path(getenv("ACT_ROOT", (home / ".act/soluna"):string())) local artifact_root = root / "artifacts" local unpack_dir = root / "unpack" local serve_root = root / "serve" local preview_dir = serve_root / "soluna" reset_dir(artifact_root) reset_dir(unpack_dir) reset_dir(preview_dir) local act_args = { "act", "workflow_dispatch", "-W", workflow_file, "--container-architecture", "linux/amd64", "--artifact-server-path", artifact_root:string(), } if workflow == "nightly" then local matrix_os = ({ windows = "windows-latest", macos = "macos-latest", })[options.host_os] or "ubuntu-latest" act_args[#act_args + 1] = "--matrix" act_args[#act_args + 1] = "os:" .. matrix_os if matrix_os == "windows-latest" or matrix_os == "macos-latest" then act_args[#act_args + 1] = "-P" act_args[#act_args + 1] = matrix_os .. "=-self-hosted" end print(("Nightly matrix selected: %s (host_os=%s)"):format(matrix_os, options.host_os)) end run(act_args) if workflow ~= "pages" then print("Workflow completed: " .. workflow) print("Artifacts root: " .. artifact_root:string()) local files = collect_files(artifact_root) if #files == 0 then print("No artifact files were found under artifacts root.") else print("Artifact files:") for i = 1, #files do local rel = files[i]:string():sub(#artifact_root:string() + 2) print(" - " .. rel) end end return end local zip_path = find_file(artifact_root, "github-pages.zip") if not zip_path then error(table.concat({ "missing github-pages.zip under: " .. artifact_root:string(), "The workflow build job was likely skipped (for example, unsupported runner image).", "You can inspect with: act -l ; act -n -W .github/workflows/pages.yml", "If needed, pass platform mapping manually, for example:", " act workflow_dispatch -W .github/workflows/pages.yml -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest", }, "\n")) end if is_windows then run { "powershell", "-NoProfile", "-Command", "Expand-Archive -LiteralPath " .. quote_ps(zip_path:string()) .. " -DestinationPath " .. quote_ps(unpack_dir:string()) .. " -Force", } else run { "unzip", "-o", zip_path:string(), "-d", unpack_dir:string(), } end local artifact_tar = find_file(unpack_dir, "artifact.tar") if not artifact_tar then error("missing artifact.tar under: " .. unpack_dir:string()) end run { "tar", "-xf", artifact_tar:string(), "-C", preview_dir:string(), } print("Preview files are ready at: " .. preview_dir:string()) print("Serving http://127.0.0.1:" .. port .. "/soluna/") print "If the page loads before the service worker takes control, refresh once." local python = detect_python() python[#python + 1] = "-m" python[#python + 1] = "http.server" python[#python + 1] = tostring(port) python[#python + 1] = "--directory" python[#python + 1] = serve_root:string() run(python) ================================================ FILE: script/act_targets.lua ================================================ local lm = require "luamake" lm:rule "act_lua" { args = { "$luamake", "lua", "$args" }, description = "$args", pool = "console", } lm:build "pages" { rule = "act_lua", args = { "@act.lua", "pages" }, inputs = { "act.lua" }, } lm:build "nightly" { rule = "act_lua", args = { "@act.lua", "nightly", "host_os=" .. lm.os }, inputs = { "act.lua" }, } lm:phony "act" { inputs = "act.lua", } ================================================ FILE: script/compile_commands.lua ================================================ local subprocess = require "bee.subprocess" local ninja_file, output = ... assert(ninja_file, "missing ninja file path") assert(output, "missing output path") local process = assert(subprocess.spawn { "ninja", "-f", ninja_file, "-t", "compdb", "-x", searchPath = true, stdout = true, }) local content = process.stdout:read "a" local code = process:wait() if code ~= 0 then os.exit(code, true) end local file = assert(io.open(output, "wb")) file:write(content) ================================================ FILE: script/datalist2c.lua ================================================ local dlsrc, cname = ... local f = assert(io.open(dlsrc,"rb")) local bin = f:read "a" f:close() local code = [[ static const unsigned char dl_$name[] = { $bytes }; ]] local count = 0 local function tohex(c) local b = string.format("0x%02x,", c:byte()) count = count + 1 if count == 16 then b = b .. "\n\t" count = 0 end return b end local p = { name = cname:match "([^/]+)%.dl%.h$", bytes = string.gsub(bin .. "\0", ".", tohex), } local source = string.gsub(code, "$(%w+)", p) local f = assert(io.open(cname, "w")) f:write(source) f:close() ================================================ FILE: script/hashversion.lua ================================================ local function get_rev() local f = io.popen "git rev-parse HEAD" local rev = f:read "a" f:close() return rev:match "[%da-f]*" end local version = get_rev() print("SOLUNA_HASH_VERSION", version) local f = assert(io.open "src/version.h") local text = f:read "a" text = text:gsub('(SOLUNA_HASH_VERSION%s+")([%da-f]*)(")', "%1".. version .. "%3") f:close() local f = assert(io.open("src/version.h", "wb")) f:write(text) f:close() ================================================ FILE: script/lua2c.lua ================================================ local luasrc, cname = ... local s = assert(loadfile(luasrc)) local bin = string.dump(s) local code = [[ static const unsigned char luasrc_$name[] = { $bytes }; ]] local count = 0 local function tohex(c) local b = string.format("0x%02x,", c:byte()) count = count + 1 if count == 16 then b = b .. "\n\t" count = 0 end return b end local p = { name = cname:match "([^/]+)%.lua%.h$", bytes = string.gsub(bin .. "\0" , ".", tohex), } local source = string.gsub(code, "$(%w+)", p) local f = assert(io.open(cname, "w")) f:write(source) f:close() ================================================ FILE: src/appevent.h ================================================ #ifndef soluna_app_event_h #define soluna_app_event_h struct event_message { const char *typestr; int p1; int p2; int p3; }; static inline void get_xy(struct event_message *em, float x, float y) { float dpi_scale = sapp_dpi_scale(); float inv; if (dpi_scale <= 0.0f) { dpi_scale = 1.0f; inv = 1.0f; } else { inv = 1.0f / dpi_scale; } float logical_x = x * inv; float logical_y = y * inv; if (logical_x >= 0.0f) em->p1 = (int)(logical_x + 0.5f); else em->p1 = (int)(logical_x - 0.5f); if (logical_y >= 0.0f) em->p2 = (int)(logical_y + 0.5f); else em->p2 = (int)(logical_y - 0.5f); } static inline void mouse_message(struct event_message *em, const sapp_event* ev) { switch (ev->type) { case SAPP_EVENTTYPE_MOUSE_MOVE: em->typestr = "mouse_move"; get_xy(em, ev->mouse_x, ev->mouse_y); break; case SAPP_EVENTTYPE_MOUSE_DOWN: case SAPP_EVENTTYPE_MOUSE_UP: em->typestr = "mouse_button"; em->p1 = ev->mouse_button; em->p2 = ev->type == SAPP_EVENTTYPE_MOUSE_DOWN; break; case SAPP_EVENTTYPE_MOUSE_SCROLL: em->typestr = "mouse_scroll"; em->p1 = ev->scroll_y; em->p2 = ev->scroll_x; break; default: em->typestr = "mouse"; em->p1 = ev->type; break; } } static inline void touch_message(struct event_message *em, const sapp_event* ev) { // todo : support multi touch points // get 1st touch point now const sapp_touchpoint *t = &ev->touches[0]; get_xy(em, t->pos_x, t->pos_y); em->p3 = t->changed; switch (ev->type) { case SAPP_EVENTTYPE_TOUCHES_BEGAN: em->typestr = "touch_begin"; break; case SAPP_EVENTTYPE_TOUCHES_MOVED: em->typestr = "touch_moved"; break; case SAPP_EVENTTYPE_TOUCHES_ENDED: em->typestr = "touch_end"; break; case SAPP_EVENTTYPE_TOUCHES_CANCELLED: em->typestr = "touch_cancelled"; break; default: em->typestr = "touch"; break; } } static inline void window_message(struct event_message *em, const sapp_event *ev) { switch (ev->type) { case SAPP_EVENTTYPE_RESIZED: em->typestr = "window_resize"; em->p1 = ev->window_width; em->p2 = ev->window_height; break; default: em->typestr = "window"; em->p1 = ev->type; break; } } static inline void key_message(struct event_message *em, const sapp_event *ev) { switch (ev->type) { case SAPP_EVENTTYPE_CHAR: em->typestr = "char"; em->p1 = (int)ev->char_code; em->p2 = 0; break; default: em->typestr = "key"; em->p1 = (int)ev->key_code; em->p2 = ev->type == SAPP_EVENTTYPE_KEY_DOWN; break; } } static inline void app_event_unpack(struct event_message *em, const sapp_event* ev) { em->typestr = NULL; em->p1 = 0; em->p2 = 0; em->p3 = 0; switch (ev->type) { case SAPP_EVENTTYPE_MOUSE_MOVE: case SAPP_EVENTTYPE_MOUSE_DOWN: case SAPP_EVENTTYPE_MOUSE_UP: case SAPP_EVENTTYPE_MOUSE_SCROLL: case SAPP_EVENTTYPE_MOUSE_ENTER: case SAPP_EVENTTYPE_MOUSE_LEAVE: mouse_message(em, ev); break; case SAPP_EVENTTYPE_TOUCHES_BEGAN: case SAPP_EVENTTYPE_TOUCHES_MOVED: case SAPP_EVENTTYPE_TOUCHES_ENDED: case SAPP_EVENTTYPE_TOUCHES_CANCELLED: touch_message(em, ev); break; case SAPP_EVENTTYPE_RESIZED: window_message(em, ev); break; case SAPP_EVENTTYPE_CHAR: case SAPP_EVENTTYPE_KEY_DOWN: case SAPP_EVENTTYPE_KEY_UP: key_message(em, ev); break; default: em->typestr = "message"; em->p1 = ev->type; break; } } #endif ================================================ FILE: src/audio.c ================================================ #include #include #include "zipreader.h" #ifdef __EMSCRIPTEN__ #include #endif #define MA_NO_WIN32_FILEIO #define MA_NO_MP3 #define MA_NO_FLAC #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" FILE * fopen_utf8(const char *filename, const char *mode); static ma_result vfs_open_local(ma_vfs* pVFS, const char* pFilePath, ma_uint32 openMode, ma_vfs_file* pFile) { FILE* pFileStd; const char* pOpenModeStr; MA_ASSERT(pFilePath != NULL); MA_ASSERT(openMode != 0); MA_ASSERT(pFile != NULL); (void)pVFS; if ((openMode & MA_OPEN_MODE_READ) != 0) { if ((openMode & MA_OPEN_MODE_WRITE) != 0) { pOpenModeStr = "r+"; } else { pOpenModeStr = "rb"; } } else { pOpenModeStr = "wb"; } pFileStd = fopen_utf8(pFilePath, pOpenModeStr); if (pFileStd == NULL) { return MA_ERROR; } *pFile = pFileStd; return MA_SUCCESS; } struct custom_vfs { ma_default_vfs base; struct zipreader_name *zipnames; }; struct custom_engine { struct ma_engine engine; struct ma_resource_manager rm; struct custom_vfs vfs; }; struct audio_group { ma_sound_group group; int alive; }; struct audio_sound { ma_sound sound; int alive; }; #define AUDIO_GROUP_METATABLE "SOLUNA_AUDIO_GROUP" #define AUDIO_SOUND_METATABLE "SOLUNA_AUDIO_SOUND" static struct custom_engine * check_engine(lua_State *L, int index) { luaL_checktype(L, index, LUA_TLIGHTUSERDATA); return (struct custom_engine *)lua_touserdata(L, index); } static struct audio_group * check_group(lua_State *L, int index) { struct audio_group *group = (struct audio_group *)luaL_checkudata(L, index, AUDIO_GROUP_METATABLE); luaL_argcheck(L, group->alive, index, "closed audio group"); return group; } static struct audio_sound * check_sound(lua_State *L, int index) { struct audio_sound *sound = (struct audio_sound *)luaL_checkudata(L, index, AUDIO_SOUND_METATABLE); luaL_argcheck(L, sound->alive, index, "closed audio sound"); return sound; } static int push_error(lua_State *L, ma_result r) { lua_pushnil(L); lua_pushstring(L, ma_result_description(r)); return 2; } static int laudio_group_uninit(lua_State *L) { struct audio_group *group = (struct audio_group *)luaL_checkudata(L, 1, AUDIO_GROUP_METATABLE); if (group->alive) { ma_sound_group_uninit(&group->group); group->alive = 0; } return 0; } static int laudio_sound_uninit(lua_State *L) { struct audio_sound *sound = (struct audio_sound *)luaL_checkudata(L, 1, AUDIO_SOUND_METATABLE); if (sound->alive) { ma_sound_uninit(&sound->sound); sound->alive = 0; } return 0; } 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; if (openMode != MA_OPEN_MODE_READ) return MA_NOT_IMPLEMENTED; zipreader_file zf = zipreader_open(vfs->zipnames, pFilePath); if (zf == NULL) { return MA_ERROR; } *pFile = (ma_vfs_file)zf; return MA_SUCCESS; } static ma_result zr_close(ma_vfs* pVFS, ma_vfs_file file) { (void)pVFS; zipreader_close((zipreader_file)file); return MA_SUCCESS; } static ma_result zr_read(ma_vfs* pVFS, ma_vfs_file file, void* pDst, size_t sizeInBytes, size_t* pBytesRead) { (void)pVFS; int bytes = (int)sizeInBytes; if (bytes!= sizeInBytes || bytes < 0) return MA_OUT_OF_RANGE; int rd = zipreader_read((zipreader_file)file, pDst, bytes); if (rd < 0) return MA_IO_ERROR; *pBytesRead = rd; return MA_SUCCESS; } static ma_result zr_seek(ma_vfs* pVFS, ma_vfs_file file, ma_int64 offset, ma_seek_origin origin) { (void)pVFS; int whence; switch (origin) { case ma_seek_origin_start : whence = SEEK_SET; break; case ma_seek_origin_current : whence = SEEK_CUR; break; case ma_seek_origin_end : whence = SEEK_END; break; default : return MA_INVALID_ARGS; } if (zipreader_seek((zipreader_file)file, offset, whence) != 0) { return MA_ERROR; } return MA_SUCCESS; } static ma_result zr_tell(ma_vfs* pVFS, ma_vfs_file file, ma_int64* pCursor) { (void)pVFS; *pCursor = zipreader_tell((zipreader_file)file); if (*pCursor < 0) return MA_ERROR; return MA_SUCCESS; } static ma_result zr_info(ma_vfs* pVFS, ma_vfs_file file, ma_file_info* pInfo) { (void)pVFS; pInfo->sizeInBytes = zipreader_size((zipreader_file)file); return MA_SUCCESS; } static int laudio_init_vfs(lua_State *L) { struct custom_engine *e = (struct custom_engine *)lua_touserdata(L, 1); luaL_checktype(L, 2, LUA_TUSERDATA); e->vfs.zipnames = lua_touserdata(L, 2); e->vfs.base.cb.onOpen = zr_open; e->vfs.base.cb.onOpenW = NULL; e->vfs.base.cb.onClose = zr_close; e->vfs.base.cb.onRead = zr_read; e->vfs.base.cb.onWrite = NULL; e->vfs.base.cb.onSeek = zr_seek; e->vfs.base.cb.onTell = zr_tell; e->vfs.base.cb.onInfo = zr_info; return 0; } static int laudio_init(lua_State *L) { struct custom_engine *e = (struct custom_engine *)lua_newuserdatauv(L, sizeof(*e), 0); ma_default_vfs_init(&e->vfs.base, NULL); e->vfs.base.cb.onOpen = vfs_open_local; e->vfs.zipnames = NULL; ma_resource_manager_config config = ma_resource_manager_config_init(); config.pVFS = &e->vfs; ma_result r = ma_resource_manager_init(&config, &e->rm); if (r != MA_SUCCESS) { return luaL_error(L, "ma_resource_manager_init() error : %s", ma_result_description(r)); } ma_engine_config ec = ma_engine_config_init(); ec.pResourceManager = &e->rm; r = ma_engine_init(&ec, &e->engine); if (r != MA_SUCCESS) { return luaL_error(L, "ma_engine_init() error : %s", ma_result_description(r)); } e->rm.config.decodedFormat = ma_format_f32; e->rm.config.decodedSampleRate = ma_engine_get_sample_rate(&e->engine); lua_pushlightuserdata(L, (void *)e); return 2; } static int laudio_deinit(lua_State *L) { struct custom_engine *e = check_engine(L, 1); ma_engine_uninit(&e->engine); ma_resource_manager_uninit(&e->rm); return 0; } static int laudio_group_init(lua_State *L) { struct custom_engine *e = check_engine(L, 1); struct audio_group *group = (struct audio_group *)lua_newuserdatauv(L, sizeof(*group), 0); group->alive = 0; ma_result r = ma_sound_group_init(&e->engine, 0, NULL, &group->group); if (r != MA_SUCCESS) { lua_pop(L, 1); return push_error(L, r); } group->alive = 1; luaL_setmetatable(L, AUDIO_GROUP_METATABLE); return 1; } static int laudio_group_set_volume(lua_State *L) { struct audio_group *group = check_group(L, 1); float volume = (float)luaL_checknumber(L, 2); ma_sound_group_set_volume(&group->group, volume); return 0; } static int laudio_sound_init(lua_State *L) { struct custom_engine *e = check_engine(L, 1); const char *filename = luaL_checkstring(L, 2); ma_uint32 flags = (ma_uint32)luaL_optinteger(L, 3, 0); struct audio_group *group = NULL; if (!lua_isnoneornil(L, 4)) { group = check_group(L, 4); } struct audio_sound *sound = (struct audio_sound *)lua_newuserdatauv(L, sizeof(*sound), 0); sound->alive = 0; ma_result r = ma_sound_init_from_file(&e->engine, filename, flags, group ? &group->group : NULL, NULL, &sound->sound); if (r != MA_SUCCESS) { lua_pop(L, 1); return push_error(L, r); } sound->alive = 1; luaL_setmetatable(L, AUDIO_SOUND_METATABLE); return 1; } static int laudio_sound_start(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); ma_result r = ma_sound_start(&sound->sound); if (r != MA_SUCCESS) { return push_error(L, r); } lua_pushboolean(L, 1); return 1; } static int laudio_sound_stop(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); ma_result r; if (lua_isnoneornil(L, 2)) { r = ma_sound_stop(&sound->sound); } else { ma_uint64 fade_ms = (ma_uint64)luaL_checkinteger(L, 2); if (fade_ms == 0) { r = ma_sound_stop(&sound->sound); } else { r = ma_sound_stop_with_fade_in_milliseconds(&sound->sound, fade_ms); } } if (r != MA_SUCCESS) { return push_error(L, r); } lua_pushboolean(L, 1); return 1; } static int laudio_sound_playing(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); lua_pushboolean(L, ma_sound_is_playing(&sound->sound)); return 1; } static int laudio_sound_set_volume(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); float volume = (float)luaL_checknumber(L, 2); ma_sound_set_volume(&sound->sound, volume); return 0; } static int laudio_sound_set_pan(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); float pan = (float)luaL_checknumber(L, 2); ma_sound_set_pan(&sound->sound, pan); return 0; } static int laudio_sound_set_pitch(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); float pitch = (float)luaL_checknumber(L, 2); ma_sound_set_pitch(&sound->sound, pitch); return 0; } static int laudio_sound_set_looping(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); int looping = lua_toboolean(L, 2); ma_sound_set_looping(&sound->sound, looping); return 0; } static int laudio_sound_seek(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); float seconds = (float)luaL_checknumber(L, 2); ma_result r = ma_sound_seek_to_second(&sound->sound, seconds); if (r != MA_SUCCESS) { return push_error(L, r); } lua_pushboolean(L, 1); return 1; } static int laudio_sound_tell(lua_State *L) { struct audio_sound *sound = check_sound(L, 1); float seconds = 0.0f; ma_result r = ma_sound_get_cursor_in_seconds(&sound->sound, &seconds); if (r != MA_SUCCESS) { return push_error(L, r); } lua_pushnumber(L, seconds); return 1; } int luaopen_soluna_audio(lua_State *L) { luaL_checkversion(L); if (luaL_newmetatable(L, AUDIO_GROUP_METATABLE)) { lua_pushcfunction(L, laudio_group_uninit); lua_setfield(L, -2, "__gc"); } lua_pop(L, 1); if (luaL_newmetatable(L, AUDIO_SOUND_METATABLE)) { lua_pushcfunction(L, laudio_sound_uninit); lua_setfield(L, -2, "__gc"); } lua_pop(L, 1); luaL_Reg l[] = { { "init", laudio_init }, { "init_vfs", laudio_init_vfs }, { "deinit", laudio_deinit }, { "group_init", laudio_group_init }, { "group_uninit", laudio_group_uninit }, { "group_set_volume", laudio_group_set_volume }, { "sound_init", laudio_sound_init }, { "sound_uninit", laudio_sound_uninit }, { "sound_start", laudio_sound_start }, { "sound_stop", laudio_sound_stop }, { "sound_playing", laudio_sound_playing }, { "sound_set_volume", laudio_sound_set_volume }, { "sound_set_pan", laudio_sound_set_pan }, { "sound_set_pitch", laudio_sound_set_pitch }, { "sound_set_looping", laudio_sound_set_looping }, { "sound_seek", laudio_sound_seek }, { "sound_tell", laudio_sound_tell }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/batch.c ================================================ #include "batch.h" #include #include #include #define DEFAULT_SIZE 1024 struct draw_batch { int cap; struct draw_primitive * stream; }; struct draw_batch * batch_new(int size) { if (size < DEFAULT_SIZE) size = DEFAULT_SIZE; struct draw_batch * batch = (struct draw_batch *)malloc(sizeof(*batch)); if (batch == NULL) return NULL; batch->cap = size; batch->stream = (struct draw_primitive *)malloc(sizeof(struct draw_primitive) * size); if (batch->stream == NULL) { free(batch); return NULL; } return batch; } void batch_delete(struct draw_batch *B) { if (B == NULL) return; free(B->stream); free(B); } struct draw_primitive * batch_reserve(struct draw_batch *B, int size) { if (size <= B->cap) return B->stream; int cap = B->cap; do { cap = cap * 3/2; } while (cap < size); struct draw_primitive * stream = realloc(B->stream, cap * sizeof(struct draw_primitive)); if (stream == NULL) return NULL; B->stream = stream; B->cap = cap; return stream; } ================================================ FILE: src/batch.h ================================================ #ifndef soluna_batch_h #define soluna_batch_h #include struct draw_primitive { int32_t x; // sign bit + 23 + 8 fix number int32_t y; uint32_t sr; // scale + rot int32_t sprite; // negative : material }; struct draw_primitive_external { int sprite; }; struct draw_batch; struct draw_batch * batch_new(int size); struct draw_primitive * batch_reserve(struct draw_batch *, int size); void batch_delete(struct draw_batch *); #endif ================================================ FILE: src/blit.glsl ================================================ @vs vs out vec2 uv; void main() { vec2 position = vec2(gl_VertexIndex & 1, gl_VertexIndex >> 1); vec2 screen = position * 2.0 - 1.0; gl_Position = vec4(screen.x, -screen.y, 0.0, 1.0); uv = position; } @end @fs fs layout(binding=0) uniform texture2D tex; layout(binding=0) uniform sampler smp; in vec2 uv; out vec4 frag_color; void main() { frag_color = texture(sampler2D(tex, smp), uv); } @end @program blit vs fs ================================================ FILE: src/colorquad.glsl ================================================ @vs vs layout(binding=0) uniform vs_params { vec2 framesize; }; struct sr_mat { mat2 m; }; layout(binding=0) readonly buffer sr_lut { sr_mat sr[]; }; in vec4 position; in uint idx; in vec4 c; out vec4 color; void main() { ivec2 u2 = ivec2(0 , position.z); ivec2 v2 = ivec2(0 , position.w); vec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]); vec2 pos = (uv_offset * sr[idx].m + position.xy) * framesize; gl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1); color = c; } @end @fs fs in vec4 color; out vec4 frag_color; void main() { frag_color = color; } @end @program colorquad vs fs ================================================ FILE: src/data/settingdefault.dl ================================================ sprite_max : 0x40000 texture_size : 2048 srbuffer_size : 0x10000 batch_size : 65536 draw_instance : 65536 entry : main.lua project : soluna service_path : "./?.lua" width : 1024 height : 768 high_dpi : false window_title : soluna background : 0x4080c0 tmpbuffer_size : 0x20000 ================================================ FILE: src/drawmgr.c ================================================ #include #include #include #include "batch.h" #include "spritemgr.h" struct draw_element { struct draw_primitive * base; int n; int material; int texture; }; struct drawmgr { struct sprite_bank *bank; int cap; int n; int bank_n; struct draw_element data[1]; }; static int ldrawmgr_len(lua_State *L) { struct drawmgr * d = lua_touserdata(L, 1); lua_pushinteger(L, d->n); return 1; } static int ldrawmgr_index(lua_State *L) { struct drawmgr * d = lua_touserdata(L, 1); int idx = luaL_checkinteger(L, 2) - 1; if (idx < 0 || idx >= d->n) { return 0; } struct draw_element *e = &(d->data[idx]); lua_pushinteger(L, e->material); lua_pushlightuserdata(L, e->base); lua_pushinteger(L, e->n); if (e->texture >= 0) { lua_pushinteger(L, e->texture); return 4; } else { return 3; } } static int ldrawmgr_reset(lua_State *L) { struct drawmgr * d = (struct drawmgr *)luaL_checkudata(L, 1, "SOLUNA_DRAWMGR"); d->n = 0; d->bank_n = d->bank->n; return 0; } static int append_external_material(struct drawmgr * d, struct draw_primitive *base, int n, int matid, int texid) { int i; struct sprite_rect * rect = d->bank->rect; for (i=1;isprite; if (sprite >= 0 && rect[sprite].texid != texid) { break; } } struct draw_element *e = &d->data[d->n++]; e->base = base; e->n = i; e->material = -matid; e->texture = texid; return i; } static int append_default_material(struct drawmgr * d, struct draw_primitive *base, int n, int texid) { int i; struct sprite_rect * rect = d->bank->rect; int rect_n = d->bank_n; for (i=1;i rect_n) break; --sprite; if (texid != rect[sprite].texid) break; } struct draw_element *e = &d->data[d->n++]; e->base = base; e->n = i; e->material = 0; e->texture = texid; return i; } static int ldrawmgr_append(lua_State *L) { struct drawmgr * d = (struct drawmgr *)luaL_checkudata(L, 1, "SOLUNA_DRAWMGR"); struct draw_primitive *prim = (struct draw_primitive *)lua_touserdata(L, 2); int prim_n = luaL_checkinteger(L, 3); struct sprite_rect * rect = d->bank->rect; int rect_n = d->bank_n; int i; struct draw_primitive *end_ptr = &prim[prim_n]; for (i=0;isprite; if (d->n >= d->cap) { return luaL_error(L, "Too many draw"); } if (index <= 0) { if (i == prim_n || index == 0) { return luaL_error(L, "Invalid batch stream"); } struct draw_primitive_external * ext = (struct draw_primitive_external *)&prim[i+1]; int sprite = ext->sprite; int texid = -1; if (sprite >= 0) { texid = rect[sprite].texid; } i += append_external_material(d, p, (end_ptr - p)/2, index, texid) * 2; } else { --index; if (index >= rect_n) return luaL_error(L, "Invalid sprite id %d", index); int texid = rect[index].texid; i += append_default_material(d, p, end_ptr - p, texid); } } return 0; } static int ldrawmgr_new(lua_State *L) { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); void * bank = lua_touserdata(L, 1); int cap = luaL_checkinteger(L, 2); struct drawmgr * d = (struct drawmgr *)lua_newuserdatauv(L, sizeof(*d) + (cap-1)*sizeof(d->data[0]), 0); d->bank = (struct sprite_bank *)bank; d->cap = cap; d->n = 0; if (luaL_newmetatable(L, "SOLUNA_DRAWMGR")) { luaL_Reg l[] = { { "__index", NULL }, { "__len", ldrawmgr_len }, { "__call", ldrawmgr_index }, { "reset", ldrawmgr_reset }, { "append", ldrawmgr_append }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } int luaopen_drawmgr(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "new", ldrawmgr_new }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/embedlua.c ================================================ #include "bootstrap.lua.h" #include "service.lua.h" #include "log.lua.h" #include "timer.lua.h" #include "root.lua.h" #include "main.lua.h" #include "start.lua.h" #include "print_r.lua.h" #include "loader.lua.h" #include "spritebundle.lua.h" #include "render.lua.h" #include "settingdefault.dl.h" #include "settings.lua.h" #include "initsetting.lua.h" #include "fontmgr.lua.h" #include "gamepad.lua.h" #include "soluna.lua.h" #include "icon.lua.h" #include "layout.lua.h" #include "text.lua.h" #include "util.lua.h" #include "coroutine.lua.h" #include "packageloader.lua.h" #include "audio.lua.h" #include "matdefault.lua.h" #include "mattext.lua.h" #include "matquad.lua.h" #include "matmask.lua.h" #include "lua.h" #include "lauxlib.h" #define REG_SOURCE(name) \ lua_pushlightuserdata(L, (void *)luasrc_##name); \ lua_pushinteger(L, sizeof(luasrc_##name) - 1); \ lua_pushcclosure(L, get_string, 2); \ lua_setfield(L, -2, #name); #define REG_MATERIAL(name) \ lua_pushstring(L, #name); \ lua_rawseti(L, -2, luaL_len(L, -2) + 1); \ REG_SOURCE(name) #define REG_DATALIST(name) \ lua_pushlightuserdata(L, (void *)dl_##name); \ lua_pushinteger(L, sizeof(dl_##name) - 1); \ lua_pushcclosure(L, get_stringloader, 2); \ lua_setfield(L, -2, #name); static int get_string(lua_State *L) { const char * s = (const char *)lua_touserdata(L, lua_upvalueindex(1)); size_t sz = (size_t)lua_tointeger(L, lua_upvalueindex(2)); lua_pushexternalstring(L, s, sz, NULL, NULL); return 1; } static int get_stringloader(lua_State *L) { lua_pushvalue(L, lua_upvalueindex(1)); lua_pushvalue(L, lua_upvalueindex(2)); return 2; } int luaopen_embedsource(lua_State *L) { lua_newtable(L); lua_newtable(L); // runtime REG_SOURCE(bootstrap) REG_SOURCE(service) REG_SOURCE(main) REG_SOURCE(print_r) REG_SOURCE(fontmgr) REG_SOURCE(packageloader) lua_setfield(L, -2, "runtime"); lua_newtable(L); // runtime REG_SOURCE(spritebundle) REG_SOURCE(icon) REG_SOURCE(layout) REG_SOURCE(text) REG_SOURCE(soluna) REG_SOURCE(util) REG_SOURCE(coroutine) REG_SOURCE(initsetting) lua_setfield(L, -2, "lib"); lua_newtable(L); // service REG_SOURCE(log) REG_SOURCE(root) REG_SOURCE(timer) REG_SOURCE(start) REG_SOURCE(loader) REG_SOURCE(render) REG_SOURCE(gamepad) REG_SOURCE(settings) REG_SOURCE(audio) lua_setfield(L, -2, "service"); lua_newtable(L); // material REG_MATERIAL(matdefault) REG_MATERIAL(mattext) REG_MATERIAL(matquad) REG_MATERIAL(matmask) lua_setfield(L, -2, "material"); lua_newtable(L); // data list REG_DATALIST(settingdefault) lua_setfield(L, -2, "data"); return 1; } ================================================ FILE: src/entry.c ================================================ #define SOKOL_IMPL #include #include #include #include #include #include #include #include #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #define SOKOL_D3D11 #elif defined(__APPLE__) #define SOKOL_METAL #elif defined(__EMSCRIPTEN__) #define SOKOL_WGPU #elif defined(__linux__) #define SOKOL_GLCORE #else #error Unsupport platform #endif #include "version.h" #define FRAME_CALLBACK 1 #define CLEANUP_CALLBACK 2 #define EVENT_CALLBACK 3 #define CALLBACK_COUNT 3 #include "sokol/sokol_app.h" #include "sokol/sokol_gfx.h" #include "sokol/sokol_glue.h" #include "sokol/sokol_log.h" #include "sokol/sokol_args.h" #include "loginfo.h" #include "appevent.h" #include "ime_state.h" #if defined(__APPLE__) #include "platform/macos/soluna_macos_ime.h" #endif #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #include "platform/windows/soluna_windows_ime.h" #endif #if defined(__linux__) #include "platform/linux/soluna_linux_ime.h" #endif #if defined(__EMSCRIPTEN__) #include "platform/wasm/soluna_wasm_ime.h" #endif static void app_event(const sapp_event* ev); #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #define PLATFORM "windows" #elif defined(__APPLE__) #define PLATFORM "macos" #elif defined(__EMSCRIPTEN__) #define PLATFORM "wasm" #elif defined(__linux__) #define PLATFORM "linux" #else #define PLATFORM "unknown" #endif struct app_context { lua_State *L; lua_State *quitL; int (*send_log)(void *ud, unsigned int id, void *data, uint32_t sz); void *send_log_ud; void *mqueue; }; static struct app_context *CTX = NULL; struct soluna_ime_rect_state g_soluna_ime_rect = { 0.0f, 0.0f, 0.0f, 0.0f, 0, false }; void soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat); struct soluna_message { const char *type; union { int p[2]; uint64_t u64; } v; }; static inline struct soluna_message * message_create(const char *type, int p1, int p2) { struct soluna_message *msg = (struct soluna_message *)malloc(sizeof(*msg)); msg->type = type; msg->v.p[0] = p1; msg->v.p[1] = p2; return msg; } static inline struct soluna_message * message_create64(const char *type, uint64_t p) { struct soluna_message *msg = (struct soluna_message *)malloc(sizeof(*msg)); msg->type = type; msg->v.u64 = p; return msg; } static inline void message_release(struct soluna_message *msg) { free(msg); } void soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat) { sapp_event ev; memset(&ev, 0, sizeof(ev)); ev.type = SAPP_EVENTTYPE_CHAR; ev.frame_count = sapp_frame_count(); ev.char_code = codepoint; ev.modifiers = modifiers; ev.key_repeat = repeat; app_event(&ev); } static int lmessage_send(lua_State *L) { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); luaL_checktype(L, 2, LUA_TLIGHTUSERDATA); int (*send_message)(void *ud, void *p) = lua_touserdata(L, 1); void *send_message_ud = lua_touserdata(L, 2); const char * what = NULL; if (lua_type(L, 3) == LUA_TSTRING) { what = lua_tostring(L, 3); } else { luaL_checktype(L, 3, LUA_TLIGHTUSERDATA); what = (const char *)lua_touserdata(L, 3); } int64_t p1 = luaL_optinteger(L, 4, 0); struct soluna_message * msg = NULL; if (lua_isnoneornil(L, 5)) { msg = message_create64(what, p1); } else { int p2 = luaL_checkinteger(L, 5); msg = message_create(what, (int)p1, p2); } int fail = send_message(send_message_ud, msg); if (fail) { message_release(msg); } lua_pushboolean(L, !fail); return 1; } static int lmessage_unpack(lua_State *L) { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); struct soluna_message *m = (struct soluna_message *)lua_touserdata(L,1); lua_pushstring(L, m->type); lua_pushinteger(L, m->v.p[0]); lua_pushinteger(L, m->v.p[1]); lua_pushinteger(L, m->v.u64); message_release(m); return 4; } static void request_app_quit(void) { if (CTX) { CTX->quitL = CTX->L; CTX->L = NULL; } } static int lquit_signal(lua_State *L) { request_app_quit(); return 0; } #if defined(__EMSCRIPTEN__) EMSCRIPTEN_KEEPALIVE void soluna_runtime_quit(void) { request_app_quit(); } #endif static int levent_unpack(lua_State *L) { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); struct event_message em; app_event_unpack(&em, lua_touserdata(L, 1)); lua_pushlightuserdata(L, (void *)em.typestr); lua_pushinteger(L, em.p1); lua_pushinteger(L, em.p2); lua_pushinteger(L, em.p3); return 4; } static int lset_window_title(lua_State *L) { if (CTX == NULL || lua_type(L, 1) != LUA_TSTRING) return 0; const char * text = lua_tostring(L, 1); sapp_set_window_title(text); return 0; } struct icon_pixels { uint8_t *ptr; }; static void icon_free_pixels(struct icon_pixels *payloads, int count) { for (int i = 0; i < count; ++i) { free(payloads[i].ptr); } } static int icon_get_int(lua_State *L, int index, const char *field, const char *fallback) { int abs_index = lua_absindex(L, index); int value = 0; int type = lua_getfield(L, abs_index, field); if (type == LUA_TNUMBER) { value = (int)lua_tointeger(L, -1); lua_pop(L, 1); return value; } lua_pop(L, 1); if (fallback) { type = lua_getfield(L, abs_index, fallback); if (type == LUA_TNUMBER) { value = (int)lua_tointeger(L, -1); lua_pop(L, 1); return value; } lua_pop(L, 1); } luaL_error(L, "icon missing %s", field); return 0; } static void icon_copy_image(lua_State *L, int index, sapp_image_desc *dst, struct icon_pixels *payload) { int abs_index = lua_absindex(L, index); int width = icon_get_int(L, abs_index, "w", "width"); int height = icon_get_int(L, abs_index, "h", "height"); if (width <= 0 || height <= 0) { luaL_error(L, "icon size (%d * %d) must be positive", width, height); } size_t stride = 0; if (lua_getfield(L, abs_index, "stride") == LUA_TNUMBER) { stride = (size_t)lua_tointeger(L, -1); } lua_pop(L, 1); size_t explicit_size = 0; if (lua_getfield(L, abs_index, "size") == LUA_TNUMBER) { explicit_size = (size_t)lua_tointeger(L, -1); } lua_pop(L, 1); int type = lua_getfield(L, abs_index, "data"); const uint8_t *src = NULL; size_t src_size = 0; if (type == LUA_TSTRING) { src = (const uint8_t *)lua_tolstring(L, -1, &src_size); } else if (type == LUA_TUSERDATA) { src = (const uint8_t *)lua_touserdata(L, -1); src_size = lua_rawlen(L, -1); } else if (type == LUA_TLIGHTUSERDATA) { src = (const uint8_t *)lua_touserdata(L, -1); src_size = explicit_size; } else { lua_pop(L, 1); luaL_error(L, "icon.data must be buffer"); } lua_pop(L, 1); if (src == NULL) { luaL_error(L, "icon data missing"); } size_t row_bytes = (size_t)width * 4; if (stride == 0) { stride = row_bytes; } if (stride < row_bytes) { luaL_error(L, "icon stride < width"); } if (!(type == LUA_TLIGHTUSERDATA && explicit_size == 0)) { size_t required = stride * (size_t)height; if (src_size < required) { luaL_error(L, "icon buffer too small"); } } size_t copy_size = row_bytes * (size_t)height; uint8_t *copy = (uint8_t *)malloc(copy_size); if (copy == NULL) { luaL_error(L, "icon alloc fail"); } if (stride == row_bytes) { memcpy(copy, src, copy_size); } else { const uint8_t *s = src; uint8_t *d = copy; int y; for (y = 0; y < height; ++y) { memcpy(d, s, row_bytes); s += stride; d += row_bytes; } } dst->width = width; dst->height = height; dst->pixels.ptr = copy; dst->pixels.size = copy_size; payload->ptr = copy; } static float get_field_float(lua_State *L, int index, const char *field) { lua_getfield(L, index, field); if (lua_getfield(L, index, field) != LUA_TNUMBER) { return luaL_error(L, "Invalid .%s type (%s is not a number)", field, lua_typename(L, lua_type(L, -1))); } float value = (float)lua_tonumber(L, -1); lua_pop(L, 1); return value; } static int lset_icon(lua_State *L) { if (lua_isnoneornil(L, 1)) return 0; luaL_checktype(L, 1, LUA_TTABLE); sapp_icon_desc desc; memset(&desc, 0, sizeof(desc)); struct icon_pixels payloads[SAPP_MAX_ICONIMAGES]; memset(payloads, 0, sizeof(payloads)); int count = 0; int abs_index = lua_absindex(L, 1); if (lua_getfield(L, abs_index, "data") != LUA_TNIL) { lua_pop(L, 1); icon_copy_image(L, abs_index, &desc.images[count], &payloads[count]); ++count; } else { lua_pop(L, 1); int len = (int)lua_rawlen(L, abs_index); for (int i = 1; i <= len; ++i) { if (count >= SAPP_MAX_ICONIMAGES) { icon_free_pixels(payloads, count); luaL_error(L, "too many icon images"); } lua_rawgeti(L, abs_index, i); luaL_checktype(L, -1, LUA_TTABLE); icon_copy_image(L, -1, &desc.images[count], &payloads[count]); ++count; lua_pop(L, 1); } } if (count > 0) { sapp_set_icon(&desc); } icon_free_pixels(payloads, count); return 0; } static int lset_ime_rect(lua_State *L) { #if defined(__APPLE__) || defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) || defined(__linux__) || defined(__EMSCRIPTEN__) if (lua_isnoneornil(L, 1)) { g_soluna_ime_rect.text_color = 0; g_soluna_ime_rect.valid = false; #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) soluna_win32_apply_ime_rect(); #elif defined(__APPLE__) soluna_macos_hide_ime_label(); #elif defined(__linux__) soluna_linux_on_rect_cleared(); #elif defined(__EMSCRIPTEN__) soluna_wasm_hide(); #endif return 0; } luaL_checktype(L, 1, LUA_TTABLE); g_soluna_ime_rect.x = get_field_float(L, 1, "x"); g_soluna_ime_rect.y = get_field_float(L, 1, "y"); g_soluna_ime_rect.w = get_field_float(L, 1, "width"); g_soluna_ime_rect.h = get_field_float(L, 1, "height"); if (lua_getfield(L, 1, "text_color") == LUA_TNIL) { g_soluna_ime_rect.text_color = 0; } else { uint32_t color = (uint32_t)luaL_checkinteger(L, -1); if ((color & 0xff000000) == 0) { color |= 0xff000000; } g_soluna_ime_rect.text_color = color; } lua_pop(L, 1); g_soluna_ime_rect.valid = true; #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) soluna_win32_apply_ime_rect(); #elif defined(__APPLE__) soluna_macos_apply_ime_rect(); #elif defined(__linux__) soluna_linux_update_spot(); #elif defined(__EMSCRIPTEN__) soluna_wasm_apply_rect(); #endif #endif return 0; } static int lset_ime_font(lua_State *L) { const char *name = NULL; float size = 0.0f; int top = lua_gettop(L); if (top == 0) { #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) soluna_win32_reset_ime_font(); #endif #if defined(__APPLE__) soluna_macos_set_ime_font(NULL, 0.0f); #endif #if defined(__EMSCRIPTEN__) soluna_wasm_set_font(NULL, 0.0f); #endif return 0; } if (top == 1) { size = (float)luaL_checknumber(L, 1); } else { if (!lua_isnoneornil(L, 1)) { if (lua_type(L, 1) != LUA_TSTRING) { return luaL_error(L, "set_ime_font expects string font name"); } name = lua_tostring(L, 1); } size = (float)luaL_checknumber(L, 2); } if (size < 0.0f) { size = 0.0f; } #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) soluna_win32_set_ime_font(name, size); #endif #if defined(__APPLE__) soluna_macos_set_ime_font(name, size); #endif #if defined(__EMSCRIPTEN__) soluna_wasm_set_font(name, size); #endif return 0; } static int lclose_window(lua_State *L) { sapp_quit(); return 0; } static int lmqueue(lua_State *L) { if (CTX == NULL || CTX->mqueue == NULL) { return luaL_error(L, "Not init mqueue"); } lua_pushlightuserdata(L, CTX->mqueue); return 1; } static int lversion(lua_State *L) { lua_pushinteger(L, SOLUNA_API_VERSION); lua_pushstring(L, SOLUNA_HASH_VERSION); return 2; } static void desc_get_boolean(lua_State *L, bool *r, int index, const char * key) { if (lua_getfield(L, index, key) == LUA_TBOOLEAN) { *r = lua_toboolean(L, -1); } else { luaL_checktype(L, -1, LUA_TNIL); } lua_pop(L, 1); } static void desc_get_int(lua_State *L, int *r, int index, const char * key) { if (lua_getfield(L, index, key) == LUA_TNUMBER) { *r = lua_tointeger(L, -1); } else { luaL_checktype(L, -1, LUA_TNIL); } lua_pop(L, 1); } static void desc_get_string(lua_State *L, const char **r, int index, const char * key) { if (lua_getfield(L, index, key) == LUA_TSTRING) { *r = lua_tostring(L, -1); } else { luaL_checktype(L, -1, LUA_TNIL); } lua_pop(L, 1); } static int linit_desc(lua_State *L) { luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); luaL_checktype(L, 2, LUA_TTABLE); sapp_desc *d = lua_touserdata(L, 1); desc_get_boolean(L, &d->high_dpi, 2, "high_dpi"); desc_get_boolean(L, &d->fullscreen, 2, "fullscreen"); desc_get_int(L, &d->width, 2, "width"); desc_get_int(L, &d->height, 2, "height"); desc_get_string(L, &d->window_title, 2, "window_title"); return 0; } int luaopen_soluna_app(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "mqueue", lmqueue }, { "unpackmessage", lmessage_unpack }, { "sendmessage", lmessage_send }, { "unpackevent", levent_unpack }, { "set_window_title", lset_window_title }, { "set_icon", lset_icon }, { "set_ime_rect", lset_ime_rect }, { "set_ime_font", lset_ime_font }, { "quit", lquit_signal }, { "close_window", lclose_window }, { "platform", NULL }, { "version", lversion }, { "init_desc", linit_desc }, { NULL, NULL }, }; luaL_newlib(L, l); lua_pushliteral(L, PLATFORM); lua_setfield(L, -2, "platform"); return 1; } static void log_func(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data) { if (CTX == NULL || CTX->send_log == NULL) { fprintf(stderr, "%s (%d) : %s\n", filename, line_nr, message); return; } struct log_info *msg = (struct log_info *)malloc(sizeof(*msg)); if (tag) { strncpy(msg->tag, tag, sizeof(msg->tag)); msg->tag[sizeof(msg->tag)-1] = 0; } else { msg->tag[0] = 0; } msg->log_level = log_level; msg->log_item = log_item; msg->line_nr = line_nr; if (message) { strncpy(msg->message, message, sizeof(msg->message)); msg->message[sizeof(msg->message)-1] = 0; } else { msg->message[0] = 0; } msg->filename = filename; CTX->send_log(CTX->send_log_ud, 0, msg, sizeof(*msg)); } void soluna_openlibs(lua_State *L); static const char *code = "local embed = require 'soluna.embedsource' ; local f = load(embed.runtime.main()) ; return f(...)"; static int pmain(lua_State *L) { char** argv = (char **)lua_touserdata(L, 1); soluna_openlibs(L); int n = sargs_num_args(); luaL_checkstack(L, n+1, NULL); int i; lua_newtable(L); int arg_table = lua_gettop(L); for (i=0;isend_log = get_ud(L, "send_log"); ctx->send_log_ud = get_ud(L, "send_log_ud"); ctx->mqueue = get_ud(L, "mqueue"); if (get_function(L, "frame", FRAME_CALLBACK)) return 1; if (get_function(L, "cleanup", CLEANUP_CALLBACK)) return 1; if (get_function(L, "event", EVENT_CALLBACK)) return 1; lua_settop(L, CALLBACK_COUNT); return 0; } static int msghandler(lua_State *L) { const char *msg = lua_tostring(L, 1); luaL_traceback(L, L, msg, 1); return 1; } static void get_app_info(lua_State *L) { lua_newtable(L); const float dpi_scale = sapp_dpi_scale(); const float safe_scale = dpi_scale > 0.0f ? dpi_scale : 1.0f; const int fb_width = sapp_width(); const int fb_height = sapp_height(); const int logical_width = (int)((float)fb_width / safe_scale + 0.5f); const int logical_height = (int)((float)fb_height / safe_scale + 0.5f); lua_pushinteger(L, logical_width); lua_setfield(L, -2, "width"); lua_pushinteger(L, logical_height); lua_setfield(L, -2, "height"); } static int start_app(lua_State *L) { if (L == NULL) { fprintf(stderr, "Can't open lua state\n"); return 1; } if (lua_islightuserdata(L, 1)) { fprintf(stderr, "Init fatal : %s\n", (const char *)lua_touserdata(L, 1)); return 1; } if (lua_gettop(L) != 2) { fprintf(stderr, "Invalid lua stack (top = %d)\n", lua_gettop(L)); return 1; } if (lua_getfield(L, -1, "start") != LUA_TFUNCTION) { fprintf(stderr, "No start function\n"); return 1; } lua_replace(L, -2); get_app_info(L); if (lua_pcall(L, 1, 1, 1) != LUA_OK) { fprintf(stderr, "Start fatal : %s\n", lua_tostring(L, -1)); return 1; } else { return init_callback(L, CTX); } } static void app_init() { #if defined(__APPLE__) soluna_macos_install_ime(); #endif #if defined(__linux__) soluna_linux_ensure_im(); #endif #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) soluna_win32_install_wndproc(); #endif sg_setup(&(sg_desc) { .environment = sglue_environment(), .logger.func = log_func, }); lua_State *L = CTX->L; if (start_app(L)) { if (L) { lua_close(L); } CTX->L = NULL; CTX->quitL = NULL; sapp_quit(); } } static lua_State * get_L(struct app_context *ctx) { if (ctx == NULL) return NULL; lua_State *L = ctx->L; if (L == NULL) { if (ctx->quitL != NULL) { sapp_quit(); } } return L; } static void invoke_callback(lua_State *L, int index, int nargs) { lua_pushvalue(L, index); if (nargs > 0) { lua_insert(L, -nargs-1); } if (lua_pcall(L, nargs, 0, 0) != LUA_OK) { fprintf(stderr, "Error : %s\n", lua_tostring(L, -1)); lua_pop(L, 1); } } static void app_frame() { lua_State *L = get_L(CTX); if (L) { lua_pushinteger(L, sapp_frame_count()); invoke_callback(L, FRAME_CALLBACK, 1); } } static void app_cleanup() { if (CTX == NULL) return; lua_State *L = CTX->quitL; if (L == NULL) { L = CTX->L; CTX->L = NULL; } if (L) { invoke_callback(L, CLEANUP_CALLBACK, 0); lua_close(L); CTX->quitL = NULL; } #if defined(__linux__) soluna_linux_shutdown_ime(); #endif #if defined(__EMSCRIPTEN__) soluna_wasm_hide(); #endif sg_shutdown(); } static void app_event(const sapp_event* ev) { #if defined(__APPLE__) if (soluna_macos_is_composition_active() && (ev->type == SAPP_EVENTTYPE_KEY_DOWN || ev->type == SAPP_EVENTTYPE_KEY_UP)) { return; } #endif #if defined(__EMSCRIPTEN__) if (soluna_wasm_should_block_key_event(ev)) { return; } #endif #if defined(__linux__) if (soluna_linux_should_skip_event(ev)) { return; } #endif #if defined(__EMSCRIPTEN__) if (soluna_wasm_filter_char_event(ev)) { return; } #endif #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) if (ev->type == SAPP_EVENTTYPE_FOCUSED && g_soluna_ime_rect.valid) { soluna_win32_apply_ime_rect(); } #endif #if defined(__linux__) soluna_linux_handle_event(ev); #endif #if defined(__EMSCRIPTEN__) soluna_wasm_handle_event(ev); #endif lua_State *L = get_L(CTX); if (L) { lua_pushlightuserdata(L, (void *)ev); invoke_callback(L, EVENT_CALLBACK, 1); } } static int init_settings(lua_State *L, sapp_desc *desc) { if (lua_gettop(L) != 2) { lua_pushlightuserdata(L, (void *)"Invalid lua stack"); return 1; } if (lua_getfield(L, -1, "init") != LUA_TFUNCTION) { lua_pushlightuserdata(L, (void *)"No start function"); return 1; } lua_pushlightuserdata(L, (void *)desc); if (lua_pcall(L, 1, 0, 1) != LUA_OK) { const char * err = lua_tostring(L, -1); lua_pushlightuserdata(L, (void *)err); return 1; } else { return 0; } } sapp_desc sokol_main(int argc, char* argv[]) { // init sargs sargs_desc arg_desc; memset(&arg_desc, 0, sizeof(arg_desc)); arg_desc.argc = argc; arg_desc.argv = argv; sargs_setup(&arg_desc); // default sapp_desc sapp_desc d; memset(&d, 0, sizeof(d)); d.init_cb = app_init; d.frame_cb = app_frame; d.cleanup_cb = app_cleanup; d.event_cb = app_event; d.logger.func = log_func; d.win32.console_utf8 = 1; d.win32.console_attach = 1; d.alpha = 0; // init L static struct app_context app; lua_State *L = luaL_newstate(); if (L) { lua_settop(L, 0); lua_pushcfunction(L, msghandler); lua_pushcfunction(L, pmain); lua_pushlightuserdata(L, (void *)argv); if (lua_pcall(L, 1, 1, 1) != LUA_OK) { const char * err = lua_tostring(L, -1); lua_pushlightuserdata(L, (void *)err); lua_replace(L, 1); } sargs_shutdown(); if (init_settings(L, &d)) { lua_replace(L, 1); } } app.L = L; app.quitL = NULL; app.send_log = NULL; app.send_log_ud = NULL; app.mqueue = NULL; CTX = &app; return d; } ================================================ FILE: src/extapi.c ================================================ #include #include #include #include #include #include "sokol/sokol_gfx.h" #include "batch.h" #include "spritemgr.h" #include "render_bindings.h" #include "extapi_types.h" #define STREAM_FIX_SCALE 256.0f #define STREAM_FIX_INV_SCALE (1.0f / STREAM_FIX_SCALE) #define STREAM_ERROR_SIZE 128 #if defined(_MSC_VER) #define SOLUNA_THREAD_LOCAL __declspec(thread) #elif defined(__GNUC__) #define SOLUNA_THREAD_LOCAL __thread #else #define SOLUNA_THREAD_LOCAL _Thread_local #endif typedef void (*material_submit_stride_func)(void *ud, struct soluna_material_stream_context ctx, int n); struct material_stream_context_impl { const char *data; int n; int material_id; soluna_material_error error; char error_buffer[STREAM_ERROR_SIZE]; }; static SOLUNA_THREAD_LOCAL char submit_error_buffer[STREAM_ERROR_SIZE]; static struct material_stream_context_impl * stream_context(struct soluna_material_stream_context ctx) { return (struct material_stream_context_impl *)ctx.ctx; } static soluna_material_error copy_error(char *buffer, size_t size, const char *error) { const char *message = error != NULL ? error : "Material stream error"; size_t len = strlen(message); if (len >= size) { len = size - 1; } memcpy(buffer, message, len); buffer[len] = '\0'; return buffer; } void material_stream_error(struct soluna_material_stream_context ctx_, const char *error) { struct material_stream_context_impl *ctx = stream_context(ctx_); if (ctx != NULL && ctx->error == NULL) { ctx->error = copy_error(ctx->error_buffer, sizeof(ctx->error_buffer), error); } } int material_stream_failed(struct soluna_material_stream_context ctx_) { struct material_stream_context_impl *ctx = stream_context(ctx_); return ctx == NULL || ctx->error != NULL; } static soluna_material_error submit_material_stride(const void *data_, int prim_n, int material_id, int batch_n, void *ud, material_submit_stride_func submit, size_t stride) { const char *data = (const char *)data_; if (data == NULL) { return "Missing material stream"; } if (material_id <= 0) { return "Invalid material id"; } if (batch_n <= 0) { return "Invalid material submit batch"; } if (prim_n < 0) { return "Invalid material primitive count"; } if (submit == NULL) { return "Missing material submit function"; } if (stride == 0) { return "Invalid material submit stride"; } int i = 0; for (;;) { int n = prim_n - i; struct material_stream_context_impl impl = { .data = data, .n = n > batch_n ? batch_n : n, .material_id = material_id, .error = NULL, .error_buffer = { 0 }, }; struct soluna_material_stream_context ctx = { .ctx = &impl, }; if (n > batch_n) { submit(ud, ctx, batch_n); if (impl.error != NULL) { return copy_error(submit_error_buffer, sizeof(submit_error_buffer), impl.error); } i += batch_n; data += stride * batch_n; } else { submit(ud, ctx, n); if (impl.error != NULL) { return copy_error(submit_error_buffer, sizeof(submit_error_buffer), impl.error); } break; } } return NULL; } soluna_material_error material_submit(const void *stream, int prim_n, int material_id, int batch_n, void *ud, soluna_material_submit_func submit) { return submit_material_stride(stream, prim_n, material_id, batch_n, ud, submit, sizeof(struct draw_primitive) * 2); } int material_sprite_rect(struct soluna_sprite_bank bank, int sprite, struct soluna_sprite_rect *out) { struct sprite_bank *b = (struct sprite_bank *)bank.ctx; if (b == NULL || out == NULL || sprite < 0 || sprite >= b->n) { return 0; } struct sprite_rect *r = &b->rect[sprite]; out->texture = r->texid; out->u = (float)(r->u >> 16); out->v = (float)(r->v >> 16); out->w = (float)(r->u & 0xffffu); out->h = (float)(r->v & 0xffffu); out->ox = (float)((r->off >> 16) & 0xffffu) - 0x8000; out->oy = (float)(r->off & 0xffffu) - 0x8000; return 1; } sg_bindings material_bindings(struct soluna_render_bindings bindings) { struct render_bindings *b = (struct render_bindings *)bindings.ctx; assert(b != NULL); return b->bindings; } static size_t stream_payload_max(void) { return sizeof(struct draw_primitive) - sizeof(struct draw_primitive_external); } void material_stream_free(void *ptr) { free(ptr); } soluna_material_error material_push_stream(int material_id, int count, size_t payload_size, soluna_material_stream_write_func write, void *ud, struct soluna_material_stream *out) { size_t payload_max = stream_payload_max(); if (out == NULL) { return "Missing material stream output"; } out->data = NULL; out->size = 0; if (material_id <= 0) { return "Invalid material id"; } if (count < 0) { return "Invalid material stream count"; } if (payload_size > payload_max) { return "Invalid material payload size"; } if (write == NULL) { return "Missing material stream writer"; } size_t item_size = sizeof(struct draw_primitive) * 2; if ((size_t)count > (~(size_t)0 - 1) / item_size) { return "Material stream is too large"; } size_t stream_size = item_size * (size_t)count; char *buffer = (char *)malloc(stream_size + 1); if (buffer == NULL) { return "No memory for material stream"; } struct draw_primitive *stream = (struct draw_primitive *)buffer; int i; for (i=0; i 0) { if (item.payload == NULL) { material_stream_free(buffer); return "Missing material stream payload"; } memcpy((char *)ext_prim + sizeof(*ext), item.payload, payload_size); } pos->x = (int32_t)(item.x * STREAM_FIX_SCALE); pos->y = (int32_t)(item.y * STREAM_FIX_SCALE); pos->sprite = -material_id; ext->sprite = item.sprite; } buffer[stream_size] = '\0'; out->data = buffer; out->size = stream_size; return NULL; } static void clear_stream_read(size_t payload_size, void *payload, struct soluna_material_stream_data *out) { if (out != NULL) { memset(out, 0, sizeof(*out)); } if (payload != NULL && payload_size > 0 && payload_size <= stream_payload_max()) { memset(payload, 0, payload_size); } } static int fail_stream_read(struct soluna_material_stream_context ctx, const char *error, size_t payload_size, void *payload, struct soluna_material_stream_data *out) { material_stream_error(ctx, error); clear_stream_read(payload_size, payload, out); return 0; } int material_stream_read(struct soluna_material_stream_context ctx_, int index, size_t payload_size, void *payload, struct soluna_material_stream_data *out) { struct material_stream_context_impl *ctx = stream_context(ctx_); size_t payload_max = stream_payload_max(); if (ctx == NULL) { clear_stream_read(payload_size, payload, out); return 0; } if (ctx->error != NULL) { clear_stream_read(payload_size, payload, out); return 0; } if (payload_size > payload_max) { return fail_stream_read(ctx_, "Invalid material payload size", payload_size, payload, out); } if (ctx->data == NULL) { return fail_stream_read(ctx_, "Missing material stream", payload_size, payload, out); } if (index < 0) { return fail_stream_read(ctx_, "Invalid material stream index", payload_size, payload, out); } if (index >= ctx->n) { return fail_stream_read(ctx_, "Invalid material stream index", payload_size, payload, out); } if (ctx->material_id <= 0) { return fail_stream_read(ctx_, "Invalid material id", payload_size, payload, out); } if (out == NULL) { return fail_stream_read(ctx_, "Missing material stream output", payload_size, payload, out); } if (payload_size > 0 && payload == NULL) { return fail_stream_read(ctx_, "Missing material stream payload output", payload_size, payload, out); } const struct draw_primitive *prim = (const struct draw_primitive *)ctx->data; const struct draw_primitive *pos = &prim[index * 2]; const struct draw_primitive *ext_prim = pos + 1; const struct draw_primitive_external *ext = (const struct draw_primitive_external *)ext_prim; if (pos->sprite != -ctx->material_id) { return fail_stream_read(ctx_, "Invalid material marker", payload_size, payload, out); } out->x = (float)pos->x * STREAM_FIX_INV_SCALE; out->y = (float)pos->y * STREAM_FIX_INV_SCALE; out->sprite = ext->sprite; if (payload_size > 0) { memcpy(payload, (const char *)ext_prim + sizeof(*ext), payload_size); } return 1; } ================================================ FILE: src/extapi_types.h ================================================ // AUTO GENERATED by ../src/extapi_types.temp.h, DONT EDIT #ifndef SOLUNA_EXTAPI_TYPES_H #define SOLUNA_EXTAPI_TYPES_H #include #include "sokol/sokol_gfx.h" #define SOLUNA_EXT_API_VERSION 1 struct soluna_sprite_rect { int texture; float u; float v; float w; float h; float ox; float oy; }; struct soluna_material_stream_item { float x; float y; int sprite; const void *payload; }; struct soluna_material_stream_data { float x; float y; int sprite; }; struct soluna_material_stream { char *data; size_t size; }; typedef const char *soluna_material_error; struct soluna_material_stream_context { void *ctx; }; struct soluna_render_bindings { void *ctx; }; struct soluna_sprite_bank { void *ctx; }; typedef void (*soluna_material_submit_func)(void *ud, struct soluna_material_stream_context ctx, int n); typedef void (*soluna_material_stream_write_func)(void *ud, int index, struct soluna_material_stream_item *item); #endif ================================================ FILE: src/extapi_types.temp.h ================================================ #ifndef SOLUNA_EXTAPI_TYPES_H #define SOLUNA_EXTAPI_TYPES_H $HOST_TYPE_DECL$ #endif ================================================ FILE: src/external.c ================================================ #include #include #include #include struct lua_api; struct sokol_api; struct soluna_api; extern struct lua_api * extlua_api(); extern struct sokol_api * extlua_sokol_api(); extern struct soluna_api * extlua_soluna_api(); struct extlua_apis { struct lua_api * lua; struct sokol_api * sokol; struct soluna_api * soluna; }; static struct extlua_apis * host_apis() { static struct extlua_apis apis; apis.lua = extlua_api(); apis.sokol = extlua_sokol_api(); apis.soluna = extlua_soluna_api(); return &apis; } static void init_extraspace(lua_State *L) { struct extlua_apis **ex = (struct extlua_apis **)lua_getextraspace(L); *ex = host_apis(); } static int get_reg(lua_State *L) { luaL_Reg *l = (luaL_Reg *)lua_touserdata(L, 1); lua_pushnil(L); int i = 0; while (lua_next(L, 2) != 0) { l[i].name = lua_tostring(L, -2); l[i].func = lua_tocfunction(L, -1); if (l[i].name == NULL || l[i].func == NULL) { return luaL_error(L, "Invalid reg table"); } lua_pop(L, 1); ++i; } return 0; } static int count_table(lua_State *L, int idx) { idx = lua_absindex(L, idx); lua_pushnil(L); int n = 0; while (lua_next(L, idx) != 0) { ++n; lua_pop(L, 1); } return n; } static void register_libs(lua_State *L, lua_State *dL, int loadonly) { if (lua_gettop(dL) == 0 || lua_type(dL, 1) != LUA_TTABLE) { luaL_error(L, "Invalid external libs, maybe lua version mismatch"); } int n = count_table(dL, 1); int tbl_index = lua_gettop(L); luaL_Reg *l = lua_newuserdatauv(L, sizeof(luaL_Reg) * n, 0); lua_pushcfunction(dL, get_reg); lua_pushlightuserdata(dL, (void *)l); lua_pushvalue(dL, 1); if (lua_pcall(dL, 2, 0, 0) != LUA_OK) { lua_pushstring(L, lua_tostring(dL, -1)); lua_error(L); } int i; if (loadonly) { for (i=0;iname, rb->name); } static int preload_libs(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); if (PRELOAD.l != NULL) { return luaL_error(L, "Already preload"); } lua_newtable(L); int result_index = lua_gettop(L); int i=0; while (lua_geti(L, 1, ++i) != LUA_TNIL) { lua_CFunction init = lua_tocfunction(L, -1); if (init == NULL) return luaL_error(L, "Invalid init function at %d", i); lua_pop(L, 1); preload_lib(L, init, result_index); } lua_pop(L, 1); int n = count_table(L, result_index); struct luaL_Reg *l = lua_newuserdatauv(L, sizeof(luaL_Reg) * n, 1); // ref name strings lua_pushvalue(L, result_index); lua_setiuservalue(L, -2, 1); lua_pushcfunction(L, get_reg); lua_pushvalue(L, -2); // l lua_pushvalue(L, result_index); lua_call(L, 2, 0); qsort(l, n, sizeof(luaL_Reg), cmpreg); PRELOAD.l = l; PRELOAD.n = n; lua_setfield(L, LUA_REGISTRYINDEX, "EXTLIBS"); return 1; } static lua_CFunction find_func(luaL_Reg *l, int n, const char *name) { int begin = 0; int end = n; while (begin < end) { int mid = (begin + end) / 2; int c = strcmp(name, l[mid].name); if (c == 0) return l[mid].func; else if (c < 0) { end = mid; } else { begin = mid + 1; } } return NULL; } static int searcher(lua_State *L) { if (PRELOAD.l == NULL || PRELOAD.n == 0) return 0; if (lua_gettop(L) == 0) { // test preload table lua_pushboolean(L, 1); return 1; } const char * name = lua_tostring(L, 1); lua_CFunction func = find_func(PRELOAD.l, PRELOAD.n, name); if (func == NULL) { lua_pushfstring(L, "No preload extlua '%s'", name); return 1; } lua_pushcfunction(L, func); return 1; } int luaopen_extlua(lua_State *L) { luaL_Reg l[] = { { "load", load_libs }, { "preload", preload_libs }, { "searcher", searcher }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/file.c ================================================ #include #include #include #include FILE * fopen_utf8(const char *filename, const char *mode); static int lfile_exist(lua_State *L) { const char *filename = luaL_checkstring(L, 1); const char *mode = "rb"; FILE *f = fopen_utf8(filename, mode); if (f == NULL) return 0; fclose(f); lua_pushboolean(L, 1); return 1; } static void * external_free(void *ud, void *ptr, size_t osize, size_t nsize) { free(ptr); return NULL; } static int lfile_load(lua_State *L) { const char *filename = luaL_checkstring(L, 1); const char *mode = luaL_optstring(L, 2, "rb"); FILE *f = fopen_utf8(filename, mode); if (f == NULL) return 0; fseek(f, 0, SEEK_END); size_t sz = ftell(f); fseek(f, 0, SEEK_SET); char * buffer = (char *)malloc(sz+1); if (buffer == NULL) { fclose(f); return luaL_error(L, "lfile_load_string : Out of memory"); } buffer[sz] = 0; size_t rd = fread(buffer, 1, sz, f); fclose(f); if (rd != sz) { free(buffer); return luaL_error(L, "Read %s fail", filename); } lua_pushexternalstring(L, buffer, sz, external_free, NULL); return 1; } int luaopen_soluna_file(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "exist", lfile_exist }, { "load", lfile_load }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/font.c ================================================ #include "font_manager.h" #include "truetype.h" #include #include #include #include #include #include #include #include "luabuffer.h" #include "stb/stb_image_write.h" static struct { struct font_manager *mgr; } G; static struct font_manager* getF(lua_State *L) { if (G.mgr == NULL) luaL_error(L, "Init font manager first"); return G.mgr; } static int lsubmit(lua_State *L){ struct font_manager *F = getF(L); int dirty = font_manager_flush(F); lua_pushboolean(L, dirty); return 1; } static int limport(lua_State *L) { struct font_manager *F = getF(L); size_t sz; void* fontdata = (void *)luaL_getbuffer(L, &sz); font_manager_import(F, fontdata, sz); return 0; } static int lname(lua_State *L) { struct font_manager *F = getF(L); const char* family = luaL_checkstring(L, 1); const int fontid = font_manager_addfont_with_family(F, family); if (fontid > 0){ lua_pushinteger(L, fontid); return 1; } return 0; } static int ltexture(lua_State *L) { struct font_manager *F = getF(L); int size = 0; const void * ptr = font_manager_texture(F, &size); lua_pushlightuserdata(L, (void *)ptr); lua_pushinteger(L, size * size); return 2; } static int ltexture_write(lua_State *L) { struct font_manager *F = getF(L); int size = 0; const void * ptr = font_manager_texture(F, &size); const char * filename = luaL_checkstring(L, 1); if (!stbi_write_png(filename, size, size, 1, ptr, size)) { return luaL_error(L, "Write %s failed", filename); } return 0; } static int ltouch(lua_State *L) { struct font_manager *F = getF(L); int fontid = luaL_checkinteger(L, 1); int codepoint = luaL_checkinteger(L, 2); struct font_glyph tmp1, tmp2; font_manager_glyph(F, fontid, codepoint, 16, &tmp1, &tmp2); return 0; } static int lcobj(lua_State *L) { struct font_manager *F = getF(L); lua_pushlightuserdata(L, (void *)F); return 1; } static int limport_icon(lua_State *L) { struct font_manager *F = getF(L); luaL_checktype(L, 1, LUA_TUSERDATA); void *data = lua_touserdata(L, 1); size_t sz = lua_rawlen(L, 1); int n = sz / FONT_MANAGER_GLYPHSIZE / FONT_MANAGER_GLYPHSIZE; if (n * FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE != sz) return luaL_error(L, "Invalid icon bundle size"); font_manager_icon_init(F, n, data); return 0; } static int lsize(lua_State *L) { struct font_manager *F = getF(L); int font_id = luaL_checkinteger(L, 1); if (font_id <= 0) { return luaL_error(L, "Invalid font_id %d", font_id); } int size = luaL_checkinteger(L, 2); int ascent, descent, lineGap; font_manager_fontheight(F, font_id, size, &ascent, &descent, &lineGap); if (!lua_istable(L, 3)) { lua_settop(L, 2); lua_createtable(L, 0, 3); } else { lua_settop(L, 3); } lua_pushinteger(L, ascent); lua_setfield(L, 3, "ascent"); lua_pushinteger(L, descent); lua_setfield(L, 3, "descent"); lua_pushinteger(L, lineGap); lua_setfield(L, 3, "lineGap"); return 1; } #define MAX_FONTNAME 256 static int llist(lua_State *L) { struct font_manager *F = getF(L); int i = 1; lua_newtable(L); char buf[MAX_FONTNAME]; for (;;) { int r = font_manager_enum_fontname(F, i, buf, MAX_FONTNAME); if (r <= 0) break; if (r >= MAX_FONTNAME) { char *tmp = (char *)malloc(r + 1); font_manager_enum_fontname(F, i, tmp, r+1); lua_pushlstring(L, tmp, r); free(tmp); } else { lua_pushlstring(L, buf, r); } lua_seti(L, -2, i); ++i; } return 1; } int luaopen_font(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "texture", ltexture }, { "texture_write", ltexture_write }, { "touch", ltouch }, // for debug { "import", limport }, { "name", lname }, { "size", lsize }, { "submit", lsubmit }, { "cobj", lcobj }, { "texture_size", NULL }, { "import_icon", limport_icon }, { "list", llist }, { NULL, NULL }, }; luaL_newlib(L, l); lua_pushinteger(L, FONT_MANAGER_TEXSIZE); lua_setfield(L, -2, "texture_size"); return 1; } void soluna_openlibs(lua_State *L); static int luavm_init(lua_State *L) { soluna_openlibs(L); const char* data = (const char*)lua_touserdata(L, 1); size_t size = (size_t)lua_tointeger(L, 2); const char* chunkname = (const char*)lua_touserdata(L, 3); if (luaL_loadbuffer(L, data, size, chunkname) != LUA_OK) { return lua_error(L); } lua_call(L, 0, 0); return 0; } static int fontm_init(lua_State *L) { if (G.mgr != NULL) { return luaL_error(L, "Do not init font manager twice"); } struct font_manager* F = (struct font_manager *)malloc(font_manager_sizeof()); if (F == NULL) { return luaL_error(L, "not enough memory"); } size_t sz; const char * src = luaL_checklstring(L, 1, &sz); lua_State* managerL = luaL_newstate(); if (!managerL) { free(F); return luaL_error(L, "not enough memory"); } lua_pushcfunction(managerL, luavm_init); lua_pushlightuserdata(managerL, (void *)src); lua_pushinteger(managerL, sz); lua_pushlightuserdata(managerL, (void*)luaL_checkstring(L, 2)); if (lua_pcall(managerL, 3, 0, 0) != LUA_OK) { lua_pushstring(L, lua_tostring(managerL, -1)); lua_close(managerL); free(F); return lua_error(L); } font_manager_init(F, managerL); G.mgr = F; return 0; } static int fontm_shutdown(lua_State *L) { struct font_manager* F = G.mgr; if (F == NULL) return 0; G.mgr = NULL; void* managerL = font_manager_shutdown(F); if (managerL) { lua_close((lua_State*)managerL); } free(F); return 0; } int luaopen_font_manager(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "init", fontm_init }, { "shutdown", fontm_shutdown }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/font_define.h ================================================ #ifndef soluna_font_define_h #define soluna_font_define_h #define FONT_MANAGER_TEXSIZE 2048 #define FONT_MANAGER_GLYPHSIZE 64 #define FONT_POSTION_FIX_POINT 8 #define MAX_FONT_NUM 64 #include struct font_glyph { int16_t offset_x; int16_t offset_y; int16_t advance_x; int16_t advance_y; uint16_t w; uint16_t h; uint16_t u; uint16_t v; }; #define IMAGE_FONT_MASK 0x40 //7 bit #define FONT_ID_MASK 0x3F //low 6 bits #define FONT_ICON 255 static inline uint32_t codepoint_key(int font, int codepoint) { return (uint32_t)((font << 24) | codepoint); } static inline int font_index(int fontid){ return FONT_ID_MASK&((uint8_t)fontid); } #endif ================================================ FILE: src/font_manager.c ================================================ #include "font_manager.h" #include "mutex.h" #include "truetype.h" #include #include #include #include #define STB_TRUETYPE_IMPLEMENTATION #include #define FONT_MANAGER_SLOTLINE (FONT_MANAGER_TEXSIZE/FONT_MANAGER_GLYPHSIZE) #define FONT_MANAGER_SLOTS (FONT_MANAGER_SLOTLINE*FONT_MANAGER_SLOTLINE) #define FONT_MANAGER_HASHSLOTS (FONT_MANAGER_SLOTS * 2) // -------------- // // xmin xmax // | | // |<-------- width -------->| // | | // | +-------------------------+----------------- ymax // | | ggggggggg ggggg | ^ ^ // | | g:::::::::ggg::::g | | | // | | g:::::::::::::::::g | | | // | | g::::::ggggg::::::gg | | | // | | g:::::g g:::::g | | | // offset_x -|-------->| g:::::g g:::::g | offset_y | // | | g:::::g g:::::g | | | // | | g::::::g g:::::g | | | // | | g:::::::ggggg:::::g | | | // | | g::::::::::::::::g | | height // | | gg::::::::::::::g | | | // baseline ---*---------|---- gggggggg::::::g-----*-------- | // / | | g:::::g | | // origin | | g:::::gg gg:::::g | | // | | g:::::gg gg:::::g | | // | | g::::::ggg:::::::g | | // | | gg:::::::::::::g | | // | | ggg::::::ggg | | // | | gggggg | v // | +-------------------------+----------------- ymin // | | // |------------- advance_x ---------->| struct font_slot { uint32_t codepoint_key; // high 8 bits (ttf index) int16_t offset_x; int16_t offset_y; int16_t advance_x; int16_t advance_y; uint16_t w; uint16_t h; }; struct priority_list { int version; int16_t prev; int16_t next; }; struct truetype_font; struct font_manager { int version; int count; int16_t list_head; struct font_slot slots[FONT_MANAGER_SLOTS]; struct priority_list priority[FONT_MANAGER_SLOTS]; int16_t hash[FONT_MANAGER_HASHSLOTS]; struct truetype_font* ttf; void *L; int dpi_perinch; int dirty; int icon_n; unsigned char *icon_data; mutex_t mutex; uint8_t texture_buffer[FONT_MANAGER_TEXSIZE*FONT_MANAGER_TEXSIZE]; }; const void * font_manager_texture(struct font_manager *F, int *sz) { *sz = FONT_MANAGER_TEXSIZE; return F->texture_buffer; } /* F->priority is a circular linked list for the LRU cache. F->hash is for lookup with [font, codepoint]. */ #define COLLISION_STEP 7 #define DISTANCE_OFFSET 8 #define ORIGINAL_SIZE (FONT_MANAGER_GLYPHSIZE - DISTANCE_OFFSET * 2) #define ONEDGE_VALUE 180 #define PIXEL_DIST_SCALE (ONEDGE_VALUE/(float)(DISTANCE_OFFSET)) static const int SAPCE_CODEPOINT[] = { ' ', '\t', '\n', '\r', }; static inline int is_space_codepoint(int codepoint){ for (int ii=0; ii < sizeof(SAPCE_CODEPOINT)/sizeof(SAPCE_CODEPOINT[0]); ++ii){ if (codepoint == SAPCE_CODEPOINT[ii]){ return 1; } } return 0; } static inline void lock(struct font_manager *F) { mutex_acquire(F->mutex); } static inline void unlock(struct font_manager *F) { mutex_release(F->mutex); } static inline const stbtt_fontinfo* get_ttf_unsafe(struct font_manager *F, int fontid){ return truetype_font(F->ttf, fontid, F->L); } static inline const stbtt_fontinfo * get_ttf(struct font_manager *F, int fontid) { lock(F); const stbtt_fontinfo * r = get_ttf_unsafe(F, fontid); unlock(F); return r; } static inline int ttf_with_family(struct font_manager *F, const char* family){ return truetype_name(F->L, family); } static inline int hash(int value) { return (value * 0xdeece66d + 0xb) % FONT_MANAGER_HASHSLOTS; } static int hash_lookup(struct font_manager *F, int cp) { int slot; int position = hash(cp); while ((slot = F->hash[position]) >= 0) { struct font_slot * s = &F->slots[slot]; if (s->codepoint_key == cp) return slot; position = (position + COLLISION_STEP) % FONT_MANAGER_HASHSLOTS; } return -1; } static void rehash(struct font_manager *F); static void hash_insert(struct font_manager *F, int cp, int slotid) { ++F->count; if (F->count > FONT_MANAGER_SLOTS + FONT_MANAGER_SLOTS/2) { rehash(F); } int position = hash(cp); int slot; while ((slot = F->hash[position]) >= 0) { struct font_slot * s = &F->slots[slot]; if (s->codepoint_key < 0) break; assert(s->codepoint_key != cp); position = (position + COLLISION_STEP) % FONT_MANAGER_HASHSLOTS; } F->hash[position] = slotid; F->slots[slotid].codepoint_key = cp; } static void rehash(struct font_manager *F) { int i; for (i=0;ihash[i] = -1; // reset slots } F->count = 0; int count = 0; (void)count; for (i=0;islots[i].codepoint_key; if (cp >= 0) { assert(++count <= FONT_MANAGER_SLOTS); hash_insert(F, cp, i); } } } static void remove_node(struct font_manager *F, struct priority_list *node) { struct priority_list *prev_node = &F->priority[node->prev]; struct priority_list *next_node = &F->priority[node->next]; prev_node->next = node->next; next_node->prev = node->prev; } static void touch_slot(struct font_manager *F, int slotid) { struct priority_list *node = &F->priority[slotid]; node->version = F->version; if (slotid == F->list_head) return; remove_node(F, node); // insert before head int head = F->list_head; int tail = F->priority[head].prev; node->prev = tail; node->next = head; struct priority_list *head_node = &F->priority[head]; struct priority_list *tail_node = &F->priority[tail]; head_node->prev = slotid; tail_node->next = slotid; F->list_head = slotid; } static int get_icon(struct font_manager *F, int cp, struct font_glyph *glyph) { if (cp < 0 || cp >= F->icon_n) { memset(glyph, 0, sizeof(*glyph)); return -1; } glyph->offset_x = 0; glyph->offset_y = -DISTANCE_OFFSET/2; glyph->advance_x = FONT_MANAGER_GLYPHSIZE; glyph->advance_y = FONT_MANAGER_GLYPHSIZE; glyph->w = FONT_MANAGER_GLYPHSIZE; glyph->h = FONT_MANAGER_GLYPHSIZE; glyph->u = 0; glyph->v = 0; return 0; } // 1 exist in cache. 0 not exist in cache , call font_manager_update. -1 failed. static int font_manager_touch_unsafe(struct font_manager *F, int font, int codepoint, struct font_glyph *glyph) { int cp = codepoint_key(font, codepoint); int slot = hash_lookup(F, cp); if (slot >= 0) { touch_slot(F, slot); struct font_slot *s = &F->slots[slot]; glyph->offset_x = s->offset_x; glyph->offset_y = s->offset_y; glyph->advance_x = s->advance_x; glyph->advance_y = s->advance_y; glyph->w = s->w; glyph->h = s->h; glyph->u = (slot % FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE; glyph->v = (slot / FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE; return 1; } int last_slot = F->priority[F->list_head].prev; struct priority_list *last_node = &F->priority[last_slot]; if (font == FONT_ICON) { if (last_node->version != F->version) { F->dirty = 1; } return get_icon(F, codepoint, glyph); } if (font_index(font) <= 0) { // invalid font memset(glyph, 0, sizeof(*glyph)); return -1; } const struct stbtt_fontinfo *fi = get_ttf_unsafe(F, font); float scale = stbtt_ScaleForMappingEmToPixels(fi, ORIGINAL_SIZE); int ascent, descent, lineGap; int advance, lsb; int ix0, iy0, ix1, iy1; if (!stbtt_GetFontVMetricsOS2(fi, &ascent, &descent, &lineGap)) { stbtt_GetFontVMetrics(fi, &ascent, &descent, &lineGap); } stbtt_GetCodepointHMetrics(fi, codepoint, &advance, &lsb); stbtt_GetCodepointBitmapBox(fi, codepoint, scale, scale, &ix0, &iy0, &ix1, &iy1); glyph->w = ix1-ix0 + DISTANCE_OFFSET * 2; glyph->h = iy1-iy0 + DISTANCE_OFFSET * 2; glyph->offset_x = (short)(lsb * scale) - DISTANCE_OFFSET; glyph->offset_y = iy0 - DISTANCE_OFFSET; glyph->advance_x = (short)(((float)advance) * scale + 0.5f); glyph->advance_y = (short)((ascent - descent) * scale + 0.5f); glyph->u = 0; glyph->v = 0; if (last_node->version == F->version) // full ? return -1; F->dirty = 1; return 0; } static int font_manager_touch(struct font_manager *F, int font, int codepoint, struct font_glyph *glyph) { lock(F); int r = font_manager_touch_unsafe(F, font, codepoint, glyph); unlock(F); return r; } static inline int scale_font(int v, float scale, int size) { return ((int)(v * scale * size) + ORIGINAL_SIZE/2) / ORIGINAL_SIZE; } static inline float fscale_font(float v, float scale, int size){ return (v * scale * size) / (float)ORIGINAL_SIZE; } void font_manager_fontheight(struct font_manager *F, int fontid, int size, int *ascent, int *descent, int *lineGap) { if (fontid <= 0) { *ascent = 0; *descent = 0; *lineGap = 0; } const struct stbtt_fontinfo *fi = get_ttf(F, fontid); float scale = stbtt_ScaleForMappingEmToPixels(fi, ORIGINAL_SIZE); if (!stbtt_GetFontVMetricsOS2(fi, ascent, descent, lineGap)) { stbtt_GetFontVMetrics(fi, ascent, descent, lineGap); } *ascent = scale_font(*ascent, scale, size); *descent = scale_font(*descent, scale, size); *lineGap = scale_font(*lineGap, scale, size); } int font_manager_underline(struct font_manager *F, int fontid, int size, float *position, float *thickness){ const struct stbtt_fontinfo *fi = get_ttf(F, fontid); float scale = stbtt_ScaleForMappingEmToPixels(fi, ORIGINAL_SIZE); stbtt_uint32 post = stbtt__find_table(fi->data, fi->fontstart, "post"); if (!post) { return -1; } int16_t underline_position = ttSHORT(fi->data + post + 8); int16_t underline_thickness = ttSHORT(fi->data + post + 10); *position = fscale_font(underline_position, scale, size); *thickness = fscale_font(underline_thickness, scale, size); return 0; } // F->dpi_perinch is a constant, so do not need to lock int font_manager_pixelsize(struct font_manager *F, int fontid, int pointsize) { //TODO: need set dpi when init font_manager const int defaultdpi = 96; const int dpi = F->dpi_perinch == 0 ? defaultdpi : F->dpi_perinch; return (int)((pointsize / 72.f) * dpi + 0.5f); } static inline void scale(short *v, int size) { *v = (*v * size + ORIGINAL_SIZE/2) / ORIGINAL_SIZE; } static inline void uscale(uint16_t *v, int size) { *v = (*v * size + ORIGINAL_SIZE/2) / ORIGINAL_SIZE; } void font_manager_scale(struct font_manager *F, struct font_glyph *glyph, int size) { (void)F; scale(&glyph->offset_x, size); scale(&glyph->offset_y, size); scale(&glyph->advance_x, size); scale(&glyph->advance_y, size); uscale(&glyph->w, size); uscale(&glyph->h, size); } static void icon_scale(struct font_glyph *glyph, int size) { glyph->offset_x = glyph->offset_x * size / FONT_MANAGER_GLYPHSIZE; glyph->offset_y = glyph->offset_y * size / FONT_MANAGER_GLYPHSIZE; glyph->advance_x = glyph->advance_x * size / FONT_MANAGER_GLYPHSIZE; glyph->advance_y = glyph->advance_y * size / FONT_MANAGER_GLYPHSIZE; glyph->w = glyph->w * size / FONT_MANAGER_GLYPHSIZE; glyph->h = glyph->h * size / FONT_MANAGER_GLYPHSIZE; } static const char * font_manager_update(struct font_manager *F, int fontid, int codepoint, struct font_glyph *glyph, uint8_t *buffer, int stride) { if (fontid <= 0) return "Invalid font"; lock(F); int cp = codepoint_key(fontid, codepoint); int slot = hash_lookup(F, cp); if (slot < 0) { // move last node to head slot = F->priority[F->list_head].prev; struct priority_list *last_node = &F->priority[slot]; if (last_node->version == F->version) { // full ? unlock(F); return "Too many glyph"; } last_node->version = F->version; F->list_head = slot; F->slots[slot].codepoint_key = -1; hash_insert(F, cp, slot); } glyph->u = (slot % FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE; glyph->v = (slot / FONT_MANAGER_SLOTLINE) * FONT_MANAGER_GLYPHSIZE; struct font_slot *s = &F->slots[slot]; s->codepoint_key = cp; s->offset_x = glyph->offset_x; s->offset_y = glyph->offset_y; s->advance_x = glyph->advance_x; s->advance_y = glyph->advance_y; s->w = glyph->w; s->h = glyph->h; if (fontid == FONT_ICON) { if (codepoint < 0 || codepoint >= F->icon_n) { unlock(F); return "Invalid icon"; } unsigned char * icon_data = F->icon_data; unlock(F); icon_data += codepoint * FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE; buffer += stride * glyph->v + glyph->u; int i; for (i=0;iv + glyph->u; int src_stride = width; if (width > FONT_MANAGER_GLYPHSIZE) width = FONT_MANAGER_GLYPHSIZE; if (height > FONT_MANAGER_GLYPHSIZE) height = FONT_MANAGER_GLYPHSIZE; if (width > glyph->w) width = glyph->w; if (height > glyph->h) height = glyph->h; int i; for (i=0;iuserdata); return NULL; } const char * font_manager_glyph(struct font_manager *F, int fontid, int codepoint, int size, struct font_glyph *g, struct font_glyph *og) { int updated = font_manager_touch(F, fontid, codepoint, g); *og = *g; if (fontid != FONT_ICON && is_space_codepoint(codepoint)){ updated = 1; // not need update og->w = og->h = 0; } if (fontid == FONT_ICON) { icon_scale(g, size); } else { font_manager_scale(F, g, size); } if (updated == 0) { const char * err = font_manager_update(F, fontid, codepoint, og, F->texture_buffer, FONT_MANAGER_TEXSIZE); if (err) { return err; } } return NULL; } int font_manager_flush(struct font_manager *F) { // todo : atomic inc lock(F); int dirty = F->dirty; ++F->version; F->dirty = 0; unlock(F); return dirty; } static void font_manager_import_unsafe(struct font_manager *F, void* fontdata, size_t sz) { truetype_import(F->L, fontdata, sz); } void font_manager_import(struct font_manager *F, void* fontdata, size_t sz) { lock(F); font_manager_import_unsafe(F, fontdata, sz); unlock(F); } static int font_manager_addfont_with_family_unsafe(struct font_manager *F, const char* family) { return ttf_with_family(F, family); } int font_manager_addfont_with_family(struct font_manager *F, const char* family) { lock(F); int r = font_manager_addfont_with_family_unsafe(F, family); unlock(F); return r; } int font_manager_enum_fontname(struct font_manager *F, int idx, char buffer[], int buf_sz) { lock(F); int r = truetype_enum(F->L, idx, buffer, buf_sz); unlock(F); return r; } float font_manager_sdf_mask(struct font_manager *F){ return (ONEDGE_VALUE) / 255.f; } float font_manager_sdf_distance(struct font_manager *F, uint8_t numpixel){ return (numpixel * PIXEL_DIST_SCALE) / 255.f; } size_t font_manager_sizeof() { return sizeof(struct font_manager); } void font_manager_icon_init(struct font_manager *F, int n, void *data) { lock(F); F->icon_n = n; F->icon_data = (unsigned char *)data; unlock(F); } void font_manager_init(struct font_manager *F, void *L) { mutex_init(F->mutex); F->version = 1; F->count = 0; F->ttf = NULL; F->L = NULL; F->dpi_perinch = 0; F->dirty = 0; F->icon_n = 0; F->icon_data = NULL; // init priority list int i; for (i=0;ipriority[i].prev = i+1; F->priority[i].next = i-1; } int lastslot = FONT_MANAGER_SLOTS-1; F->priority[0].next = lastslot; F->priority[lastslot].prev = 0; F->list_head = lastslot; // init hash for (i=0;islots[i].codepoint_key = -1; } for (i=0;ihash[i] = -1; // empty slot } memset(F->texture_buffer, 0, sizeof(F->texture_buffer)); F->ttf = truetype_cstruct(L); F->L = L; } void* font_manager_shutdown(struct font_manager *F) { lock(F); void *L = F->L; F->ttf = NULL; F->L = NULL; unlock(F); return L; } ================================================ FILE: src/font_manager.h ================================================ #ifndef font_manager_h #define font_manager_h #include #include #include #include "font_define.h" struct font_manager; size_t font_manager_sizeof(); void font_manager_init(struct font_manager *, void *L); void* font_manager_shutdown(struct font_manager *); void font_manager_import(struct font_manager *F, void* fontdata, size_t sz); int font_manager_addfont_with_family(struct font_manager *F, const char* family); void font_manager_fontheight(struct font_manager *F, int fontid, int size, int *ascent, int *descent, int *lineGap); int font_manager_pixelsize(struct font_manager *F, int fontid, int pointsize); const char* font_manager_glyph(struct font_manager *F, int fontid, int codepoint, int size, struct font_glyph *g, struct font_glyph *og); //int font_manager_touch(struct font_manager *, int font, int codepoint, struct font_glyph *glyph); //const char * font_manager_update(struct font_manager *, int font, int codepoint, struct font_glyph *glyph, uint8_t *buffer, int stride); int font_manager_flush(struct font_manager *); void font_manager_scale(struct font_manager *F, struct font_glyph *glyph, int size); int font_manager_underline(struct font_manager *F, int fontid, int size, float *underline_position, float *thickness); float font_manager_sdf_mask(struct font_manager *F); float font_manager_sdf_distance(struct font_manager *F, uint8_t numpixel); void font_manager_icon_init(struct font_manager *F, int n, void *data); int font_manager_enum_fontname(struct font_manager *F, int idx, char buffer[], int buf_sz); // for debug const void * font_manager_texture(struct font_manager *F, int *sz); #endif //font_manager_h ================================================ FILE: src/font_system.c ================================================ #include #include #include #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #include #define MAX_NAME 1024 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); WCHAR familyNameW[MAX_NAME]; int n = MultiByteToWideChar(CP_UTF8,0,(const char*)familyName,-1,familyNameW,MAX_NAME); if (n == 0 || n > LF_FACESIZE) return luaL_error(L, "Invalid family name %s", familyName); HDC hdc = CreateCompatibleDC(0); LOGFONTW lf; memset(&lf, 0, sizeof(LOGFONT)); memcpy(lf.lfFaceName, familyNameW, n * sizeof(WCHAR)); lf.lfCharSet = DEFAULT_CHARSET; HFONT hfont = CreateFontIndirectW(&lf); if (!hfont) { DeleteDC(hdc); return luaL_error(L, "Create font failed: %d", GetLastError()); } HGDIOBJ oldobj = SelectObject(hdc, hfont); uint32_t tags[2] = {0x66637474/*ttcf*/, 0}; int i; DWORD bytes = 0; char *buf = NULL; for (i=0;i<2;i++) { uint32_t tag = tags[i]; bytes = GetFontData(hdc, tag, 0, 0, 0); if (bytes != GDI_ERROR) { buf = malloc(bytes+1);//lua_newuserdatauv(L, bytes, 0); if (buf == NULL) return luaL_error(L, "Out of memory : sysfont"); buf[bytes] = 0; bytes = GetFontData(hdc, tag, 0, (void *)buf, bytes); if (bytes != GDI_ERROR) { break; } else { free(buf); bytes = 0; } } } SelectObject(hdc, oldobj); DeleteObject(hfont); DeleteDC(hdc); if (bytes == 0) { return luaL_error(L, "Read font data failed"); } lua_pushexternalstring(L, buf, bytes, free_data , NULL); return 1; } #elif defined(__APPLE__) #include #include #define kCTFontTableTtcf 'ttcf' static void * free_data(void *ud, void *ptr, size_t oszie, size_t nsize) { free(ptr); return NULL; } static CFDataRef read_font_file_data(CFURLRef url) { CFReadStreamRef stream = CFReadStreamCreateWithFile(kCFAllocatorDefault, url); if (!stream) { return NULL; } if (!CFReadStreamOpen(stream)) { CFRelease(stream); return NULL; } CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 0); UInt8 buffer[8192]; CFIndex bytesRead; while ((bytesRead = CFReadStreamRead(stream, buffer, sizeof(buffer))) > 0) { CFDataAppendBytes(data, buffer, bytesRead); } CFReadStreamClose(stream); CFRelease(stream); if (bytesRead < 0) { CFRelease(data); return NULL; } return data; } static int lttfdata(lua_State *L) { const char *familyName = luaL_checkstring(L, 1); CFStringRef fontNameStr = CFStringCreateWithCString(kCFAllocatorDefault, familyName, kCFStringEncodingUTF8); if (!fontNameStr) { return luaL_error(L, "Failed to create font name string"); } CFDictionaryRef attributes = CFDictionaryCreate( kCFAllocatorDefault, (const void**)&kCTFontFamilyNameAttribute, (const void**)&fontNameStr, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(attributes); CFRelease(attributes); CFRelease(fontNameStr); if (!descriptor) { return luaL_error(L, "Failed to create font descriptor"); } CTFontRef font = CTFontCreateWithFontDescriptor(descriptor, 12.0, NULL); CFRelease(descriptor); if (!font) { return luaL_error(L, "Failed to create font for family: %s", familyName); } CFDataRef fontData = NULL; CFDataRef ttcfData = CTFontCopyTable(font, kCTFontTableTtcf, kCTFontTableOptionNoOptions); if (ttcfData) { fontData = ttcfData; } else { CTFontDescriptorRef fontDesc = CTFontCopyFontDescriptor(font); if (fontDesc) { CFURLRef fontURL = CTFontDescriptorCopyAttribute(fontDesc, kCTFontURLAttribute); if (fontURL) { fontData = read_font_file_data(fontURL); CFRelease(fontURL); } CFRelease(fontDesc); } } CFRelease(font); if (!fontData) { return luaL_error(L, "Failed to get font data for family: %s", familyName); } CFIndex dataLength = CFDataGetLength(fontData); char *buf = malloc(dataLength + 1); if (!buf) { CFRelease(fontData); return luaL_error(L, "Out of memory : sysfont"); } CFDataGetBytes(fontData, CFRangeMake(0, dataLength), (UInt8*)buf); buf[dataLength] = 0; CFRelease(fontData); lua_pushexternalstring(L, buf, dataLength, free_data, NULL); return 1; } #elif defined(__EMSCRIPTEN__) static int lttfdata(lua_State *L) { // todo : return 0; } #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 lttfdata(lua_State *L) { // always failed return 0; } #endif int luaopen_font_system(lua_State *L) { luaL_Reg l[] = { { "ttfdata", lttfdata }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/gamepad.c ================================================ #include #include #include #include #include "mutex.h" // It's the same with XINPUT_GAMEAD static const char * gamepad_buttons[] = { "UP", "DOWN", "LEFT", "RIGHT", "START", "BACK", "LS", "RS", "LB", "RB", "U0", // Undef "U1", // undef "A", "B", "X", "Y", }; #define BUTTON_COUNT (sizeof(gamepad_buttons)/sizeof(gamepad_buttons[0])) struct gamepad_state { uint32_t packet; uint16_t buttons; uint8_t lt; uint8_t rt; int16_t ls_x; int16_t ls_y; int16_t rs_x; int16_t rs_y; }; #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #include #include static int gamepad_getstate(int index, struct gamepad_state *state) { XINPUT_STATE * result = (XINPUT_STATE *)state; DWORD err = XInputGetState(index, result); if (err == ERROR_SUCCESS) return 0; // should be ERROR_DEVICE_NOT_CONNECTED return 1; } #elif defined(__APPLE__) #include #include #include static IOHIDManagerRef hid_manager = NULL; static CFMutableArrayRef gamepad_devices = NULL; static void gamepad_init() { if (hid_manager != NULL) return; hid_manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); gamepad_devices = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); CFMutableDictionaryRef matching = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); int usage_page = kHIDPage_GenericDesktop; int usage = kHIDUsage_GD_GamePad; CFNumberRef page_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage_page); CFNumberRef usage_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); CFDictionarySetValue(matching, CFSTR(kIOHIDDeviceUsagePageKey), page_ref); CFDictionarySetValue(matching, CFSTR(kIOHIDDeviceUsageKey), usage_ref); IOHIDManagerSetDeviceMatching(hid_manager, matching); IOHIDManagerOpen(hid_manager, kIOHIDOptionsTypeNone); CFSetRef device_set = IOHIDManagerCopyDevices(hid_manager); if (device_set) { CFIndex count = CFSetGetCount(device_set); IOHIDDeviceRef *devices = malloc(count * sizeof(IOHIDDeviceRef)); CFSetGetValues(device_set, (const void**)devices); for (CFIndex i = 0; i < count; i++) { CFArrayAppendValue(gamepad_devices, devices[i]); } free(devices); CFRelease(device_set); } CFRelease(page_ref); CFRelease(usage_ref); CFRelease(matching); } static int gamepad_getstate(int index, struct gamepad_state *state) { gamepad_init(); memset(state, 0, sizeof(struct gamepad_state)); if (index < 0 || index >= CFArrayGetCount(gamepad_devices)) { return 1; } IOHIDDeviceRef device = (IOHIDDeviceRef)CFArrayGetValueAtIndex(gamepad_devices, index); if (!device) { return 1; } CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone); if (!elements) { return 1; } CFIndex element_count = CFArrayGetCount(elements); for (CFIndex i = 0; i < element_count; i++) { IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, i); IOHIDElementType type = IOHIDElementGetType(element); if (type == kIOHIDElementTypeInput_Button || type == kIOHIDElementTypeInput_Axis) { IOHIDValueRef value_ref; if (IOHIDDeviceGetValue(device, element, &value_ref) == kIOReturnSuccess) { CFIndex value = IOHIDValueGetIntegerValue(value_ref); uint32_t usage_page = IOHIDElementGetUsagePage(element); uint32_t usage = IOHIDElementGetUsage(element); if (usage_page == kHIDPage_Button) { if (usage >= 1 && usage <= 16) { if (value) { state->buttons |= (1 << (usage - 1)); } } } else if (usage_page == kHIDPage_GenericDesktop) { CFIndex min = IOHIDElementGetLogicalMin(element); CFIndex max = IOHIDElementGetLogicalMax(element); int16_t normalized = (int16_t)(((value - min) * 65535) / (max - min) - 32768); switch (usage) { case kHIDUsage_GD_X: state->ls_x = normalized; break; case kHIDUsage_GD_Y: state->ls_y = -normalized; break; case kHIDUsage_GD_Z: state->rs_x = normalized; break; case kHIDUsage_GD_Rz: state->rs_y = -normalized; break; case kHIDUsage_GD_Rx: state->lt = (uint8_t)((normalized + 32768) >> 8); break; case kHIDUsage_GD_Ry: state->rt = (uint8_t)((normalized + 32768) >> 8); break; } } } } } CFRelease(elements); static uint32_t packet_counter = 0; state->packet = ++packet_counter; 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; } #elif defined(__EMSCRIPTEN__) static int gamepad_getstate(int index, struct gamepad_state *state) { (void)index; (void)state; return 1; } #else // todo : linux and mac support #error Unsupport gamepad #endif #define MAX_GAMEPAD 4 static struct gamepad_global { mutex_t lock[MAX_GAMEPAD]; int connected[MAX_GAMEPAD]; uint32_t packet[MAX_GAMEPAD]; struct gamepad_state state[MAX_GAMEPAD]; } GAMEPAD; struct gamepad_local { int connected; struct gamepad_state state; }; static int lgamepad_update(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); int index = luaL_optinteger(L, 2, 0); if (index < 0 || index >= MAX_GAMEPAD) { return luaL_error(L, "Invalid gamepad id %d", index); } struct gamepad_local * last_state = (struct gamepad_local *)lua_touserdata(L, lua_upvalueindex(1)); last_state += index; struct gamepad_state tmp; mutex_acquire(GAMEPAD.lock[index]); int connected = GAMEPAD.connected[index]; if (connected) { tmp = GAMEPAD.state[index]; } mutex_release(GAMEPAD.lock[index]); int i; if (connected != last_state->connected) { if (connected) { // reconnected, set all states last_state->state = tmp; lua_pushinteger(L, tmp.lt); lua_setfield(L, 1, "LT"); lua_pushinteger(L, tmp.rt); lua_setfield(L, 1, "RT"); lua_pushinteger(L, tmp.ls_x); lua_setfield(L, 1, "LS_X"); lua_pushinteger(L, tmp.ls_y); lua_setfield(L, 1, "LS_Y"); lua_pushinteger(L, tmp.rs_x); lua_setfield(L, 1, "RS_X"); lua_pushinteger(L, tmp.rs_y); uint16_t mask = tmp.buttons; for (i=0;i>= 1; } } else { // disconnected, clear all states memset(&last_state->state, 0, sizeof(last_state->state)); lua_pushinteger(L, 0); lua_setfield(L, 1, "LT"); lua_pushinteger(L, 0); lua_setfield(L, 1, "RT"); lua_pushinteger(L, 0); lua_setfield(L, 1, "LS_X"); lua_pushinteger(L, 0); lua_setfield(L, 1, "LS_Y"); lua_pushinteger(L, 0); lua_setfield(L, 1, "RS_X"); for (i=0;istate; if (tmp.lt != state->lt) { state->lt = tmp.lt; lua_pushinteger(L, tmp.lt); lua_setfield(L, 1, "LT"); } if (tmp.rt != state->rt) { state->rt = tmp.rt; lua_pushinteger(L, tmp.rt); lua_setfield(L, 1, "RT"); } if (tmp.ls_x != state->ls_x) { state->ls_x = tmp.ls_x; lua_pushinteger(L, tmp.ls_x); lua_setfield(L, 1, "LS_X"); } if (tmp.ls_y != state->ls_y) { state->ls_y = tmp.ls_y; lua_pushinteger(L, tmp.ls_y); lua_setfield(L, 1, "LS_Y"); } if (tmp.rs_x != state->rs_x) { state->rs_x = tmp.rs_x; lua_pushinteger(L, tmp.rs_x); lua_setfield(L, 1, "RS_X"); } if (tmp.rs_y != state->rs_y) { state->rs_y = tmp.rs_y; lua_pushinteger(L, tmp.rs_y); lua_setfield(L, 1, "RS_Y"); } uint16_t mask = tmp.buttons; uint16_t last_mask = state->buttons; if (mask != last_mask) { for (i=0;i>= 1; last_mask >>= 1; } state->buttons = tmp.buttons; } } lua_pushboolean(L, connected); lua_setfield(L, 1, "connect"); last_state->connected = connected; lua_pushboolean(L, connected); return 1; } int luaopen_gamepad(lua_State *L) { luaL_checkversion(L); lua_newtable(L); struct gamepad_local * state = (struct gamepad_local *)lua_newuserdatauv(L, sizeof(*state) * MAX_GAMEPAD, 0); memset(state, 0, sizeof(*state) * MAX_GAMEPAD); lua_pushcclosure(L, lgamepad_update, 1); lua_setfield(L, -2, "update"); return 1; } static int lgamepad_device_init(lua_State *L) { int i; memset(&GAMEPAD, 0, sizeof(GAMEPAD)); for (i=0;i #include #include #include #include static void * malloc_luastring(size_t sz) { unsigned char * buffer = malloc(sz+1); if (buffer) { buffer[sz] = 0; return (void *)buffer; } else { return NULL; } } static void * realloc_luastring(void *ptr, size_t sz) { unsigned char * buffer = realloc(ptr, sz+1); if (buffer) { buffer[sz] = 0; return (void *)buffer; } else { return NULL; } } #define STBI_MALLOC malloc_luastring #define STBI_FREE free #define STBI_REALLOC realloc_luastring #define STBI_ONLY_PNG #define STBI_MAX_DIMENSIONS 65536 #define STBI_NO_STDIO #define STB_IMAGE_IMPLEMENTATION #include "stb/stb_image.h" #define STBIW_WINDOWS_UTF8 #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb/stb_image_write.h" #include "stb/stb_image_resize2.h" #include "luabuffer.h" static void * free_image(void *ud, void *ptr, size_t osize, size_t nsize) { stbi_image_free(ptr); return NULL; } static void * free_buffer(void *ud, void *ptr, size_t osize, size_t nsize) { free(ptr); return NULL; } static int load_image(lua_State *L, int *x, int *y, stbi_uc **output) { size_t sz; int c; const stbi_uc *buffer = luaL_getbuffer(L, &sz); stbi_uc * img = stbi_load_from_memory(buffer, sz, x, y, &c, 4); if (img == NULL) { lua_pushnil(L); lua_pushstring(L, stbi_failure_reason()); return 2; } *output = img; return 0; } static int image_load(lua_State *L) { int x, y; stbi_uc * img = NULL; int r = load_image(L, &x, &y, &img); if (r) return r; lua_pushexternalstring(L, (const char *)img, x * y * 4, free_image, NULL); lua_pushinteger(L, x); lua_pushinteger(L, y); return 3; } static int image_load_alpha(lua_State *L) { int x, y; stbi_uc * img = NULL; int r = load_image(L, &x, &y, &img); if (r) return r; int i, j; stbi_uc * ptr = img; for (i=0;i x) { return 0; } if (dy < 0) { h += dy; dy = 0; } else if (dy > y) { return 0; } if (w + dx > x) { w = x - dx; } if (h + dy > y) { h = y - dy; } if (w <=0 || h <= 0) return 0; r->ptr = buffer + x * 4 * dy + 4 * dx; r->stride = 4 * x; r->width = w; r->line = h; return 1; } static int remove_top(struct rect *r) { int x, y; const uint8_t * ptr = r->ptr; for (y=0;yline;y++) { const uint8_t *cur = ptr; for (x=0;xwidth;x++) { if (cur[3]) { r->ptr = ptr; r->line -= y; return y; } cur += 4; } ptr += r->stride; } return r->line; } static int remove_bottom(struct rect *r) { int x, y; const uint8_t * ptr = r->ptr + (r->line - 1) * r->stride; for (y=0;yline-1;y++) { const uint8_t *cur = ptr; for (x=0;xwidth;x++) { if (cur[3]) { r->line -= y; return y; } cur += 4; } ptr -= r->stride; } return r->line; } static int remove_left(struct rect *r) { int x, y; const uint8_t * ptr = r->ptr; int min_left = r->width; for (y=0;yline;y++) { const uint8_t *cur = ptr; for (x=0;xstride; } return min_left; } static int remove_right(struct rect *r) { int x, y; const uint8_t * ptr = r->ptr + r->width * 4; int min_right = r->width; for (y=0;yline;y++) { const uint8_t *cur = ptr; for (x=0;xstride; } return min_right; } static int image_crop(lua_State *L) { size_t sz; const uint8_t * image = luaL_getbuffer(L, &sz); int x = luaL_checkinteger(L, 2); int y = luaL_checkinteger(L, 3); if (x * y * 4 != sz) return luaL_error(L, "Invalid image size %d * %d * 4 != %z\n", x, y, sz); int dx = luaL_optinteger(L, 4, 0); int dy = luaL_optinteger(L, 5, 0); int w = luaL_optinteger(L, 6, x - dx); int h = luaL_optinteger(L, 7, y - dy); struct rect r; if (!(rect_init(&r, image, x, y, dx, dy, w, h))) { return 0; } int top = remove_top(&r); if (top == h) return 0; remove_bottom(&r); int left = remove_left(&r); int right = remove_right(&r); // reserve border for alpha channel lua_pushinteger(L, left); lua_pushinteger(L, top); lua_pushinteger(L, r.width - (left + right)); lua_pushinteger(L, r.line); return 4; } static uint8_t * get_image_buffer(lua_State *L, int *w, int *h) { uint8_t * buffer = lua_touserdata(L, 1); if (buffer == NULL || !lua_getmetatable(L, 1)) luaL_error(L, "Neet image userdata"); if (lua_getfield(L, -1, "width") != LUA_TNUMBER) { luaL_error(L, "No .width"); } int width = lua_tointeger(L, -1); lua_pop(L, 1); if (lua_getfield(L, -1, "height") != LUA_TNUMBER) { luaL_error(L, "No .height"); } int height = lua_tointeger(L, -1); lua_pop(L, 1); int size = lua_rawlen(L, 1); if (width * height * 4 != size) luaL_error(L, "Invalid size %d * %d * 4 != %d", width, height, size); *w = width; *h = height; return buffer; } static int limage_write_png(lua_State *L) { int width, height; uint8_t * buffer = get_image_buffer(L, &width, &height); const char * filename = luaL_checkstring(L, 2); if (!stbi_write_png(filename, width, height, 4, buffer, width * 4)) { return luaL_error(L, "Write %s failed", filename); } return 0; } struct canvas { void * buffer; int width; int height; int stride; }; static int limage_tocanvas(lua_State *L) { int width, height; uint8_t * buffer = get_image_buffer(L, &width, &height); struct canvas * c = (struct canvas *)lua_newuserdatauv(L, sizeof(*c), 1); lua_pushvalue(L, 1); lua_setiuservalue(L, -2, 1); c->buffer = buffer; c->width = width; c->height = height; c->stride = width * 4; return 1; } static int image_new(lua_State *L) { int w = luaL_checkinteger(L, 1); int h = luaL_checkinteger(L, 2); uint8_t * buffer = (uint8_t *)lua_newuserdatauv(L, w * h * 4, 0); size_t sz = 0; const char *data = NULL; switch (lua_type(L, 3)) { case LUA_TSTRING: data = lua_tolstring(L, 3, &sz); break; case LUA_TLIGHTUSERDATA: data = (const char *)lua_touserdata(L, 3); sz = luaL_checkinteger(L, 4); break; case LUA_TUSERDATA: data = (const char *)lua_touserdata(L, 3); sz = lua_rawlen(L, 3); break; } if (sz == 0) { memset(buffer, 0, w * h * 4); } else if (sz == w*h*4) { memcpy(buffer, data, sz); } else if (sz == w*h) { int i,j; uint8_t *dst = buffer; const uint8_t *src = (const uint8_t *)data; for (i=0;i width || y+h > height) { return luaL_error(L, "Invalid rect (%d %d %d %d) in (%d %d)", x, y, w, h, width, height); } width = w; height = h; } struct canvas * c = (struct canvas *)lua_newuserdatauv(L, sizeof(*c), 1); lua_pushvalue(L, 1); lua_setiuservalue(L, -2, 1); if (t == LUA_TSTRING) { size_t sz; c->buffer = (void *)lua_tolstring(L, 1, &sz); if (stride * (y + height) > sz) return luaL_error(L, "Invalid buffer size %d * %d > %d", stride, (y + height), sz); } else { c->buffer = lua_touserdata(L, 1); } c->buffer = (void *)((char *)c->buffer + y * stride + x * 4); c->width = width; c->height = height; c->stride = stride; return 1; } static int check_canvas(lua_State *L, int index) { if (lua_type(L, index) != LUA_TUSERDATA) return luaL_error(L, "Need canvas"); int t = lua_getiuservalue(L, index, 1); if (t != LUA_TSTRING && t != LUA_TUSERDATA && t != LUA_TLIGHTUSERDATA) return luaL_error(L, "Invalid canvas at %d", index); lua_pop(L, 1); return t; } static int image_canvas_size(lua_State *L) { check_canvas(L, 1); struct canvas * c = (struct canvas *)lua_touserdata(L, 1); lua_pushinteger(L, c->width); lua_pushinteger(L, c->height); lua_pushlightuserdata(L, c->buffer); return 3; } static int canvas_blit(lua_State *L) { if (check_canvas(L, 1) == LUA_TSTRING) return luaL_error(L, "dst canvas is readonly"); check_canvas(L, 2); struct canvas * dst = (struct canvas *)lua_touserdata(L, 1); struct canvas * src = (struct canvas *)lua_touserdata(L, 2); int x = luaL_optinteger(L, 3, 0); int y = luaL_optinteger(L, 4, 0); int w = src->width; int h = src->height; int sx = 0; int sy = 0; if (x < 0) { w += x; sx = -x; x = 0; } if (y < 0) { h += y; sy = -y; y = 0; } if (x + w > dst->width) { w = dst->width - x; } if (y + h > dst->height) { h = dst->height - y; } if (w <=0 || h <= 0) return 0; int i; uint8_t * dst_ptr = (uint8_t *)dst->buffer + y * dst->stride + 4 * x; const uint8_t *src_ptr = (const uint8_t *)src->buffer + sy * src->stride + 4 * sx; for (i=0;istride; dst_ptr += dst->stride; } return 0; } static int image_makeindex(lua_State *L) { if (lua_isnoneornil(L, 1)) { lua_pushinteger(L, -1); return 1; } int x = luaL_checkinteger(L, 1); int y = luaL_checkinteger(L, 2); int w = luaL_checkinteger(L, 3); int h = luaL_checkinteger(L, 4); union { uint64_t index; uint16_t v[4]; } u; u.v[0] = (uint16_t)x; u.v[1] = (uint16_t)y; u.v[2] = (uint16_t)w; u.v[3] = (uint16_t)h; lua_pushinteger(L, u.index); return 1; } static int image_resize(lua_State *L) { size_t sz; const char *buffer = luaL_checklstring(L, 1, &sz); int x = luaL_checkinteger(L, 2); int y = luaL_checkinteger(L, 3); float scale_x = luaL_checknumber(L, 4); float scale_y = luaL_optnumber(L, 5, scale_x); int channel; size_t output_sz; int tx = (int)(x * scale_x + 0.5); int ty = (int)(y * scale_y + 0.5); int stride_p; if (x * y * 4 == sz) { channel = STBIR_4CHANNEL; output_sz = tx * ty * 4; stride_p = 4; } else { if (x * y != sz) { return luaL_error(L, "Invalid size (%d) != %d * %d", (int)sz, x, y); } channel = STBIR_1CHANNEL; output_sz = tx * ty; stride_p = 1; } unsigned char *output = (unsigned char *)malloc(output_sz +1); if (output == NULL) return luaL_error(L, "Out of memory"); output[output_sz] = 0; memcpy(output, buffer, output_sz); stbir_resize_uint8_linear((const unsigned char *)buffer , x , y, stride_p * x, output, tx, ty, stride_p * tx, channel); lua_pushexternalstring(L, (const char *)output, output_sz, free_buffer, NULL); lua_pushinteger(L, tx); lua_pushinteger(L, ty); return 3; } int luaopen_image(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "load", image_load }, { "load_alpha", image_load_alpha }, { "resize", image_resize }, { "info", image_info }, { "crop", image_crop }, { "canvas", image_canvas }, { "canvas_size", image_canvas_size }, { "new", image_new }, { "blit", canvas_blit }, { "makeindex", image_makeindex }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/ime_char_filter.h ================================================ #ifndef SOLUNA_IME_CHAR_FILTER_H #define SOLUNA_IME_CHAR_FILTER_H #include #include #include struct soluna_ime_char_filter_state { uint32_t *expected_chars; int *expected_count; uint32_t *ignore_chars; int *ignore_count; int capacity; }; static inline void soluna_ime_char_queue_push(uint32_t *buffer, int *count, int max, uint32_t code) { if (*count == max) { memmove(buffer, buffer + 1, (size_t)(max - 1) * sizeof(uint32_t)); buffer[max - 1] = code; } else { buffer[*count] = code; (*count)++; } } static inline bool soluna_ime_char_queue_consume(uint32_t *buffer, int *count, uint32_t code) { for (int i = 0; i < *count; ++i) { if (buffer[i] == code) { if (i < *count - 1) { memmove(buffer + i, buffer + i + 1, (size_t)(*count - i - 1) * sizeof(uint32_t)); } (*count)--; return true; } } return false; } static inline void soluna_ime_char_filter_reset(struct soluna_ime_char_filter_state state) { *state.expected_count = 0; *state.ignore_count = 0; } static inline void soluna_ime_char_filter_push_expected(struct soluna_ime_char_filter_state state, uint32_t code) { soluna_ime_char_queue_push(state.expected_chars, state.expected_count, state.capacity, code); } static inline bool soluna_ime_char_filter_should_skip(struct soluna_ime_char_filter_state state, uint32_t code) { if (soluna_ime_char_queue_consume(state.expected_chars, state.expected_count, code)) { soluna_ime_char_queue_push(state.ignore_chars, state.ignore_count, state.capacity, code); return false; } if (*state.ignore_count > 0) { if (soluna_ime_char_queue_consume(state.ignore_chars, state.ignore_count, code)) { return true; } else if (*state.ignore_count > 0) { uint32_t stale = state.ignore_chars[0]; soluna_ime_char_queue_consume(state.ignore_chars, state.ignore_count, stale); } } return false; } #endif /* SOLUNA_IME_CHAR_FILTER_H */ ================================================ FILE: src/ime_state.h ================================================ #ifndef SOLUNA_IME_STATE_H #define SOLUNA_IME_STATE_H #include #include struct soluna_ime_rect_state { float x; float y; float w; float h; uint32_t text_color; bool valid; }; extern struct soluna_ime_rect_state g_soluna_ime_rect; #endif /* SOLUNA_IME_STATE_H */ ================================================ FILE: src/lcrypt.c ================================================ #include #include #include #include #include #include #define PADDING_MODE_ISO7816_4 0 #define PADDING_MODE_PKCS7 1 #define PADDING_MODE_COUNT 2 #define SMALL_CHUNK 256 /* the eight DES S-boxes */ static uint32_t SB1[64] = { 0x01010400, 0x00000000, 0x00010000, 0x01010404, 0x01010004, 0x00010404, 0x00000004, 0x00010000, 0x00000400, 0x01010400, 0x01010404, 0x00000400, 0x01000404, 0x01010004, 0x01000000, 0x00000004, 0x00000404, 0x01000400, 0x01000400, 0x00010400, 0x00010400, 0x01010000, 0x01010000, 0x01000404, 0x00010004, 0x01000004, 0x01000004, 0x00010004, 0x00000000, 0x00000404, 0x00010404, 0x01000000, 0x00010000, 0x01010404, 0x00000004, 0x01010000, 0x01010400, 0x01000000, 0x01000000, 0x00000400, 0x01010004, 0x00010000, 0x00010400, 0x01000004, 0x00000400, 0x00000004, 0x01000404, 0x00010404, 0x01010404, 0x00010004, 0x01010000, 0x01000404, 0x01000004, 0x00000404, 0x00010404, 0x01010400, 0x00000404, 0x01000400, 0x01000400, 0x00000000, 0x00010004, 0x00010400, 0x00000000, 0x01010004 }; static uint32_t SB2[64] = { 0x80108020, 0x80008000, 0x00008000, 0x00108020, 0x00100000, 0x00000020, 0x80100020, 0x80008020, 0x80000020, 0x80108020, 0x80108000, 0x80000000, 0x80008000, 0x00100000, 0x00000020, 0x80100020, 0x00108000, 0x00100020, 0x80008020, 0x00000000, 0x80000000, 0x00008000, 0x00108020, 0x80100000, 0x00100020, 0x80000020, 0x00000000, 0x00108000, 0x00008020, 0x80108000, 0x80100000, 0x00008020, 0x00000000, 0x00108020, 0x80100020, 0x00100000, 0x80008020, 0x80100000, 0x80108000, 0x00008000, 0x80100000, 0x80008000, 0x00000020, 0x80108020, 0x00108020, 0x00000020, 0x00008000, 0x80000000, 0x00008020, 0x80108000, 0x00100000, 0x80000020, 0x00100020, 0x80008020, 0x80000020, 0x00100020, 0x00108000, 0x00000000, 0x80008000, 0x00008020, 0x80000000, 0x80100020, 0x80108020, 0x00108000 }; static uint32_t SB3[64] = { 0x00000208, 0x08020200, 0x00000000, 0x08020008, 0x08000200, 0x00000000, 0x00020208, 0x08000200, 0x00020008, 0x08000008, 0x08000008, 0x00020000, 0x08020208, 0x00020008, 0x08020000, 0x00000208, 0x08000000, 0x00000008, 0x08020200, 0x00000200, 0x00020200, 0x08020000, 0x08020008, 0x00020208, 0x08000208, 0x00020200, 0x00020000, 0x08000208, 0x00000008, 0x08020208, 0x00000200, 0x08000000, 0x08020200, 0x08000000, 0x00020008, 0x00000208, 0x00020000, 0x08020200, 0x08000200, 0x00000000, 0x00000200, 0x00020008, 0x08020208, 0x08000200, 0x08000008, 0x00000200, 0x00000000, 0x08020008, 0x08000208, 0x00020000, 0x08000000, 0x08020208, 0x00000008, 0x00020208, 0x00020200, 0x08000008, 0x08020000, 0x08000208, 0x00000208, 0x08020000, 0x00020208, 0x00000008, 0x08020008, 0x00020200 }; static uint32_t SB4[64] = { 0x00802001, 0x00002081, 0x00002081, 0x00000080, 0x00802080, 0x00800081, 0x00800001, 0x00002001, 0x00000000, 0x00802000, 0x00802000, 0x00802081, 0x00000081, 0x00000000, 0x00800080, 0x00800001, 0x00000001, 0x00002000, 0x00800000, 0x00802001, 0x00000080, 0x00800000, 0x00002001, 0x00002080, 0x00800081, 0x00000001, 0x00002080, 0x00800080, 0x00002000, 0x00802080, 0x00802081, 0x00000081, 0x00800080, 0x00800001, 0x00802000, 0x00802081, 0x00000081, 0x00000000, 0x00000000, 0x00802000, 0x00002080, 0x00800080, 0x00800081, 0x00000001, 0x00802001, 0x00002081, 0x00002081, 0x00000080, 0x00802081, 0x00000081, 0x00000001, 0x00002000, 0x00800001, 0x00002001, 0x00802080, 0x00800081, 0x00002001, 0x00002080, 0x00800000, 0x00802001, 0x00000080, 0x00800000, 0x00002000, 0x00802080 }; static uint32_t SB5[64] = { 0x00000100, 0x02080100, 0x02080000, 0x42000100, 0x00080000, 0x00000100, 0x40000000, 0x02080000, 0x40080100, 0x00080000, 0x02000100, 0x40080100, 0x42000100, 0x42080000, 0x00080100, 0x40000000, 0x02000000, 0x40080000, 0x40080000, 0x00000000, 0x40000100, 0x42080100, 0x42080100, 0x02000100, 0x42080000, 0x40000100, 0x00000000, 0x42000000, 0x02080100, 0x02000000, 0x42000000, 0x00080100, 0x00080000, 0x42000100, 0x00000100, 0x02000000, 0x40000000, 0x02080000, 0x42000100, 0x40080100, 0x02000100, 0x40000000, 0x42080000, 0x02080100, 0x40080100, 0x00000100, 0x02000000, 0x42080000, 0x42080100, 0x00080100, 0x42000000, 0x42080100, 0x02080000, 0x00000000, 0x40080000, 0x42000000, 0x00080100, 0x02000100, 0x40000100, 0x00080000, 0x00000000, 0x40080000, 0x02080100, 0x40000100 }; static uint32_t SB6[64] = { 0x20000010, 0x20400000, 0x00004000, 0x20404010, 0x20400000, 0x00000010, 0x20404010, 0x00400000, 0x20004000, 0x00404010, 0x00400000, 0x20000010, 0x00400010, 0x20004000, 0x20000000, 0x00004010, 0x00000000, 0x00400010, 0x20004010, 0x00004000, 0x00404000, 0x20004010, 0x00000010, 0x20400010, 0x20400010, 0x00000000, 0x00404010, 0x20404000, 0x00004010, 0x00404000, 0x20404000, 0x20000000, 0x20004000, 0x00000010, 0x20400010, 0x00404000, 0x20404010, 0x00400000, 0x00004010, 0x20000010, 0x00400000, 0x20004000, 0x20000000, 0x00004010, 0x20000010, 0x20404010, 0x00404000, 0x20400000, 0x00404010, 0x20404000, 0x00000000, 0x20400010, 0x00000010, 0x00004000, 0x20400000, 0x00404010, 0x00004000, 0x00400010, 0x20004010, 0x00000000, 0x20404000, 0x20000000, 0x00400010, 0x20004010 }; static uint32_t SB7[64] = { 0x00200000, 0x04200002, 0x04000802, 0x00000000, 0x00000800, 0x04000802, 0x00200802, 0x04200800, 0x04200802, 0x00200000, 0x00000000, 0x04000002, 0x00000002, 0x04000000, 0x04200002, 0x00000802, 0x04000800, 0x00200802, 0x00200002, 0x04000800, 0x04000002, 0x04200000, 0x04200800, 0x00200002, 0x04200000, 0x00000800, 0x00000802, 0x04200802, 0x00200800, 0x00000002, 0x04000000, 0x00200800, 0x04000000, 0x00200800, 0x00200000, 0x04000802, 0x04000802, 0x04200002, 0x04200002, 0x00000002, 0x00200002, 0x04000000, 0x04000800, 0x00200000, 0x04200800, 0x00000802, 0x00200802, 0x04200800, 0x00000802, 0x04000002, 0x04200802, 0x04200000, 0x00200800, 0x00000000, 0x00000002, 0x04200802, 0x00000000, 0x00200802, 0x04200000, 0x00000800, 0x04000002, 0x04000800, 0x00000800, 0x00200002 }; static uint32_t SB8[64] = { 0x10001040, 0x00001000, 0x00040000, 0x10041040, 0x10000000, 0x10001040, 0x00000040, 0x10000000, 0x00040040, 0x10040000, 0x10041040, 0x00041000, 0x10041000, 0x00041040, 0x00001000, 0x00000040, 0x10040000, 0x10000040, 0x10001000, 0x00001040, 0x00041000, 0x00040040, 0x10040040, 0x10041000, 0x00001040, 0x00000000, 0x00000000, 0x10040040, 0x10000040, 0x10001000, 0x00041040, 0x00040000, 0x00041040, 0x00040000, 0x10041000, 0x00001000, 0x00000040, 0x10040040, 0x00001000, 0x00041040, 0x10001000, 0x00000040, 0x10000040, 0x10040000, 0x10040040, 0x10000000, 0x00040000, 0x10001040, 0x00000000, 0x10041040, 0x00040040, 0x10000040, 0x10040000, 0x10001000, 0x10001040, 0x00000000, 0x10041040, 0x00041000, 0x00041000, 0x00001040, 0x00001040, 0x00040040, 0x10000000, 0x10041000 }; /* PC1: left and right halves bit-swap */ static uint32_t LHs[16] = { 0x00000000, 0x00000001, 0x00000100, 0x00000101, 0x00010000, 0x00010001, 0x00010100, 0x00010101, 0x01000000, 0x01000001, 0x01000100, 0x01000101, 0x01010000, 0x01010001, 0x01010100, 0x01010101 }; static uint32_t RHs[16] = { 0x00000000, 0x01000000, 0x00010000, 0x01010000, 0x00000100, 0x01000100, 0x00010100, 0x01010100, 0x00000001, 0x01000001, 0x00010001, 0x01010001, 0x00000101, 0x01000101, 0x00010101, 0x01010101, }; /* platform-independant 32-bit integer manipulation macros */ #define GET_UINT32(n,b,i) \ { \ (n) = ( (uint32_t) (b)[(i) ] << 24 ) \ | ( (uint32_t) (b)[(i) + 1] << 16 ) \ | ( (uint32_t) (b)[(i) + 2] << 8 ) \ | ( (uint32_t) (b)[(i) + 3] ); \ } #define PUT_UINT32(n,b,i) \ { \ (b)[(i) ] = (uint8_t) ( (n) >> 24 ); \ (b)[(i) + 1] = (uint8_t) ( (n) >> 16 ); \ (b)[(i) + 2] = (uint8_t) ( (n) >> 8 ); \ (b)[(i) + 3] = (uint8_t) ( (n) ); \ } /* Initial Permutation macro */ #define DES_IP(X,Y) \ { \ T = ((X >> 4) ^ Y) & 0x0F0F0F0F; Y ^= T; X ^= (T << 4); \ T = ((X >> 16) ^ Y) & 0x0000FFFF; Y ^= T; X ^= (T << 16); \ T = ((Y >> 2) ^ X) & 0x33333333; X ^= T; Y ^= (T << 2); \ T = ((Y >> 8) ^ X) & 0x00FF00FF; X ^= T; Y ^= (T << 8); \ Y = ((Y << 1) | (Y >> 31)) & 0xFFFFFFFF; \ T = (X ^ Y) & 0xAAAAAAAA; Y ^= T; X ^= T; \ X = ((X << 1) | (X >> 31)) & 0xFFFFFFFF; \ } /* Final Permutation macro */ #define DES_FP(X,Y) \ { \ X = ((X << 31) | (X >> 1)) & 0xFFFFFFFF; \ T = (X ^ Y) & 0xAAAAAAAA; X ^= T; Y ^= T; \ Y = ((Y << 31) | (Y >> 1)) & 0xFFFFFFFF; \ T = ((Y >> 8) ^ X) & 0x00FF00FF; X ^= T; Y ^= (T << 8); \ T = ((Y >> 2) ^ X) & 0x33333333; X ^= T; Y ^= (T << 2); \ T = ((X >> 16) ^ Y) & 0x0000FFFF; Y ^= T; X ^= (T << 16); \ T = ((X >> 4) ^ Y) & 0x0F0F0F0F; Y ^= T; X ^= (T << 4); \ } /* DES round macro */ #define DES_ROUND(X,Y) \ { \ T = *SK++ ^ X; \ Y ^= SB8[ (T ) & 0x3F ] ^ \ SB6[ (T >> 8) & 0x3F ] ^ \ SB4[ (T >> 16) & 0x3F ] ^ \ SB2[ (T >> 24) & 0x3F ]; \ \ T = *SK++ ^ ((X << 28) | (X >> 4)); \ Y ^= SB7[ (T ) & 0x3F ] ^ \ SB5[ (T >> 8) & 0x3F ] ^ \ SB3[ (T >> 16) & 0x3F ] ^ \ SB1[ (T >> 24) & 0x3F ]; \ } /* DES key schedule */ static void des_main_ks( uint32_t SK[32], const uint8_t key[8] ) { int i; uint32_t X, Y, T; GET_UINT32( X, key, 0 ); GET_UINT32( Y, key, 4 ); /* Permuted Choice 1 */ T = ((Y >> 4) ^ X) & 0x0F0F0F0F; X ^= T; Y ^= (T << 4); T = ((Y ) ^ X) & 0x10101010; X ^= T; Y ^= (T ); X = (LHs[ (X ) & 0xF] << 3) | (LHs[ (X >> 8) & 0xF ] << 2) | (LHs[ (X >> 16) & 0xF] << 1) | (LHs[ (X >> 24) & 0xF ] ) | (LHs[ (X >> 5) & 0xF] << 7) | (LHs[ (X >> 13) & 0xF ] << 6) | (LHs[ (X >> 21) & 0xF] << 5) | (LHs[ (X >> 29) & 0xF ] << 4); Y = (RHs[ (Y >> 1) & 0xF] << 3) | (RHs[ (Y >> 9) & 0xF ] << 2) | (RHs[ (Y >> 17) & 0xF] << 1) | (RHs[ (Y >> 25) & 0xF ] ) | (RHs[ (Y >> 4) & 0xF] << 7) | (RHs[ (Y >> 12) & 0xF ] << 6) | (RHs[ (Y >> 20) & 0xF] << 5) | (RHs[ (Y >> 28) & 0xF ] << 4); X &= 0x0FFFFFFF; Y &= 0x0FFFFFFF; /* calculate subkeys */ for( i = 0; i < 16; i++ ) { if( i < 2 || i == 8 || i == 15 ) { X = ((X << 1) | (X >> 27)) & 0x0FFFFFFF; Y = ((Y << 1) | (Y >> 27)) & 0x0FFFFFFF; } else { X = ((X << 2) | (X >> 26)) & 0x0FFFFFFF; Y = ((Y << 2) | (Y >> 26)) & 0x0FFFFFFF; } *SK++ = ((X << 4) & 0x24000000) | ((X << 28) & 0x10000000) | ((X << 14) & 0x08000000) | ((X << 18) & 0x02080000) | ((X << 6) & 0x01000000) | ((X << 9) & 0x00200000) | ((X >> 1) & 0x00100000) | ((X << 10) & 0x00040000) | ((X << 2) & 0x00020000) | ((X >> 10) & 0x00010000) | ((Y >> 13) & 0x00002000) | ((Y >> 4) & 0x00001000) | ((Y << 6) & 0x00000800) | ((Y >> 1) & 0x00000400) | ((Y >> 14) & 0x00000200) | ((Y ) & 0x00000100) | ((Y >> 5) & 0x00000020) | ((Y >> 10) & 0x00000010) | ((Y >> 3) & 0x00000008) | ((Y >> 18) & 0x00000004) | ((Y >> 26) & 0x00000002) | ((Y >> 24) & 0x00000001); *SK++ = ((X << 15) & 0x20000000) | ((X << 17) & 0x10000000) | ((X << 10) & 0x08000000) | ((X << 22) & 0x04000000) | ((X >> 2) & 0x02000000) | ((X << 1) & 0x01000000) | ((X << 16) & 0x00200000) | ((X << 11) & 0x00100000) | ((X << 3) & 0x00080000) | ((X >> 6) & 0x00040000) | ((X << 15) & 0x00020000) | ((X >> 4) & 0x00010000) | ((Y >> 2) & 0x00002000) | ((Y << 8) & 0x00001000) | ((Y >> 14) & 0x00000808) | ((Y >> 9) & 0x00000400) | ((Y ) & 0x00000200) | ((Y << 7) & 0x00000100) | ((Y >> 7) & 0x00000020) | ((Y >> 3) & 0x00000011) | ((Y << 2) & 0x00000004) | ((Y >> 21) & 0x00000002); } } /* DES 64-bit block encryption/decryption */ static void des_crypt( const uint32_t SK[32], const uint8_t input[8], uint8_t output[8] ) { uint32_t X, Y, T; GET_UINT32( X, input, 0 ); GET_UINT32( Y, input, 4 ); DES_IP( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_ROUND( Y, X ); DES_ROUND( X, Y ); DES_FP( Y, X ); PUT_UINT32( Y, output, 0 ); PUT_UINT32( X, output, 4 ); } static int lrandomkey(lua_State *L) { char tmp[8]; int i; char x = 0; for (i=0;i<8;i++) { tmp[i] = rand() & 0xff; x ^= tmp[i]; } if (x==0) { tmp[0] |= 1; // avoid 0 } lua_pushlstring(L, tmp, 8); return 1; } static void padding_mode_table(lua_State *L) { // see macros PADDING_MODE_ISO7816_4, etc. const char * mode[] = { "iso7816_4", "pkcs7", }; int n = sizeof(mode) / sizeof(mode[0]); int i; lua_createtable(L,0,n); for (i=0;i= PADDING_MODE_COUNT) luaL_error(L, "Invalid padding mode %d", mode); } static void add_padding(lua_State *L, uint8_t buf[8], const uint8_t *src, int offset, int mode) { check_padding_mode(L, mode); if (offset >= 8) luaL_error(L, "Invalid padding"); memcpy(buf, src, offset); padding_add_func[mode](buf, offset); } static int remove_padding(lua_State *L, const uint8_t *last, int mode) { check_padding_mode(L, mode); return padding_remove_func[mode](last); } static void des_key(lua_State *L, uint32_t SK[32]) { size_t keysz = 0; const void * key = luaL_checklstring(L, 1, &keysz); if (keysz != 8) { luaL_error(L, "Invalid key size %d, need 8 bytes", (int)keysz); } des_main_ks(SK, (const uint8_t*)key); } static int ldesencode(lua_State *L) { uint32_t SK[32]; des_key(L, SK); size_t textsz = 0; const uint8_t * text = (const uint8_t *)luaL_checklstring(L, 2, &textsz); size_t chunksz = (textsz + 8) & ~7; int padding_mode = luaL_optinteger(L, 3, PADDING_MODE_ISO7816_4); uint8_t tmp[SMALL_CHUNK]; uint8_t *buffer = tmp; if (chunksz > SMALL_CHUNK) { buffer = (uint8_t*)lua_newuserdatauv(L, chunksz, 0); } int i; for (i=0;i<(int)textsz-7;i+=8) { des_crypt(SK, text+i, buffer+i); } uint8_t tail[8]; add_padding(L, tail, text+i, textsz - i, padding_mode); des_crypt(SK, tail, buffer+i); lua_pushlstring(L, (const char *)buffer, chunksz); return 1; } static int ldesdecode(lua_State *L) { uint32_t ESK[32]; des_key(L, ESK); uint32_t SK[32]; int i; for( i = 0; i < 32; i += 2 ) { SK[i] = ESK[30 - i]; SK[i + 1] = ESK[31 - i]; } size_t textsz = 0; const uint8_t *text = (const uint8_t *)luaL_checklstring(L, 2, &textsz); if ((textsz & 7) || textsz == 0) { return luaL_error(L, "Invalid des crypt text length %d", (int)textsz); } int padding_mode = luaL_optinteger(L, 3, PADDING_MODE_ISO7816_4); uint8_t tmp[SMALL_CHUNK]; uint8_t *buffer = tmp; if (textsz > SMALL_CHUNK) { buffer = (uint8_t*)lua_newuserdatauv(L, textsz, 0); } for (i=0;i 8) { return luaL_error(L, "Invalid des crypt text"); } lua_pushlstring(L, (const char *)buffer, textsz - padding); return 1; } static void Hash(const char * str, int sz, uint8_t key[8]) { uint32_t djb_hash = 5381L; uint32_t js_hash = 1315423911L; int i; for (i=0;i> 2)); } key[0] = djb_hash & 0xff; key[1] = (djb_hash >> 8) & 0xff; key[2] = (djb_hash >> 16) & 0xff; key[3] = (djb_hash >> 24) & 0xff; key[4] = js_hash & 0xff; key[5] = (js_hash >> 8) & 0xff; key[6] = (js_hash >> 16) & 0xff; key[7] = (js_hash >> 24) & 0xff; } static int lhashkey(lua_State *L) { size_t sz = 0; const char * key = luaL_checklstring(L, 1, &sz); uint8_t realkey[8]; Hash(key,(int)sz,realkey); lua_pushlstring(L, (const char *)realkey, 8); return 1; } static int ltohex(lua_State *L) { static char hex[] = "0123456789abcdef"; size_t sz = 0; const uint8_t * text = (const uint8_t *)luaL_checklstring(L, 1, &sz); char tmp[SMALL_CHUNK]; char *buffer = tmp; if (sz > SMALL_CHUNK/2) { buffer = (char*)lua_newuserdatauv(L, sz * 2, 0); } int i; for (i=0;i> 4]; buffer[i*2+1] = hex[text[i] & 0xf]; } lua_pushlstring(L, buffer, sz * 2); return 1; } #define HEX(v,c) { char tmp = (char) c; if (tmp >= '0' && tmp <= '9') { v = tmp-'0'; } else { v = tmp - 'a' + 10; } } static int lfromhex(lua_State *L) { size_t sz = 0; const char * text = luaL_checklstring(L, 1, &sz); if (sz & 1) { return luaL_error(L, "Invalid hex text size %d", (int)sz); } char tmp[SMALL_CHUNK]; char *buffer = tmp; if (sz > SMALL_CHUNK*2) { buffer = (char*)lua_newuserdatauv(L, sz / 2, 0); } int i; for (i=0;i 16 || low > 16) { return luaL_error(L, "Invalid hex text", text); } buffer[i/2] = hi<<4 | low; } lua_pushlstring(L, buffer, i/2); return 1; } // Constants are the integer part of the sines of integers (in radians) * 2^32. static const uint32_t k[64] = { 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee , 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501 , 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be , 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821 , 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa , 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8 , 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed , 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a , 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c , 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70 , 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05 , 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665 , 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039 , 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1 , 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1 , 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 }; // r specifies the per-round shift amounts static const uint32_t r[] = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21}; // leftrotate function definition #define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c)))) static void digest_md5(uint32_t w[16], uint32_t result[4]) { uint32_t a, b, c, d, f, g, temp; int i; a = 0x67452301u; b = 0xefcdab89u; c = 0x98badcfeu; d = 0x10325476u; for(i = 0; i<64; i++) { if (i < 16) { f = (b & c) | ((~b) & d); g = i; } else if (i < 32) { f = (d & b) | ((~d) & c); g = (5*i + 1) % 16; } else if (i < 48) { f = b ^ c ^ d; g = (3*i + 5) % 16; } else { f = c ^ (b | (~d)); g = (7*i) % 16; } temp = d; d = c; c = b; b = b + LEFTROTATE((a + f + k[i] + w[g]), r[i]); a = temp; } result[0] = a; result[1] = b; result[2] = c; result[3] = d; } // hmac64 use md5 algorithm without padding, and the result is (c^d .. a^b) static void hmac(uint32_t x[2], uint32_t y[2], uint32_t result[2]) { uint32_t w[16]; uint32_t r[4]; int i; for (i=0;i<16;i+=4) { w[i] = x[1]; w[i+1] = x[0]; w[i+2] = y[1]; w[i+3] = y[0]; } digest_md5(w,r); result[0] = r[2]^r[3]; result[1] = r[0]^r[1]; } static void hmac_md5(uint32_t x[2], uint32_t y[2], uint32_t result[2]) { uint32_t w[16]; uint32_t r[4]; int i; for (i=0;i<12;i+=4) { w[i] = x[0]; w[i+1] = x[1]; w[i+2] = y[0]; w[i+3] = y[1]; } w[12] = 0x80; w[13] = 0; w[14] = 384; w[15] = 0; digest_md5(w,r); result[0] = (r[0] + 0x67452301u) ^ (r[2] + 0x98badcfeu); result[1] = (r[1] + 0xefcdab89u) ^ (r[3] + 0x10325476u); } static void read64(lua_State *L, uint32_t xx[2], uint32_t yy[2]) { size_t sz = 0; const uint8_t *x = (const uint8_t *)luaL_checklstring(L, 1, &sz); if (sz != 8) { luaL_error(L, "Invalid uint64 x"); } const uint8_t *y = (const uint8_t *)luaL_checklstring(L, 2, &sz); if (sz != 8) { luaL_error(L, "Invalid uint64 y"); } xx[0] = x[0] | x[1]<<8 | x[2]<<16 | x[3]<<24; xx[1] = x[4] | x[5]<<8 | x[6]<<16 | x[7]<<24; yy[0] = y[0] | y[1]<<8 | y[2]<<16 | y[3]<<24; yy[1] = y[4] | y[5]<<8 | y[6]<<16 | y[7]<<24; } static int pushqword(lua_State *L, uint32_t result[2]) { uint8_t tmp[8]; tmp[0] = result[0] & 0xff; tmp[1] = (result[0] >> 8 )& 0xff; tmp[2] = (result[0] >> 16 )& 0xff; tmp[3] = (result[0] >> 24 )& 0xff; tmp[4] = result[1] & 0xff; tmp[5] = (result[1] >> 8 )& 0xff; tmp[6] = (result[1] >> 16 )& 0xff; tmp[7] = (result[1] >> 24 )& 0xff; lua_pushlstring(L, (const char *)tmp, 8); return 1; } static int lhmac64(lua_State *L) { uint32_t x[2], y[2]; read64(L, x, y); uint32_t result[2]; hmac(x,y,result); return pushqword(L, result); } /* h1 = crypt.hmac64_md5(a,b) m = md5.sum((a..b):rep(3)) h2 = crypt.xor_str(m:sub(1,8), m:sub(9,16)) assert(h1 == h2) */ static int lhmac64_md5(lua_State *L) { uint32_t x[2], y[2]; read64(L, x, y); uint32_t result[2]; hmac_md5(x,y,result); return pushqword(L, result); } /* 8bytes key string text */ static int lhmac_hash(lua_State *L) { uint32_t key[2]; size_t sz = 0; const uint8_t *x = (const uint8_t *)luaL_checklstring(L, 1, &sz); if (sz != 8) { luaL_error(L, "Invalid uint64 key"); } key[0] = x[0] | x[1]<<8 | x[2]<<16 | x[3]<<24; key[1] = x[4] | x[5]<<8 | x[6]<<16 | x[7]<<24; const char * text = luaL_checklstring(L, 2, &sz); uint8_t h[8]; Hash(text,(int)sz,h); uint32_t htext[2]; htext[0] = h[0] | h[1]<<8 | h[2]<<16 | h[3]<<24; htext[1] = h[4] | h[5]<<8 | h[6]<<16 | h[7]<<24; uint32_t result[2]; hmac(htext,key,result); return pushqword(L, result); } // powmodp64 for DH-key exchange // The biggest 64bit prime #define P 0xffffffffffffffc5ull static inline uint64_t mul_mod_p(uint64_t a, uint64_t b) { uint64_t m = 0; while(b) { if(b&1) { uint64_t t = P-a; if ( m >= t) { m -= t; } else { m += a; } } if (a >= P - a) { a = a * 2 - P; } else { a = a * 2; } b>>=1; } return m; } static inline uint64_t pow_mod_p(uint64_t a, uint64_t b) { if (b==1) { return a; } uint64_t t = pow_mod_p(a, b>>1); t = mul_mod_p(t,t); if (b % 2) { t = mul_mod_p(t, a); } return t; } // calc a^b % p static uint64_t powmodp(uint64_t a, uint64_t b) { if (a > P) a%=P; return pow_mod_p(a,b); } static void push64(lua_State *L, uint64_t r) { uint8_t tmp[8]; tmp[0] = r & 0xff; tmp[1] = (r >> 8 )& 0xff; tmp[2] = (r >> 16 )& 0xff; tmp[3] = (r >> 24 )& 0xff; tmp[4] = (r >> 32 )& 0xff; tmp[5] = (r >> 40 )& 0xff; tmp[6] = (r >> 48 )& 0xff; tmp[7] = (r >> 56 )& 0xff; lua_pushlstring(L, (const char *)tmp, 8); } static int ldhsecret(lua_State *L) { uint32_t x[2], y[2]; read64(L, x, y); uint64_t xx = (uint64_t)x[0] | (uint64_t)x[1]<<32; uint64_t yy = (uint64_t)y[0] | (uint64_t)y[1]<<32; if (xx == 0 || yy == 0) return luaL_error(L, "Can't be 0"); uint64_t r = powmodp(xx, yy); push64(L, r); return 1; } #define G 5 static int ldhexchange(lua_State *L) { size_t sz = 0; const uint8_t *x = (const uint8_t *)luaL_checklstring(L, 1, &sz); if (sz != 8) { luaL_error(L, "Invalid dh uint64 key"); } uint32_t xx[2]; xx[0] = x[0] | x[1]<<8 | x[2]<<16 | x[3]<<24; xx[1] = x[4] | x[5]<<8 | x[6]<<16 | x[7]<<24; uint64_t x64 = (uint64_t)xx[0] | (uint64_t)xx[1]<<32; if (x64 == 0) return luaL_error(L, "Can't be 0"); uint64_t r = powmodp(G, x64); push64(L, r); return 1; } // base64 static int lb64encode(lua_State *L) { static const char* encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; size_t sz = 0; const uint8_t * text = (const uint8_t *)luaL_checklstring(L, 1, &sz); int encode_sz = (sz + 2)/3*4; char tmp[SMALL_CHUNK]; char *buffer = tmp; if (encode_sz > SMALL_CHUNK) { buffer = (char*)lua_newuserdatauv(L, encode_sz, 0); } int i,j; j=0; for (i=0;i<(int)sz-2;i+=3) { uint32_t v = text[i] << 16 | text[i+1] << 8 | text[i+2]; buffer[j] = encoding[v >> 18]; buffer[j+1] = encoding[(v >> 12) & 0x3f]; buffer[j+2] = encoding[(v >> 6) & 0x3f]; buffer[j+3] = encoding[(v) & 0x3f]; j+=4; } int padding = sz-i; uint32_t v; switch(padding) { case 1 : v = text[i]; buffer[j] = encoding[v >> 2]; buffer[j+1] = encoding[(v & 3) << 4]; buffer[j+2] = '='; buffer[j+3] = '='; break; case 2 : v = text[i] << 8 | text[i+1]; buffer[j] = encoding[v >> 10]; buffer[j+1] = encoding[(v >> 4) & 0x3f]; buffer[j+2] = encoding[(v & 0xf) << 2]; buffer[j+3] = '='; break; } lua_pushlstring(L, buffer, encode_sz); return 1; } static inline int b64index(uint8_t c) { static const int decoding[] = {62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-2,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51}; int decoding_size = sizeof(decoding)/sizeof(decoding[0]); if (c<43) { return -1; } c -= 43; if (c>=decoding_size) return -1; return decoding[c]; } static int lb64decode(lua_State *L) { size_t sz = 0; const uint8_t * text = (const uint8_t *)luaL_checklstring(L, 1, &sz); int decode_sz = (sz+3)/4*3; char tmp[SMALL_CHUNK]; char *buffer = tmp; if (decode_sz > SMALL_CHUNK) { buffer = (char*)lua_newuserdatauv(L, decode_sz, 0); } int i,j; int output = 0; for (i=0;i=sz && 4>j){ /*To improve compatibility, there may not be enough equal signs */ c[j] = -2; }else{ c[j] = b64index(text[i]); } if (c[j] == -1) { ++i; continue; } if (c[j] == -2) { ++padding; } ++i; ++j; } uint32_t v; switch (padding) { case 0: v = (unsigned)c[0] << 18 | c[1] << 12 | c[2] << 6 | c[3]; buffer[output] = v >> 16; buffer[output+1] = (v >> 8) & 0xff; buffer[output+2] = v & 0xff; output += 3; break; case 1: if (c[3] != -2 || (c[2] & 3)!=0) { return luaL_error(L, "Invalid base64 text"); } v = (unsigned)c[0] << 10 | c[1] << 4 | c[2] >> 2 ; buffer[output] = v >> 8; buffer[output+1] = v & 0xff; output += 2; break; case 2: if (c[3] != -2 || c[2] != -2 || (c[1] & 0xf) !=0) { return luaL_error(L, "Invalid base64 text"); } v = (unsigned)c[0] << 2 | c[1] >> 4; buffer[output] = v; ++ output; break; default: return luaL_error(L, "Invalid base64 text"); } } lua_pushlstring(L, buffer, output); return 1; } static int lxor_str(lua_State *L) { size_t len1,len2; const char *s1 = luaL_checklstring(L,1,&len1); const char *s2 = luaL_checklstring(L,2,&len2); if (len2 == 0) { return luaL_error(L, "Can't xor empty string"); } luaL_Buffer b; char * buffer = luaL_buffinitsize(L, &b, len1); int i; for (i=0;i #include #include #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #define LONGPATH_MAX 4096 #include #include #include #include #define STAT_STRUCT struct _stati64 #define STAT_FUNC _wstati64 #ifndef S_ISDIR #define S_ISDIR(mode) (mode&_S_IFDIR) #endif #ifndef S_ISREG #define S_ISREG(mode) (mode&_S_IFREG) #endif #ifndef S_ISLNK #define S_ISLNK(mode) (0) #endif #ifndef S_ISSOCK #define S_ISSOCK(mode) (0) #endif #ifndef S_ISFIFO #define S_ISFIFO(mode) (0) #endif #ifndef S_ISCHR #define S_ISCHR(mode) (mode&_S_IFCHR) #endif #ifndef S_ISBLK #define S_ISBLK(mode) (0) #endif static int utf8_filename(lua_State *L, const wchar_t * winfilename, int wsz, char *utf8buffer, int sz) { int result = WideCharToMultiByte(CP_UTF8, 0, winfilename, wsz, utf8buffer, sz, NULL, NULL); if (result == 0) return luaL_error(L, "convert to utf-8 filename fail"); if (wsz < 0) // not include end \0 return result - 1; if (result >= sz) return luaL_error(L, "convert to utf-8 filename : buffer overflow"); utf8buffer[result] = 0; return result; } #define DIR_METATABLE "SOLUNA_DIR" struct dir_data { HANDLE findfile; int closed; }; static int windows_filename(lua_State *L, const char * utf8filename, int usz, wchar_t * winbuffer, int wsz) { int result = MultiByteToWideChar(CP_UTF8, 0, utf8filename, usz, winbuffer, wsz); if (result == 0) return luaL_error(L, "convert to windows utf-16 filename fail"); if (result < 0) return result - 1; if (result >= wsz) return luaL_error(L, "convert to windows utf-16 filename : buffer overflow"); winbuffer[result] = 0; return result; } static void system_error(lua_State *L, DWORD errcode) { wchar_t * errormsg; DWORD n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errcode, 0, (void *)&errormsg, sizeof(errormsg), NULL); if (n == 0) { lua_pushfstring(L, "Unknown error %04X", errcode); } else { int i; for (i=n;i>=0;i--) { if (errormsg[i] == 0 || errormsg[i] == '\n' || errormsg[i] == '\r') --n; else { break; } } char tmp[LONGPATH_MAX]; int len = utf8_filename(L, errormsg, n, tmp, LONGPATH_MAX); lua_pushlstring(L, tmp, len); HeapFree(GetProcessHeap(), 0, errormsg); } } static int error_return(lua_State *L) { lua_pushnil(L); system_error(L, GetLastError()); return 2; } static void push_filename(lua_State *L, WIN32_FIND_DATAW *data) { char firstname[LONGPATH_MAX]; int ulen = utf8_filename(L, data->cFileName, -1, firstname, LONGPATH_MAX); lua_pushlstring(L, firstname, ulen); } static int dir_iter(lua_State *L) { struct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE); luaL_argcheck (L, d->closed == 0, 1, "closed directory"); if (d->findfile == INVALID_HANDLE_VALUE) { // no find found d->closed = 1; return 0; } if (lua_getuservalue(L, 1) == LUA_TSTRING) { // find time lua_pushnil(L); lua_setuservalue(L, 1); return 1; } else { WIN32_FIND_DATAW data; if (FindNextFileW(d->findfile, &data)) { push_filename(L, &data); return 1; } else { DWORD errcode = GetLastError(); FindClose(d->findfile); d->findfile = INVALID_HANDLE_VALUE; d->closed = 1; if (errcode == ERROR_NO_MORE_FILES) return 0; lua_pushnil(L); system_error(L, errcode); return 2; } } } static int dir_close(lua_State *L) { struct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE); if (d->findfile != INVALID_HANDLE_VALUE) { FindClose(d->findfile); d->findfile = INVALID_HANDLE_VALUE; } d->closed = 1; return 0; } static int ldir(lua_State *L) { size_t sz; const char * pathname = luaL_checklstring(L, 1, &sz); wchar_t winname[LONGPATH_MAX-3]; int winsz = windows_filename(L, pathname, sz, winname, LONGPATH_MAX-3); winname[winsz] = '\\'; winname[winsz+1] = '*'; winname[winsz+2] = 0; WIN32_FIND_DATAW data; HANDLE findfile = FindFirstFileW(winname, &data); lua_pushcfunction(L, dir_iter); if (findfile == INVALID_HANDLE_VALUE) { DWORD errcode = GetLastError(); if (errcode == ERROR_FILE_NOT_FOUND) { struct dir_data *d = lua_newuserdata(L, sizeof(*d)); d->findfile = INVALID_HANDLE_VALUE; d->closed = 0; } else { system_error(L, errcode); return lua_error(L); } } else { struct dir_data *d = lua_newuserdata(L, sizeof(*d)); d->findfile = findfile; d->closed = 0; push_filename(L, &data); lua_setuservalue(L, -2); // set firstname } if (luaL_newmetatable(L, DIR_METATABLE)) { lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); lua_pushcfunction (L, dir_iter); lua_setfield(L, -2, "next"); lua_pushcfunction (L, dir_close); lua_setfield(L, -2, "close"); lua_pushcfunction (L, dir_close); lua_setfield (L, -2, "__gc"); } lua_setmetatable(L, -2); return 2; } static int lpersonaldir(lua_State *L) { wchar_t document[LONGPATH_MAX] = {0}; if (SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, document) == S_OK) { char utf8path[LONGPATH_MAX]; int sz = utf8_filename(L, document, -1, utf8path, LONGPATH_MAX); lua_pushlstring(L, utf8path, sz); return 1; } else { return error_return(L); } } static int lcurrentdir(lua_State *L) { wchar_t path[LONGPATH_MAX]; char utf8path[LONGPATH_MAX]; DWORD sz = GetCurrentDirectoryW(LONGPATH_MAX, path); if (sz == 0) { return error_return(L); } int usz = utf8_filename(L, path, -1, utf8path, LONGPATH_MAX); lua_pushlstring(L, utf8path, usz); return 1; } static int lchdir(lua_State *L) { size_t sz; const char * utf8path = luaL_checklstring(L, 1, &sz); wchar_t path[LONGPATH_MAX]; windows_filename(L, utf8path, sz, path, LONGPATH_MAX); if (SetCurrentDirectoryW(path) == 0) { return error_return(L); } lua_pushboolean(L, 1); return 1; } static const char * mode2string (unsigned short mode) { if ( S_ISREG(mode) ) return "file"; else if ( S_ISDIR(mode) ) return "directory"; else if ( S_ISLNK(mode) ) return "link"; else if ( S_ISSOCK(mode) ) return "socket"; else if ( S_ISFIFO(mode) ) return "named pipe"; else if ( S_ISCHR(mode) ) return "char device"; else if ( S_ISBLK(mode) ) return "block device"; else return "other"; } /* inode protection mode */ static void push_st_mode (lua_State *L, STAT_STRUCT *info) { lua_pushstring (L, mode2string (info->st_mode)); } /* device inode resides on */ static void push_st_dev (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer) info->st_dev); } /* inode's number */ static void push_st_ino (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer) info->st_ino); } /* number of hard links to the file */ static void push_st_nlink (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer)info->st_nlink); } /* user-id of owner */ static void push_st_uid (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer)info->st_uid); } /* group-id of owner */ static void push_st_gid (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer)info->st_gid); } /* device type, for special file inode */ static void push_st_rdev (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer) info->st_rdev); } /* time of last access */ static void push_st_atime (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer) info->st_atime); } /* time of last data modification */ static void push_st_mtime (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer) info->st_mtime); } /* time of last file status change */ static void push_st_ctime (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer) info->st_ctime); } /* file size, in bytes */ static void push_st_size (lua_State *L, STAT_STRUCT *info) { lua_pushinteger (L, (lua_Integer)info->st_size); } static const char *perm2string (unsigned short mode) { static char perms[10] = "---------"; int i; for (i=0;i<9;i++) perms[i]='-'; if (mode & _S_IREAD) { perms[0] = 'r'; perms[3] = 'r'; perms[6] = 'r'; } if (mode & _S_IWRITE) { perms[1] = 'w'; perms[4] = 'w'; perms[7] = 'w'; } if (mode & _S_IEXEC) { perms[2] = 'x'; perms[5] = 'x'; perms[8] = 'x'; } return perms; } /* permssions string */ static void push_st_perm (lua_State *L, STAT_STRUCT *info) { lua_pushstring (L, perm2string (info->st_mode)); } typedef void (*_push_function) (lua_State *L, STAT_STRUCT *info); struct _stat_members { const char *name; _push_function push; }; struct _stat_members members[] = { { "mode", push_st_mode }, { "dev", push_st_dev }, { "ino", push_st_ino }, { "nlink", push_st_nlink }, { "uid", push_st_uid }, { "gid", push_st_gid }, { "rdev", push_st_rdev }, { "access", push_st_atime }, { "modification", push_st_mtime }, { "change", push_st_ctime }, { "size", push_st_size }, { "permissions", push_st_perm }, { NULL, NULL } }; /* ** Get file or symbolic link information */ static int file_info (lua_State *L) { STAT_STRUCT info; size_t sz; int i; const char * utf8path = luaL_checklstring(L, 1, &sz); wchar_t file[LONGPATH_MAX]; windows_filename(L, utf8path, sz, file, LONGPATH_MAX); if (STAT_FUNC(file, &info)) { lua_pushnil(L); lua_pushfstring(L, "cannot obtain information from file '%s': %s", file, strerror(errno)); lua_pushinteger(L, errno); return 3; } if (lua_isstring (L, 2)) { const char *member = lua_tostring (L, 2); for (i = 0; members[i].name; i++) { if (strcmp(members[i].name, member) == 0) { /* push member value and return */ members[i].push (L, &info); return 1; } } /* member not found */ return luaL_error(L, "invalid attribute name '%s'", member); } /* creates a table if none is given, removes extra arguments */ lua_settop(L, 2); if (!lua_istable (L, 2)) { lua_newtable (L); } /* stores all members in table on top of the stack */ for (i = 0; members[i].name; i++) { lua_pushstring (L, members[i].name); members[i].push (L, &info); lua_rawset (L, -3); } return 1; } static int lrealpath(lua_State *L) { size_t sz; const char * pathname = luaL_checklstring(L, 1, &sz); wchar_t winname[LONGPATH_MAX]; wchar_t fullname[LONGPATH_MAX]; windows_filename(L, pathname, sz, winname, LONGPATH_MAX); DWORD r = GetFullPathNameW(winname, LONGPATH_MAX, fullname, NULL); if (r == 0) { return error_return(L); } if (r > LONGPATH_MAX) { return luaL_error(L, "Invalid path %s", pathname); } char result[LONGPATH_MAX]; int len = utf8_filename(L, fullname, r, result, LONGPATH_MAX); lua_pushlstring(L, result, len); return 1; } static inline int create_dir_wchar_(const WCHAR *filenameW) { WIN32_FIND_DATAW FindFileData; HANDLE h = FindFirstFileW(filenameW, &FindFileData); if (h == INVALID_HANDLE_VALUE) { // create dir if (CreateDirectoryW(filenameW, NULL) == 0) return -1; } else { if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { FindClose(h); // dir exist } else { FindClose(h); // not a dir return 0; } } return 1; } static int mkdir_utf8(const char *name) { WCHAR filenameW[FILENAME_MAX + 0x200 + 1]; int n = MultiByteToWideChar(CP_UTF8,0,(const char*)name,-1,filenameW,FILENAME_MAX + 0x200); if (n == 0) return -1; return create_dir_wchar_(filenameW); } static int pusherror(lua_State * L) { lua_pushnil(L); DWORD err = GetLastError(); LPVOID lpMsgBuf; if (FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, 0, (LPWSTR) &lpMsgBuf, 0, NULL) == 0) { lua_pushstring(L, "FormatMessage failed"); } char errtext[1024] = "unknown"; size_t sz = wcslen((LPWSTR)lpMsgBuf); WideCharToMultiByte(CP_UTF8, 0, (LPWSTR)lpMsgBuf, sz, errtext, 1024, NULL, NULL); LocalFree(lpMsgBuf); lua_pushstring(L, errtext); lua_pushinteger(L, err); return 3; } #else #define LONGPATH_MAX 4096 #include #include #include #include #include #include #include #define STAT_STRUCT struct stat #define STAT_FUNC stat #define DIR_METATABLE "SOLUNA_DIR" struct dir_data { DIR* dir; int closed; }; static void system_error(lua_State *L, int errcode) { lua_pushstring(L, strerror(errcode)); } static int error_return(lua_State *L) { lua_pushnil(L); system_error(L, errno); return 2; } static int dir_iter(lua_State *L) { struct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE); luaL_argcheck(L, d->closed == 0, 1, "closed directory"); if (d->dir == NULL) { // no find found d->closed = 1; return 0; } if (lua_getuservalue(L, 1) == LUA_TSTRING) { // first time lua_pushnil(L); lua_setuservalue(L, 1); return 1; } else { struct dirent *entry = readdir(d->dir); if (entry) { lua_pushstring(L, entry->d_name); return 1; } else { closedir(d->dir); d->dir = NULL; d->closed = 1; return 0; } } } static int dir_close(lua_State *L) { struct dir_data *d = luaL_checkudata(L, 1, DIR_METATABLE); if (d->dir != NULL) { closedir(d->dir); d->dir = NULL; } d->closed = 1; return 0; } static int ldir(lua_State *L) { size_t sz; const char * pathname = luaL_checklstring(L, 1, &sz); DIR* dir = opendir(pathname); lua_pushcfunction(L, dir_iter); if (dir == NULL) { if (errno == ENOENT) { struct dir_data *d = lua_newuserdata(L, sizeof(*d)); d->dir = NULL; d->closed = 0; } else { system_error(L, errno); return lua_error(L); } } else { struct dirent *entry = readdir(dir); struct dir_data *d = lua_newuserdata(L, sizeof(*d)); d->dir = dir; d->closed = 0; if (entry) { lua_pushstring(L, entry->d_name); lua_setuservalue(L, -2); // set firstname } } if (luaL_newmetatable(L, DIR_METATABLE)) { lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, dir_iter); lua_setfield(L, -2, "next"); lua_pushcfunction(L, dir_close); lua_setfield(L, -2, "close"); lua_pushcfunction(L, dir_close); lua_setfield(L, -2, "__gc"); } lua_setmetatable(L, -2); return 2; } static int lpersonaldir(lua_State *L) { #if defined(__EMSCRIPTEN__) lua_pushstring(L, "/"); return 1; #else struct passwd *pw = getpwuid(getuid()); if (pw && pw->pw_dir) { lua_pushstring(L, pw->pw_dir); return 1; } else { return error_return(L); } #endif } static int lcurrentdir(lua_State *L) { char path[LONGPATH_MAX]; if (getcwd(path, LONGPATH_MAX) == NULL) { return error_return(L); } lua_pushstring(L, path); return 1; } static int lchdir(lua_State *L) { size_t sz; const char * path = luaL_checklstring(L, 1, &sz); if (chdir(path) != 0) { return error_return(L); } lua_pushboolean(L, 1); return 1; } static const char * mode2string(mode_t mode) { if (S_ISREG(mode)) return "file"; else if (S_ISDIR(mode)) return "directory"; else if (S_ISLNK(mode)) return "link"; else if (S_ISSOCK(mode)) return "socket"; else if (S_ISFIFO(mode)) return "named pipe"; else if (S_ISCHR(mode)) return "char device"; else if (S_ISBLK(mode)) return "block device"; else return "other"; } /* inode protection mode */ static void push_st_mode(lua_State *L, STAT_STRUCT *info) { lua_pushstring(L, mode2string(info->st_mode)); } /* device inode resides on */ static void push_st_dev(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer) info->st_dev); } /* inode's number */ static void push_st_ino(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer) info->st_ino); } /* number of hard links to the file */ static void push_st_nlink(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer)info->st_nlink); } /* user-id of owner */ static void push_st_uid(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer)info->st_uid); } /* group-id of owner */ static void push_st_gid(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer)info->st_gid); } /* device type, for special file inode */ static void push_st_rdev(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer) info->st_rdev); } /* time of last access */ static void push_st_atime(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer) info->st_atime); } /* time of last data modification */ static void push_st_mtime(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer) info->st_mtime); } /* time of last file status change */ static void push_st_ctime(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer) info->st_ctime); } /* file size, in bytes */ static void push_st_size(lua_State *L, STAT_STRUCT *info) { lua_pushinteger(L, (lua_Integer)info->st_size); } static const char *perm2string(mode_t mode) { static char perms[10] = "---------"; int i; for (i=0;i<9;i++) perms[i]='-'; if (mode & S_IRUSR) perms[0] = 'r'; if (mode & S_IWUSR) perms[1] = 'w'; if (mode & S_IXUSR) perms[2] = 'x'; if (mode & S_IRGRP) perms[3] = 'r'; if (mode & S_IWGRP) perms[4] = 'w'; if (mode & S_IXGRP) perms[5] = 'x'; if (mode & S_IROTH) perms[6] = 'r'; if (mode & S_IWOTH) perms[7] = 'w'; if (mode & S_IXOTH) perms[8] = 'x'; return perms; } /* permissions string */ static void push_st_perm(lua_State *L, STAT_STRUCT *info) { lua_pushstring(L, perm2string(info->st_mode)); } typedef void (*_push_function) (lua_State *L, STAT_STRUCT *info); struct _stat_members { const char *name; _push_function push; }; struct _stat_members members[] = { { "mode", push_st_mode }, { "dev", push_st_dev }, { "ino", push_st_ino }, { "nlink", push_st_nlink }, { "uid", push_st_uid }, { "gid", push_st_gid }, { "rdev", push_st_rdev }, { "access", push_st_atime }, { "modification", push_st_mtime }, { "change", push_st_ctime }, { "size", push_st_size }, { "permissions", push_st_perm }, { NULL, NULL } }; /* ** Get file or symbolic link information */ static int file_info(lua_State *L) { STAT_STRUCT info; size_t sz; int i; const char * path = luaL_checklstring(L, 1, &sz); if (STAT_FUNC(path, &info)) { lua_pushnil(L); lua_pushfstring(L, "cannot obtain information from file '%s': %s", path, strerror(errno)); lua_pushinteger(L, errno); return 3; } if (lua_isstring(L, 2)) { const char *member = lua_tostring(L, 2); for (i = 0; members[i].name; i++) { if (strcmp(members[i].name, member) == 0) { /* push member value and return */ members[i].push(L, &info); return 1; } } /* member not found */ return luaL_error(L, "invalid attribute name '%s'", member); } /* creates a table if none is given, removes extra arguments */ lua_settop(L, 2); if (!lua_istable(L, 2)) { lua_newtable(L); } /* stores all members in table on top of the stack */ for (i = 0; members[i].name; i++) { lua_pushstring(L, members[i].name); members[i].push(L, &info); lua_rawset(L, -3); } return 1; } static int lrealpath(lua_State *L) { size_t sz; const char * pathname = luaL_checklstring(L, 1, &sz); char resolved[LONGPATH_MAX]; if (realpath(pathname, resolved) == NULL) { return error_return(L); } lua_pushstring(L, resolved); return 1; } #define mkdir_utf8(path) (mkdir((path), \ S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH)) // todo: succ when dir exist static int pusherror(lua_State * L) { lua_pushnil(L); lua_pushstring(L, strerror(errno)); lua_pushinteger(L, errno); return 3; } #endif static int pushresult(lua_State * L, int res) { if (res == -1) { return pusherror(L); } else if (res == 0) { lua_pushnil(L); lua_pushfstring(L, "%s already exist", lua_tostring(L, 1)); return 2; } else { lua_pushboolean(L, 1); return 1; } } static int lmkdir(lua_State * L) { const char *path = luaL_checkstring(L, 1); return pushresult(L, mkdir_utf8(path)); } int luaopen_localfs(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "personaldir" , lpersonaldir }, { "dir", ldir }, { "currentdir", lcurrentdir }, { "chdir", lchdir }, { "attributes", file_info }, // the same with lfs, but support utf-8 filename { "realpath", lrealpath }, { "mkdir", lmkdir }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/loginfo.h ================================================ #ifndef soluna_loginfo_h #define soluna_loginfo_h #include struct log_info { char tag[64]; uint32_t log_level; uint32_t log_item; uint32_t line_nr; char message[256]; const char *filename; }; #endif ================================================ FILE: src/lsha1.c ================================================ /* SHA-1 in C By Steve Reid 100% Public Domain ----------------- Modified 7/98 By James H. Brown Still 100% Public Domain Corrected a problem which generated improper hash values on 16 bit machines Routine SHA1Update changed from void SHA1Update(SHA1_CTX* context, unsigned char* data, unsigned int len) to void SHA1Update(SHA1_CTX* context, unsigned char* data, unsigned long len) The 'len' parameter was declared an int which works fine on 32 bit machines. However, on 16 bit machines an int is too small for the shifts being done against it. This caused the hash function to generate incorrect values if len was greater than 8191 (8K - 1) due to the 'len << 3' on line 3 of SHA1Update(). Since the file IO in main() reads 16K at a time, any file 8K or larger would be guaranteed to generate the wrong hash (e.g. Test Vector #3, a million "a"s). I also changed the declaration of variables i & j in SHA1Update to unsigned long from unsigned int for the same reason. These changes should make no difference to any 32 bit implementations since an int and a long are the same size in those environments. -- I also corrected a few compiler warnings generated by Borland C. 1. Added #include for exit() prototype 2. Removed unused variable 'j' in SHA1Final 3. Changed exit(0) to return(0) at end of main. ALL changes I made can be located by searching for comments containing 'JHB' ----------------- Modified 8/98 By Steve Reid Still 100% public domain 1- Removed #include and used return() instead of exit() 2- Fixed overwriting of finalcount in SHA1Final() (discovered by Chris Hall) 3- Changed email address from steve@edmweb.com to sreid@sea-to-sky.net ----------------- Modified 4/01 By Saul Kravitz Still 100% PD Modified to run on Compaq Alpha hardware. ----------------- Modified 07/2002 By Ralph Giles Still 100% public domain modified for use with stdint types, autoconf code cleanup, removed attribution comments switched SHA1Final() argument order for consistency use SHA1_ prefix for public api move public api to sha1.h ----------------- Modufiled 08/2014 By Cloud Wu Still 100% PD Lua binding */ /* Test Vectors (from FIPS PUB 180-1) "abc" A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 A million repetitions of "a" 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F */ #include #include #include typedef struct { uint32_t state[5]; uint32_t count[2]; uint8_t buffer[64]; } SHA1_CTX; #define SHA1_DIGEST_SIZE 20 static void SHA1_Transform(uint32_t state[5], const uint8_t buffer[64]); #define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) /* blk0() and blk() perform the initial expand. */ /* I got the idea of expanding during the round function from SSLeay */ /* FIXME: can we do this in an endian-proof way? */ #ifdef WORDS_BIGENDIAN #define blk0(i) block.l[i] #else #define blk0(i) (block.l[i] = (rol(block.l[i],24)&0xFF00FF00) \ |(rol(block.l[i],8)&0x00FF00FF)) #endif #define blk(i) (block.l[i&15] = rol(block.l[(i+13)&15]^block.l[(i+8)&15] \ ^block.l[(i+2)&15]^block.l[i&15],1)) /* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ #define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30); #define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30); #define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30); #define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30); #define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30); /* Hash a single 512-bit block. This is the core of the algorithm. */ static void SHA1_Transform(uint32_t state[5], const uint8_t buffer[64]) { uint32_t a, b, c, d, e; typedef union { uint8_t c[64]; uint32_t l[16]; } CHAR64LONG16; CHAR64LONG16 block; memcpy(&block, buffer, 64); /* Copy context->state[] to working vars */ a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4]; /* 4 rounds of 20 operations each. Loop unrolled. */ R0(a,b,c,d,e, 0); R0(e,a,b,c,d, 1); R0(d,e,a,b,c, 2); R0(c,d,e,a,b, 3); R0(b,c,d,e,a, 4); R0(a,b,c,d,e, 5); R0(e,a,b,c,d, 6); R0(d,e,a,b,c, 7); R0(c,d,e,a,b, 8); R0(b,c,d,e,a, 9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11); R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15); R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19); R2(a,b,c,d,e,20); R2(e,a,b,c,d,21); R2(d,e,a,b,c,22); R2(c,d,e,a,b,23); R2(b,c,d,e,a,24); R2(a,b,c,d,e,25); R2(e,a,b,c,d,26); R2(d,e,a,b,c,27); R2(c,d,e,a,b,28); R2(b,c,d,e,a,29); R2(a,b,c,d,e,30); R2(e,a,b,c,d,31); R2(d,e,a,b,c,32); R2(c,d,e,a,b,33); R2(b,c,d,e,a,34); R2(a,b,c,d,e,35); R2(e,a,b,c,d,36); R2(d,e,a,b,c,37); R2(c,d,e,a,b,38); R2(b,c,d,e,a,39); R3(a,b,c,d,e,40); R3(e,a,b,c,d,41); R3(d,e,a,b,c,42); R3(c,d,e,a,b,43); R3(b,c,d,e,a,44); R3(a,b,c,d,e,45); R3(e,a,b,c,d,46); R3(d,e,a,b,c,47); R3(c,d,e,a,b,48); R3(b,c,d,e,a,49); R3(a,b,c,d,e,50); R3(e,a,b,c,d,51); R3(d,e,a,b,c,52); R3(c,d,e,a,b,53); R3(b,c,d,e,a,54); R3(a,b,c,d,e,55); R3(e,a,b,c,d,56); R3(d,e,a,b,c,57); R3(c,d,e,a,b,58); R3(b,c,d,e,a,59); R4(a,b,c,d,e,60); R4(e,a,b,c,d,61); R4(d,e,a,b,c,62); R4(c,d,e,a,b,63); R4(b,c,d,e,a,64); R4(a,b,c,d,e,65); R4(e,a,b,c,d,66); R4(d,e,a,b,c,67); R4(c,d,e,a,b,68); R4(b,c,d,e,a,69); R4(a,b,c,d,e,70); R4(e,a,b,c,d,71); R4(d,e,a,b,c,72); R4(c,d,e,a,b,73); R4(b,c,d,e,a,74); R4(a,b,c,d,e,75); R4(e,a,b,c,d,76); R4(d,e,a,b,c,77); R4(c,d,e,a,b,78); R4(b,c,d,e,a,79); /* Add the working vars back into context.state[] */ state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e; /* Wipe variables */ a = b = c = d = e = 0; } /* SHA1Init - Initialize new context */ static void sat_SHA1_Init(SHA1_CTX* context) { /* SHA1 initialization constants */ context->state[0] = 0x67452301; context->state[1] = 0xEFCDAB89; context->state[2] = 0x98BADCFE; context->state[3] = 0x10325476; context->state[4] = 0xC3D2E1F0; context->count[0] = context->count[1] = 0; } /* Run your data through this. */ static void sat_SHA1_Update(SHA1_CTX* context, const uint8_t* data, const size_t len) { size_t i, j; #ifdef VERBOSE SHAPrintContext(context, "before"); #endif j = (context->count[0] >> 3) & 63; if ((context->count[0] += len << 3) < (len << 3)) context->count[1]++; context->count[1] += (len >> 29); if ((j + len) > 63) { memcpy(&context->buffer[j], data, (i = 64-j)); SHA1_Transform(context->state, context->buffer); for ( ; i + 63 < len; i += 64) { SHA1_Transform(context->state, data + i); } j = 0; } else i = 0; memcpy(&context->buffer[j], &data[i], len - i); #ifdef VERBOSE SHAPrintContext(context, "after "); #endif } /* Add padding and return the message digest. */ static void sat_SHA1_Final(SHA1_CTX* context, uint8_t digest[SHA1_DIGEST_SIZE]) { uint32_t i; uint8_t finalcount[8]; for (i = 0; i < 8; i++) { finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] >> ((3-(i & 3)) * 8) ) & 255); /* Endian independent */ } sat_SHA1_Update(context, (uint8_t *)"\200", 1); while ((context->count[0] & 504) != 448) { sat_SHA1_Update(context, (uint8_t *)"\0", 1); } sat_SHA1_Update(context, finalcount, 8); /* Should cause a SHA1_Transform() */ for (i = 0; i < SHA1_DIGEST_SIZE; i++) { digest[i] = (uint8_t) ((context->state[i>>2] >> ((3-(i & 3)) * 8) ) & 255); } /* Wipe variables */ i = 0; memset(context->buffer, 0, 64); memset(context->state, 0, 20); memset(context->count, 0, 8); memset(finalcount, 0, 8); /* SWR */ } #include #include int lsha1(lua_State *L) { size_t sz = 0; const uint8_t * buffer = (const uint8_t *)luaL_checklstring(L, 1, &sz); uint8_t digest[SHA1_DIGEST_SIZE]; SHA1_CTX ctx; sat_SHA1_Init(&ctx); sat_SHA1_Update(&ctx, buffer, sz); sat_SHA1_Final(&ctx, digest); lua_pushlstring(L, (const char *)digest, SHA1_DIGEST_SIZE); return 1; } #define BLOCKSIZE 64 static inline void xor_key(uint8_t key[BLOCKSIZE], uint32_t xor_) { int i; for (i=0;i BLOCKSIZE) { SHA1_CTX ctx; sat_SHA1_Init(&ctx); sat_SHA1_Update(&ctx, key, key_sz); sat_SHA1_Final(&ctx, rkey); key_sz = SHA1_DIGEST_SIZE; } else { memcpy(rkey, key, key_sz); } xor_key(rkey, 0x5c5c5c5c); sat_SHA1_Init(&ctx1); sat_SHA1_Update(&ctx1, rkey, BLOCKSIZE); xor_key(rkey, 0x5c5c5c5c ^ 0x36363636); sat_SHA1_Init(&ctx2); sat_SHA1_Update(&ctx2, rkey, BLOCKSIZE); sat_SHA1_Update(&ctx2, text, text_sz); sat_SHA1_Final(&ctx2, digest2); sat_SHA1_Update(&ctx1, digest2, SHA1_DIGEST_SIZE); sat_SHA1_Final(&ctx1, digest1); lua_pushlstring(L, (const char *)digest1, SHA1_DIGEST_SIZE); return 1; } ================================================ FILE: src/luabuffer.h ================================================ #ifndef soluna_luabuffer_h #define soluna_luabuffer_h #include #include #include static inline uint8_t const * luaL_getbuffer(lua_State *L, size_t *sz) { uint8_t const * ret = NULL; switch (lua_type(L, 1)) { case LUA_TFUNCTION: { lua_pushvalue(L, 1); lua_call(L, 0, 3); ret = (uint8_t const*)lua_touserdata(L, -3); *sz = (size_t)luaL_checkinteger(L, -2); lua_copy(L, -1, 1); int t = lua_type(L, 1); if (t == LUA_TUSERDATA || t == LUA_TTABLE) lua_toclose(L, 1); lua_pop(L, 3); break; } case LUA_TSTRING: ret = (uint8_t const *)luaL_checklstring(L, 1, sz); break; default: luaL_error(L, "Invalid buffer type %s", lua_typename(L, lua_type(L, 1))); } return ret; } #endif ================================================ FILE: src/lualib/coroutine.lua ================================================ local co = require "coroutine" global assert, setmetatable, error local coroutine_create = co.create local coroutine_resume = co.resume local coroutine_close = co.close local coroutine_yield = co.yield local coroutine_status = co.status local coroutine = {} do -- begin coroutine local ltask_coroutines = setmetatable({}, { __mode = "kv" }) -- true : coroutine -- false : suspend -- nil : exit function coroutine.create(f) local co = coroutine_create(f) ltask_coroutines[co] = true return co end do -- begin coroutine.resume local function unlock(co, ...) ltask_coroutines[co] = true return ... end local function ltask_yielding(co, ...) ltask_coroutines[co] = false return unlock(co, coroutine_resume(co, coroutine_yield(...))) end local function resume(co, ok, tag, ...) if not ok then return ok, tag, ... elseif coroutine_status(co) == "dead" then -- the main function exit ltask_coroutines[co] = nil return true, tag, ... elseif tag == "USER" then return true, ... else -- blocked in ltask framework, so raise the yielding message return resume(co, ltask_yielding(co, tag, ...)) end end function coroutine.resume(co, ...) local co_status = ltask_coroutines[co] if not co_status then if co_status == false then -- is running return false, "cannot resume a ltask coroutine suspend by ltask framework" end if coroutine_status(co) == "dead" then -- always return false, "cannot resume dead coroutine" return coroutine_resume(co, ...) else return false, "cannot resume none ltask coroutine" end end return resume(co, coroutine_resume(co, ...)) end end -- end coroutine.resume function coroutine.status(co) local status = ltask_coroutines(co) if status == "suspended" then if ltask_coroutines[co] == false then return "blocked" else return "suspended" end else return status end end function coroutine.yield(...) return coroutine_yield("USER", ...) end do -- begin coroutine.wrap local function wrap_co(ok, ...) if ok then return ... else error(...) end end function coroutine.wrap(f) local co = coroutine.create(function(...) return f(...) end) return function(...) return wrap_co(coroutine.resume(co, ...)) end end end -- end coroutine.wrap function coroutine.close(co) ltask_coroutines[co] = nil return coroutine_close(co) end end -- end corotuine _ENV.coroutine = coroutine return coroutine ================================================ FILE: src/lualib/fontmgr.lua ================================================ local ttf = require "soluna.font.truetype" local string = string local utf8 = utf8 local table = table local debug = debug global pairs, ipairs, assert, rawget, setmetatable local MAXFONT = 64 local namelist = {} local CACHE = {} local function utf16toutf8(s) local surrogate return (s:gsub("..", function(utf16) local cp = string.unpack(">H", utf16) if (cp & 0xFC00) == 0xD800 then surrogate = cp return "" else if surrogate then cp = ((surrogate - 0xD800) << 10) + (cp - 0xDC00) + 0x10000 surrogate = nil end return utf8.char(cp) end end)) end local ids = { UNICODE = { id = 0, encoding = { UNICODE_1_0 = 0, UNICODE_1_1 = 1, ISO_10646 = 2, UNICODE_2_0_BMP = 3, UNICODE_2_0_FULL = 4, }, 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, encoding = { UNICODE_BMP = 1, UNICODE_FULL = 10, }, lang = { ENGLISH =0x0409, CHINESE =0x0804, DUTCH =0x0413, FRENCH =0x040c, GERMAN =0x0407, HEBREW =0x040d, ITALIAN =0x0410, JAPANESE =0x0411, KOREAN =0x0412, RUSSIAN =0x0419, SPANISH =0x0409, SWEDISH =0x041D, }, }, MACINTOSH = { id = 1, encoding = { ROMAN = 0, JAPANESE = 1, CHINESE_TRADITIONAL = 2, KOREAN = 3, ARABIC = 4, HEBREW = 5, GREEK = 6, RUSSIAN = 7, RSYMBOL = 8, DEVANAGARI = 9, GURMUKHI = 10, GUJARATI = 11, ORIYA = 12, BENGALI = 13, TAMIL = 14, TELUGU = 15, KANNADA = 16, MALAYALAM = 17, SINHALESE = 18, BURMESE = 19, KHMER = 20, THAI = 21, LAOTIAN = 22, GEORGIAN = 23, ARMENIAN = 24, CHINESE_SIMPLIFIED = 25, TIBETAN = 26, MONGOLIAN = 27, GEEZ = 28, SLAVIC = 29, VIETNAMESE = 30, SINDHI = 31, }, lang = { ENGLISH = 0, FRENCH = 1, GERMAN = 2, ITALIAN = 3, DUTCH = 4, SWEDISH = 5, SPANISH = 6, DANISH = 7, PORTUGUESE = 8, NORWEGIAN = 9, HEBREW = 10, JAPANESE = 11, ARABIC = 12, FINNISH = 13, GREEK = 14, ICELANDIC = 15, MALTESE = 16, TURKISH = 17, CROATIAN = 18, CHINESE_TRADITIONAL = 19, URDU = 20, HINDI = 21, THAI = 22, KOREAN = 23, LITHUANIAN = 24, POLISH = 25, HUNGARIAN = 26, ESTONIAN = 27, LATVIAN = 28, SAMI = 29, FAROESE = 30, FARSI = 31, RUSSIAN = 32, CHINESE_SIMPLIFIED = 33, FLEMISH = 34, IRISH_GAELIC = 35, ALBANIAN = 36, ROMANIAN = 37, CZECH = 38, SLOVAK = 39, SLOVENIAN = 40, YIDDISH = 41, SERBIAN = 42, MACEDONIAN = 43, BULGARIAN = 44, UKRAINIAN = 45, BYELORUSSIAN = 46, UZBEK = 47, KAZAKH = 48, AZERBAIJANI_CYRILLIC = 49, AZERBAIJANI_ARABIC = 50, ARMENIAN = 51, GEORGIAN = 52, MOLDAVIAN = 53, KIRGHIZ = 54, TAJIKI = 55, TURKMEN = 56, MONGOLIAN = 57, MONGOLIAN_CYRILLIC = 58, PASHTO = 59, KURDISH = 60, KASHMIRI = 61, SINDHI = 62, TIBETAN = 63, NEPALI = 64, SANSKRIT = 65, MARATHI = 66, BENGALI = 67, ASSAMESE = 68, GUJARATI = 69, PUNJABI = 70, ORIYA = 71, MALAYALAM = 72, KANNADA = 73, TAMIL = 74, TELUGU = 75, SINHALESE = 76, BURMESE = 77, KHMER = 78, LAO = 79, VIETNAMESE = 80, INDONESIAN = 81, TAGALOG = 82, MALAY_ROMAN = 83, MALAY_ARABIC = 84, AMHARIC = 85, TIGRINYA = 86, GALLA = 87, SOMALI = 88, SWAHILI = 89, KINYARWANDA = 90, RUNDI = 91, NYANJA = 92, MALAGASY = 93, ESPERANTO = 94, }, }, } local function import(fontdata) local index = 0 local cache = {} while true do for _, obj in pairs(ids) do for _, encoding_id in pairs(obj.encoding) do for _, lang_id in pairs(obj.lang) do local fname, sname = ttf.namestring(fontdata, index, obj.id, encoding_id, lang_id) if fname then fname = utf16toutf8(fname) local fullname = fname fname = string.lower(fname) if sname then sname = utf16toutf8(sname) fullname = fullname .. " " .. sname sname = string.lower(sname) end if not cache[fullname] then cache[fullname] = true table.insert(namelist, { fontdata = fontdata, index = index, family = fname, sfamily = sname, -- sub family name name = string.lower(fullname), }) end elseif fname == nil then return end end end end index = index + 1 end end local FONT_ID = 0 local function alloc_fontid() FONT_ID = FONT_ID + 1 assert(FONT_ID <= MAXFONT) return FONT_ID end local function matching(obj, name) if obj.family == name or obj.name == name then return true end end local function fetch_name(nametable, name_) if name_ == "" and namelist[1] then name_ = namelist[1].name local id = rawget(nametable, name_) if id then nametable[""] = id return id end end local name = string.lower(name_) for _, obj in ipairs(namelist) do if matching(obj, name) then if not obj.id then obj.id = alloc_fontid() CACHE[obj.id] = obj end local id = obj.id nametable[name_] = id return id end end end setmetatable(ttf.nametable, { __index = fetch_name }) local function fetch_id(_, id) local obj = assert(CACHE[id]) return ttf.update(id, obj.fontdata, obj.index) end setmetatable(ttf.idtable, { __index = fetch_id }) local function enum_name(_, idx) local i = idx // 2 local what = (idx % 2 == 1) and "family" or "name" local n = namelist[i+1] if not n then return else return n[what] end end setmetatable(ttf.enum, { __index = enum_name }) debug.getregistry().TRUETYPE_IMPORT = import ================================================ FILE: src/lualib/icon.lua ================================================ local sdf = require "soluna.image.sdf" local datalist = require "soluna.datalist" local file = require "soluna.file" local mattext = require "soluna.material.text" global error, tostring, print local icon = {} function icon.bundle(filename) local path = filename:match "(.*[/\\])[^/\\]+$" local b = datalist.parse(file.load(filename)) local names = {} local icons = {} local n = #b for i = 1, n do local icon = b[i] names[icon.name] = i - 1 local src = file.load(path .. icon.image) or error ("Open icon fail : " .. tostring(icon.name)) local img = sdf.load(src) icons[i] = img end icon.names = names return sdf.bundle(icons) end function icon.symbol(name, size, color) local id = icon.names[name] or error "No icon " .. name return mattext.char(id, 255, size, color) end return icon ================================================ FILE: src/lualib/initsetting.lua ================================================ local datalist = require "soluna.datalist" local source = require "soluna.embedsource" local lfs = require "soluna.lfs" local file = require "soluna.file" global type, error, pairs, assert, tonumber, print local S = {} local function patch(s, k, v) if type(k) == "number" then -- ignore return end local branch, key = k:match "^([^.]+)%.(.+)" if branch then local tree = s[branch] if tree == nil then tree = {} s[branch] = tree elseif type(tree) ~= "table" then error ("Conflict setting key : " .. k) end s = tree k = key end if type(v) == "table" then local orig_v = s[k] if orig_v == nil then s[k] = v elseif type(orig_v) == "table" then for sub_k,sub_v in pairs(v) do patch(orig_v, sub_k, sub_v) end else error ("Conflict setting key : " .. k) end else s[k] = v end end local function settings_filename(filename, change_root) if filename then local realname = assert(lfs.realpath(filename)) if change_root then local curpath, name = realname:match "(.*)[/\\]([^/\\]+)$" if curpath and name then lfs.chdir(curpath) end return name else return realname end end if file.exist "main.game" then return "main.game" end end function S.init(args, change_root) local default_settings = datalist.parse(source.data.settingdefault) local realname = settings_filename(args[1], change_root) if realname then local data = file.load(realname) or error ("Can't open " .. realname) local game_settings = datalist.parse(data) for k,v in pairs(game_settings) do patch(default_settings, k,v) end end for k,v in pairs(args) do if type(k) == "string" then if v == "true" then v = true elseif v == "false" then v = false else v = tonumber(v) or v end patch(default_settings, k,v) end end return default_settings end return S ================================================ FILE: src/lualib/layout.lua ================================================ local yoga = require "soluna.layout.yoga" local datalist = require "soluna.datalist" local file = require "soluna.file" local table = table global next, error, assert, type, setmetatable, pairs local layout = {} local document = {} local element = {} ; element.__index = element function document:__gc() local root = self._root -- root yoga object if root then yoga.node_free(root) self._root = nil end self._yoga = nil -- yoga objects for elements self._list = nil -- image/text element lists self._element = nil -- elements can be update end function document:__index(id) return self._element[id] end function document:__tostring() return "[document]" end function document:__pairs() return next, self._element end function element:__tostring() return "[element:"..self._id.."]" end function element:__newindex(key, value) local _yoga = self._document._yoga local cobj = (_yoga and _yoga[self._id]) or error ("No id : " .. self._id) yoga.node_set(cobj, key, value) end -- update attr function element:update(attr) local _yoga = self._document._yoga local cobj = (_yoga and _yoga[self._id]) or error ("No id : " .. self._id) yoga.node_set(cobj, attr) end function element:get() local _yoga = self._document._yoga local cobj = (_yoga and _yoga[self._id]) or error ("No id : " .. self._id) return yoga.node_get(cobj) end function element:attribs() local _yoga = self._document._yoga local cobj = (_yoga and _yoga[self._id]) or error ("No id : " .. self._id) return _yoga[cobj] end do local function parse_node(v, scripts) local attr = {} local content = {} local n = 1 for i = 1, #v, 2 do local name = v[i] local value = v[i+1] if name == "children" then local c = scripts(value) local len = #c assert(type(c) == "table" and len % 2 == 0) table.move(c, 1, len, n, content) n = n + len elseif type(value) == "table" then content[n] = name content[n+1] = value n = n + 2 else attr[name] = value end end if n == 1 then content = nil end return content, attr end local function new_element(doc, cobj, attr) yoga.node_set(cobj, attr) local id = attr.id if id then if doc._element[id] then error (id .. " exist") end local elem = { _document = doc, _id = id } doc._element[id] = setmetatable(elem, element) doc._yoga[id] = cobj end if attr.image or attr.text or attr.background or attr.region then local obj = {} for k,v in pairs(attr) do obj[k] = v end doc._yoga[obj] = cobj doc._yoga[cobj] = obj doc._list[#doc._list + 1] = obj end end local function add_children(doc, parent, list, scripts) for i = 1, #list, 2 do local name = list[i] -- ignore local content, attr = parse_node(list[i+1], scripts) local cobj = yoga.node_new(parent) new_element(doc, cobj, attr) if content then add_children(doc, cobj, content, scripts) end end end function layout.load(filename_or_list, scripts) local list if type(filename_or_list) == "string" then list = datalist.parse_list(file.load(filename_or_list)) else list = filename_or_list end local doc = { _root = yoga.node_new(), _yoga = {}, _list = {}, _element = {}, } local children, attr = parse_node(list, scripts) new_element(doc, doc._root, attr) if children then add_children(doc, doc._root, children, scripts) end return setmetatable(doc, document) end function layout.calc(doc) yoga.node_calc(doc._root) local list = doc._list local yogaobj = doc._yoga for i = 1, #list do local obj = list[i] local cobj = yogaobj[obj] do local _ENV = obj global x, y, w, h x,y,w,h = yoga.node_get(cobj) end end local _,_,w,h = yoga.node_get(doc._root) list.width = w list.height = h return list end end return layout ================================================ FILE: src/lualib/main.lua ================================================ local package = package local table = table global load, require, assert, select, error, tostring, print, type local init_func_temp = [=[ local name, service_path = ... local embedsource = require "soluna.embedsource" local file = require "soluna.file" package.path = [[${lua_path}]] package.cpath = [[${lua_cpath}]] local zipfile = [[${zipfile}]] if zipfile == "" then zipfile = nil end _G.print_r = load(embedsource.runtime.print_r(), "@src/lualib/print_r.lua")() local packageloader = load(embedsource.runtime.packageloader(), "@src/lualib/packageloader.lua") packageloader(zipfile) local function embedloader(name) local ename if name == "soluna" then ename = "soluna" else ename = name:match "^soluna%.(.*)" end if ename then local code = embedsource.lib[ename] if code then return function() local srcname = "src/lualib/"..ename..".lua" local f = load(code(), "@" .. srcname) return f(ename, srcname) end end return "no embed soluna." .. ename end end package.searchers[#package.searchers+1] = embedloader local extlua = require "soluna.extlua" if extlua.searcher() then -- has preload libs package.searchers[#package.searchers+1] = extlua.searcher end local embedcode = embedsource.service[name] if embedcode then return load(embedcode(),"=("..name..")") end local filename, err = file.searchpath(name, service_path or "${service_path}") if not filename then return nil, err end return load(file.load(filename), "@"..filename) ]=] local api = {} local function start(config) local boot = require "ltask.bootstrap" local mqueue = require "ltask.mqueue" local embedsource = require "soluna.embedsource" local soluna_app = require "soluna.app" -- set callback message handler local root_config = { bootstrap = config.bootstrap, service_source = embedsource.runtime.service(), service_chunkname = "@3rd/ltask/lualib/service.lua", initfunc = init_func_temp:gsub("%$%{([^}]*)%}", { lua_path = package.path, lua_cpath = package.cpath, service_path = config.service_path or "", zipfile = config.args.zipfile or "", }), } table.insert(root_config.bootstrap, { name = "start", args = { config.args, }, }) boot.init_socket() local bootstrap = load(embedsource.runtime.bootstrap(), "@3rd/ltask/lualib/bootstrap.lua")() local core = config.core or {} core.external_queue = core.external_queue or 4096 local ctx = bootstrap.start { core = core, root = root_config, root_initfunc = root_config.initfunc, mainthread = config.mainthread, } -- wait for INIT_EVENT, see start.lua boot.mainthread_wait() local sender, sender_ud = bootstrap.external_sender(ctx) local c_sendmessage = require "soluna.app".sendmessage local function send_message(...) return c_sendmessage(sender, sender_ud, ...) end local logger, logger_ud = bootstrap.log_sender(ctx) local unpackevent = assert(soluna_app.unpackevent) local appmsg_queue = mqueue.new(128) local recvmsg = mqueue.recv local appmsg = {} function appmsg.set_title(text) soluna_app.set_window_title(text) end function appmsg.set_icon(data) soluna_app.set_icon(data) end local function do_appmsg(what, ...) local f = appmsg[what] or error ("Unknown app message " .. tostring(what)) f(...) end local function dispatch_appmsg(v) while v do do_appmsg(boot.unpack_remove(v)) v = recvmsg(appmsg_queue, appmsg) end end return { send_log = logger, send_log_ud = logger_ud, mqueue = appmsg_queue, cleanup = function() while not send_message "cleanup" do end bootstrap.wait(ctx) api.deinit() mqueue.delete(appmsg_queue) appmsg_queue = nil end, frame = function(count) local v = recvmsg(appmsg_queue) if v then dispatch_appmsg(v) end if send_message("frame", count) then boot.mainthread_wait() end end, event = function(ev) send_message(unpackevent(ev)) end, } end local args = ... or {} for i = 2, select("#", ...) do args[i-1] = select(i, ...) end if args.path then package.path = args.path end if args.cpath then package.cpath = args.cpath end local audio_device function api.start(app) app.audio_device = audio_device args.app = app return start { args = args, core = { debuglog = "=", -- stdout }, bootstrap = { { name = "timer", unique = true, }, { name = "log", unique = true, }, { name = "loader", unique = true, }, { name = "audio", unique = true, }, }, } end local function preload_ext(list, entry_name) if list == nil then return end if type(list) == "string" then list = { list } end for i = 1, #list do local name = list[i] local f = package.loadlib(package.searchpath(name, package.cpath), entry_name) or error ("Can't load extlua " .. name) list[i] = f end local extlua = require "soluna.extlua" extlua.preload(list) end function api.init(desc) -- todo : settings local zipfile = args[1] or args.zipfile or "main.zip" local embedsource = require "soluna.embedsource" local packageloader = load(embedsource.runtime.packageloader(), "@src/lualib/packageloader.lua") if packageloader(zipfile) then args.zipfile = zipfile if zipfile == args[1] then table.remove(args, 1) end end local initsetting = load(embedsource.lib.initsetting, "@3rd/ltask/lualib/initsetting.lua")() local settings = initsetting.init(args) preload_ext(settings.extlua_preload, settings.extlua_entry) local soluna_app = require "soluna.app" soluna_app.init_desc(desc, settings) local audio = require "soluna.audio" audio.device, audio_device = audio.init() end function api.deinit() if audio_device then local audio = require "soluna.audio" audio.deinit(audio_device) audio_device = nil end end return api ================================================ FILE: src/lualib/packageloader.lua ================================================ local file = require "soluna.file" local zip = require "soluna.zip" local lfs = require "soluna.lfs" local package = package local string = string local io = io global load, print, setmetatable, table, type, tostring, ipairs, require, error, assert local dir_sep, temp_sep, temp_marker = package.config:match "(.)\n(.)\n(.)" local temp_pat = "[^"..temp_sep.."]+" local function load_zips(zipnames) if zipnames == nil then return end local n = 0 local r = {} for fullname in zipnames:gmatch "[^:;]+" do local name, root = fullname:match "(.-)@(.*)" if name then root = root .. "/" else name = fullname end local zf = zip.open(name, "r") if not zf then -- print("Can't open patch", name) else -- print("Load patch", name) n = n + 1 r[n] = { zip = zf, root = root, name = name } end end r.n = n if n > 0 then return r else -- print("No zip, use local files") end end local zipfile = load_zips(...) local file_load = file.load local file_exist = file.exist if zipfile then local function find_file(cache, fullname) local name = fullname:match "%./(.*)" or fullname for i = zipfile.n, 1, -1 do local root = zipfile[i].root local name_in_zip if root then local n = #root if name:sub(1, n) == root then name_in_zip = name:sub(n+1) end else name_in_zip = name end local zf = zipfile[i].zip if name_in_zip and zf:exist(name_in_zip) then cache[name] = function() return zf:readfile(name_in_zip) end -- print(name, "in zipfile", i) return cache[name] end end end local list local names_cache = setmetatable({}, { __index = find_file}) function file_load(name) local loader = names_cache[name] return loader and loader() end function file_exist(name) return names_cache[name] ~= nil end file.local_load = file.load file.local_exist = file.exist file.load = file_load file.ziplist = function () return zip.list(zipfile) end file.exist = file_exist local function gen_list() local tmp = {} local r = {} local n = 1 for i = zipfile.n, 1, -1 do local flist = zipfile[i].zip:list() local root = zipfile[i].root for j = 1, #flist do local name = flist[j] if root then name = root and root .. name end if tmp[name] == nil then tmp[name] = true -- todo : add path of name end r[n] = name n = n + 1 end end table.sort(r) return r end function file.dir(root) list = list or gen_list() root = root:gsub("[^/]$", "%0/") local iter = 1 local n = #list local root_n = #root local last return function() while iter <= n do local t = list[iter] iter = iter + 1 if t:sub(1, root_n) == root then local sname = t:sub(root_n+1):match "[^/]+" if sname ~= last then last = sname return sname end end end end end function file.attributes(fullname) list = list or gen_list() local pathname = fullname .. "/" local pathn = #pathname for i = 1, #list do local t = list[i] if fullname == t then return "file" elseif t:sub(1, pathn) == pathname then return "directory" end end end function file.searchpath(name, path) local cname = name:gsub("%.", "/") for temp in path:gmatch(temp_pat) do local fullname = temp:gsub(temp_marker, cname) if dir_sep ~= '/' then fullname = fullname:gsub(dir_sep, "/") end if file_exist(fullname) then return fullname end end end else file.dir = lfs.dir file.attributes = lfs.attributes file.local_load = file.load file.local_exist = file.exist file.searchpath = package.searchpath end local function fileload(name, fullname) local s, err = file_load(fullname) local f = assert(load(s, "@"..fullname)) return f(name, fullname) end local function search_file(name) local cname = name:gsub("%.", "/") for temp in package.path:gmatch(temp_pat) do local fullname = temp:gsub(temp_marker, cname) if dir_sep ~= '/' then fullname = fullname:gsub(dir_sep, "/") end if file_exist(fullname) then return fileload, fullname end end return "No package : " .. name end package.searchers[2] = search_file return zipfile ================================================ FILE: src/lualib/print_r.lua ================================================ local ltask = require "ltask" local log_info = ltask.log.info local table = table local math = math global pairs, tostring, type, assert, pcall, error, select local function keys(o) local len = #o local skeys = {} local ukeys = {} local n = 1 for k,v in pairs(o) do local nk = math.tointeger(k) if nk == nil or nk <= 0 or nk > len then local sk = tostring(k) if type(k) == "string" then skeys[n] = k; n = n + 1 else ukeys[k] = true end end end table.sort(skeys) for k in pairs(ukeys) do skeys[n] = k; n = n + 1 end return skeys end local function try_no_circular(o) local cache = {} local function seri_no_circular(o) assert(cache[o] == nil, cache) if type(o) == "table" then local result = { "{" } local n = 2 cache[o] = true for i = 1, #o do local v = o[i] result[n] = seri_no_circular(v); n = n + 1 result[n] = " "; n = n + 1 end local key = keys(o) for i = 1, #key do local k = key[i] local v = seri_no_circular(o[k]) result[n] = seri_no_circular(k); n = n + 1 result[n] = ":"; n = n + 1 result[n] = v; n = n + 1 result[n] = " "; n = n + 1 end result[n] = "}" return table.concat(result) else return tostring(o) end end local ok, r = pcall(seri_no_circular, o) if not ok then if r == cache then return else error(r) end end return r end local function mark_circular(o) local cache = {} local n = 1 local function mark(o) if type(o) == "table" then local v = cache[o] if v == nil then cache[o] = false else if v == false then cache[o] = n; n = n + 1 end return end for k,v in pairs(o) do mark(k) mark(v) end end end mark(o) for k,v in pairs(cache) do if not v then cache[k] = nil end end return cache end local function seri_circular(o) local cache = mark_circular(o) local function seri_object(o) if type(o) ~= "table" then return tostring(o) end local result local s = cache[o] if s then if type(s) == "number" then result = { "#"..s.."{" } cache[o] = "[#"..s.."]" else return s end else result = { "{" } end local n = 2 for i = 1, #o do local v = o[i] result[n] = seri_object(v); n = n + 1 result[n] = " "; n = n + 1 end local key = keys(o) for i = 1, #key do local k = key[i] local v = seri_object(o[k]) result[n] = seri_object(k); n = n + 1 result[n] = ":"; n = n + 1 result[n] = v; n = n + 1 result[n] = " "; n = n + 1 end result[n] = "}" return table.concat(result) end return seri_object(o) end local function seri(o) return try_no_circular(o) or seri_circular(o) end local function print_r(...) local len = select("#", ...) local str if len == 1 then str = seri(...) else local r = {} local n = 1 for i = 1, len do local o = select(i, ...) r[n] = seri(o); n = n + 1 end str = table.concat(r, "\t") end local n = #str if n > 1024 then str = str:sub(1, 1024) .. "[..." .. n .. "]" end ltask.pushlog(ltask.pack("print", str)) end return print_r ================================================ FILE: src/lualib/soluna.lua ================================================ local ltask = require "ltask" local app = require "soluna.app" local mqueue = require "ltask.mqueue" global require, error, string, assert, package, setmetatable, tostring local soluna = { platform = app.platform } function soluna.gamepad_init() local gamepad = require "soluna.gamepad" local state = {} soluna.gamepad = state local gs = ltask.uniqueservice "gamepad" local S = ltask.dispatch() function S._gamepad_update() gamepad.update(state) end ltask.call(gs, "register", ltask.self(), "_gamepad_update") return state end local settings function soluna.settings() if settings == nil then local s = ltask.queryservice "settings" settings = ltask.call(s, "get") end return settings end function soluna.set_window_title(text) mqueue.send(app.mqueue(), ltask.pack("set_title", text)) end function soluna.set_icon(data) mqueue.send(app.mqueue(), ltask.pack("set_icon", data)) end local function recursion_mkdir(root, path) local lfs = require "soluna.lfs" for p in path:gmatch "[^/\\]+" do root = root .. "/" .. p lfs.mkdir(root) end return (root:gsub("[^/\\]$", "%0/")) end function soluna.gamedir(name) if name == nil then settings = settings and soluna.settings() name = settings.project or error "missing project name in settings" end local lfs = require "soluna.lfs" local path if soluna.platform == "windows" then path = "My Games/" elseif soluna.platform == "macos" or soluna.platform == "linux" then path = ".local/share/" elseif soluna.platform == "wasm" then path = "persistent/games/" else error "TODO: support none windows" end path = path .. name return recursion_mkdir(lfs.personaldir(), path) end function soluna.load_sprites(filename) local render = ltask.uniqueservice "render" local sprites = ltask.call(render, "load_sprites", filename) return sprites end local audio_service local voice_index = {} local voice_mt = { __index = voice_index } function voice_index:stop(fade_seconds) return ltask.call(audio_service, "voice_stop", self.id, fade_seconds) end function voice_index:playing() return ltask.call(audio_service, "voice_playing", self.id) end function voice_index:set_volume(volume) return ltask.call(audio_service, "voice_set_volume", self.id, volume) end function voice_index:set_pan(pan) return ltask.call(audio_service, "voice_set_pan", self.id, pan) end function voice_index:set_pitch(pitch) return ltask.call(audio_service, "voice_set_pitch", self.id, pitch) end function voice_index:set_loop(loop) return ltask.call(audio_service, "voice_set_loop", self.id, loop) end function voice_index:seek(seconds) return ltask.call(audio_service, "voice_seek", self.id, seconds) end function voice_index:tell() return ltask.call(audio_service, "voice_tell", self.id) end local bus_index = {} local bus_mt = { __index = bus_index } function bus_index:set_volume(volume) return ltask.call(audio_service, "bus_set_volume", self.name, volume) end function soluna.load_sounds(filename) audio_service = audio_service or ltask.uniqueservice "audio" ltask.call(audio_service, "init", filename) end function soluna.play_sound(name, opts) local id, err = ltask.call(audio_service, "play_sound", name, opts) if not id then return nil, err end return setmetatable({ id = id }, voice_mt) end function soluna.audio_bus(name) if not ltask.call(audio_service, "has_bus", name) then return nil, "Unknown audio bus " .. tostring(name) end return setmetatable({ name = name }, bus_mt) end function soluna.preload(spr) local loader = ltask.uniqueservice "loader" if #spr == 0 then ltask.call(loader, "preload", spr.filename, spr.content, spr.w, spr.h) else local async = ltask.async() for i = 1, #spr do local s = spr[i] async:request(loader, "preload", s.filename, s.content, s.w, s.h) end async:wait() end end local function version() local api, hash = app.version() soluna.version_api = api return string.format("%03x", api) .. hash:sub(1, 7) end soluna.version = version() return soluna ================================================ FILE: src/lualib/spritebundle.lua ================================================ local image = require "soluna.image" local file = require "soluna.file" local datalist = require "soluna.datalist" global type, tonumber, error, assert, ipairs, print local M = {} local function load_bundle(filename) local b = datalist.parse(file.load(filename)) return b end local function crop_(item, c) local x = item.cx local y = item.cy local w = item.cw local h = item.ch local offx = item.x or 0 local offy = item.y or 0 if offx < 0 then offx = - c.w * offx // 1 | 0 end if offy < 0 then offy = - c.h * offy // 1 | 0 end local cx, cy, cw, ch = image.crop(c.data, c.w, c.h, x, y, w, h) offx = offx - cx offy = offy - cy item.x = offx item.y = offy item.cx = cx + (x or 0) item.cy = cy + (y or 0) item.cw = cw item.ch = ch end local function unpack_size(size) if type(size) == "number" then return size, size else local x, y = size:match "(%d+)[xX*](%d+)" return tonumber(x), tonumber(y) end end local function crop(item, filecache) local c = filecache[item.filename] if c == nil then error("No file : " .. item.filename) end local number = item.number if number then -- multi sprites local cw, ch = unpack_size(assert(item.size)) local gap = item.gap local gap_x = 0 local gap_y = 0 if gap then gap_x, gap_y = unpack_size(gap) end local cx = 0 local cy = 0 local col = 1 local row if type(number) == "number" then row = number else row, col = unpack_size(number) end local count = 1 local offx = item.x local offy = item.y gap_x = gap_x + cw gap_y = gap_y + ch local filename = item.filename for i = 1, col do cx = 0 for j = 1, row do local s = { cx = cx, cy = cy, cw = cw, ch = ch , x = offx , y = offy, filename = filename } item[count] = s crop_(s, c) count = count + 1 cx = cx + gap_x end cy = cy + gap_y end else crop_(item, c) end end local function load_list(filecache, v, path) for idx, item in ipairs(v) do local fname = item.filename or "Need filename for item " .. idx if path then item.filename = path .. fname end crop(item, filecache) end end function M.load(filecache, filename, path) local v if type(filename) == "table" then v = filename else path = path or filename:match "(.*[/\\])[^/\\]+$" v = load_bundle(filename) end load_list(filecache, v, path) return v end function M.loadimage(filecache, filename) local content = file.load(filename) if not content then if not filecache.__missing[filename] then filecache.__missing[filename] = true print("Missing file : " .. filename) end return end local load = image.load if filename:find "%.alpha%." then load = image.load_alpha end local data, w, h = load(content) if data == nil then if not filecache.__missing[filename] then filecache.__missing[filename] = true print("Invalid image : " .. filename .. "(" .. w .. ")") end return end local r = { data = data, w = w, h = h } filecache[filename] = r return r end return M ================================================ FILE: src/lualib/text.lua ================================================ local font = require "soluna.font" local icon = require "soluna.icon" global setmetatable, print, tonumber local text = {} local bundle_data function text.init(bundle) bundle_data = icon.bundle(bundle) -- prevent gc to collect bundle_data font.import_icon(bundle_data) end local colors = { red = "[FF0000]", green = "[00FF00]", blue = "[0000FF]", white = "[FFFFFF]", black = "[000000]", aqua = "[00FFFF]", yellow = "[FFFF00]", pink = "[FF00FF]", gray = "[808080]", bracket = "[bracket]", } local function user_color(self, name) if name:byte() == 99 then -- 'c' local cvalue = name:sub(2) local c = tonumber(cvalue, 16) if c then local cname = "[" .. cvalue .. "]" self[name] = cname return cname end end end setmetatable(colors, { __index = user_color }) local function icon_id(name) local cname = colors[name] if cname then return cname end local id = icon.names[name] if not id then return "["..name.."]" end return "[i"..id.."]" end local function convert(tbl, key) local escape = key:gsub("%[%[", "[bracket]") local value = escape:gsub("%[(%w+)%]", icon_id) if escape ~= key then value = value:gsub("%[bracket%]", "[[") elseif value == key then -- uniforming long string value = key end tbl[key]=value return value end text.convert = setmetatable({}, { __mode = "kv", __index = convert }) return text ================================================ FILE: src/lualib/util.lua ================================================ local table = table global setmetatable local util = {} local func_chain = {}; func_chain.__index = func_chain function func_chain:add(f) table.insert(self, f) end function func_chain:__call() for i = 1, #self do local f = self[i] f() end end function util.func_chain() return setmetatable({}, func_chain) end return util ================================================ FILE: src/luamods.c ================================================ #include #include int luaopen_ltask(lua_State *L); int luaopen_ltask_root(lua_State *L); int luaopen_ltask_bootstrap(lua_State *L); int luaopen_ltask_mqueue(lua_State *L); int luaopen_embedsource(lua_State *L); int luaopen_appmessage(lua_State *L); int luaopen_applog(lua_State *L); int luaopen_image(lua_State *L); int luaopen_render(lua_State *L); int luaopen_spritemgr(lua_State *L); int luaopen_datalist(lua_State *L); int luaopen_soluna_file(lua_State *L); int luaopen_font_truetype(lua_State *L); int luaopen_font_manager(lua_State *L); int luaopen_font(lua_State *L); int luaopen_drawmgr(lua_State *L); int luaopen_material_default(lua_State *L); int luaopen_material_text(lua_State *L); int luaopen_material_quad(lua_State *L); int luaopen_material_mask(lua_State *L); int luaopen_material_blit(lua_State *L); int luaopen_soluna_app(lua_State *L); int luaopen_font_system(lua_State *L); int luaopen_gamepad_device(lua_State *L); int luaopen_gamepad(lua_State *L); int luaopen_localfs(lua_State *L); int luaopen_image_sdf(lua_State *L); int luaopen_layout_yoga(lua_State *L); int luaopen_url(lua_State *L); int luaopen_skynet_crypt(lua_State *L); int luaopen_zip(lua_State *L); int luaopen_extlua(lua_State *L); int luaopen_soluna_audio(lua_State *L); void soluna_embed(lua_State* L) { static const luaL_Reg modules[] = { { "ltask", luaopen_ltask}, { "ltask.root", luaopen_ltask_root}, { "ltask.bootstrap", luaopen_ltask_bootstrap}, { "ltask.mqueue", luaopen_ltask_mqueue}, { "soluna.app", luaopen_soluna_app }, { "soluna.embedsource", luaopen_embedsource}, { "soluna.log", luaopen_applog }, { "soluna.image", luaopen_image }, { "soluna.render", luaopen_render }, { "soluna.spritemgr", luaopen_spritemgr }, { "soluna.drawmgr", luaopen_drawmgr }, { "soluna.material.default", luaopen_material_default }, { "soluna.material.text", luaopen_material_text }, { "soluna.material.quad", luaopen_material_quad }, { "soluna.material.mask", luaopen_material_mask }, { "soluna.material.blit", luaopen_material_blit }, { "soluna.datalist", luaopen_datalist }, { "soluna.file", luaopen_soluna_file }, { "soluna.font", luaopen_font }, { "soluna.font.truetype", luaopen_font_truetype }, { "soluna.font.manager", luaopen_font_manager }, { "soluna.font.system", luaopen_font_system }, { "soluna.gamepad", luaopen_gamepad }, { "soluna.gamepad.device", luaopen_gamepad_device }, { "soluna.lfs", luaopen_localfs }, { "soluna.image.sdf", luaopen_image_sdf }, { "soluna.layout.yoga", luaopen_layout_yoga }, { "soluna.url", luaopen_url }, { "soluna.crypt", luaopen_skynet_crypt }, { "soluna.zip", luaopen_zip }, { "soluna.extlua", luaopen_extlua }, { "soluna.audio", luaopen_soluna_audio }, { NULL, NULL }, }; const luaL_Reg *lib; luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE); for (lib = modules; lib->func; lib++) { lua_pushcfunction(L, lib->func); lua_setfield(L, -2, lib->name); } lua_pop(L, 1); } ================================================ FILE: src/luayoga.c ================================================ #define LUA_LIB #include #include #include #include #include "yoga/Yoga.h" #define FlexDirection 1 #define Justify 2 #define Align 4 #define Wrap 8 #define Display 16 #define PositionType 32 #define ENUM(x, what) { YG##x##ToString(YG##x##what), (x) << 16 | (YG##x##what) }, struct enum_string { const char * name; int value; }; struct set_number { void (*set)(YGNodeRef node, float width); void (*setPercent)(YGNodeRef node, float width); void (*setAuto)(YGNodeRef node); void (*setMaxContent)(YGNodeRef node); void (*setFitContent)(YGNodeRef node); void (*setStretch)(YGNodeRef node); }; struct set_edge_number { void (*set)(YGNodeRef node, YGEdge edge, float v); void (*setPercent)(YGNodeRef node, YGEdge edge, float v); void (*setAuto)(YGNodeRef node, YGEdge edge); }; struct set_two_number { void (*set)(YGNodeRef node, YGGutter gutter, float v); void (*setPercent)(YGNodeRef node, YGGutter gutter, float v); }; static int lnodeNew(lua_State *L) { YGNodeRef node = YGNodeNew(); if (lua_islightuserdata(L, 1)) { YGNodeRef parent = lua_touserdata(L, 1); size_t n = YGNodeGetChildCount(parent); YGNodeInsertChild(parent, node, n); } lua_pushlightuserdata(L, node); return 1; } static int lnodeFree(lua_State *L) { YGNodeRef node = lua_touserdata(L, 1); YGNodeFreeRecursive(node); return 0; } static int lnodeCalc(lua_State *L) { YGNodeRef node = lua_touserdata(L, 1); YGNodeCalculateLayout(node, YGUndefined, YGUndefined, YGDirectionLTR); return 0; } struct pos { float x; float y; }; static void get_pos(struct pos *p, YGNodeRef node) { p->x = 0; p->y = 0; while (node) { p->x += YGNodeLayoutGetLeft(node); p->y += YGNodeLayoutGetTop(node); node = YGNodeGetParent(node); } } static int lnodeGet(lua_State *L) { YGNodeRef node = lua_touserdata(L, 1); struct pos p; get_pos(&p, node); float r[] = { p.x, p.y, YGNodeLayoutGetWidth(node), YGNodeLayoutGetHeight(node) }; int i; for (i=0;i<4;i++) { lua_pushnumber(L, r[i]); } return 4; } typedef void (*setfunc)(lua_State *L, YGNodeRef node); static inline int is_whitespace(char c) { return c =='\0' || c == ' ' || c == '\t'; } static void setNumberString(lua_State *L, YGNodeRef node, const char *v, const struct set_number *setter) { char* endptr = NULL; float number = strtof(v, &endptr); if (*endptr == '%') { setter->setPercent(node, number); } else if (is_whitespace(*endptr)) { setter->set(node, number); } else if (setter->setAuto && strcmp(v, "auto") == 0) { setter->setAuto(node); } else if (strcmp(v, "stretch") == 0) { setter->setStretch(node); } else if (strcmp(v, "max-content") == 0) { setter->setMaxContent(node); } else if (strcmp(v, "fit-content") == 0) { setter->setFitContent(node); } else { luaL_error(L, "Invalid number %s", v); } } static void setNumber(lua_State *L, YGNodeRef node, const struct set_number *setter) { if (lua_type(L, -1) == LUA_TNUMBER) { float v = lua_tonumber(L, -1); setter->set(node, v); } else { const char * v = luaL_checkstring(L, -1); setNumberString(L, node, v, setter); } } static void lsetWidth(lua_State *L, YGNodeRef node) { static const struct set_number setter = { YGNodeStyleSetWidth, YGNodeStyleSetWidthPercent, YGNodeStyleSetWidthAuto, YGNodeStyleSetWidthMaxContent, YGNodeStyleSetWidthFitContent, }; setNumber(L, node, &setter); } static void lsetMinWidth(lua_State *L, YGNodeRef node) { static const struct set_number setter = { YGNodeStyleSetMinWidth, YGNodeStyleSetMinWidthPercent, NULL, YGNodeStyleSetMinWidthMaxContent, YGNodeStyleSetMinWidthFitContent, }; setNumber(L, node, &setter); } static void lsetMaxWidth(lua_State *L, YGNodeRef node) { static const struct set_number setter = { YGNodeStyleSetMaxWidth, YGNodeStyleSetMaxWidthPercent, NULL, YGNodeStyleSetMaxWidthMaxContent, YGNodeStyleSetMaxWidthFitContent, }; setNumber(L, node, &setter); } static void lsetHeight(lua_State *L, YGNodeRef node) { static const struct set_number setter = { YGNodeStyleSetHeight, YGNodeStyleSetHeightPercent, YGNodeStyleSetHeightAuto, YGNodeStyleSetHeightMaxContent, YGNodeStyleSetHeightFitContent, }; setNumber(L, node, &setter); } static void lsetMinHeight(lua_State *L, YGNodeRef node) { static const struct set_number setter = { YGNodeStyleSetMinHeight, YGNodeStyleSetMinHeightPercent, NULL, YGNodeStyleSetMinHeightMaxContent, YGNodeStyleSetMinHeightFitContent, }; setNumber(L, node, &setter); } static void lsetMaxHeight(lua_State *L, YGNodeRef node) { static const struct set_number setter = { YGNodeStyleSetMaxHeight, YGNodeStyleSetMaxHeightPercent, NULL, YGNodeStyleSetMaxHeightMaxContent, YGNodeStyleSetMaxHeightFitContent, }; setNumber(L, node, &setter); } static const char * skip_whitespace(const char *v) { while(*v == ' ' || *v == '\t') { ++v; } return v; } static int count_words(const char *v) { int n = 0; do { v = skip_whitespace(v); if (*v != '\0') { ++n; while (!is_whitespace(*v)) ++v; } } while (*v != '\0'); return n; } static const char * setEdgeNumber(lua_State *L, YGNodeRef node, YGEdge edge, const char *v, const struct set_edge_number *setter) { v = skip_whitespace(v); char* endptr = NULL; float number = strtof(v, &endptr); if (is_whitespace(*endptr)) { setter->set(node, edge, number); return endptr; } else if (setter->setPercent && *endptr == '%') { setter->setPercent(node, edge, number); return endptr+1; } else if (setter->setAuto && memcmp("auto", v, 4) == 0) { if (!is_whitespace(v[4])) luaL_error(L, "Invalid number %s", v); setter->setAuto(node, edge); return v + 4; } else { luaL_error(L, "Invalid number %s", v); } return NULL; } static void setFourNumber(lua_State *L, YGNodeRef node, const struct set_edge_number *setter) { if (lua_type(L, -1) == LUA_TNUMBER) { float v = lua_tonumber(L, -1); setter->set(node, YGEdgeAll, v); } else { const char * v = luaL_checkstring(L, -1); switch (count_words(v)) { case 1: setEdgeNumber(L, node, YGEdgeAll, v, setter); break; case 2: v = setEdgeNumber(L, node, YGEdgeVertical, v, setter); setEdgeNumber(L, node, YGEdgeHorizontal, v, setter); break; case 3: v = setEdgeNumber(L, node, YGEdgeTop, v, setter); v = setEdgeNumber(L, node, YGEdgeHorizontal, v, setter); setEdgeNumber(L, node, YGEdgeBottom, v, setter); break; case 4: v = setEdgeNumber(L, node, YGEdgeTop, v, setter); v = setEdgeNumber(L, node, YGEdgeEnd, v, setter); v = setEdgeNumber(L, node, YGEdgeBottom, v, setter); setEdgeNumber(L, node, YGEdgeStart, v, setter); break; default: luaL_error(L, "Invalid numbers %s", v); } } } static const char * setGapNumber(lua_State *L, YGNodeRef node, YGGutter gutter, const char *v, const struct set_two_number *setter) { v = skip_whitespace(v); char* endptr = NULL; float number = strtof(v, &endptr); if (is_whitespace(*endptr)) { setter->set(node, gutter, number); return endptr; } else if (setter->setPercent && *endptr == '%') { setter->setPercent(node, gutter, number); return endptr+1; } else { luaL_error(L, "Invalid number %s", v); } return NULL; } static void setTwoNumber(lua_State *L, YGNodeRef node, const struct set_two_number *setter) { if (lua_type(L, -1) == LUA_TNUMBER) { float v = lua_tonumber(L, -1); setter->set(node, YGGutterAll, v); } else { const char * v = luaL_checkstring(L, -1); switch (count_words(v)) { case 1: setGapNumber(L, node, YGGutterAll, v, setter); break; case 2: v = setGapNumber(L, node, YGGutterRow, v, setter); setGapNumber(L, node, YGGutterColumn, v, setter); break; default: luaL_error(L, "Invalid numbers %s", v); } } } static const char * getNumber(lua_State *L, const char *v, float *num) { char* endptr = NULL; *num = strtof(v, &endptr); if (!is_whitespace(*endptr)) luaL_error(L, "Invalid number %s", v); return endptr; } static void setFlexBasis(lua_State *L, YGNodeRef node, const char *v) { static const struct set_number setter = { YGNodeStyleSetFlexBasis, YGNodeStyleSetFlexBasisPercent, YGNodeStyleSetFlexBasisAuto, YGNodeStyleSetFlexBasisMaxContent, YGNodeStyleSetFlexBasisFitContent, }; v = skip_whitespace(v); setNumberString(L, node, v, &setter); } static void lsetFlex(lua_State *L, YGNodeRef node) { if (lua_type(L, -1) == LUA_TNUMBER) { float v = lua_tonumber(L, -1); YGNodeStyleSetFlex(node, v); } else { const char * v = luaL_checkstring(L, -1); char* endptr = NULL; float number; // https://developer.mozilla.org/en-US/docs/Web/CSS/flex switch (count_words(v)) { case 1: // only one word number = strtof(v, &endptr); if (is_whitespace(*endptr)) { // is number, example : 1 // flex-glow 1 0% YGNodeStyleSetFlex(node, number); } else { // not a number, example : 50% // 1 1 flex-basis YGNodeStyleSetFlexGrow(node, 1); YGNodeStyleSetFlexShrink(node, 1); setFlexBasis(L, node, v); } break; case 2: v = getNumber(L, v, &number); YGNodeStyleSetFlexGrow(node, number); number = strtof(v, &endptr); if (is_whitespace(*endptr)) { YGNodeStyleSetFlexShrink(node, number); YGNodeStyleSetFlexBasisPercent(node, 0); } else { YGNodeStyleSetFlexShrink(node, 1); setFlexBasis(L, node, v); } break; case 3: v = getNumber(L, v, &number); YGNodeStyleSetFlexGrow(node, number); v = getNumber(L, v, &number); YGNodeStyleSetFlexShrink(node, number); setFlexBasis(L, node, v); break; default: luaL_error(L, "Invalid flex %s", v); } } } static void lsetMargin(lua_State *L, YGNodeRef node) { static const struct set_edge_number setter = { YGNodeStyleSetMargin, YGNodeStyleSetMarginPercent, YGNodeStyleSetMarginAuto, }; setFourNumber(L, node, &setter); } static void lsetPadding(lua_State *L, YGNodeRef node) { static const struct set_edge_number setter = { YGNodeStyleSetPadding, YGNodeStyleSetPaddingPercent, NULL, }; setFourNumber(L, node, &setter); } static void lsetBorder(lua_State *L, YGNodeRef node) { static const struct set_edge_number setter = { YGNodeStyleSetBorder, NULL, NULL, }; setFourNumber(L, node, &setter); } static void lsetGap(lua_State *L, YGNodeRef node) { static const struct set_two_number setter = { YGNodeStyleSetGap, YGNodeStyleSetGapPercent, }; setTwoNumber(L, node, &setter); } static int getEnum(lua_State *L, int type, const char *pname) { lua_pushvalue(L, -1); if (lua_rawget(L, lua_upvalueindex(2)) == LUA_TNUMBER) { int v = lua_tointeger(L, -1); if (((v >> 16) & type) == type) { v &= 0xffff; return v; } } return luaL_error(L, "Invalid enum %s for %s", luaL_tolstring(L, -2, NULL), pname); } static int getEnumHigh(lua_State *L, int type, const char *pname) { int e = getEnum(L, type, pname); return e >> 8; } static int getEnumLow(lua_State *L, int type, const char *pname) { int e = getEnum(L, type, pname); return e & 0xff; } static void lsetFlexDirection(lua_State *L, YGNodeRef node) { YGNodeStyleSetFlexDirection(node, getEnum(L, FlexDirection, "flex-direction")); } static void lsetJustifyContent(lua_State *L, YGNodeRef node) { YGNodeStyleSetJustifyContent(node, getEnumHigh(L, Justify, "justify-content")); } static void lsetAlignItems(lua_State *L, YGNodeRef node) { YGNodeStyleSetAlignItems(node, getEnumLow(L, Align, "align-items")); } static void lsetAlignContent(lua_State *L, YGNodeRef node) { YGNodeStyleSetAlignContent(node, getEnumLow(L, Align, "align-content")); } static void lsetAlignSelf(lua_State *L, YGNodeRef node) { YGNodeStyleSetAlignSelf(node, getEnumLow(L, Align, "align-self")); } static void lsetWrap(lua_State *L, YGNodeRef node) { YGNodeStyleSetFlexWrap(node, getEnum(L, Wrap, "wrap")); } static void lsetDisplay(lua_State *L, YGNodeRef node) { YGNodeStyleSetDisplay(node, getEnum(L, Display, "display")); } static void lsetPosition(lua_State *L, YGNodeRef node) { YGNodeStyleSetPositionType(node, getEnum(L, PositionType, "position")); } static void setPosition(lua_State *L, YGNodeRef node, YGEdge edge) { static const struct set_edge_number setter = { YGNodeStyleSetPosition, YGNodeStyleSetPositionPercent, YGNodeStyleSetPositionAuto, }; if (lua_type(L, -1) == LUA_TNUMBER) { float v = luaL_checknumber(L, -1); setter.set(node, edge, v); } else { const char *v = luaL_checkstring(L, -1); setEdgeNumber(L, node, edge, v, &setter); } } static void lsetTop(lua_State *L, YGNodeRef node) { setPosition(L, node, YGEdgeTop); } static void lsetBottom(lua_State *L, YGNodeRef node) { setPosition(L, node, YGEdgeBottom); } static void lsetLeft(lua_State *L, YGNodeRef node) { setPosition(L, node, YGEdgeLeft); } static void lsetRight(lua_State *L, YGNodeRef node) { setPosition(L, node, YGEdgeRight); } static void lsetAspectRatio(lua_State *L, YGNodeRef node) { float v = luaL_checknumber(L, -1); YGNodeStyleSetAspectRatio(node, v); } static void set_array(lua_State *L, YGNodeRef node) { lua_pushnil(L); int top = lua_gettop(L); while (lua_next(L, 2) != 0) { lua_pushvalue(L, -2); if (lua_rawget(L, lua_upvalueindex(1)) == LUA_TLIGHTUSERDATA) { setfunc func = (setfunc)lua_touserdata(L, -1); lua_pop(L, 1); func(L, node); } lua_settop(L, top); } } static void set_one(lua_State *L, YGNodeRef node) { lua_settop(L, 3); lua_pushvalue(L, 2); if (lua_rawget(L, lua_upvalueindex(1)) == LUA_TLIGHTUSERDATA) { setfunc func = (setfunc)lua_touserdata(L, -1); lua_pop(L, 1); func(L, node); } else { luaL_error(L, "Invalid attrib name : %s", lua_tostring(L, 2)); } } static int lnodeSet(lua_State *L) { YGNodeRef node = lua_touserdata(L, 1); switch(lua_type(L, 2)) { case LUA_TTABLE: set_array(L, node); break; case LUA_TSTRING: set_one(L, node); break; default: return luaL_error(L, "Set table or key, value"); } return 0; } LUAMOD_API int luaopen_layout_yoga(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "node_new", lnodeNew }, { "node_free", lnodeFree }, { "node_calc", lnodeCalc }, { "node_get", lnodeGet }, { "node_set", NULL }, { NULL, NULL }, }; luaL_newlib(L, l); struct { const char *name; setfunc func; } setter [] = { { "width", lsetWidth }, { "height", lsetHeight }, { "minWidth", lsetMinWidth }, { "maxWidth", lsetMaxWidth }, { "minHeight", lsetMinHeight }, { "maxHeight", lsetMaxHeight }, { "direction", lsetFlexDirection }, { "justify", lsetJustifyContent }, { "alignItems", lsetAlignItems }, { "alignContent", lsetAlignContent }, { "alignSelf", lsetAlignSelf }, { "margin", lsetMargin }, { "padding", lsetPadding }, { "border", lsetBorder }, { "gap", lsetGap }, { "wrap", lsetWrap }, { "display", lsetDisplay }, { "flex", lsetFlex }, { "position", lsetPosition }, { "top", lsetTop }, { "bottom", lsetBottom }, { "left", lsetLeft }, { "right", lsetRight }, { "aspectRatio", lsetAspectRatio }, }; int n = sizeof(setter) / sizeof(setter[0]); int i; lua_createtable(L, n, 0); for (i=0;i #include #include #include #define ZLIB_UTF8_FLAG (1<<11) #define FILECHUNK (4096 * 4) static void * external_free(void *ud, void *ptr, size_t osize, size_t nsize) { free(ptr); return NULL; } static int lcompress(lua_State *L) { size_t sz; const char * src = luaL_checklstring(L, 1, &sz); uLongf len = compressBound(sz); char * buf = (char *)malloc(len + 1); if (buf == NULL) { return luaL_error(L, "Compress OOM"); } if (compress((void *)(buf + 1), &len, (void *)src, sz) != Z_OK) { free(buf); return luaL_error(L, "Compress error"); } int idx = 0; size_t tmp = len; while (sz > tmp) { ++idx; tmp *= 2; } // 0 : the same size // 1 : 2x size // 2...n : (1< #include "zlib/contrib/minizip/iowin32.h" struct filename_convert { WCHAR tmp[4096]; }; static zipFile zip_open(lua_State *L, const char *filename, int append) { struct filename_convert tmp; if (MultiByteToWideChar(CP_UTF8, 0, filename, -1, tmp.tmp, sizeof(tmp)) == 0) { if (L == NULL) return NULL; luaL_error(L, "Can't convert %s to utf16", filename); } zlib_filefunc64_def ffunc; fill_win32_filefunc64W(&ffunc); return zipOpen2_64((const char *)tmp.tmp, append ? APPEND_STATUS_ADDINZIP : 0, NULL, &ffunc); } static unzFile unzip_open(lua_State *L, const char *filename) { struct filename_convert tmp; if (MultiByteToWideChar(CP_UTF8, 0, filename, -1, tmp.tmp, sizeof(tmp)) == 0) { if (L == NULL) return NULL; luaL_error(L, "Can't convert %s to utf16", filename); } zlib_filefunc64_def ffunc; fill_win32_filefunc64W(&ffunc); return unzOpen2_64((const char *)tmp.tmp, &ffunc); } static FILE * file_open(lua_State *L, const char *filename, const char *mode, struct filename_convert *tmp) { if (MultiByteToWideChar(CP_UTF8, 0, filename, -1, tmp->tmp, sizeof(*tmp)) == 0) { if (L == NULL) return NULL; luaL_error(L, "Can't convert %s to utf16", filename); } WCHAR m[32]; int i; for (i=0; mode[i]; i++) { if (i == 31) { if (L == NULL) return NULL; luaL_error(L, "Invalid mode %s", mode); } m[i] = mode[i]; } m[i] = 0; return _wfopen(tmp->tmp, m); } #else struct filename_convert {}; static zipFile zip_open(lua_State *L, const char *filename, int append) { return zipOpen(filename, append ? APPEND_STATUS_ADDINZIP : 0); } static unzFile unzip_open(lua_State *L, const char *filename) { return unzOpen2(filename, 0); } static FILE * file_open(lua_State *L, const char *filename, const char *mode, struct filename_convert *tmp) { return fopen(filename, mode); } #endif struct ziphandle { zipFile h; }; struct zipraw { int method; int level; }; static zipFile open_new(lua_State *L, int index, const struct zipraw *raw, int level) { const char *filename = luaL_checkstring(L, index); struct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, "ZIP_WRITE"); if (z->h == NULL) luaL_error(L, "Error: closed"); if (lua_getiuservalue(L, 1, 1) != LUA_TTABLE) luaL_error(L, "Invalid zip userdata"); int cache = lua_gettop(L); lua_pushvalue(L, index); if (lua_rawget(L, cache) != LUA_TNIL) { luaL_error(L, "Error: %s exist", filename); } int err = zipOpenNewFileInZip4(z->h, filename, NULL, NULL, 0, NULL, 0, NULL, raw ? raw->method : Z_DEFLATED, raw ? raw->level : level, raw != NULL, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, NULL, 0, 0, ZLIB_UTF8_FLAG ); if (err != ZIP_OK) { luaL_error(L, "Error: open in file"); } lua_pushvalue(L, 2); lua_pushboolean(L, 1); lua_rawset(L, cache); lua_settop(L, cache - 1); return z->h; } static inline void close_inzip(lua_State *L, zipFile zf) { if (zipCloseFileInZip(zf) != ZIP_OK) { luaL_error(L, "Error: close in file"); } } static int zipwrite_add(lua_State *L) { int level = luaL_optinteger(L, 4, Z_DEFAULT_COMPRESSION); zipFile zf = open_new(L, 2, NULL, level); size_t sz; const char * content = luaL_checklstring(L, 3, &sz); int err = zipWriteInFileInZip(zf, content, sz); if (err != ZIP_OK) { return luaL_error(L, "Error: write in file"); } close_inzip(L, zf); return 0; } static int zipwrite_addfile(lua_State *L) { int level = luaL_optinteger(L, 4, Z_DEFAULT_COMPRESSION); zipFile zf = open_new(L, 2, NULL, level); const char * addfile = luaL_checkstring(L, 3); struct filename_convert tmp; FILE *f = file_open(L, addfile, "rb", &tmp); if (f == NULL) return luaL_error(L, "Can't open %s", addfile); char buf[FILECHUNK]; for (;;) { int bytes = fread(buf, 1, FILECHUNK, f); if (bytes <= 0) { if (bytes == 0) break; return luaL_error(L, "Error: read file %s", addfile); } int err = zipWriteInFileInZip(zf, buf, bytes); if (err != ZIP_OK) { fclose(f); return luaL_error(L, "Error: write in file"); } if (bytes < FILECHUNK) break; } fclose(f); close_inzip(L, zf); return 0; } static int zipwrite_open(lua_State *L) { int level = luaL_optinteger(L, 3, Z_DEFAULT_COMPRESSION); open_new(L, 2, NULL, level); return 0; } static int zipwrite_close(lua_State *L) { struct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, "ZIP_WRITE"); if (z->h == NULL) luaL_error(L, "Error: closed"); close_inzip(L, z->h); return 0; } static int zipwrite_closezip(lua_State *L) { struct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, "ZIP_WRITE"); if (z->h == NULL) return 0; int err = zipClose(z->h, NULL); z->h = NULL; if (err != Z_OK) return luaL_error(L, "Error: close"); return 0; } static int zipwrite_write(lua_State *L) { struct ziphandle *z = (struct ziphandle *)luaL_checkudata(L, 1, "ZIP_WRITE"); if (z->h == NULL) luaL_error(L, "Error: closed"); size_t sz; const char *content = luaL_checklstring(L, 2, &sz); int err = zipWriteInFileInZip(z->h, content, sz); if (err != ZIP_OK) { return luaL_error(L, "Error: write in file"); } return 0; } struct unzhandle { unzFile h; }; static int zipread_closezip(lua_State *L) { struct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, 1, "ZIP_READ"); if (z->h == NULL) luaL_error(L, "Error: closed"); int err = unzClose(z->h); z->h = NULL; if (err != UNZ_OK) return luaL_error(L, "Error: close"); return 0; } static inline lua_Integer file_pos_to_luaint(const unz_file_pos *pos) { uint64_t p = pos->pos_in_zip_directory; uint64_t n = pos->num_of_file; return (lua_Integer)(p << 32 | n); } static inline unz_file_pos * luaint_to_file_pos(lua_Integer v, unz_file_pos *pos) { pos->pos_in_zip_directory = (uint64_t)v >> 32; pos->num_of_file = v & 0xffffffff; return pos; } static void get_filelist(lua_State *L, unzFile zf) { lua_newtable(L); int err = unzGoToFirstFile(zf); if (err != UNZ_OK) luaL_error(L, "Error: goto first file"); char filename[4096]; for (;;) { unz_file_pos pos; unzGetFilePos(zf, &pos); int err = unzGetCurrentFileInfo(zf, NULL, filename, sizeof(filename), NULL, 0, NULL, 0); if (err != UNZ_OK) luaL_error(L, "Error: get file info %d", pos.num_of_file); lua_pushinteger(L, file_pos_to_luaint(&pos)); lua_setfield(L, -2, filename); err = unzGoToNextFile(zf); if (err != UNZ_OK) { if (err == UNZ_END_OF_LIST_OF_FILE) break; luaL_error(L, "Error: goto next file %d", pos.num_of_file); } } } static int zipread_list(lua_State *L) { if (lua_getiuservalue(L, 1, 1) != LUA_TTABLE) return luaL_error(L, "Invalid zip userdata"); int t = lua_gettop(L); lua_newtable(L); int r = t+1; lua_pushnil(L); while (lua_next(L, t) != 0) { if (lua_type(L, -1) == LUA_TNUMBER) { int n = luaL_checkinteger(L, -1); lua_pop(L, 1); lua_pushvalue(L, -1); lua_rawseti(L, r, n+1); } else { // filename lua_pop(L, 1); } } return 1; } static void locate_file(lua_State *L, unzFile zf, lua_Integer pos) { unz_file_pos tmp; int err = unzGoToFilePos(zf, luaint_to_file_pos(pos, &tmp)); if (err != UNZ_OK) luaL_error(L, "Error: unzGoToFilePos"); } static int zipread_exist(lua_State *L) { if (lua_getiuservalue(L, 1, 1) != LUA_TTABLE) { luaL_error(L, "Invalid zip userdata"); } lua_pushvalue(L, 2); // filename int exist = (lua_rawget(L, -2) == LUA_TNUMBER); lua_pushboolean(L, exist); return 1; } static unzFile open_file(lua_State *L, int rzip, int filename, struct zipraw *raw) { if (lua_getiuservalue(L, rzip, 1) != LUA_TTABLE) { luaL_error(L, "Invalid zip userdata"); } lua_pushvalue(L, filename); // filename if (lua_rawget(L, -2) != LUA_TNUMBER) { lua_pop(L, 1); return NULL; } lua_Integer pos = luaL_checkinteger(L, -1); lua_pop(L, 2); struct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, rzip, "ZIP_READ"); if (z->h == NULL) luaL_error(L, "Error: closed"); locate_file(L, z->h, pos); int err; if (raw) { err = unzOpenCurrentFile2(z->h, &raw->method, &raw->level, 1); } else { err = unzOpenCurrentFile(z->h); } if (err != UNZ_OK) luaL_error(L, "Error: open file %s", lua_tostring(L, filename)); return z->h; } static void close_file(lua_State *L, unzFile zf) { int err = unzCloseCurrentFile(zf); if (err != UNZ_OK) { if (err == UNZ_CRCERROR) luaL_error(L, "Error: CRC"); else luaL_error(L, "Error: close file"); } } static int zipread_readfile(lua_State *L) { unzFile zf = open_file(L, 1, 2, NULL); if (zf == NULL) return 0; unz_file_info info; int err = unzGetCurrentFileInfo(zf, &info, NULL, 0, NULL, 0, NULL, 0); if (err != UNZ_OK) { close_file(L, zf); luaL_error(L, "Error: get file info %s", lua_tostring(L, 2)); } void *buf = malloc(info.uncompressed_size); if (buf == NULL) { close_file(L, zf); luaL_error(L, "Error: out of memory"); } int bytes = unzReadCurrentFile(zf, buf, info.uncompressed_size); if (bytes != info.uncompressed_size) { free(buf); close_file(L, zf); luaL_error(L, "Error: read file %s", lua_tostring(L, 2)); } lua_pushexternalstring(L, buf, bytes, external_free, NULL); close_file(L, zf); return 1; } static int zipread_size(lua_State *L) { unzFile zf = open_file(L, 1, 2, NULL); if (zf == NULL) return 0; unz_file_info info; int err = unzGetCurrentFileInfo(zf, &info, NULL, 0, NULL, 0, NULL, 0); if (err != UNZ_OK) luaL_error(L, "Error: get file info %s", lua_tostring(L, 2)); lua_pushinteger(L, info.uncompressed_size); return 1; } static int zipread_extract(lua_State *L) { unzFile zf = open_file(L, 1, 2, NULL); if (zf == NULL) return luaL_error(L, "Error: no file %s", lua_tostring(L, 2)); const char * filename = luaL_checkstring(L, 3); struct filename_convert tmp; FILE *f = file_open(L, filename, "wb", &tmp); if (f == NULL) { close_file(L, zf); return luaL_error(L, "Error: open %s", filename); } char buf[FILECHUNK]; int bytes = 0; do { bytes = unzReadCurrentFile(zf, buf, sizeof(buf)); if (bytes < 0) { close_file(L, zf); return luaL_error(L, "Error: read %s", lua_tostring(L, 2)); } if (bytes > 0 && fwrite(buf, 1, bytes, f) != bytes) { close_file(L, zf); return luaL_error(L, "Error: write %s", filename); } } while (bytes == sizeof(buf)); fclose(f); close_file(L, zf); return 0; } static int zipread_openfile(lua_State *L) { open_file(L, 1, 2, NULL); return 0; } static int zipread_closefile(lua_State *L) { struct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, 1, "ZIP_READ"); if (z->h == NULL) luaL_error(L, "Error: closed"); close_file(L, z->h); return 0; } static int zipread_read(lua_State *L) { struct unzhandle *z = (struct unzhandle *)luaL_checkudata(L, 1, "ZIP_READ"); if (z->h == NULL) luaL_error(L, "Error: closed"); int n = luaL_checkinteger(L, 2); void *buf = malloc(n); if (buf == NULL) return luaL_error(L, "Error: out of memory"); int bytes = unzReadCurrentFile(z->h, buf, n); if (bytes <= 0) { free(buf); if (bytes == 0) return 0; luaL_error(L, "Error: read file"); } lua_pushexternalstring(L, buf, bytes, external_free, NULL); return 1; } // 2: filename // 3: readzip (userdata) // 4: opt: altername static int zipwrite_copyfrom(lua_State *L) { int filename = lua_isnoneornil(L, 4) ? 2 : 4; struct zipraw raw; unzFile rd = open_file(L, 3, filename, &raw); if (rd == NULL) return luaL_error(L, "Error: open %s", lua_tostring(L, filename)); zipFile zf = open_new(L, 2, &raw, raw.level); if (zf == NULL) { close_file(L, rd); return luaL_error(L, "Error: open %s", lua_tostring(L, 2)); } // copy file from rd to zf // todo : zipCloseFileInZipRaw on error unz_file_info info; int err = unzGetCurrentFileInfo(rd, &info, NULL, 0, NULL, 0, NULL, 0); if (err != UNZ_OK) { close_file(L, rd); luaL_error(L, "Error: get file info %s", lua_tostring(L, filename)); } char buf[FILECHUNK]; int bytes; do { bytes = unzReadCurrentFile(rd, buf, sizeof(buf)); if (bytes < 0) { close_file(L, rd); return luaL_error(L, "Error: read %s", lua_tostring(L, filename)); } if (bytes > 0 && zipWriteInFileInZip(zf, buf, bytes) != ZIP_OK) { close_file(L, rd); return luaL_error(L, "Error: write %s", lua_tostring(L, 2)); } } while (bytes == sizeof(buf)); close_file(L, rd); if (zipCloseFileInZipRaw(zf, info.uncompressed_size, info.crc) != ZIP_OK) return luaL_error(L, "Error: close %s", 2); return 0; } static int zipread_filename(lua_State *L) { const int rzip = 1; const int filename = 2; if (lua_getiuservalue(L, rzip, 1) != LUA_TTABLE) { luaL_error(L, "Invalid zip userdata"); } lua_pushvalue(L, filename); // filename if (lua_rawget(L, -2) != LUA_TNUMBER) { return 0; } lua_Integer v = luaL_checkinteger(L, -1); lua_pop(L, 1); unz_file_pos pos; luaint_to_file_pos(v, &pos); if (lua_rawgeti(L, -1, 0) != LUA_TSTRING) { return luaL_error(L, "No zip filename"); } lua_pushfstring(L, "%s|%d", lua_tostring(L, -1), pos.num_of_file); return 1; } static int unzip(lua_State *L, const char *filename) { unzFile zf = unzip_open(L, filename); if (zf == NULL) return 0; struct unzhandle *z = (struct unzhandle *)lua_newuserdatauv(L, sizeof(*z), 1); get_filelist(L, zf); lua_pushstring(L, filename); lua_rawseti(L, -2, 0); lua_setiuservalue(L, -2, 1); z->h = zf; if (luaL_newmetatable(L, "ZIP_READ")) { luaL_Reg l[] = { { "__index", NULL }, { "__gc", zipread_closezip }, { "close", zipread_closezip }, { "list", zipread_list }, { "extract", zipread_extract }, { "readfile", zipread_readfile }, { "exist", zipread_exist }, { "openfile", zipread_openfile }, { "closefile", zipread_closefile }, { "read", zipread_read }, { "size", zipread_size }, { "filename", zipread_filename }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } static int zip(lua_State *L, const char *filename, int append) { if (append) { unzFile uzf = unzip_open(L, filename); if (uzf == NULL) return 0; get_filelist(L, uzf); unzClose(uzf); } else { lua_newtable(L); // cache filenames } zipFile zf = zip_open(L, filename, append); if (zf == NULL) return 0; struct ziphandle *z = (struct ziphandle *)lua_newuserdatauv(L, sizeof(*z), 1); lua_insert(L, -2); lua_setiuservalue(L, -2, 1); z->h = zf; if (luaL_newmetatable(L, "ZIP_WRITE")) { luaL_Reg l[] = { { "__index", NULL }, { "__gc", zipwrite_closezip }, { "copyfrom", zipwrite_copyfrom }, { "addfile", zipwrite_addfile }, { "add", zipwrite_add }, { "openfile", zipwrite_open }, { "closefile", zipwrite_close }, { "write", zipwrite_write }, { "close", zipwrite_closezip }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } static int lzip(lua_State *L) { const char * filename = luaL_checkstring(L, 1); const char * mode = luaL_checkstring(L, 2); switch (mode[0]) { case 'w': return zip(L, filename, 0); case 'r': return unzip(L, filename); case 'a': return zip(L, filename, 1); default: return luaL_error(L, "Invalid Mode %s", mode); } } static int lziplist(lua_State *L) { if (lua_isnoneornil(L, 1)) { return 0; } luaL_checktype(L, 1, LUA_TTABLE); int n = lua_rawlen(L, 1); struct zipreader_name * names = (struct zipreader_name *)lua_newuserdatauv(L, sizeof(struct zipreader_name) * (n + 1), n * 2); int ud_index = lua_gettop(L); names[n].zipfile = NULL; names[n].root = NULL; names[n].root_size = 0; int i; for (i = 0; i < n; i++) { struct zipreader_name * name = &names[i]; if (lua_geti(L, 1, n - i) != LUA_TTABLE) { return luaL_error(L, "Invalid ziplist table"); } if (lua_getfield(L, -1, "name") != LUA_TSTRING) { return luaL_error(L, "Invalid ziplist table, missing .name"); } name->zipfile = lua_tostring(L, -1); lua_setiuservalue(L, ud_index, i * 2 + 1); if (lua_getfield(L, -1, "root") == LUA_TSTRING) { name->root = lua_tolstring(L, -1, &name->root_size); lua_setiuservalue(L, ud_index, i * 2 + 2); } else if (lua_isnil(L, -1)) { name->root = NULL; name->root_size = 0; lua_pop(L, 1); } else { return luaL_error(L, "Invalid ziplist table, .root is not a string"); } lua_pop(L, 1); } return 1; } // #define ZIPTEST #ifdef ZIPTEST static int lziptest(lua_State *L); #endif int luaopen_zip(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "compress", lcompress }, { "uncompress", luncompress }, { "open", lzip }, { "list", lziplist }, #ifdef ZIPTEST { "test", lziptest }, #endif { NULL, NULL }, }; luaL_newlib(L, l); return 1; } // zip reader static unzFile try_open(struct zipreader_name *name, const char *filename) { if (name->root) { if (memcmp(name->root, filename, name->root_size) != 0) { return NULL; } filename += name->root_size; } unzFile zf = unzip_open(NULL, name->zipfile); if (zf == NULL) return NULL; if (unzLocateFile(zf, filename, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK) { unzClose(zf); return NULL; } return zf; } zipreader_file zipreader_open(struct zipreader_name *names, const char * filename) { int i; for (i=0;names[i].zipfile;i++) { unzFile zf = try_open(&names[i], filename); if (zf) return (zipreader_file)zf; } return NULL; } void zipreader_close(zipreader_file zf) { unzClose((unzFile)zf); } int zipreader_read(zipreader_file zf, void *dst, int bytes) { return unzReadCurrentFile(zf, dst, bytes); } int64_t zipreader_tell(zipreader_file zf) { return unztell64(zf); } size_t zipreader_size(zipreader_file zf) { unz_file_info64 info; int err = unzGetCurrentFileInfo64(zf, &info, NULL, 0, NULL, 0, NULL, 0); if (err != UNZ_OK) { return 0; } return info.uncompressed_size; } static void reopen_file(zipreader_file zf) { unz64_file_pos pos; unzGetFilePos64(zf, &pos); unzCloseCurrentFile(zf); unzGoToFilePos64(zf, &pos); unzOpenCurrentFile(zf); } #define TMP_SKIP_BUFFER 4096 static int skip_bytes(zipreader_file zf, int64_t offset) { char tmp[TMP_SKIP_BUFFER]; while (offset >= TMP_SKIP_BUFFER) { if (unzReadCurrentFile(zf, tmp, TMP_SKIP_BUFFER) != TMP_SKIP_BUFFER) return -1; offset -= TMP_SKIP_BUFFER; } if (offset > 0 && unzReadCurrentFile(zf, tmp, offset) != offset) return -1; return 0; } int zipreader_seek(zipreader_file zf, int64_t offset, int origin) { if (origin == SEEK_CUR && offset >= 0) { return skip_bytes(zf, offset); } size_t size = zipreader_size(zf); size_t cur_pos = unztell64(zf); int64_t new_pos; switch (origin) { case SEEK_SET: new_pos = offset; break; case SEEK_CUR: new_pos = (int64_t)cur_pos + offset; break; case SEEK_END: new_pos = (int64_t)size + offset; break; default : return -1; } if (new_pos < 0) new_pos = 0; else if (new_pos > size) new_pos = size; if (new_pos >= cur_pos) { return skip_bytes(zf, new_pos - cur_pos); } reopen_file(zf); return skip_bytes(zf, new_pos); } #ifdef ZIPTEST static int lziptest(lua_State *L) { luaL_checktype(L, 1, LUA_TUSERDATA); struct zipreader_name *names = (struct zipreader_name *)lua_touserdata(L, 1); const char *filename = luaL_checkstring(L, 2); zipreader_file zf = zipreader_open(names, filename); if (zf == NULL) return 0; int sz = zipreader_size(zf); void * buf = lua_newuserdatauv(L, sz, 0); int rd = zipreader_read(zf, buf, sz); if (sz != rd) { zipreader_close(zf); return luaL_error(L, "Read zipfile error"); } lua_pushlstring(L, (const char *)buf, rd); zipreader_seek(zf, 10, SEEK_SET); rd = zipreader_read(zf, buf, sz); lua_pushlstring(L, (const char *)buf, rd); zipreader_close(zf); return 2; } #endif ================================================ FILE: src/maskquad.glsl ================================================ @vs vs layout(binding=0) uniform vs_params { vec2 framesize; float texsize; }; struct sr_mat { mat2 m; }; layout(binding=0) readonly buffer sr_lut { sr_mat sr[]; }; in vec3 position; in vec4 color; in uint offset; in uint u; in uint v; out vec2 uv; out vec4 maskcolor; void main() { ivec2 uv_base = ivec2(u >> 16, v >> 16); ivec2 u2 = ivec2(0 , u & 0xffff); ivec2 v2 = ivec2(0 , v & 0xffff); ivec2 off = ivec2(offset >> 16 , offset & 0xffff) - 0x8000; vec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]); vec2 pos = ((uv_offset - off) * sr[int(position.z)].m + position.xy) * framesize; gl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1); uv = (uv_base + uv_offset) * texsize; maskcolor = color; } @end @fs fs layout(binding=1) uniform texture2D tex; layout(binding=0) uniform sampler smp; in vec2 uv; in vec4 maskcolor; out vec4 frag_color; void main() { float alpha = texture(sampler2D(tex,smp), uv).a; frag_color = maskcolor; frag_color.a = alpha * maskcolor.a; } @end @program maskquad vs fs ================================================ FILE: src/material/matdefault.lua ================================================ local render = require "soluna.render" local defmat = require "soluna.material.default" local ctx = ... local state = ctx.state local setting = ctx.settings local inst_buffer = render.buffer { type = "vertex", usage = "stream", label = "texquad-instance", size = defmat.instance_size * setting.draw_instance, } local bindings = render.bindings() bindings:vbuffer(0, inst_buffer) bindings:view(0, state.views.storage) bindings:sampler(0, state.default_sampler) state.inst = assert(inst_buffer) state.bindings = bindings state.material = defmat.new { inst_buffer = state.inst, bindings = state.bindings, uniform = state.uniform, sr_buffer = state.srbuffer_mem, 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) state.material:submit(ptr, n) end function material.draw(ptr, n, tex) bindings:view(1, state.views[tex + 1]) state.material:draw(ptr, n, tex) end return material ================================================ FILE: src/material/matmask.lua ================================================ local render = require "soluna.render" local maskmat = require "soluna.material.mask" local ctx = ... local state = ctx.state maskmat.set_material_id(ctx.id) state.mask_inst = render.buffer { type = "vertex", usage = "stream", label = "mask-instance", size = maskmat.instance_size * ctx.settings.draw_instance, } local mask_bindings = render.bindings() mask_bindings:vbuffer(0, state.mask_inst) mask_bindings:view(0, state.views.storage) mask_bindings:sampler(0, state.default_sampler) state.mask_bindings = mask_bindings state.material_mask = maskmat.new { inst_buffer = state.mask_inst, bindings = state.mask_bindings, uniform = state.uniform, sr_buffer = state.srbuffer_mem, sprite_bank = ctx.arg.bank_ptr, tmp_buffer = ctx.tmp_buffer, } local material = {} function material.reset() mask_bindings:base(0) end function material.submit(ptr, n) state.material_mask:submit(ptr, n) end function material.draw(ptr, n, tex) mask_bindings:view(1, state.views[tex + 1]) state.material_mask:draw(ptr, n, tex) end return material ================================================ FILE: src/material/matquad.lua ================================================ local render = require "soluna.render" local quadmat = require "soluna.material.quad" local ctx = ... local state = ctx.state quadmat.set_material_id(ctx.id) state.quad_inst = render.buffer { type = "vertex", usage = "stream", label = "quad-instance", size = quadmat.instance_size * ctx.settings.draw_instance, } local quad_bindings = render.bindings() quad_bindings:vbuffer(0, state.quad_inst) quad_bindings:view(0, state.views.storage) state.quad_bindings = quad_bindings state.material_quad = quadmat.new { inst_buffer = state.quad_inst, bindings = state.quad_bindings, uniform = state.uniform, sr_buffer = state.srbuffer_mem, tmp_buffer = ctx.tmp_buffer, } local material = {} function material.reset() quad_bindings:base(0) end function material.submit(ptr, n) state.material_quad:submit(ptr, n) end function material.draw(ptr, n) state.material_quad:draw(ptr, n) end return material ================================================ FILE: src/material/mattext.lua ================================================ local render = require "soluna.render" local textmat = require "soluna.material.text" local ctx = ... local state = ctx.state local setting = ctx.settings textmat.set_material_id(ctx.id) local text_bindings local text_sampler_desc = setting.text_sampler if text_sampler_desc then text_sampler_desc.label = text_sampler_desc.label or "text-sampler" state.text_sampler = render.sampler(text_sampler_desc) state.text_inst = render.buffer { type = "vertex", usage = "stream", label = "text-instance", size = textmat.instance_size * setting.draw_instance, } text_bindings = render.bindings() text_bindings:vbuffer(0, state.text_inst) text_bindings:view(0, state.views.storage) text_bindings:sampler(0, state.text_sampler) else state.text_inst = state.inst text_bindings = state.bindings end state.text_bindings = text_bindings state.material_text = textmat.normal { inst_buffer = state.text_inst, bindings = state.text_bindings, uniform = state.uniform, sr_buffer = state.srbuffer_mem, font_manager = ctx.font.cobj, tmp_buffer = ctx.tmp_buffer, } local material = {} function material.reset() text_bindings:base(0) end function material.submit(ptr, n) state.material_text:submit(ptr, n) end function material.draw(ptr, n) text_bindings:view(1, state.views.font) state.material_text:draw(ptr, n) end return material ================================================ FILE: src/material_blit.c ================================================ #include #include #include "sokol/sokol_gfx.h" #include "blit.glsl.h" #include "render_bindings.h" #include "material_util.h" struct material_blit { sg_pipeline pip; struct render_bindings *bind; }; static int lmaterial_blit_draw(lua_State *L) { struct material_blit *m = (struct material_blit *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_BLIT"); sg_apply_pipeline(m->pip); sg_apply_bindings(&m->bind->bindings); sg_draw(0, 4, 1); return 0; } static void init_pipeline(struct material_blit *p) { sg_pipeline_desc desc = { .layout.buffers[0].step_func = SG_VERTEXSTEP_PER_VERTEX, }; p->pip = util_make_pipeline(&desc, blit_shader_desc, "blit-pipeline", 0); } static int lnew_material_blit(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct material_blit *m = (struct material_blit *)lua_newuserdatauv(L, sizeof(*m), 1); init_pipeline(m); util_ref_object(L, &m->bind, 1, "bindings", "SOKOL_BINDINGS", 1); if (luaL_newmetatable(L, "SOLUNA_MATERIAL_BLIT")) { luaL_Reg l[] = { { "__index", NULL }, { "draw", lmaterial_blit_draw }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } int luaopen_material_blit(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "new", lnew_material_blit }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/material_default.c ================================================ #include #include #include #include #include "sokol/sokol_gfx.h" #include "texquad.glsl.h" #include "srbuffer.h" #include "batch.h" #include "spritemgr.h" #include "material_util.h" #include "render_bindings.h" #include "tmpbuffer.h" struct inst_object { float x, y; float sr_index; uint32_t offset; uint32_t u; uint32_t v; }; struct material_default { sg_pipeline pip; sg_buffer inst; struct render_bindings *bind; vs_params_t *uniform; struct sr_buffer *srbuffer; struct sprite_bank *bank; struct tmp_buffer tmp; }; static void submit(lua_State *L, void *m_, struct draw_primitive *prim, int n) { struct material_default *m = (struct material_default *)m_; struct sprite_rect *rect = m->bank->rect; struct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp); int i; for (i=0;isrbuffer, p->sr); if (sr_index < 0) { // todo: support multiply srbuffer luaL_error(L, "sr buffer is full"); } tmp[i].x = (float)p->x / 256.0f; tmp[i].y = (float)p->y / 256.0f; tmp[i].sr_index = (float)sr_index; int index = p->sprite - 1; assert(index >= 0); struct sprite_rect *r = &rect[index]; tmp[i].offset = r->off; tmp[i].u = r->u; tmp[i].v = r->v; } sg_append_buffer(m->inst, &(sg_range) { tmp , n * sizeof(tmp[0]) }); } static int lmaterial_default_submit(lua_State *L) { struct material_default *m = (struct material_default *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_DEFAULT"); int batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp); util_submit_material(L, batch_n, m, submit); return 0; } static inline int lmaterial_default_draw_(lua_State *L, int ex) { struct material_default *m = (struct material_default *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_DEFAULT"); // struct draw_primitive *prim = lua_touserdata(L, 2); int prim_n = luaL_checkinteger(L, 3); // int tex_id = luaL_checkinteger(L, 4); sg_apply_pipeline(m->pip); sg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) }); if (ex) { sg_apply_bindings(&m->bind->bindings); sg_draw_ex(0, 4, prim_n, 0, m->bind->base); } else { size_t base = m->bind->base * sizeof(struct inst_object); m->bind->bindings.vertex_buffer_offsets[0] += base; sg_apply_bindings(&m->bind->bindings); sg_draw(0, 4, prim_n); m->bind->bindings.vertex_buffer_offsets[0] -= base; } m->bind->base += prim_n; return 0; } static int lmaterial_default_draw(lua_State *L) { return lmaterial_default_draw_(L, 0); } static int lmaterial_default_draw_ex(lua_State *L) { return lmaterial_default_draw_(L, 1); } static void init_pipeline(struct material_default *p) { sg_pipeline_desc desc = { .layout.attrs = { [ATTR_texquad_position].format = SG_VERTEXFORMAT_FLOAT3, [ATTR_texquad_offset].format = SG_VERTEXFORMAT_UINT, [ATTR_texquad_u].format = SG_VERTEXFORMAT_UINT, [ATTR_texquad_v].format = SG_VERTEXFORMAT_UINT, }, }; p->pip = util_make_pipeline(&desc, texquad_shader_desc, "default-pipeline", 1); } static int lnew_material_default(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct material_default *m = (struct material_default *)lua_newuserdatauv(L, sizeof(*m), 5); init_pipeline(m); util_ref_object(L, &m->inst, 1, "inst_buffer", "SOKOL_BUFFER", 0); util_ref_object(L, &m->bind, 2, "bindings", "SOKOL_BINDINGS", 1); util_ref_object(L, &m->uniform, 3, "uniform", "SOKOL_UNIFORM", 1); util_ref_object(L, &m->srbuffer, 4, "sr_buffer", "SOLUNA_SRBUFFER", 1); tmp_buffer_init(L, &m->tmp, 5, "tmp_buffer"); if (lua_getfield(L, 1, "sprite_bank") != LUA_TLIGHTUSERDATA) { return luaL_error(L, "Missing .sprite_bank"); } m->bank = lua_touserdata(L, -1); lua_pop(L, 1); if (luaL_newmetatable(L, "SOLUNA_MATERIAL_DEFAULT")) { luaL_Reg l[] = { { "__index", NULL }, { "submit", lmaterial_default_submit }, { "draw", DRAWFUNC(lmaterial_default_draw) }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } int luaopen_material_default(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "new", lnew_material_default }, { "instance_size", NULL }, { NULL, NULL }, }; luaL_newlib(L, l); lua_pushinteger(L, sizeof(struct inst_object)); lua_setfield(L, -2, "instance_size"); return 1; } ================================================ FILE: src/material_mask.c ================================================ #include #include #include #include #include "sokol/sokol_gfx.h" #include "maskquad.glsl.h" #include "srbuffer.h" #include "batch.h" #include "spritemgr.h" #include "material_util.h" #include "render_bindings.h" #include "tmpbuffer.h" struct color { unsigned char channel[4]; }; struct inst_object { float x, y; float sr_index; struct color maskcolor; uint32_t offset; uint32_t u; uint32_t v; }; struct mask { struct draw_primitive_external header; struct color c; }; struct material_mask { sg_pipeline pip; sg_buffer inst; struct render_bindings *bind; vs_params_t *uniform; struct sr_buffer *srbuffer; struct sprite_bank *bank; struct tmp_buffer tmp; }; static int material_id = 0; static void submit(lua_State *L, void *m_, struct draw_primitive *prim, int n) { struct material_mask *m =(struct material_mask *)m_; struct sprite_rect *rect = m->bank->rect; struct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp); int i; for (i=0;isprite == -material_id); struct mask * mask = (struct mask *)&prim[i*2+1]; // calc scale/rot index int sr_index = srbuffer_add(m->srbuffer, p->sr); if (sr_index < 0) { // todo: support multiply srbuffer luaL_error(L, "sr buffer is full"); } tmp[i].x = (float)p->x / 256.0f; tmp[i].y = (float)p->y / 256.0f; tmp[i].sr_index = (float)sr_index; tmp[i].maskcolor = mask->c; int index = mask->header.sprite; assert(index >= 0); struct sprite_rect *r = &rect[index]; tmp[i].offset = r->off; tmp[i].u = r->u; tmp[i].v = r->v; } sg_append_buffer(m->inst, &(sg_range) { tmp , n * sizeof(tmp[0]) }); } static int lmaterial_mask_submit(lua_State *L) { struct material_mask *m = (struct material_mask *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_MASK"); int batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp); util_submit_material(L, batch_n, m, submit); return 0; } static inline int lmaterial_mask_draw_(lua_State *L, int ex) { struct material_mask *m = (struct material_mask *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_MASK"); // struct draw_primitive *prim = lua_touserdata(L, 2); int prim_n = luaL_checkinteger(L, 3); // int tex_id = luaL_checkinteger(L, 4); sg_apply_pipeline(m->pip); sg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) }); if (ex) { sg_apply_bindings(&m->bind->bindings); sg_draw_ex(0, 4, prim_n, 0, m->bind->base); } else { size_t base = m->bind->base * sizeof(struct inst_object); m->bind->bindings.vertex_buffer_offsets[0] += base; sg_apply_bindings(&m->bind->bindings); sg_draw(0, 4, prim_n); m->bind->bindings.vertex_buffer_offsets[0] -= base; } m->bind->base += prim_n; return 0; } static int lmaterial_mask_draw(lua_State *L) { return lmaterial_mask_draw_(L, 0); } static int lmaterial_mask_draw_ex(lua_State *L) { return lmaterial_mask_draw_(L, 1); } static int lset_material_id(lua_State *L) { int id = luaL_checkinteger(L, 1); if (id <= 0) { return luaL_error(L, "Invalid mask material id %d", id); } material_id = id; return 0; } static void init_pipeline(struct material_mask *p) { sg_pipeline_desc desc = { .layout.attrs = { [ATTR_maskquad_position].format = SG_VERTEXFORMAT_FLOAT3, [ATTR_maskquad_color].format = SG_VERTEXFORMAT_UBYTE4N, [ATTR_maskquad_offset].format = SG_VERTEXFORMAT_UINT, [ATTR_maskquad_u].format = SG_VERTEXFORMAT_UINT, [ATTR_maskquad_v].format = SG_VERTEXFORMAT_UINT, }, }; p->pip = util_make_pipeline(&desc, maskquad_shader_desc, "mask-pipeline", 1); } static int lnew_material_mask(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct material_mask *m = (struct material_mask *)lua_newuserdatauv(L, sizeof(*m), 5); init_pipeline(m); util_ref_object(L, &m->inst, 1, "inst_buffer", "SOKOL_BUFFER", 0); util_ref_object(L, &m->bind, 2, "bindings", "SOKOL_BINDINGS", 1); util_ref_object(L, &m->uniform, 3, "uniform", "SOKOL_UNIFORM", 1); util_ref_object(L, &m->srbuffer, 4, "sr_buffer", "SOLUNA_SRBUFFER", 1); tmp_buffer_init(L, &m->tmp, 5, "tmp_buffer"); if (lua_getfield(L, 1, "sprite_bank") != LUA_TLIGHTUSERDATA) { return luaL_error(L, "Missing .sprite_bank"); } m->bank = lua_touserdata(L, -1); lua_pop(L, 1); if (luaL_newmetatable(L, "SOLUNA_MATERIAL_MASK")) { luaL_Reg l[] = { { "__index", NULL }, { "submit", lmaterial_mask_submit }, { "draw", DRAWFUNC(lmaterial_mask_draw) }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } struct mask_primitive { struct draw_primitive pos; union { struct draw_primitive dummy; struct mask m; } u; }; static int lmask(lua_State *L) { if (material_id <= 0) { return luaL_error(L, "Mask material is not registered"); } struct mask_primitive prim; prim.pos.x = 0; prim.pos.y = 0; prim.pos.sr = 0; prim.pos.sprite = -material_id; prim.u.m.header.sprite = luaL_checkinteger(L, 1) - 1; uint32_t color = luaL_checkinteger(L, 2); if (!(color & 0xff000000)) color |= 0xff000000; prim.u.m.c.channel[0] = (color >> 16) & 0xff; prim.u.m.c.channel[1] = (color >> 8) & 0xff; prim.u.m.c.channel[2] = color & 0xff; prim.u.m.c.channel[3] = (color >> 24) & 0xff; lua_pushlstring(L, (const char *)&prim, sizeof(prim)); return 1; } int luaopen_material_mask(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "set_material_id", lset_material_id }, { "mask", lmask }, { "new", lnew_material_mask }, { "instance_size", NULL }, { NULL, NULL }, }; luaL_newlib(L, l); lua_pushinteger(L, sizeof(struct inst_object)); lua_setfield(L, -2, "instance_size"); return 1; } ================================================ FILE: src/material_quad.c ================================================ #include #include #include #include #include "sokol/sokol_gfx.h" #include "colorquad.glsl.h" #include "srbuffer.h" #include "batch.h" #include "spritemgr.h" #include "material_util.h" #include "render_bindings.h" #include "tmpbuffer.h" struct color { unsigned char channel[4]; }; struct quad { struct draw_primitive_external header; float w; float h; struct color c; }; struct inst_object { float x, y; float w, h; int sr_index; struct color c; }; struct material_quad { sg_pipeline pip; sg_buffer inst; struct render_bindings *bind; vs_params_t *uniform; struct sr_buffer *srbuffer; struct tmp_buffer tmp; }; static int material_id = 0; static void submit(lua_State *L, void *m_, struct draw_primitive *prim, int n) { struct material_quad *m = (struct material_quad *)m_; struct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp); int i; for (i=0;isprite == -material_id); struct quad * q = (struct quad *)&prim[i*2+1]; // calc scale/rot index int sr_index = srbuffer_add(m->srbuffer, p->sr); if (sr_index < 0) { // todo: support multiply srbuffer luaL_error(L, "sr buffer is full"); } struct inst_object *inst = &tmp[i]; inst->x = (float)p->x / 256.0f; inst->y = (float)p->y / 256.0f; inst->w = q->w; inst->h = q->h; inst->sr_index = sr_index; inst->c = q->c; } sg_append_buffer(m->inst, &(sg_range) { tmp , n * sizeof(tmp[0]) }); } static int lmateraial_quad_submit(lua_State *L) { struct material_quad *m = (struct material_quad *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_QUAD"); int batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp); util_submit_material(L, batch_n, m, submit); return 0; } static inline int lmateraial_quad_draw_(lua_State *L, int ex) { struct material_quad *m = (struct material_quad *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_QUAD"); // struct draw_primitive *prim = lua_touserdata(L, 2); int prim_n = luaL_checkinteger(L, 3); if (prim_n <= 0) return 0; sg_apply_pipeline(m->pip); sg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) }); if (ex) { sg_apply_bindings(&m->bind->bindings); sg_draw_ex(0, 4, prim_n, 0, m->bind->base); } else { size_t base = m->bind->base * sizeof(struct inst_object); m->bind->bindings.vertex_buffer_offsets[0] += base; sg_apply_bindings(&m->bind->bindings); sg_draw(0, 4, prim_n); m->bind->bindings.vertex_buffer_offsets[0] -= base; } m->bind->base += prim_n; return 0; } static int lmateraial_quad_draw(lua_State *L) { return lmateraial_quad_draw_(L, 0); } static int lmateraial_quad_draw_ex(lua_State *L) { return lmateraial_quad_draw_(L, 1); } static int lset_material_id(lua_State *L) { int id = luaL_checkinteger(L, 1); if (id <= 0) { return luaL_error(L, "Invalid quad material id %d", id); } material_id = id; return 0; } static void init_pipeline(struct material_quad *p) { sg_pipeline_desc desc = { .layout.attrs = { [ATTR_colorquad_position].format = SG_VERTEXFORMAT_FLOAT4, [ATTR_colorquad_idx].format = SG_VERTEXFORMAT_UINT, [ATTR_colorquad_c].format = SG_VERTEXFORMAT_UBYTE4N, }, }; p->pip = util_make_pipeline(&desc, colorquad_shader_desc, "colorquad-pipeline", 1); } static int lnew_material_quad(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct material_quad *m = (struct material_quad *)lua_newuserdatauv(L, sizeof(*m), 5); util_ref_object(L, &m->inst, 1, "inst_buffer", "SOKOL_BUFFER", 0); util_ref_object(L, &m->bind, 2, "bindings", "SOKOL_BINDINGS", 1); util_ref_object(L, &m->uniform, 3, "uniform", "SOKOL_UNIFORM", 1); util_ref_object(L, &m->srbuffer, 4, "sr_buffer", "SOLUNA_SRBUFFER", 1); tmp_buffer_init(L, &m->tmp, 5, "tmp_buffer"); init_pipeline(m); if (luaL_newmetatable(L, "SOLUNA_MATERIAL_QUAD")) { luaL_Reg l[] = { { "__index", NULL }, { "submit", lmateraial_quad_submit }, { "draw", DRAWFUNC(lmateraial_quad_draw) }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } struct quad_primitive { struct draw_primitive pos; union { struct draw_primitive dummy; struct quad q; } u; }; static int lquad(lua_State *L) { if (material_id <= 0) { return luaL_error(L, "Quad material is not registered"); } struct quad_primitive prim; prim.pos.x = 0; prim.pos.y = 0; prim.pos.sr = 0; prim.pos.sprite = -material_id; prim.u.q.w = luaL_checkinteger(L, 1); prim.u.q.h = luaL_checkinteger(L, 2); uint32_t color = luaL_checkinteger(L, 3); if (!(color & 0xff000000)) color |= 0xff000000; prim.u.q.header.sprite = -1; prim.u.q.c.channel[0] = (color >> 16) & 0xff; prim.u.q.c.channel[1] = (color >> 8) & 0xff; prim.u.q.c.channel[2] = color & 0xff; prim.u.q.c.channel[3] = (color >> 24) & 0xff; lua_pushlstring(L, (const char *)&prim, sizeof(prim)); return 1; } int luaopen_material_quad(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "set_material_id", lset_material_id }, { "quad", lquad }, { "new", lnew_material_quad }, { "instance_size", NULL }, { NULL, NULL }, }; luaL_newlib(L, l); lua_pushinteger(L, sizeof(struct inst_object)); lua_setfield(L, -2, "instance_size"); return 1; } ================================================ FILE: src/material_text.c ================================================ #include #include #include #include #include #include #include #include "sokol/sokol_gfx.h" #include "sdftext.glsl.h" #include "srbuffer.h" #include "batch.h" #include "spritemgr.h" #include "font_manager.h" #include "sprite_submit.h" #include "material_util.h" #include "render_bindings.h" #include "tmpbuffer.h" struct text { struct draw_primitive_external header; int codepoint; uint16_t font; uint16_t size; uint32_t color; }; struct inst_object { float x, y; float sr_index; uint32_t offset; uint32_t u; uint32_t v; }; struct material_text { sg_pipeline pip; sg_buffer inst; struct render_bindings *bind; vs_params_t *uniform; struct sr_buffer *srbuffer; struct font_manager *font; fs_params_t fs_uniform; struct tmp_buffer tmp; }; static int material_id = 0; static void submit(lua_State *L, void *m_, struct draw_primitive *prim, int n) { struct material_text *m = (struct material_text *)m_; struct inst_object *tmp = TMPBUFFER_PTR(struct inst_object, &m->tmp); int i; int count = 0; for (i=0;isprite == -material_id); struct text * t = (struct text *)&prim[i*2+1]; struct font_glyph g, og; const char* err = font_manager_glyph(m->font, t->font, t->codepoint, t->size, &g, &og); if (err == NULL) { tmp[count].offset = (-og.offset_x + 0x8000) << 16 | (-og.offset_y + 0x8000); tmp[count].u = og.u << 16 | FONT_MANAGER_GLYPHSIZE; tmp[count].v = og.v << 16 | FONT_MANAGER_GLYPHSIZE; uint32_t scale_fix = og.w == 0 ? 0 : (g.w << 12) / og.w; sprite_apply_scale(p, scale_fix); // calc scale/rot index int sr_index = srbuffer_add(m->srbuffer, p->sr); if (sr_index < 0) { // todo: support multiply srbuffer luaL_error(L, "sr buffer is full"); } tmp[count].x = (float)p->x / 256.0f; tmp[count].y = (float)p->y / 256.0f; tmp[count].sr_index = (float)sr_index; ++count; } else { t->codepoint = -1; } } sg_append_buffer(m->inst, &(sg_range) { tmp , count * sizeof(tmp[0]) }); } static int lmateraial_text_submit(lua_State *L) { struct material_text *m = (struct material_text *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_TEXT"); int batch_n = TMPBUFFER_SIZE(struct inst_object, &m->tmp); util_submit_material(L, batch_n, m, submit); return 0; } static inline void draw_text(struct material_text *m, uint32_t color, int count, int ex) { m->fs_uniform.color = color; sg_apply_uniforms(UB_vs_params, &(sg_range){ m->uniform, sizeof(vs_params_t) }); sg_apply_uniforms(UB_fs_params, &(sg_range){ &m->fs_uniform, sizeof(fs_params_t) }); if (ex) { sg_apply_bindings(&m->bind->bindings); sg_draw_ex(0, 4, count, 0, m->bind->base); } else { size_t base = m->bind->base * sizeof(struct inst_object); m->bind->bindings.vertex_buffer_offsets[0] += base; sg_apply_bindings(&m->bind->bindings); sg_draw(0, 4, count); m->bind->bindings.vertex_buffer_offsets[0] -= base; } m->bind->base += count; } static inline int lmateraial_text_draw_(lua_State *L, int ex) { struct material_text *m = (struct material_text *)luaL_checkudata(L, 1, "SOLUNA_MATERIAL_TEXT"); struct draw_primitive *prim = lua_touserdata(L, 2); int prim_n = luaL_checkinteger(L, 3); if (prim_n <= 0) return 0; int i; float texsize = m->uniform->texsize; m->uniform->texsize = 1.0f / FONT_MANAGER_TEXSIZE; sg_apply_pipeline(m->pip); int count = -1; uint32_t color = 0; for (i=0;icodepoint >= 0) { if (count < 0) { color = t->color; count = 1; } else if (t->color != color) { draw_text(m, color, count, ex); color = t->color; count = 1; } else { ++count; } } } draw_text(m, color, count, ex); m->uniform->texsize = texsize; return 0; } static int lmateraial_text_draw(lua_State *L) { return lmateraial_text_draw_(L, 0); } static int lmateraial_text_draw_ex(lua_State *L) { return lmateraial_text_draw_(L, 1); } static void init_pipeline(struct material_text *p) { sg_pipeline_desc desc = { .layout.attrs = { [ATTR_texquad_position].format = SG_VERTEXFORMAT_FLOAT3, [ATTR_texquad_offset].format = SG_VERTEXFORMAT_UINT, [ATTR_texquad_u].format = SG_VERTEXFORMAT_UINT, [ATTR_texquad_v].format = SG_VERTEXFORMAT_UINT, }, }; p->pip = util_make_pipeline(&desc, texquad_shader_desc, "text-pipeline", 1); } static int lnew_material_text_normal(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct material_text *m = (struct material_text *)lua_newuserdatauv(L, sizeof(*m), 5); util_ref_object(L, &m->inst, 1, "inst_buffer", "SOKOL_BUFFER", 0); util_ref_object(L, &m->bind, 2, "bindings", "SOKOL_BINDINGS", 1); util_ref_object(L, &m->uniform, 3, "uniform", "SOKOL_UNIFORM", 1); util_ref_object(L, &m->srbuffer, 4, "sr_buffer", "SOLUNA_SRBUFFER", 1); tmp_buffer_init(L, &m->tmp, 5, "tmp_buffer"); init_pipeline(m); if (lua_getfield(L, 1, "font_manager") != LUA_TLIGHTUSERDATA) { return luaL_error(L, "Missing .font_manager"); } m->font = lua_touserdata(L, -1); lua_pop(L, 1); 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[] = { { "__index", NULL }, { "submit", lmateraial_text_submit }, { "draw", DRAWFUNC(lmateraial_text_draw) }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } static int lchar_for_batch(lua_State *L) { if (material_id <= 0) { return luaL_error(L, "Text material is not registered"); } struct text * t = (struct text *)lua_touserdata(L, lua_upvalueindex(1)); t->header.sprite = -1; t->codepoint = luaL_checkinteger(L, 1); t->font = luaL_checkinteger(L, 2); t->size = luaL_checkinteger(L, 3); t->color = luaL_checkinteger(L, 4); if (!(t->color & 0xff000000)) t->color |= 0xff000000; lua_pushvalue(L, lua_upvalueindex(1)); return 1; } static int lset_material_id(lua_State *L) { int id = luaL_checkinteger(L, 1); if (id <= 0) { return luaL_error(L, "Invalid text material id %d", id); } material_id = id; lua_pushvalue(L, lua_upvalueindex(1)); lua_pushinteger(L, id); lua_setiuservalue(L, -2, 1); lua_pop(L, 1); return 0; } struct text_primitive { struct draw_primitive pos; union { struct draw_primitive dummy; struct text text; } u; }; /* ** From lua utf8lib : ** Decode one UTF-8 sequence, returning NULL if byte sequence is ** invalid. The array 'limits' stores the minimum value for each ** sequence length, to check for overlong representations. Its first ** entry forces an error for non-ascii bytes with no continuation ** bytes (count == 0). */ #define iscont(c) (((c) & 0xC0) == 0x80) #define l_uint32 uint32_t #define MAXUTF 0x7FFFFFFFu static const char *utf8_decode (const char *s, l_uint32 *val) { static const l_uint32 limits[] = {~(l_uint32)0, 0x80, 0x800, 0x10000u, 0x200000u, 0x4000000u}; unsigned int c = (unsigned char)s[0]; l_uint32 res = 0; /* final result */ if (c < 0x80) /* ascii? */ res = c; else { int count = 0; /* to count number of continuation bytes */ for (; c & 0x40; c <<= 1) { /* while it needs continuation bytes... */ unsigned int cc = (unsigned char)s[++count]; /* read next byte */ if (!iscont(cc)) /* not a continuation byte? */ return NULL; /* invalid byte sequence */ res = (res << 6) | (cc & 0x3F); /* add lower 6 bits from cont. byte */ } res |= ((l_uint32)(c & 0x7F) << (count * 5)); /* add first byte */ if (count > 5 || res > MAXUTF || res < limits[count]) return NULL; /* invalid byte sequence */ s += count; /* skip continuation bytes read */ } *val = res; return s + 1; /* +1 to include first byte */ } static const char * skip_bracket(const char *str) { for (;;) { if (*str == ']') { return str + 1; } else if (*str == '\0') { return str; } ++str; } } static int count_string(const char *str) { uint32_t val = 0; int n = 0; while ((str = utf8_decode(str, &val))) { if (val == 0) break; if (val > 32) { if (val == '[') { char c = *str; if (c == '[') { ++str; ++n; } else { if (c == 'i') { // icons ++n; } str = skip_bracket(str); } } else { ++n; } } } return n; } #define MAX_WIDTH 4096 #define MAX_HEIGHT 4096 #define DEFAULT_FONTSIZE 24 static void * free_primitive(void *ud, void *ptr, size_t osize, size_t nsize) { free(ptr); return NULL; } #define ALIGNMENT_LEFT 0 #define ALIGNMENT_CENTER 1 #define ALIGNMENT_RIGHT 2 #define ALIGNMENT_MASK 3 #define VALIGNMENT_TOP (1<<2) #define VALIGNMENT_CENTER 0 #define VALIGNMENT_BOTTOM (2<<2) #define VALIGNMENT_MASK (3<<2) // todo: support multi font/size struct block_context { int width; int height; int x; int y; int ascent; int decent; int line_prim; int line_width; int alignment; uint32_t default_color; uint32_t color; }; static inline int advance(struct block_context *ctx, int x) { if (x + ctx->x > ctx->width) return 1; ctx->x += x; return 0; } struct position { // input int n; // output int x; int y; int w; int h; int decent; }; static inline int newline(struct block_context *ctx, struct text_primitive * prim, int n, struct position *pos) { int from = ctx->line_prim; ctx->line_prim = n; int line_width = ctx->line_width; ctx->line_width = 0; int offx = 0; int align = ctx->alignment & ALIGNMENT_MASK; switch (align) { case ALIGNMENT_CENTER: offx = (ctx->width - line_width) / 2 * 256; break; case ALIGNMENT_RIGHT: offx = (ctx->width - line_width) * 256; break; } if (pos && pos->n <= 0 && pos->y == ctx->y) { pos->x += offx / 256; } if (prim != NULL && offx > 0) { int i; for (i=from;iy + ctx->ascent + ctx->decent > ctx->height) { return 1; } else { ctx->y += ctx->ascent + ctx->decent; ctx->x = 0; return 0; } } static inline int tohex(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } static const char * parse_bracket(struct block_context *ctx, const char *str, int *icon) { char c = *str; int hex = -1; if (c == 'i') { ++str; int num = 0; while (*str >= '0' && *str <= '9') { num = num * 10 + (*str - '0'); ++str; } *icon = num + 1; } else if ((hex = tohex(c)) >= 0) { int color = hex; for (;;) { ++str; if ((hex = tohex(*str)) >= 0) { color = color * 16 + hex; } else { break; } } if (!(color & 0xff000000)) color |= 0xff000000; ctx->color = color; } else if (c == 'n') { ctx->color = ctx->default_color; } // todo: other command return skip_bracket(str); } static inline void advance_position(struct position *pos, struct block_context *ctx) { if (pos) { if (pos->n == 0) { pos->x = ctx->x; pos->y = ctx->y; } --pos->n; } } // todo: support color static int ltext_(lua_State *L, struct position *pos) { const char * str = luaL_checkstring(L, 1); int count = count_string(str); struct block_context ctx; ctx.width = luaL_optinteger(L, 2, MAX_WIDTH); ctx.height = luaL_optinteger(L, 3, MAX_HEIGHT); struct font_manager *mgr = (struct font_manager *)lua_touserdata(L, lua_upvalueindex(1)); int fontid = lua_tointeger(L, lua_upvalueindex(2)); int fontsize = lua_tointeger(L, lua_upvalueindex(3)); ctx.default_color = lua_tointeger(L, lua_upvalueindex(4)); ctx.color = ctx.default_color; ctx.x = 0; int decent, gap; font_manager_fontheight(mgr, fontid, fontsize, &ctx.ascent, &decent, &gap); if (gap == 0) gap = 1; ctx.decent = -decent + gap; ctx.y = ctx.ascent; ctx.line_prim = 0; ctx.line_width = 0; ctx.alignment = lua_tointeger(L, lua_upvalueindex(5)); int pos_n = 0; if (pos) { if (pos->n < 0) { pos->n = 0; } pos_n = pos->n; } char * buffer = NULL; struct text_primitive * prim = NULL; if (pos == NULL) { buffer = (char *)malloc(count * sizeof(struct text_primitive)+1); prim = (struct text_primitive *)buffer; } int i; int n = 0; for (i=0;i ctx.line_width) ctx.line_width = ctx.x; if (newline(&ctx, prim, n, pos)) break; } else { struct font_glyph g, og; if (font_manager_glyph(mgr, fontid, ' ', fontsize, &g, &og) == NULL) { if (ctx.x > ctx.line_width) ctx.line_width = ctx.x; if (advance(&ctx, g.advance_x)) { if (newline(&ctx, prim, n, pos)) break; advance(&ctx, g.advance_x); } } } } else { int icon = 0; if (val == '[') { if (*str != '[') { str = parse_bracket(&ctx, str, &icon); if (!icon) { continue; } } else { ++str; } } int dy = 0; int codepoint = val; int font = fontid; if (icon > 0) { codepoint = icon -1; font = FONT_ICON; dy = - ( ctx.ascent ) * 256; } if (prim) { prim[n].pos.x = ctx.x * 256; prim[n].pos.y = ctx.y * 256 + dy; prim[n].pos.sr = 0; prim[n].pos.sprite = -material_id; } struct font_glyph g, og; if (font_manager_glyph(mgr, font, codepoint, fontsize, &g, &og) == NULL) { advance_position(pos, &ctx); if (ctx.x > ctx.line_width) ctx.line_width = ctx.x; if (advance(&ctx, g.advance_x)) { if (newline(&ctx, prim, n, pos)) break; if (prim) { prim[n].pos.x = ctx.x * 256; prim[n].pos.y = ctx.y * 256 + dy; } advance(&ctx, g.advance_x); } if (prim) { prim[n].u.text.header.sprite = -1; prim[n].u.text.codepoint = codepoint; prim[n].u.text.font = font; prim[n].u.text.size = fontsize; prim[n].u.text.color = ctx.color; } ++n; } ++i; } } if (ctx.x > ctx.line_width) ctx.line_width = ctx.x; int height = ctx.y + ctx.decent - gap; if (pos && pos->n >= 0) { pos_n -= pos->n; pos->n = 0; advance_position(pos, &ctx); } newline(&ctx, prim, n, pos); int offy; int valign = ctx.alignment & VALIGNMENT_MASK; switch (valign) { case VALIGNMENT_CENTER: offy = (ctx.height - height) / 2 * 256; break; case VALIGNMENT_BOTTOM: offy = (ctx.height - height) * 256; break; default: offy = 0; break; } if (prim != NULL) { if (offy != 0) { for (i=0;iy += offy / 256 - ctx.ascent; pos->w = 2; pos->h = ctx.ascent + ctx.decent - gap; pos->n = pos_n; pos->decent = ctx.decent; return 0; } } static int ltext(lua_State *L) { return ltext_(L, NULL); } static int ltext_position(lua_State *L) { struct position pos; pos.n = luaL_checkinteger(L, 2); pos.x = 0; pos.y = 0; pos.w = 0; pos.h = 0; lua_remove(L, 2); ltext_(L, &pos); lua_pushinteger(L, pos.x); lua_pushinteger(L, pos.y); lua_pushinteger(L, pos.w); lua_pushinteger(L, pos.h); lua_pushinteger(L, pos.n); lua_pushinteger(L, pos.decent); return 6; } static uint32_t parse_alignment(lua_State *L, int index) { const char *alignment_string = lua_tostring(L, index); int i; char c; uint32_t alignment = 0; for (i=0;(c = alignment_string[i]);i++) { switch(c) { case 'l' : case 'L' : alignment |= ALIGNMENT_LEFT; break; case 'r' : case 'R' : alignment |= ALIGNMENT_RIGHT; break; case 'c' : case 'C' : alignment |= ALIGNMENT_CENTER; break; case 't' : case 'T' : alignment |= VALIGNMENT_TOP; break; case 'v' : case 'V' : alignment |= VALIGNMENT_CENTER; break; case 'b' : case 'B' : alignment |= VALIGNMENT_BOTTOM; break; } } return alignment; } static int ltext_block(lua_State *L) { if (material_id <= 0) { return luaL_error(L, "Text material is not registered"); } luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); void * font_mgr = lua_touserdata(L, 1); int fontid = luaL_checkinteger(L, 2); int fontsize = luaL_optinteger(L, 3, DEFAULT_FONTSIZE); uint32_t color = luaL_optinteger(L, 4, 0xff000000); uint32_t alignment = 0; if (lua_type(L, 5) == LUA_TSTRING) { alignment = parse_alignment(L, 5); } if (!(color & 0xff000000)) color |= 0xff000000; lua_pushlightuserdata(L, font_mgr); // 1 lua_pushinteger(L, fontid); // 2 lua_pushinteger(L, fontsize); // 3 lua_pushinteger(L, color); // 4 lua_pushinteger(L, alignment); // 5 lua_pushcclosure(L, ltext, 5); lua_pushlightuserdata(L, font_mgr); // 1 lua_pushinteger(L, fontid); // 2 lua_pushinteger(L, fontsize); // 3 lua_pushinteger(L, color); // 4 lua_pushinteger(L, alignment); // 5 lua_pushcclosure(L, ltext_position, 5); return 2; } int luaopen_material_text(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "char", NULL }, { "set_material_id", NULL }, { "block", ltext_block }, { "normal", lnew_material_text_normal }, { "instance_size", NULL }, { NULL, NULL }, }; luaL_newlib(L, l); // char() struct text * t = lua_newuserdatauv(L, sizeof(*t), 1); memset(t, 0, sizeof(*t)); lua_pushvalue(L, -1); lua_pushcclosure(L, lset_material_id, 1); lua_setfield(L, -3, "set_material_id"); lua_pushcclosure(L, lchar_for_batch, 1); lua_setfield(L, -2, "char"); lua_pushinteger(L, sizeof(struct inst_object)); lua_setfield(L, -2, "instance_size"); return 1; } ================================================ FILE: src/material_util.c ================================================ #include #include #include "material_util.h" void util_ref_object(lua_State *L, void *ptr, int uv_index, const char *key, const char *luatype, int direct) { if (lua_getfield(L, 1, key) != LUA_TUSERDATA) luaL_error(L, "Invalid key .%s", key); void *obj = luaL_checkudata(L, -1, luatype); lua_pushvalue(L, -1); // ud, object, object lua_setiuservalue(L, -3, uv_index); if (!direct) { lua_pushlightuserdata(L, ptr); lua_call(L, 1, 0); } else { lua_pop(L, 1); void **ref = (void **)ptr; *ref = obj; } } void util_submit_material(lua_State *L, int batch_n, void *mat, util_submit_func submit) { struct draw_primitive *prim = lua_touserdata(L, 2); int prim_n = luaL_checkinteger(L, 3); int i = 0; for (;;) { int n = prim_n - i; if (n > batch_n) { submit(L, mat, prim, batch_n); i += batch_n; prim += batch_n; } else { submit(L, mat, prim, n); i += batch_n; break; } } } sg_pipeline util_make_pipeline(sg_pipeline_desc *desc, util_shader_desc_func func, const char *what, int blend) { sg_shader shd = sg_make_shader(func(sg_query_backend())); if (sg_query_shader_state(shd) != SG_RESOURCESTATE_VALID) { fprintf(stderr, "Failed to create shader for %s!\n", what); } desc->shader = shd; if (desc->primitive_type == 0) { desc->primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP; } desc->label = what; if (desc->layout.buffers[0].step_func == 0) { desc->layout.buffers[0].step_func = SG_VERTEXSTEP_PER_INSTANCE; } if (blend) { desc->colors[0].blend = (sg_blend_state) { .enabled = true, .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA, .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, .src_factor_alpha = SG_BLENDFACTOR_ONE, .dst_factor_alpha = SG_BLENDFACTOR_ZERO }; } sg_pipeline pip = sg_make_pipeline(desc); if (sg_query_pipeline_state(pip) != SG_RESOURCESTATE_VALID) { fprintf(stderr, "failed to create pipeline %s\n", what); } return pip; } ================================================ FILE: src/material_util.h ================================================ #ifndef soluna_material_util_h #define soluna_material_util_h #include #include "sokol/sokol_gfx.h" #include "batch.h" void util_ref_object(lua_State *L, void *ptr, int uv_index, const char *key, const char *luatype, int direct); typedef void (*util_submit_func)(lua_State *L, void *m_, struct draw_primitive *prim, int n); void util_submit_material(lua_State *L, int batch_n, void *mat, util_submit_func submit); typedef const sg_shader_desc* (*util_shader_desc_func)(sg_backend backend); sg_pipeline util_make_pipeline(sg_pipeline_desc *desc, util_shader_desc_func func, const char *what, int blend); #endif ================================================ FILE: src/mutex.h ================================================ #ifndef __fontmutex_h_ #define __fontmutex_h_ #if defined(_WIN32) #include #define mutex_t SRWLOCK #define mutex_init(m) InitializeSRWLock(&m) #define mutex_acquire(m) AcquireSRWLockExclusive(&m) #define mutex_release(m) ReleaseSRWLockExclusive(&m) #else #include #define mutex_t pthread_mutex_t #define mutex_init(m) pthread_mutex_init(&m, NULL) #define mutex_acquire(m) pthread_mutex_lock(&m) #define mutex_release(m) pthread_mutex_unlock(&m) #endif #endif ================================================ FILE: src/openlibs.c ================================================ #define linit_c #define LUA_LIB #include #include "lua.h" #include "lualib.h" #include "lauxlib.h" void soluna_embed(lua_State* L); void soluna_openlibs(lua_State *L) { // ignore env. vars. lua_pushboolean(L, 1); lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV"); luaL_openlibs(L); soluna_embed(L); } ================================================ FILE: src/openurl.c ================================================ #include #include #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #include static void open_url(lua_State *L, const char *url) { int n = MultiByteToWideChar(CP_UTF8, 0, url, -1, NULL, 0); if (n == 0) luaL_error(L, "Invalid url string : %s", url); void * buf = lua_newuserdatauv(L, n * sizeof(WCHAR), 0); MultiByteToWideChar(CP_UTF8, 0, url, -1, (WCHAR *)buf, n); ShellExecuteW(NULL, L"open", (WCHAR *)buf, NULL, NULL, SW_SHOWNORMAL); } #elif defined(__EMSCRIPTEN__) #include #if defined(__EMSCRIPTEN_PTHREADS__) #include #endif extern void soluna_wasm_open_url(const char *url); static void soluna_wasm_call_open_url(const char *url) { #if defined(__EMSCRIPTEN_PTHREADS__) if (!emscripten_is_main_browser_thread()) { emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VI, soluna_wasm_open_url, url); return; } #endif soluna_wasm_open_url(url); } static void open_url(lua_State *L, const char *url) { soluna_wasm_call_open_url(url); } #elif defined(__APPLE__) || defined(__linux__) #include #include #include #include static void open_url(lua_State *L, const char *url) { pid_t pid = fork(); if (pid < 0) { luaL_error(L, "fork() failed"); } else if (pid == 0) { // child #if defined(__APPLE__) execl("/usr/bin/open", "open", url, (char *)NULL); #else execl("/usr/bin/xdg-open", "xdg-open", url, (char *)NULL); #endif // if execl return, it's error _exit(127); } else { // parent int status; waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { luaL_error(L, "failed to open url: %s", url); } } } #else static void open_url(lua_State *L, const char *url) { } #endif static int lurl_open(lua_State *L) { const char * url = luaL_checkstring(L, 1); open_url(L, url); return 0; } int luaopen_url(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "open", lurl_open }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/platform/linux/soluna_linux_ime.c ================================================ #include #include #include #include #include #include #include #include #include #include #include "sokol/sokol_app.h" #include "ime_state.h" #include "ime_char_filter.h" #include "soluna_linux_ime.h" void soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat); static XIM g_soluna_linux_im = NULL; static XIC g_soluna_linux_ic = NULL; static bool g_soluna_linux_xim_failed = false; static bool g_soluna_linux_has_focus = false; static bool g_soluna_linux_locale_ready = false; static const int SOLUNA_LINUX_CHAR_QUEUE_CAP = 32; static uint32_t g_soluna_linux_expected_chars[32]; static int g_soluna_linux_expected_count = 0; static uint32_t g_soluna_linux_ignore_chars[32]; static int g_soluna_linux_ignore_count = 0; static inline struct soluna_ime_char_filter_state soluna_linux_char_filter_state(void) { return (struct soluna_ime_char_filter_state) { .expected_chars = g_soluna_linux_expected_chars, .expected_count = &g_soluna_linux_expected_count, .ignore_chars = g_soluna_linux_ignore_chars, .ignore_count = &g_soluna_linux_ignore_count, .capacity = SOLUNA_LINUX_CHAR_QUEUE_CAP, }; } static void soluna_linux_reset_char_queues(void) { soluna_ime_char_filter_reset(soluna_linux_char_filter_state()); } static Display * soluna_linux_display(void) { return (Display *)sapp_x11_get_display(); } static Window soluna_linux_window(void) { return (Window)(uintptr_t)sapp_x11_get_window(); } static void soluna_linux_ensure_locale(void) { if (g_soluna_linux_locale_ready) { return; } if (!setlocale(LC_CTYPE, "")) { fprintf(stderr, "soluna: failed to set locale\n"); } if (!XSupportsLocale()) { fprintf(stderr, "soluna: current locale is not supported by Xlib\n"); } XSetLocaleModifiers(""); g_soluna_linux_locale_ready = true; } static void soluna_linux_set_spot(short x, short y) { if (!g_soluna_linux_ic) { return; } XVaNestedList preedit = XVaCreateNestedList(0, XNSpotLocation, &(XPoint){ x, y }, NULL); if (!preedit) { return; } XSetICValues(g_soluna_linux_ic, XNPreeditAttributes, preedit, NULL); XFree(preedit); } bool soluna_linux_ensure_im(void) { if (g_soluna_linux_ic) { return true; } if (g_soluna_linux_xim_failed) { return false; } Display *dpy = soluna_linux_display(); Window win = soluna_linux_window(); if (!dpy || !win) { return false; } soluna_linux_ensure_locale(); XIM im = XOpenIM(dpy, NULL, NULL, NULL); if (!im) { g_soluna_linux_xim_failed = true; return false; } XIC ic = XCreateIC( im, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, win, XNFocusWindow, win, NULL); if (!ic) { XCloseIM(im); return false; } g_soluna_linux_im = im; g_soluna_linux_ic = ic; soluna_linux_set_spot(0, 0); return true; } void soluna_linux_update_spot(void) { if (!g_soluna_ime_rect.valid) { return; } if (!soluna_linux_ensure_im()) { return; } float scale = sapp_dpi_scale(); if (scale <= 0.0f) { scale = 1.0f; } float caret_x = g_soluna_ime_rect.x; float caret_y = g_soluna_ime_rect.y + g_soluna_ime_rect.h; if (caret_x < 0.0f) { caret_x = 0.0f; } if (caret_y < 0.0f) { caret_y = 0.0f; } short spot_x = (short)(caret_x * scale + 0.5f); short spot_y = (short)(caret_y * scale + 0.5f); soluna_linux_set_spot(spot_x, spot_y); } static void soluna_linux_emit_utf8(const char *text, int len, uint32_t mods, bool repeat) { if (!text || len <= 0) { return; } mbstate_t state; memset(&state, 0, sizeof(state)); const char *ptr = text; const char *end = text + len; bool first = true; while (ptr < end) { char32_t ch = 0; size_t consumed = mbrtoc32(&ch, ptr, (size_t)(end - ptr), &state); if (consumed == (size_t)-1 || consumed == (size_t)-2) { memset(&state, 0, sizeof(state)); ++ptr; continue; } if (consumed == 0) { consumed = 1; } soluna_ime_char_filter_push_expected(soluna_linux_char_filter_state(), (uint32_t)ch); soluna_emit_char((uint32_t)ch, mods, first ? repeat : false); first = false; ptr += consumed; } } static bool soluna_linux_handle_keypress(XKeyEvent *kev) { if (!kev) { return false; } Display *dpy = soluna_linux_display(); if (!dpy || kev->display != dpy || kev->window != soluna_linux_window()) { return false; } if (!soluna_linux_ensure_im()) { return false; } char local_buf[128]; char *buf = local_buf; int cap = sizeof(local_buf); KeySym ks = 0; Status status = 0; int len = Xutf8LookupString(g_soluna_linux_ic, kev, buf, cap, &ks, &status); if (status == XBufferOverflow) { buf = (char *)malloc((size_t)len); if (!buf) { return false; } cap = len; len = Xutf8LookupString(g_soluna_linux_ic, kev, buf, cap, &ks, &status); } bool handled = false; if (status == XLookupChars || status == XLookupBoth) { uint32_t mods = 0; if (kev->state & ShiftMask) mods |= SAPP_MODIFIER_SHIFT; if (kev->state & ControlMask) mods |= SAPP_MODIFIER_CTRL; if (kev->state & Mod1Mask) mods |= SAPP_MODIFIER_ALT; bool repeat = (kev->type == KeyPress) && (kev->state & (1 << 14)); soluna_linux_emit_utf8(buf, len, mods, repeat); handled = true; } if (buf != local_buf) { free(buf); } return handled; } static bool soluna_linux_filter_event(Display *dpy, XEvent *event) { if (!event) { return true; } if (XFilterEvent(event, None)) { return true; } if (event->type == KeyPress) { if (!g_soluna_ime_rect.valid) { return false; } if (event->xkey.display != soluna_linux_display() || event->xkey.window != soluna_linux_window()) { return false; } if (soluna_linux_handle_keypress(&event->xkey)) { return true; } } return false; } static int (*soluna_linux_real_XNextEvent)(Display *, XEvent *) = NULL; static int soluna_linux_XNextEvent(Display *display, XEvent *event) { if (!soluna_linux_real_XNextEvent) { soluna_linux_real_XNextEvent = (int (*)(Display *, XEvent *))dlsym(RTLD_NEXT, "XNextEvent"); if (!soluna_linux_real_XNextEvent) { fprintf(stderr, "soluna: failed to resolve XNextEvent\n"); abort(); } } int r = soluna_linux_real_XNextEvent(display, event); if (r != 0) { return r; } if (soluna_linux_filter_event(display, event)) { event->type = 0; } return 0; } int XNextEvent(Display *display, XEvent *event) { return soluna_linux_XNextEvent(display, event); } static void soluna_linux_focus_reset(void) { soluna_linux_reset_char_queues(); } void soluna_linux_focus_in(void) { g_soluna_linux_xim_failed = false; if (!soluna_linux_ensure_im()) { return; } if (!g_soluna_linux_has_focus) { XSetICFocus(g_soluna_linux_ic); g_soluna_linux_has_focus = true; } soluna_linux_update_spot(); } void soluna_linux_focus_out(void) { if (!g_soluna_linux_ic) { return; } if (g_soluna_linux_has_focus) { XUnsetICFocus(g_soluna_linux_ic); g_soluna_linux_has_focus = false; } soluna_linux_focus_reset(); } void soluna_linux_shutdown_ime(void) { if (g_soluna_linux_ic) { XDestroyIC(g_soluna_linux_ic); g_soluna_linux_ic = NULL; } if (g_soluna_linux_im) { XCloseIM(g_soluna_linux_im); g_soluna_linux_im = NULL; } g_soluna_linux_has_focus = false; g_soluna_linux_xim_failed = false; soluna_linux_focus_reset(); } void soluna_linux_on_rect_cleared(void) { if (soluna_linux_ensure_im()) { soluna_linux_set_spot(0, 0); } soluna_linux_focus_reset(); } bool soluna_linux_should_skip_event(const sapp_event *ev) { if (!ev || ev->type != SAPP_EVENTTYPE_CHAR) { return false; } return soluna_ime_char_filter_should_skip(soluna_linux_char_filter_state(), ev->char_code); } void soluna_linux_handle_event(const sapp_event *ev) { if (!ev) { return; } switch (ev->type) { case SAPP_EVENTTYPE_FOCUSED: soluna_linux_focus_in(); if (g_soluna_ime_rect.valid) { soluna_linux_update_spot(); } break; case SAPP_EVENTTYPE_UNFOCUSED: soluna_linux_focus_out(); break; case SAPP_EVENTTYPE_RESIZED: if (g_soluna_ime_rect.valid) { soluna_linux_update_spot(); } break; default: break; } } ================================================ FILE: src/platform/linux/soluna_linux_ime.h ================================================ #ifndef SOLUNA_LINUX_IME_H #define SOLUNA_LINUX_IME_H #include bool soluna_linux_ensure_im(void); void soluna_linux_on_rect_cleared(void); void soluna_linux_update_spot(void); void soluna_linux_focus_in(void); void soluna_linux_focus_out(void); void soluna_linux_shutdown_ime(void); bool soluna_linux_should_skip_event(const sapp_event *ev); void soluna_linux_handle_event(const sapp_event *ev); #endif /* SOLUNA_LINUX_IME_H */ ================================================ FILE: src/platform/macos/soluna_macos_ime.h ================================================ #ifndef SOLUNA_MACOS_IME_H #define SOLUNA_MACOS_IME_H #include void soluna_macos_install_ime(void); void soluna_macos_hide_ime_label(void); void soluna_macos_apply_ime_rect(void); void soluna_macos_set_ime_font(const char *font_name, float height_px); bool soluna_macos_is_composition_active(void); #endif /* SOLUNA_MACOS_IME_H */ ================================================ FILE: src/platform/macos/soluna_macos_ime.m ================================================ #import #import #include #include "ime_state.h" #include "soluna_macos_ime.h" #include "sokol/sokol_app.h" @interface _sapp_macos_view : NSView @end static void soluna_emit_nsstring(NSString *text); @interface SolunaIMETextView : NSTextView @end @implementation SolunaIMETextView - (BOOL)isOpaque { return NO; } - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { [self setEditable:NO]; [self setSelectable:NO]; [self setRichText:NO]; [self setImportsGraphics:NO]; [self setAutomaticQuoteSubstitutionEnabled:NO]; [self setAutomaticDataDetectionEnabled:NO]; [self setAutomaticSpellingCorrectionEnabled:NO]; [self setDrawsBackground:NO]; [self setHorizontallyResizable:YES]; [self setVerticallyResizable:NO]; [self setTextContainerInset:NSMakeSize(0, 0)]; NSTextContainer *container = [self textContainer]; if (container) { [container setLineFragmentPadding:0.0f]; [container setWidthTracksTextView:NO]; [container setContainerSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)]; } [self setHidden:YES]; } return self; } - (void)doCommandBySelector:(SEL)selector { (void)selector; } @end // Forward declarations implemented in entry.c void soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat); static bool g_soluna_macos_composition = false; static SolunaIMETextView *g_soluna_ime_label = nil; static NSString *g_soluna_macos_ime_font_name = nil; static CGFloat g_soluna_macos_ime_font_size = 14.0f; static SolunaIMETextView *soluna_macos_ensure_ime_label(NSView *view); static const void *const kSolunaMarkedTextKey = &kSolunaMarkedTextKey; static const void *const kSolunaSelectedRangeKey = &kSolunaSelectedRangeKey; static const void *const kSolunaConsumedFlagKey = &kSolunaConsumedFlagKey; static NSFont * soluna_macos_current_ime_font(void) { CGFloat size = g_soluna_macos_ime_font_size > 0.0f ? g_soluna_macos_ime_font_size : 14.0f; NSFont *font = nil; if (g_soluna_macos_ime_font_name) { font = [NSFont fontWithName:g_soluna_macos_ime_font_name size:size]; } if (font == nil) { font = [NSFont systemFontOfSize:size]; } return font; } static NSColor * soluna_macos_current_text_color(void) { if (g_soluna_ime_rect.text_color == 0) { return [NSColor textColor]; } uint32_t c = g_soluna_ime_rect.text_color; CGFloat a = ((c >> 24) & 0xff) / 255.0f; CGFloat r = ((c >> 16) & 0xff) / 255.0f; CGFloat g = ((c >> 8) & 0xff) / 255.0f; CGFloat b = (c & 0xff) / 255.0f; return [NSColor colorWithRed:r green:g blue:b alpha:a]; } static void soluna_macos_apply_ime_font(void) { if (g_soluna_ime_label) { NSFont *font = soluna_macos_current_ime_font(); if (font) { [g_soluna_ime_label setFont:font]; } } } void soluna_macos_set_ime_font(const char *font_name, float height_px) { if (g_soluna_macos_ime_font_name) { [g_soluna_macos_ime_font_name release]; g_soluna_macos_ime_font_name = nil; } if (font_name && font_name[0]) { NSString *converted = [[NSString alloc] initWithUTF8String:font_name]; if (converted) { g_soluna_macos_ime_font_name = converted; } else { [converted release]; } } if (height_px > 0.0f) { g_soluna_macos_ime_font_size = (CGFloat)height_px; } else { g_soluna_macos_ime_font_size = 0.0f; } soluna_macos_apply_ime_font(); } static NSRect soluna_current_caret_local_rect(NSView *view) { NSRect caret = NSMakeRect(0, 0, 1, 1); if (g_soluna_ime_rect.valid) { CGFloat dpi_scale = sapp_dpi_scale(); if (dpi_scale <= 0.0f) { dpi_scale = 1.0f; } CGFloat logical_height = (CGFloat)sapp_height() / dpi_scale; CGFloat caret_y = logical_height - (g_soluna_ime_rect.y + g_soluna_ime_rect.h); caret = NSMakeRect(g_soluna_ime_rect.x, caret_y, g_soluna_ime_rect.w, g_soluna_ime_rect.h); } return caret; } static void soluna_macos_position_ime_input_view(NSView *view) { SolunaIMETextView *imeView = soluna_macos_ensure_ime_label(view); if (!imeView || !g_soluna_ime_rect.valid) { return; } NSRect caret = soluna_current_caret_local_rect(view); NSRect bounds = view.bounds; NSFont *font = soluna_macos_current_ime_font(); CGFloat ascender = 0.0f; CGFloat descender = 0.0f; CGFloat leading = 0.0f; if (font) { ascender = MAX(font.ascender, 0.0f); descender = MIN(font.descender, 0.0f); leading = MAX(font.leading, 0.0f); } CGFloat lineHeight = ascender - descender + leading; if (lineHeight <= 0.0f) { lineHeight = g_soluna_macos_ime_font_size > 0.0f ? g_soluna_macos_ime_font_size : 14.0f; } lineHeight = MAX(lineHeight, MAX(caret.size.height, 1.0f)); CGFloat baselineY = caret.origin.y + MAX(caret.size.height, 1.0f) * 0.5f; CGFloat frameX = caret.origin.x; CGFloat frameY = baselineY - ascender; CGFloat frameW = MAX(1.0f, NSMaxX(bounds) - frameX); CGFloat frameH = lineHeight; NSRect frame = NSMakeRect(frameX, frameY, frameW, frameH); CGFloat maxOriginX = NSMaxX(bounds) - 1.0f; if (maxOriginX < bounds.origin.x) { maxOriginX = bounds.origin.x; } if (frame.origin.x < bounds.origin.x) frame.origin.x = bounds.origin.x; if (frame.origin.x > maxOriginX) frame.origin.x = maxOriginX; if (frame.origin.y < bounds.origin.y) frame.origin.y = bounds.origin.y; if (NSMaxX(frame) > NSMaxX(bounds)) frame.size.width = MAX(1.0f, NSMaxX(bounds) - frame.origin.x); if (NSMaxY(frame) > NSMaxY(bounds)) frame.origin.y = NSMaxY(bounds) - frame.size.height; [imeView setFrame:NSIntegralRect(frame)]; [imeView setHidden:NO]; } static SolunaIMETextView * soluna_macos_ensure_ime_label(NSView *view) { if (g_soluna_ime_label == nil) { g_soluna_ime_label = [[SolunaIMETextView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; [g_soluna_ime_label setHidden:YES]; [g_soluna_ime_label setTranslatesAutoresizingMaskIntoConstraints:YES]; } if (g_soluna_ime_label.superview != view) { [g_soluna_ime_label removeFromSuperview]; if (view) { [view addSubview:g_soluna_ime_label]; } } soluna_macos_apply_ime_font(); return g_soluna_ime_label; } void soluna_macos_hide_ime_label(void) { if (g_soluna_ime_label) { [g_soluna_ime_label setString:@""]; [g_soluna_ime_label setHidden:YES]; } } static uint32_t soluna_modifiers_from_event(NSEvent *event) { NSEventModifierFlags flags = event ? event.modifierFlags : NSEvent.modifierFlags; uint32_t mods = 0; if (flags & NSEventModifierFlagShift) { mods |= SAPP_MODIFIER_SHIFT; } if (flags & NSEventModifierFlagControl) { mods |= SAPP_MODIFIER_CTRL; } if (flags & NSEventModifierFlagOption) { mods |= SAPP_MODIFIER_ALT; } if (flags & NSEventModifierFlagCommand) { mods |= SAPP_MODIFIER_SUPER; } return mods; } static uint32_t soluna_utf32_from_substring(NSString *substr) { if (substr == nil || substr.length == 0) { return 0; } unichar buffer[2] = {0}; NSUInteger len = substr.length; [substr getCharacters:buffer range:NSMakeRange(0, len)]; if (len >= 2 && buffer[0] >= 0xD800 && buffer[0] <= 0xDBFF && buffer[1] >= 0xDC00 && buffer[1] <= 0xDFFF) { uint32_t high = buffer[0] - 0xD800; uint32_t low = buffer[1] - 0xDC00; return (high << 10) + low + 0x10000; } return buffer[0]; } static void soluna_emit_nsstring(NSString *text) { if (text == nil || text.length == 0) { return; } uint32_t mods = soluna_modifiers_from_event([NSApp currentEvent]); [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) { (void)substringRange; (void)enclosingRange; (void)stop; uint32_t codepoint = soluna_utf32_from_substring(substring); if (codepoint != 0) { soluna_emit_char(codepoint, mods, false); } }]; } static NSString * soluna_plain_string(id string) { if ([string isKindOfClass:[NSAttributedString class]]) { return [(NSAttributedString *)string string]; } if ([string isKindOfClass:[NSString class]]) { return (NSString *)string; } return [string description]; } static void soluna_store_marked_text(NSView *view, NSString *text, NSRange selected_range) { if (text != nil && text.length > 0) { objc_setAssociatedObject(view, kSolunaMarkedTextKey, text, OBJC_ASSOCIATION_COPY_NONATOMIC); NSValue *value = [NSValue valueWithRange:selected_range]; objc_setAssociatedObject(view, kSolunaSelectedRangeKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } else { objc_setAssociatedObject(view, kSolunaMarkedTextKey, nil, OBJC_ASSOCIATION_ASSIGN); objc_setAssociatedObject(view, kSolunaSelectedRangeKey, nil, OBJC_ASSOCIATION_ASSIGN); } } static NSString * soluna_current_marked_text(NSView *view) { return objc_getAssociatedObject(view, kSolunaMarkedTextKey); } static bool soluna_view_has_marked_text(NSView *view) { NSString *text = soluna_current_marked_text(view); return text != nil && text.length > 0; } static NSRange soluna_current_selected_range(NSView *view) { NSValue *value = objc_getAssociatedObject(view, kSolunaSelectedRangeKey); if (value == nil) { return NSMakeRange(NSNotFound, 0); } return [value rangeValue]; } static void soluna_set_event_consumed(NSView *view, bool consumed) { if (consumed) { objc_setAssociatedObject(view, kSolunaConsumedFlagKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } else { objc_setAssociatedObject(view, kSolunaConsumedFlagKey, nil, OBJC_ASSOCIATION_ASSIGN); } } static bool soluna_event_consumed(NSView *view) { NSNumber *flag = objc_getAssociatedObject(view, kSolunaConsumedFlagKey); return flag && [flag boolValue]; } static void soluna_update_ime_label(NSView *view, id markedText, NSRange selectedRange) { NSString *plain = soluna_plain_string(markedText); if (!g_soluna_ime_rect.valid || plain.length == 0) { soluna_macos_hide_ime_label(); return; } SolunaIMETextView *imeView = soluna_macos_ensure_ime_label(view); if (!imeView) { return; } soluna_macos_position_ime_input_view(view); NSMutableAttributedString *attr = nil; if ([markedText isKindOfClass:[NSAttributedString class]]) { attr = [[(NSAttributedString *)markedText mutableCopy] autorelease]; } else { attr = [[[NSMutableAttributedString alloc] initWithString:plain] autorelease]; } if (attr.length > 0) { NSRange full = NSMakeRange(0, attr.length); NSFont *font = soluna_macos_current_ime_font(); if (font) { [attr addAttribute:NSFontAttributeName value:font range:full]; } [attr addAttribute:NSForegroundColorAttributeName value:soluna_macos_current_text_color() range:full]; if ([attr attribute:NSUnderlineStyleAttributeName atIndex:0 effectiveRange:NULL] == nil) { [attr addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:full]; } } [[imeView textStorage] setAttributedString:attr]; if (selectedRange.location != NSNotFound && NSMaxRange(selectedRange) <= attr.length) { [imeView setSelectedRange:selectedRange]; } else { [imeView setSelectedRange:NSMakeRange(attr.length, 0)]; } [imeView setHidden:NO]; } static NSRect soluna_current_caret_screen_rect(NSView *view) { NSRect caret = soluna_current_caret_local_rect(view); caret = [view convertRect:caret toView:nil]; if (view.window) { caret = [view.window convertRectToScreen:caret]; } return caret; } @interface _sapp_macos_view (SolunaIME) - (void)soluna_keyDown:(NSEvent *)event; @end @implementation _sapp_macos_view (SolunaIME) - (void)soluna_keyDown:(NSEvent *)event { bool wasComposing = g_soluna_macos_composition || soluna_view_has_marked_text(self); if (g_soluna_ime_rect.valid && wasComposing) { soluna_macos_position_ime_input_view(self); } soluna_set_event_consumed(self, false); BOOL handled = [[self inputContext] handleEvent:event]; bool consumed = soluna_event_consumed(self); bool hasMarked = soluna_view_has_marked_text(self); if (handled && (consumed || hasMarked)) { g_soluna_macos_composition = true; return; } if (g_soluna_macos_composition && hasMarked) { return; } g_soluna_macos_composition = false; [self soluna_keyDown:event]; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { (void)replacementRange; NSString *plain = soluna_plain_string(string); bool commit = (plain.length > 0) && g_soluna_macos_composition; soluna_store_marked_text(self, nil, NSMakeRange(NSNotFound, 0)); if (commit) { soluna_emit_nsstring(plain); } soluna_set_event_consumed(self, commit); g_soluna_macos_composition = false; soluna_macos_hide_ime_label(); } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { (void)replacementRange; NSString *plain = soluna_plain_string(string); soluna_store_marked_text(self, plain, selectedRange); g_soluna_macos_composition = true; soluna_update_ime_label(self, string, selectedRange); soluna_set_event_consumed(self, true); } - (void)unmarkText { soluna_store_marked_text(self, nil, NSMakeRange(NSNotFound, 0)); g_soluna_macos_composition = false; soluna_macos_hide_ime_label(); } - (NSRange)selectedRange { NSRange range = soluna_current_selected_range(self); if (range.location == NSNotFound) { return NSMakeRange(0, 0); } return range; } - (NSRange)markedRange { NSString *text = soluna_current_marked_text(self); if (text != nil && text.length > 0) { return NSMakeRange(0, text.length); } return NSMakeRange(NSNotFound, 0); } - (BOOL)hasMarkedText { return soluna_view_has_marked_text(self); } - (NSArray *)validAttributesForMarkedText { return @[]; } - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { NSString *text = soluna_current_marked_text(self); if (text == nil || range.location == NSNotFound) { return nil; } NSUInteger end = range.location + range.length; if (end > text.length) { return nil; } NSString *substr = [text substringWithRange:range]; if (actualRange) { *actualRange = range; } return [[[NSAttributedString alloc] initWithString:substr] autorelease]; } - (NSUInteger)characterIndexForPoint:(NSPoint)point { (void)point; return 0; } - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { if (actualRange) { *actualRange = range; } return soluna_current_caret_screen_rect(self); } - (void)doCommandBySelector:(SEL)selector { id nextResponder = [self nextResponder]; if ([nextResponder respondsToSelector:@selector(doCommandBySelector:)]) { [nextResponder doCommandBySelector:selector]; } } - (BOOL)acceptsFirstResponder { return YES; } @end void soluna_macos_install_ime(void) { static bool installed = false; if (installed) { return; } Class viewCls = NSClassFromString(@"_sapp_macos_view"); if (!viewCls) { return; } Method original = class_getInstanceMethod(viewCls, @selector(keyDown:)); Method replacement = class_getInstanceMethod(viewCls, @selector(soluna_keyDown:)); if (original && replacement) { method_exchangeImplementations(original, replacement); } installed = true; } void soluna_macos_apply_ime_rect(void) { if (!g_soluna_ime_rect.valid) { soluna_macos_hide_ime_label(); return; } if (g_soluna_ime_label) { NSView *view = [g_soluna_ime_label superview]; if (view) { NSString *text = soluna_current_marked_text(view); if (text && text.length > 0) { soluna_update_ime_label(view, text, soluna_current_selected_range(view)); } else { soluna_macos_position_ime_input_view(view); } } } } bool soluna_macos_is_composition_active(void) { return g_soluna_macos_composition; } ================================================ FILE: src/platform/wasm/soluna_ime.js ================================================ mergeInto(LibraryManager.library, { soluna_wasm_setup_ime__deps: ['$withStackSave', '$lengthBytesUTF8', '$stringToUTF8', '$stackAlloc'], soluna_wasm_setup_ime__sig: 'v', soluna_wasm_setup_ime: function () { if (Module.solunaIme) { return; } if (typeof document === 'undefined' || !document.body) { return; } var globalScope = (typeof window !== 'undefined') ? window : self; var ta = document.createElement('textarea'); ta.setAttribute('autocapitalize', 'off'); ta.setAttribute('autocomplete', 'off'); ta.setAttribute('autocorrect', 'off'); ta.setAttribute('spellcheck', 'false'); ta.setAttribute('tabindex', '-1'); ta.style.position = 'absolute'; ta.style.opacity = '0'; ta.style.pointerEvents = 'none'; ta.style.zIndex = '2147483647'; ta.style.resize = 'none'; ta.style.overflow = 'hidden'; ta.style.border = '0'; ta.style.margin = '0'; ta.style.padding = '0'; ta.style.background = 'transparent'; ta.style.color = '#000'; ta.style.whiteSpace = 'pre'; ta.style.width = '1px'; ta.style.height = '1px'; ta.style.left = '-10000px'; ta.style.top = '-10000px'; ta.style.display = 'none'; document.body.appendChild(ta); var label = document.createElement('div'); label.setAttribute('aria-hidden', 'true'); label.style.position = 'absolute'; label.style.pointerEvents = 'none'; label.style.zIndex = '2147483647'; label.style.whiteSpace = 'pre'; label.style.margin = '0'; label.style.padding = '0'; label.style.border = '0'; label.style.background = 'transparent'; label.style.display = 'none'; label.style.left = '-10000px'; label.style.top = '-10000px'; label.style.font = '16px sans-serif'; label.style.color = '#000'; document.body.appendChild(label); var callSetComposing = function (flag) { if (Module._soluna_wasm_set_composing) { Module._soluna_wasm_set_composing(flag | 0); } }; Module.solunaSetComposing = callSetComposing; var makeLatestTaskScheduler = function (run) { var token = 0; var schedule = function () { var current = ++token; setTimeout(function () { if (current !== token) { return; } run(); }, 0); }; schedule.cancel = function () { ++token; }; return schedule; }; var state = { node: ta, preedit: label, preeditText: '', active: false, composing: false, expectNextInput: false, suppressNextInput: false, mods: 0, rect: { x: 0, y: 0, w: 1, h: 1 }, customFont: null, customFontSize: 0, customTextColor: null, metricCanvas: null, metricContext: null, }; state.focusNode = function () { var el = state.node; if (!el) { return; } try { el.focus({ preventScroll: true }); } catch (err) { el.focus(); } }; state.queueFocus = makeLatestTaskScheduler(function () { var el = state.node; if (!el || !state.active) { return; } el.style.display = 'block'; state.focusNode(); }); state.queueBlur = makeLatestTaskScheduler(function () { var el = state.node; if (!el) { return; } el.blur(); }); state.resolveCanvas = function () { if (Module.canvas) { return Module.canvas; } if (typeof document === 'undefined') { return null; } var selector = Module.solunaCanvasSelector || '#canvas'; var canvas = null; try { canvas = document.querySelector(selector); } catch (err) { canvas = null; } if (!canvas) { canvas = document.querySelector('canvas'); } return canvas || null; }; state.updateModsFromEvent = function (ev) { var mods = 0; if (ev && ev.shiftKey) { mods |= 1; } if (ev && ev.ctrlKey) { mods |= 2; } if (ev && ev.altKey) { mods |= 4; } if (ev && ev.metaKey) { mods |= 8; } state.mods = mods; }; state.applyFontOverride = function () { var textEl = state.node; var labelEl = state.preedit; if (!textEl) { return; } if (state.customFont && state.customFont.length > 0) { textEl.style.fontFamily = state.customFont; if (labelEl) { labelEl.style.fontFamily = state.customFont; } } else { textEl.style.fontFamily = ''; } if (state.customFontSize > 0) { var sizePx = state.customFontSize + 'px'; textEl.style.fontSize = sizePx; if (labelEl) { labelEl.style.fontSize = sizePx; } } else { textEl.style.fontSize = ''; } }; state.applyColorOverride = function () { var textEl = state.node; var labelEl = state.preedit; if (!textEl) { return; } if (state.customTextColor && state.customTextColor.length > 0) { textEl.style.color = state.customTextColor; if (labelEl) { labelEl.style.color = state.customTextColor; } } else { textEl.style.color = ''; if (labelEl) { labelEl.style.color = ''; } } }; state.refreshColorFromContext = function () { var canvas = state.resolveCanvas ? state.resolveCanvas() : null; if (!state.customTextColor && canvas) { state.syncStylesFromCanvas(canvas, state.rect ? state.rect.h : 0); return; } state.applyColorOverride(); }; state.commitText = function (text) { if (!text || text.length === 0) { return; } var mods = state.mods; withStackSave(function () { var len = lengthBytesUTF8(text) + 1; var ptr = stackAlloc(len); stringToUTF8(text, ptr, len); if (Module._soluna_wasm_ime_commit) { Module._soluna_wasm_ime_commit(ptr, mods); } }); }; state.syncStylesFromCanvas = function (canvas, height) { var textEl = state.node; var labelEl = state.preedit; if (canvas && typeof window !== 'undefined' && window.getComputedStyle) { var computed = null; try { computed = window.getComputedStyle(canvas); } catch (err) { computed = null; } if (computed) { if (textEl && computed.color) { textEl.style.color = computed.color; } if (labelEl && computed.color) { labelEl.style.color = computed.color; } if (computed.font && computed.font.length > 0) { if (labelEl) { labelEl.style.font = computed.font; } } else { if (computed.fontSize) { if (labelEl) { labelEl.style.fontSize = computed.fontSize; } } if (computed.fontFamily) { if (labelEl) { labelEl.style.fontFamily = computed.fontFamily; } } if (computed.fontWeight) { if (labelEl) { labelEl.style.fontWeight = computed.fontWeight; } } } } } if (labelEl && Number.isFinite(height) && height > 0) { labelEl.style.lineHeight = height + 'px'; } state.applyFontOverride(); if (state.customTextColor && state.customTextColor.length > 0) { state.applyColorOverride(); } }; state.hidePreedit = function () { state.preeditText = ''; var labelEl = state.preedit; if (labelEl) { labelEl.textContent = ''; labelEl.style.display = 'none'; } }; state.parseCssPixel = function (value) { if (!value || typeof value !== 'string') { return 0; } var n = parseFloat(value); return Number.isFinite(n) ? n : 0; }; state.resolvePreeditAscent = function (labelEl, caretHeight) { var lineHeight = caretHeight > 0 ? caretHeight : 16; var fontSize = lineHeight; var fontSpec = ''; if (typeof window !== 'undefined' && window.getComputedStyle) { var computed = null; try { computed = window.getComputedStyle(labelEl); } catch (err) { computed = null; } if (computed) { var parsedLineHeight = state.parseCssPixel(computed.lineHeight); if (parsedLineHeight > 0) { lineHeight = parsedLineHeight; } var parsedFontSize = state.parseCssPixel(computed.fontSize); if (parsedFontSize > 0) { fontSize = parsedFontSize; } if (computed.font && computed.font.length > 0) { fontSpec = computed.font; } } } if (!fontSpec || fontSpec.length === 0) { fontSpec = labelEl.style.font || (fontSize + 'px sans-serif'); } var ascent = 0; var descent = 0; if (typeof document !== 'undefined') { if (!state.metricCanvas) { state.metricCanvas = document.createElement('canvas'); } if (!state.metricContext && state.metricCanvas) { state.metricContext = state.metricCanvas.getContext('2d'); } } if (state.metricContext) { try { state.metricContext.font = fontSpec; var metrics = state.metricContext.measureText('Mg'); if (metrics) { if (Number.isFinite(metrics.actualBoundingBoxAscent) && metrics.actualBoundingBoxAscent > 0) { ascent = metrics.actualBoundingBoxAscent; } if (Number.isFinite(metrics.actualBoundingBoxDescent) && metrics.actualBoundingBoxDescent > 0) { descent = metrics.actualBoundingBoxDescent; } } } catch (err) { ascent = 0; descent = 0; } } if (ascent <= 0) { ascent = fontSize * 0.8; } if (descent <= 0) { descent = Math.max(fontSize - ascent, fontSize * 0.2); } var contentHeight = ascent + descent; var leading = lineHeight - contentHeight; if (!Number.isFinite(leading) || leading < 0) { leading = 0; } return ascent + leading * 0.5; }; state.positionPreedit = function () { var labelEl = state.preedit; if (!labelEl || labelEl.style.display === 'none') { return; } var canvas = state.resolveCanvas(); if (!canvas) { return; } var rect = canvas.getBoundingClientRect(); var scrollX = (typeof window !== 'undefined' && typeof window.scrollX === 'number') ? window.scrollX : 0; var scrollY = (typeof window !== 'undefined' && typeof window.scrollY === 'number') ? window.scrollY : 0; var canvasLeft = rect.left + scrollX; var canvasTop = rect.top + scrollY; var caretX = canvasLeft + state.rect.x; var caretY = canvasTop + state.rect.y; var caretWidth = state.rect.w; var caretHeight = state.rect.h; if (!Number.isFinite(caretWidth) || caretWidth <= 0) { caretWidth = 1; } if (!Number.isFinite(caretHeight) || caretHeight <= 0) { caretHeight = 16; } var labelWidth = labelEl.offsetWidth; var labelHeight = labelEl.offsetHeight; if (labelWidth <= 0) { labelWidth = caretWidth; } if (labelHeight <= 0) { labelHeight = caretHeight; } var canvasRight = canvasLeft + rect.width; var canvasBottom = canvasTop + rect.height; var left = caretX; if (left + labelWidth > canvasRight) { left = canvasRight - labelWidth; } if (left < canvasLeft) { left = canvasLeft; } var baseline = caretY + caretHeight; var ascent = state.resolvePreeditAscent(labelEl, caretHeight); if (!Number.isFinite(ascent) || ascent <= 0) { ascent = labelHeight; } var top = baseline - ascent; if (top < canvasTop) { top = caretY + caretHeight; if (top + labelHeight > canvasBottom) { top = Math.max(canvasTop, Math.min(canvasBottom - labelHeight, baseline - ascent)); } } labelEl.style.left = Math.round(left) + 'px'; labelEl.style.top = Math.round(top) + 'px'; }; state.setPreeditText = function (text) { var labelEl = state.preedit; if (!labelEl) { return; } state.preeditText = text || ''; if (!state.preeditText) { state.hidePreedit(); return; } labelEl.textContent = state.preeditText; labelEl.style.display = 'inline-block'; state.positionPreedit(); }; ta.addEventListener('compositionstart', function (ev) { state.composing = true; state.expectNextInput = false; state.suppressNextInput = false; state.updateModsFromEvent(ev); state.setPreeditText(''); if (Module.solunaSetComposing) { Module.solunaSetComposing(1); } }); ta.addEventListener('compositionupdate', function (ev) { state.updateModsFromEvent(ev); var text = (ev && typeof ev.data === 'string') ? ev.data : ''; state.setPreeditText(text); }); ta.addEventListener('compositionend', function (ev) { state.composing = false; state.updateModsFromEvent(ev); var text = (ev && ev.data) ? ev.data : ''; if (text.length > 0) { state.commitText(text); state.suppressNextInput = true; } else { state.expectNextInput = true; if (Module._soluna_wasm_block_next_keypair) { Module._soluna_wasm_block_next_keypair(); } } state.node.value = ''; state.hidePreedit(); if (Module.solunaSetComposing) { Module.solunaSetComposing(0); } }); ta.addEventListener('input', function (ev) { if (!state.active) { return; } if (state.composing) { return; } if (state.suppressNextInput) { state.suppressNextInput = false; state.node.value = ''; return; } if (!state.expectNextInput) { return; } var text = (ev && typeof ev.data === 'string') ? ev.data : state.node.value; if (text && text.length > 0) { state.commitText(text); } state.expectNextInput = false; state.node.value = ''; }); var updateMods = function (ev) { state.updateModsFromEvent(ev); }; var reposition = function () { state.positionPreedit(); }; var ensureFocusOnTouch = function () { if (!state.active) { return; } var el = state.node; if (!el) { return; } el.style.display = 'block'; state.focusNode(); }; var clearFocusOnTouchEnd = function () { if (state.active) { return; } var el = state.node; if (!el) { return; } el.blur(); el.style.display = 'none'; }; globalScope.addEventListener('keydown', updateMods, true); globalScope.addEventListener('keyup', updateMods, true); globalScope.addEventListener('focus', function () { state.queueBlur.cancel(); if (state.active) { state.queueFocus(); } }, true); globalScope.addEventListener('blur', function () { state.queueBlur.cancel(); state.queueFocus.cancel(); state.mods = 0; state.composing = false; state.expectNextInput = false; state.suppressNextInput = false; state.node.value = ''; state.hidePreedit(); if (Module.solunaSetComposing) { Module.solunaSetComposing(0); } }); globalScope.addEventListener('resize', reposition); globalScope.addEventListener('touchstart', ensureFocusOnTouch, true); globalScope.addEventListener('touchend', clearFocusOnTouchEnd, true); globalScope.addEventListener('touchcancel', clearFocusOnTouchEnd, true); if (typeof document !== 'undefined') { document.addEventListener('scroll', reposition, true); } Module.solunaIme = state; }, soluna_wasm_dom_show__sig: 'vffff', soluna_wasm_dom_show: function (x, y, w, h) { var state = Module.solunaIme; if (!state || typeof document === 'undefined') { return; } var canvas = state.resolveCanvas(); if (!canvas) { return; } var rect = canvas.getBoundingClientRect(); var scrollX = (typeof window !== 'undefined' && typeof window.scrollX === 'number') ? window.scrollX : 0; var scrollY = (typeof window !== 'undefined' && typeof window.scrollY === 'number') ? window.scrollY : 0; var canvasLeft = rect.left + scrollX; var canvasTop = rect.top + scrollY; var left = canvasLeft + x; var top = canvasTop + y; var width = Math.max(1, Number.isFinite(w) ? w : 1); var height = Math.max(1, (Number.isFinite(h) && h > 0) ? h : 16); state.rect.x = x; state.rect.y = y; state.rect.w = width; state.rect.h = height; var el = state.node; el.style.display = 'block'; el.style.left = left + 'px'; el.style.top = top + 'px'; el.style.width = width + 'px'; el.style.height = height + 'px'; el.style.lineHeight = height + 'px'; if (!state.active) { el.value = ''; } state.active = true; state.syncStylesFromCanvas(canvas, height); state.positionPreedit(); state.queueFocus(); }, soluna_wasm_dom_hide__sig: 'v', soluna_wasm_dom_hide: function () { var state = Module.solunaIme; if (!state || typeof document === 'undefined') { return; } if (!state.active) { return; } state.active = false; state.queueFocus.cancel(); state.queueBlur.cancel(); state.composing = false; state.expectNextInput = false; state.suppressNextInput = false; var el = state.node; el.value = ''; el.style.display = 'none'; state.hidePreedit(); state.queueBlur(); if (Module.solunaSetComposing) { Module.solunaSetComposing(0); } }, soluna_wasm_dom_set_font__deps: ['$UTF8ToString'], soluna_wasm_dom_set_font__sig: 'vif', soluna_wasm_dom_set_font: function (namePtr, size) { var state = Module.solunaIme; if (!state) { return; } var resolvedName = null; if (namePtr) { try { resolvedName = UTF8ToString(namePtr); } catch (err) { resolvedName = ''; } } if (resolvedName && resolvedName.length === 0) { resolvedName = null; } var numericSize = 0; if (Number.isFinite(size) && size > 0) { numericSize = size; } var hasCustomFont = !!(resolvedName && resolvedName.length > 0); state.customFont = hasCustomFont ? resolvedName : null; state.customFontSize = numericSize; if (!hasCustomFont && numericSize === 0) { var canvas = state.resolveCanvas ? state.resolveCanvas() : null; if (canvas) { state.syncStylesFromCanvas(canvas, state.rect ? state.rect.h : 0); } else { state.applyFontOverride(); } } else { state.applyFontOverride(); } state.positionPreedit(); }, soluna_wasm_dom_set_color__sig: 'vi', soluna_wasm_dom_set_color: function (color) { var state = Module.solunaIme; if (!state) { return; } var value = (color >>> 0); if (value !== 0) { var a = ((value >>> 24) & 0xff) / 255; var r = (value >>> 16) & 0xff; var g = (value >>> 8) & 0xff; var b = value & 0xff; state.customTextColor = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } else { state.customTextColor = null; } state.refreshColorFromContext(); state.positionPreedit(); } }); ================================================ FILE: src/platform/wasm/soluna_openurl.js ================================================ mergeInto(LibraryManager.library, { soluna_wasm_open_url__deps: ['$UTF8ToString'], soluna_wasm_open_url__sig: 'vi', soluna_wasm_open_url: function (urlPtr) { if (!urlPtr) { return; } var href = UTF8ToString(urlPtr); if (!href) { return; } if (typeof window !== 'undefined' && typeof window.open === 'function') { window.open(href, '_blank'); return; } if (typeof self !== 'undefined' && typeof self.open === 'function') { self.open(href, '_blank'); } } }); ================================================ FILE: src/platform/wasm/soluna_wasm_ime.h ================================================ #ifndef SOLUNA_WASM_IME_H #define SOLUNA_WASM_IME_H #if defined(__EMSCRIPTEN__) #include #include #if defined(__EMSCRIPTEN_PTHREADS__) #include #endif #include #include #include #include #include #include #include #include "ime_state.h" #include "ime_char_filter.h" void soluna_emit_char(uint32_t codepoint, uint32_t modifiers, bool repeat); extern void soluna_wasm_setup_ime(void); extern void soluna_wasm_dom_show(float x, float y, float w, float h); extern void soluna_wasm_dom_hide(void); extern void soluna_wasm_dom_set_font(const char *name, float size); extern void soluna_wasm_dom_set_color(uint32_t color); static const int SOLUNA_WASM_CHAR_QUEUE_CAP = 32; static uint32_t g_soluna_wasm_expected_chars[32]; static int g_soluna_wasm_expected_count = 0; static uint32_t g_soluna_wasm_ignore_chars[32]; static int g_soluna_wasm_ignore_count = 0; static bool g_soluna_wasm_composing = false; static bool g_soluna_wasm_locale_ready = false; static int g_soluna_wasm_block_keys = 0; static inline struct soluna_ime_char_filter_state soluna_wasm_char_filter_state(void) { return (struct soluna_ime_char_filter_state) { .expected_chars = g_soluna_wasm_expected_chars, .expected_count = &g_soluna_wasm_expected_count, .ignore_chars = g_soluna_wasm_ignore_chars, .ignore_count = &g_soluna_wasm_ignore_count, .capacity = SOLUNA_WASM_CHAR_QUEUE_CAP, }; } static void soluna_wasm_call_setup(void) { #if defined(__EMSCRIPTEN_PTHREADS__) if (!emscripten_is_main_browser_thread()) { emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_V, soluna_wasm_setup_ime); return; } #endif soluna_wasm_setup_ime(); } static void soluna_wasm_call_show(float x, float y, float w, float h) { #if defined(__EMSCRIPTEN_PTHREADS__) if (!emscripten_is_main_browser_thread()) { emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VFFFF, soluna_wasm_dom_show, x, y, w, h); return; } #endif soluna_wasm_dom_show(x, y, w, h); } static void soluna_wasm_call_hide(void) { #if defined(__EMSCRIPTEN_PTHREADS__) if (!emscripten_is_main_browser_thread()) { emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_V, soluna_wasm_dom_hide); return; } #endif soluna_wasm_dom_hide(); } static void soluna_wasm_call_set_font(const char *name, float size) { #if defined(__EMSCRIPTEN_PTHREADS__) if (!emscripten_is_main_browser_thread()) { emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VIF, soluna_wasm_dom_set_font, (intptr_t)name, size); return; } #endif soluna_wasm_dom_set_font(name, size); } static void soluna_wasm_call_set_color(uint32_t color) { #if defined(__EMSCRIPTEN_PTHREADS__) if (!emscripten_is_main_browser_thread()) { emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_VI, soluna_wasm_dom_set_color, color); return; } #endif soluna_wasm_dom_set_color(color); } static void soluna_wasm_reset_queues(void) { soluna_ime_char_filter_reset(soluna_wasm_char_filter_state()); } void soluna_wasm_set_font(const char *name, float size) { soluna_wasm_call_setup(); soluna_wasm_call_set_font(name, size); } static void soluna_wasm_ensure_locale(void) { if (g_soluna_wasm_locale_ready) { return; } if (!setlocale(LC_CTYPE, "C.UTF-8")) { if (!setlocale(LC_CTYPE, "en_US.UTF-8")) { setlocale(LC_CTYPE, "C"); } } g_soluna_wasm_locale_ready = true; } static void soluna_wasm_emit_utf8(const char *text, uint32_t mods) { if (!text || text[0] == '\0') { return; } soluna_wasm_ensure_locale(); mbstate_t state; memset(&state, 0, sizeof(state)); const char *ptr = text; while (*ptr) { char32_t ch = 0; size_t consumed = mbrtoc32(&ch, ptr, MB_CUR_MAX, &state); if (consumed == (size_t)-1 || consumed == (size_t)-2) { memset(&state, 0, sizeof(state)); ++ptr; continue; } if (consumed == 0) { consumed = 1; } soluna_ime_char_filter_push_expected(soluna_wasm_char_filter_state(), (uint32_t)ch); soluna_emit_char((uint32_t)ch, mods, false); ptr += consumed; } } static inline bool soluna_wasm_is_composing(void) { return g_soluna_wasm_composing; } void soluna_wasm_hide(void) { soluna_wasm_reset_queues(); soluna_wasm_call_hide(); g_soluna_wasm_composing = false; } void soluna_wasm_apply_rect(void) { if (!g_soluna_ime_rect.valid) { soluna_wasm_hide(); return; } soluna_wasm_call_setup(); soluna_wasm_call_set_color(g_soluna_ime_rect.text_color); soluna_wasm_call_show(g_soluna_ime_rect.x, g_soluna_ime_rect.y, g_soluna_ime_rect.w, g_soluna_ime_rect.h); } static inline bool soluna_wasm_should_block_key_event(const sapp_event *ev) { if (!ev) { return false; } bool is_key_event = ev->type == SAPP_EVENTTYPE_KEY_DOWN || ev->type == SAPP_EVENTTYPE_KEY_UP; if (g_soluna_wasm_block_keys > 0 && is_key_event) { --g_soluna_wasm_block_keys; return true; } if (!soluna_wasm_is_composing()) { return false; } return is_key_event; } static inline bool soluna_wasm_filter_char_event(const sapp_event *ev) { if (!ev || ev->type != SAPP_EVENTTYPE_CHAR) { return false; } return soluna_ime_char_filter_should_skip(soluna_wasm_char_filter_state(), ev->char_code); } static inline void soluna_wasm_handle_event(const sapp_event *ev) { if (!ev) { return; } switch (ev->type) { case SAPP_EVENTTYPE_UNFOCUSED: soluna_wasm_hide(); break; case SAPP_EVENTTYPE_FOCUSED: case SAPP_EVENTTYPE_RESIZED: if (g_soluna_ime_rect.valid) { soluna_wasm_apply_rect(); } break; default: break; } } EMSCRIPTEN_KEEPALIVE void soluna_wasm_ime_commit(const char *text, int modifiers) { soluna_wasm_emit_utf8(text, (uint32_t)modifiers); } EMSCRIPTEN_KEEPALIVE void soluna_wasm_set_composing(int active) { g_soluna_wasm_composing = (active != 0); } EMSCRIPTEN_KEEPALIVE void soluna_wasm_block_next_keypair(void) { g_soluna_wasm_block_keys = 2; } #endif /* __EMSCRIPTEN__ */ #endif /* SOLUNA_WASM_IME_H */ ================================================ FILE: src/platform/windows/soluna_windows_ime.c ================================================ #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include "sokol/sokol_app.h" #include "ime_state.h" #include "soluna_windows_ime.h" static WNDPROC g_soluna_prev_wndproc = NULL; static BOOL g_soluna_wndproc_installed = FALSE; static LOGFONTW g_soluna_ime_font; static BOOL g_soluna_ime_font_valid = FALSE; static BOOL g_soluna_composition = FALSE; static void soluna_win32_set_candidate_position(HIMC imc, LONG caret_x, LONG caret_y, LONG caret_w, LONG caret_h) { RECT exclude_rect; exclude_rect.left = caret_x; exclude_rect.top = caret_y; exclude_rect.right = caret_x + caret_w; exclude_rect.bottom = caret_y + caret_h; MapWindowPoints((HWND)sapp_win32_get_hwnd(), NULL, (LPPOINT)&exclude_rect, 2); CANDIDATEFORM cand; memset(&cand, 0, sizeof(cand)); cand.dwIndex = 0; cand.dwStyle = CFS_EXCLUDE; cand.rcArea = exclude_rect; cand.ptCurrentPos.x = exclude_rect.left; cand.ptCurrentPos.y = exclude_rect.bottom; ImmSetCandidateWindow(imc, &cand); } void soluna_win32_apply_ime_rect(void) { HWND hwnd = (HWND)sapp_win32_get_hwnd(); if (!hwnd) { return; } HIMC imc = ImmGetContext(hwnd); if (!imc) { fprintf(stderr, "ImmGetContext failed\n"); return; } if (g_soluna_ime_rect.valid) { float scale = sapp_dpi_scale(); if (scale <= 0.0f) { scale = 1.0f; } float rect_top = g_soluna_ime_rect.y; if (rect_top < 0.0f) { rect_top = 0.0f; } float rect_height = (g_soluna_ime_rect.h > 0.0f ? g_soluna_ime_rect.h : 1.0f); float win_height = (float)sapp_height(); float rect_bottom = rect_top + rect_height; if (win_height > 0.0f && rect_bottom > win_height) { rect_bottom = win_height; } float actual_height = rect_bottom - rect_top; if (actual_height <= 0.0f) { actual_height = 1.0f; } LONG caret_x = (LONG)(g_soluna_ime_rect.x * scale + 0.5f); LONG caret_y = (LONG)(rect_top * scale + 0.5f); LONG caret_w = (LONG)((g_soluna_ime_rect.w > 0.0f ? g_soluna_ime_rect.w : 1.0f) * scale + 0.5f); LONG caret_h = (LONG)(actual_height * scale + 0.5f); COMPOSITIONFORM cf; memset(&cf, 0, sizeof(cf)); cf.dwStyle = CFS_POINT; cf.ptCurrentPos.x = caret_x; cf.ptCurrentPos.y = caret_y; ImmSetCompositionWindow(imc, &cf); soluna_win32_set_candidate_position(imc, caret_x, caret_y, caret_w, caret_h); if (g_soluna_ime_font_valid) { LOGFONTW lf = g_soluna_ime_font; if (lf.lfHeight == 0) { lf.lfHeight = -(LONG)caret_h; } ImmSetCompositionFontW(imc, &lf); } } ImmReleaseContext(hwnd, imc); } static LRESULT CALLBACK soluna_win32_wndproc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_IME_COMPOSITION: case WM_IME_STARTCOMPOSITION: g_soluna_composition = TRUE; if (g_soluna_ime_rect.valid) { soluna_win32_apply_ime_rect(); } break; case WM_IME_ENDCOMPOSITION: g_soluna_composition = FALSE; break; case WM_DESTROY: g_soluna_composition = FALSE; if (g_soluna_prev_wndproc) { SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)g_soluna_prev_wndproc); g_soluna_prev_wndproc = NULL; g_soluna_wndproc_installed = FALSE; } break; case WM_KEYDOWN: case WM_KEYUP: if (g_soluna_composition) { return TRUE; } break; default: break; } if (g_soluna_prev_wndproc) { return CallWindowProc(g_soluna_prev_wndproc, hwnd, msg, wParam, lParam); } return DefWindowProc(hwnd, msg, wParam, lParam); } void soluna_win32_install_wndproc(void) { if (g_soluna_wndproc_installed) { return; } HWND hwnd = (HWND)sapp_win32_get_hwnd(); if (!hwnd) { return; } WNDPROC prev = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)soluna_win32_wndproc); if (prev) { g_soluna_prev_wndproc = prev; g_soluna_wndproc_installed = TRUE; } } void soluna_win32_set_ime_font(const char *font_name, float height_px) { float scale = sapp_dpi_scale(); if (scale <= 0.0f) { scale = 1.0f; } LOGFONTW lf; memset(&lf, 0, sizeof(lf)); lf.lfCharSet = DEFAULT_CHARSET; lf.lfQuality = CLEARTYPE_QUALITY; if (height_px > 0.0f) { lf.lfHeight = -(LONG)(height_px * scale + 0.5f); } if (font_name && font_name[0]) { int wlen = MultiByteToWideChar(CP_UTF8, 0, font_name, -1, NULL, 0); if (wlen > 0 && wlen <= (int)(sizeof(lf.lfFaceName) / sizeof(wchar_t))) { MultiByteToWideChar(CP_UTF8, 0, font_name, -1, lf.lfFaceName, wlen); } } g_soluna_ime_font = lf; g_soluna_ime_font_valid = TRUE; } void soluna_win32_reset_ime_font(void) { g_soluna_ime_font_valid = FALSE; } ================================================ FILE: src/platform/windows/soluna_windows_ime.h ================================================ #ifndef SOLUNA_WINDOWS_IME_H #define SOLUNA_WINDOWS_IME_H void soluna_win32_install_wndproc(void); void soluna_win32_apply_ime_rect(void); void soluna_win32_set_ime_font(const char *font_name, float height_px); void soluna_win32_reset_ime_font(void); #endif /* SOLUNA_WINDOWS_IME_H */ ================================================ FILE: src/render.c ================================================ #include #include #include #include #include "sokol/sokol_gfx.h" #include "sokol/sokol_glue.h" #include "sokol/sokol_app.h" #include "texquad.glsl.h" #include "srbuffer.h" #include "sprite_submit.h" #include "batch.h" #include "spritemgr.h" #define UNIFORM_MAX 4 #define BINDINGNAME_MAX 32 struct buffer { sg_buffer handle; struct sg_buffer_usage usage; }; struct image { sg_image img; int size; }; struct sampler { sg_sampler handle; }; static struct sg_buffer_usage get_buffer_type(lua_State *L, int index) { if (lua_getfield(L, index, "type") != LUA_TSTRING) { luaL_error(L, "Need .type"); } const char * str = lua_tostring(L, -1); struct sg_buffer_usage usage = { 0 }; if (strcmp(str, "vertex") == 0) { usage.vertex_buffer = true; } else if (strcmp(str, "index") == 0) { usage.index_buffer = true; } else if (strcmp(str, "storage") == 0) { usage.storage_buffer = true; } else { luaL_error(L, "Invalid buffer .type = %s", str); } lua_pop(L, 1); return usage; } static void get_buffer_usage(lua_State *L, int index, struct sg_buffer_usage *usage) { if (lua_getfield(L, index, "usage") != LUA_TSTRING) { if (lua_isnil(L, -1)) { lua_pop(L, 1); usage->immutable = true; return; } luaL_error(L, "Invalid .usage"); } const char * str = lua_tostring(L, -1); if (strcmp(str, "stream") == 0) { usage->stream_update = true; } else if (strcmp(str, "dynamic") == 0) { usage->dynamic_update = true; } else if (strcmp(str, "immutable") == 0) { usage->immutable = true; } else { luaL_error(L, "Invalid buffer .usage = %s", str); } lua_pop(L, 1); } static const void * get_buffer_data(lua_State *L, int index, size_t *sz) { int t = lua_getfield(L, index, "data"); if (t == LUA_TNIL) { // no ptr lua_pop(L, 1); if (lua_getfield(L, index, "size") != LUA_TNUMBER) { luaL_error(L, "No .data and .size"); } *sz = luaL_checkinteger(L, -1); lua_pop(L, 1); return NULL; } size_t size = 0; if (lua_getfield(L, index, "size") == LUA_TNUMBER) { size = luaL_checkinteger(L, -1); } lua_pop(L, 1); if (t == LUA_TLIGHTUSERDATA) { if (size == 0) { luaL_error(L, "lightuserdata for .data without .size"); } *sz = size; const void * ptr = lua_touserdata(L, -1); lua_pop(L, 1); return ptr; } else if (t == LUA_TUSERDATA) { size_t rawlen = lua_rawlen(L, -1); if (size > 0 && size != rawlen) luaL_error(L, "size of userdata %d != %d", rawlen, size); const void * ptr = lua_touserdata(L, -1); lua_pop(L, 1); *sz = size; return ptr; } else if (t == LUA_TSTRING) { size_t rawlen; const void * ptr = (const void *)lua_tolstring(L, -1, &rawlen); if (size > 0 && size != rawlen) luaL_error(L, "size of string %d != %d", rawlen, size); lua_pop(L, 1); *sz = rawlen; return ptr; } luaL_error(L, "Invalid .data type = %s", lua_typename(L, t)); *sz = 0; return NULL; } static int lbuffer_update(lua_State *L) { if (lua_gettop(L) == 1) return 0; struct buffer *p = (struct buffer *)luaL_checkudata(L, 1, "SOKOL_BUFFER"); size_t sz; const void *ptr; switch (lua_type(L, 2)) { case LUA_TSTRING: ptr = (const void *)lua_tolstring(L, 2, &sz); break; case LUA_TUSERDATA: ptr = (const void *)lua_touserdata(L, 2); sz = lua_rawlen(L, 2); if (lua_isinteger(L, 3)) { int usersize = lua_tointeger(L, 3); if (usersize > sz) return luaL_error(L, "Invalid size %d > %d", usersize, sz); sz = usersize; } break; case LUA_TLIGHTUSERDATA: ptr = (const void *)lua_touserdata(L, 2); sz = luaL_checkinteger(L, 3); break; default: return luaL_error(L, "Invalid data type %s", lua_typename(L, lua_type(L, 2))); } sg_update_buffer(p->handle, &(sg_range) { ptr, sz }); return 0; } static int lbuffer_ref(lua_State *L) { struct buffer *p = (struct buffer *)lua_touserdata(L, 1); luaL_checktype(L, 2, LUA_TLIGHTUSERDATA); sg_buffer *ref = (sg_buffer *)lua_touserdata(L, 2); *ref = p->handle; return 0; } static int lbuffer_tostring(lua_State *L) { struct buffer *p = (struct buffer *)lua_touserdata(L, 1); const char * name = "Invalid"; if (p->usage.vertex_buffer) { name = "VB"; } else if (p->usage.index_buffer) { name = "IB"; } else if (p->usage.storage_buffer) { name = "SB"; } lua_pushfstring(L, "[%s:%d]", name, p->handle.id); return 1; } static int lbuffer(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct buffer * p = (struct buffer *)lua_newuserdatauv(L, sizeof(*p), 0); p->usage = get_buffer_type(L, 1); get_buffer_usage(L, 1, &p->usage); size_t sz; const void *ptr = get_buffer_data(L, 1, &sz); if (p->usage.immutable && ptr == NULL) { return luaL_error(L, "immutable buffer needs init data"); } const char *label = NULL; if (lua_getfield(L, 1, "label") == LUA_TSTRING) { label = lua_tostring(L, -1); } lua_pop(L, 1); p->handle = sg_make_buffer(&(sg_buffer_desc) { .size = sz, .usage = p->usage, .label = label, .data.ptr = ptr, .data.size = ptr == NULL ? 0 : sz, }); if (luaL_newmetatable(L, "SOKOL_BUFFER")) { luaL_Reg l[] = { { "__index", NULL }, { "__call", lbuffer_ref }, { "__tostring", lbuffer_tostring }, { "update", lbuffer_update }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } // todo : offscreen pass struct pass { sg_pass pass; int swapchain; }; static int read_color_action(lua_State *L, int index, sg_pass_action *action, int idx) { char key[] = { 'c', 'o', 'l', 'o' , 'r' , '0' + idx, '\0' }; int t = lua_getfield(L, index, key); if (t == LUA_TNIL) { lua_pop(L, 1); return 0; } if (idx >= SG_MAX_COLOR_ATTACHMENTS) return luaL_error(L, "Too many color attachments %d >= %d", idx , SG_MAX_COLOR_ATTACHMENTS); if ( t == LUA_TSTRING ) { const char * key = lua_tostring(L, -1); if (strcmp(key, "load") == 0) { action->colors[idx].load_action = SG_LOADACTION_LOAD; } else if (strcmp(key, "dontcare") == 0 ) { action->colors[idx].load_action = SG_LOADACTION_DONTCARE; } else { return luaL_error(L, "Invalid load action (%d) = %s", idx, key); } } else { uint32_t c = luaL_checkinteger(L, -1); if (c <= 0xffffff) { action->colors[idx].clear_value.a = 1.0f; } else { action->colors[idx].clear_value.a = ((c & 0xff000000) >> 24) / 255.0f; } action->colors[idx].clear_value.r = ((c & 0xff0000) >> 16) / 255.0f; action->colors[idx].clear_value.g = ((c & 0x00ff00) >> 8) / 255.0f; action->colors[idx].clear_value.b = ((c & 0x0000ff)) / 255.0f; action->colors[idx].load_action = SG_LOADACTION_CLEAR; } lua_pop(L, 1); return 1; } static int lpass_begin(lua_State *L) { struct pass * p = (struct pass *)luaL_checkudata(L, 1, "SOKOL_PASS"); if (p->swapchain) { p->pass.swapchain = sglue_swapchain(); } sg_begin_pass(&p->pass); return 0; } static int lpass_end(lua_State *L) { sg_end_pass(); return 0; } static void read_attachments(lua_State *L, sg_attachments *attachments) { luaL_checktype(L, -1, LUA_TTABLE); int i; for (i = 0; i < SG_MAX_COLOR_ATTACHMENTS; i++) { char key[] = { 'c', 'o', 'l', 'o' , 'r' , '0' + i, '\0' }; if (lua_getfield(L, -1, key) == LUA_TNIL) { lua_pop(L, 1); break; } luaL_checkudata(L, -1, "SOKOL_VIEW"); lua_call(L, 0, 1); sg_view *color_v = (sg_view *)lua_touserdata(L, -1); if (color_v == NULL) { luaL_error(L, "Invalid %s view", key); } attachments->colors[i] = *color_v; lua_pop(L, 1); } lua_getfield(L, -1, "depth_stencil"); luaL_checkudata(L, -1, "SOKOL_VIEW"); lua_call(L, 0, 1); sg_view *ds_v = (sg_view *)lua_touserdata(L, -1); if (ds_v == NULL) { luaL_error(L, "Invalid depth_stencil view"); } attachments->depth_stencil = *ds_v; lua_pop(L, 1); } static int lpass_new(lua_State *L) { struct pass * p = lua_newuserdatauv(L, sizeof(*p), 0); memset(p, 0, sizeof(*p)); luaL_checktype(L, 1, LUA_TTABLE); if (lua_getfield(L, 1, "swapchain") == LUA_TBOOLEAN && lua_toboolean(L, -1)) { p->swapchain = true; } lua_pop(L, 1); sg_pass_action *action = &p->pass.action; int i = 0; while (read_color_action(L, 1, action, i)) { ++i; } if (lua_getfield(L, 1, "depth") == LUA_TNIL) { action->depth.load_action = SG_LOADACTION_DONTCARE; } else { float depth = luaL_checknumber(L, -1); action->depth.load_action = SG_LOADACTION_CLEAR; action->depth.clear_value = depth; } lua_pop(L, 1); if (lua_getfield(L, 1, "stencil") == LUA_TNIL) { action->depth.load_action = SG_LOADACTION_DONTCARE; } else { int s = luaL_checkinteger(L, -1); if (s < 0 || s > 255) return luaL_error(L, "Invalid stencil %d", s); action->depth.load_action = SG_LOADACTION_CLEAR; action->depth.clear_value = s; } lua_pop(L, 1); if (lua_getfield(L, 1, "attachment") != LUA_TNIL) { if (p->swapchain) { return luaL_error(L, "swapchain not allows with attachment"); } read_attachments(L, &p->pass.attachments); } else if (!p->swapchain) { return luaL_error(L, "missing swapchain"); } lua_pop(L, 1); if (luaL_newmetatable(L, "SOKOL_PASS")) { luaL_Reg l[] = { { "__index", NULL }, { "begin", lpass_begin }, { "finish", lpass_end }, // end is a reserved keyword in lua, use finish instead { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } static int lsubmit(lua_State *L) { sg_commit(); return 0; } static int limage_update(lua_State *L) { struct image *p = (struct image *)luaL_checkudata(L, 1, "SOKOL_IMAGE"); // todo: support subimage void *buffer = lua_touserdata(L, 2); if (buffer == NULL) return luaL_error(L, "Need data"); sg_image_data data = { .mip_levels[0].ptr = buffer, .mip_levels[0].size = p->size, }; sg_update_image(p->img, &data); return 0; } static int get_pixel_format(lua_State *L, const char * type, int *pixel_size) { if (strcmp(type, "RGBA8") == 0) { *pixel_size = 4; return SG_PIXELFORMAT_RGBA8; } else if (strcmp(type, "R8") == 0) { *pixel_size = 1; return SG_PIXELFORMAT_R8; } else if (strcmp(type, "DEPTH") == 0) { *pixel_size = 0; return SG_PIXELFORMAT_DEPTH; } return luaL_error(L, "Invalid pixel format %s", type); } static int limage_ref(lua_State *L) { struct image *p = (struct image *)lua_touserdata(L, 1); luaL_checktype(L, 2, LUA_TLIGHTUSERDATA); sg_image *ref = (sg_image *)lua_touserdata(L, 2); *ref = p->img; return 0; } static int limage(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); sg_image_desc img = { .usage.dynamic_update = true }; if (lua_getfield(L, 1, "width") != LUA_TNUMBER) { return luaL_error(L, "Need .width"); } img.width = luaL_checkinteger(L, -1); lua_pop(L, 1); if (lua_getfield(L, 1, "height") != LUA_TNUMBER) { return luaL_error(L, "Need .height"); } img.height = luaL_checkinteger(L, -1); lua_pop(L, 1); if (lua_getfield(L, 1, "label") == LUA_TSTRING) { img.label = lua_tostring(L, -1); } lua_pop(L, 1); int pixel_size = 4; if (lua_getfield(L, 1, "pixel_format") == LUA_TSTRING) { img.pixel_format = get_pixel_format(L, lua_tostring(L, -1), &pixel_size); } else { img.pixel_format = SG_PIXELFORMAT_RGBA8; pixel_size = 4; } lua_pop(L, 1); if (lua_getfield(L, 1, "color_attachment") == LUA_TBOOLEAN && lua_toboolean(L, -1)) { img.pixel_format = 0; img.usage.color_attachment = 1; img.usage.dynamic_update = 0; } lua_pop(L, 1); if (lua_getfield(L, 1, "depth_stencil_attachment") == LUA_TBOOLEAN && lua_toboolean(L, -1)) { img.pixel_format = 0; img.usage.depth_stencil_attachment = 1; img.usage.dynamic_update = 0; } lua_pop(L, 1); // todo: type, render_target, num_slices, num_mipmaps, pixel_format, etc struct image * p = (struct image *)lua_newuserdatauv(L, sizeof(*p), 0); memset(p, 0, sizeof(*p)); if (luaL_newmetatable(L, "SOKOL_IMAGE")) { luaL_Reg l[] = { { "__index", NULL }, { "__call", limage_ref }, { "update", limage_update }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); p->img = sg_make_image(&img); p->size = img.width * img.height * pixel_size; return 1; } static int lsampler_ref(lua_State *L) { struct sampler *p = (struct sampler *)lua_touserdata(L, 1); luaL_checktype(L, 2, LUA_TLIGHTUSERDATA); sg_sampler *ref = (sg_sampler *)lua_touserdata(L, 2); *ref = p->handle; return 0; } struct enum_string { const char *name; int v; }; static int convert_enum(lua_State *L, struct enum_string *es, const char *name) { while (es->name) { if (strcmp(name, es->name) == 0) { return es->v; } ++es; } return luaL_error(L, "Invalid enum %s", name); } static int read_enum(lua_State *L, const char *key, struct enum_string *es) { int r = 0; switch (lua_getfield(L, 1, key)) { case LUA_TNIL: break; case LUA_TSTRING: r = convert_enum(L, es, lua_tostring(L, -1)); break; default : return luaL_error(L, "Invalid .%s (should be a string, it's %s)", key, lua_typename(L, lua_type(L, -1))); } lua_pop(L, 1); return r; } static int lsampler(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct sampler * s = (struct sampler *)lua_newuserdatauv(L, sizeof(*s), 0); struct sg_sampler_desc desc = { 0 }; if (lua_getfield(L, 1, "label") == LUA_TSTRING) { desc.label = lua_tostring(L, -1); } lua_pop(L, 1); static struct enum_string filter[] = { { "nearest", SG_FILTER_NEAREST }, { "linear", SG_FILTER_LINEAR }, { NULL, 0 }, }; desc.min_filter = read_enum(L, "min_filter", filter); desc.mag_filter = read_enum(L, "mag_filter", filter); desc.mipmap_filter = read_enum(L, "mipmap_filter", filter); static struct enum_string wrap[] = { { "repeat", SG_WRAP_REPEAT }, { "edge", SG_WRAP_CLAMP_TO_EDGE }, { "border", SG_WRAP_CLAMP_TO_BORDER }, { "mirror", SG_WRAP_MIRRORED_REPEAT }, { NULL, 0 }, }; desc.wrap_u = read_enum(L, "wrap_u", wrap); desc.wrap_v = read_enum(L, "wrap_v", wrap); desc.wrap_w = read_enum(L, "wrap_w", wrap); // todo : set min_lod/max_lod/border_color etc. s->handle = sg_make_sampler(&desc); if (luaL_newmetatable(L, "SOKOL_SAMPLER")) { lua_pushcfunction(L, lsampler_ref ), lua_setfield(L, -2, "__call"); } lua_setmetatable(L, -2); return 1; } static int ldraw(lua_State *L) { int base = luaL_checkinteger(L, 1); int n = luaL_checkinteger(L, 2); int inst = luaL_checkinteger(L, 3); sg_draw(base, n, inst); return 0; } static int lsrbuffer_add(lua_State *L) { struct sr_buffer *b = (struct sr_buffer *)luaL_checkudata(L, 1, "SOLUNA_SRBUFFER"); float scale = luaL_checknumber(L, 2); float rot = luaL_checknumber(L, 3); struct draw_primitive tmp; sprite_set_sr(&tmp, scale, rot); int index = srbuffer_add(b, tmp.sr); if (index < 0) return 0; lua_pushinteger(L, index); return 1; } static int lsrbuffer_ptr(lua_State *L) { struct sr_buffer *b = (struct sr_buffer *)luaL_checkudata(L, 1, "SOLUNA_SRBUFFER"); int sz; void * ptr = srbuffer_commit(b, &sz); if (ptr == NULL) return 0; lua_pushlightuserdata(L, ptr); lua_pushinteger(L, sz); return 2; } static int lsrbuffer(lua_State *L) { int n = luaL_checkinteger(L, 1); size_t sz = srbuffer_size(n); struct sr_buffer *b = (struct sr_buffer *)lua_newuserdatauv(L, sz, 0); srbuffer_init(b, n); if (luaL_newmetatable(L, "SOLUNA_SRBUFFER")) { luaL_Reg l[] = { { "__index", NULL }, { "add", lsrbuffer_add }, { "ptr", lsrbuffer_ptr }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } struct inst_object { float x, y; float sr_index; }; struct sprite_object { uint32_t off; uint32_t u; uint32_t v; }; static int lbuffer_size(lua_State *L) { const char * name = luaL_checkstring(L, 1); int n = luaL_optinteger(L, 2, 1); size_t sz = 0; if (strcmp(name, "srbuffer") == 0) { sz = sizeof(struct sr_mat); } else if (strcmp(name, "inst") == 0) { sz = sizeof(struct inst_object); } else if (strcmp(name, "sprite") == 0) { sz = sizeof(struct sprite_object); } else { return luaL_error(L, "Invalid buffer type %s", name); } lua_pushinteger(L, sz * n); return 1; } static int ltmp_buffer(lua_State *L) { size_t sz = luaL_optinteger(L, 1, 128 * 1024); lua_newuserdatauv(L, sz, 0); return 1; } int lbindings_new(lua_State *L); int lview_new(lua_State *L); int luniform_new(lua_State *L); int luaopen_render(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "pass", lpass_new }, { "submit", lsubmit }, { "image", limage }, { "buffer", lbuffer }, { "sampler", lsampler }, { "draw", ldraw }, { "srbuffer", lsrbuffer }, { "buffer_size", lbuffer_size }, { "bindings", lbindings_new }, { "view", lview_new }, { "uniform", luniform_new }, { "tmp_buffer", ltmp_buffer }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } ================================================ FILE: src/render_bindings.c ================================================ #include #include #include #include "render_bindings.h" static inline sg_bindings * get_bindings(lua_State *L) { struct render_bindings *b = luaL_checkudata(L, 1, "SOKOL_BINDINGS"); return &b->bindings; } static int lbindings_set_base(lua_State *L) { struct render_bindings *b = luaL_checkudata(L, 1, "SOKOL_BINDINGS"); int base = luaL_checkinteger(L, 2); b->base = base; return 0; } static int lbindings_set_vb(lua_State *L) { sg_bindings *b = get_bindings(L); int index = luaL_checkinteger(L, 2); if (index < 0 || index >= SG_MAX_VERTEXBUFFER_BINDSLOTS) return luaL_error(L, "Invalid vbuffer slot %d", index); luaL_checkudata(L, 3, "SOKOL_BUFFER"); lua_settop(L, 3); lua_pushlightuserdata(L, &b->vertex_buffers[index]); lua_call(L, 1, 0); return 0; } static int lbindings_set_voff(lua_State *L) { sg_bindings *b = get_bindings(L); int index = luaL_checkinteger(L, 2); if (index < 0 || index >= SG_MAX_VERTEXBUFFER_BINDSLOTS) return luaL_error(L, "Invalid vbuffer slot %d", index); b->vertex_buffer_offsets[index] = luaL_checkinteger(L, 3); return 0; } static int lbindings_set_ib(lua_State *L) { sg_bindings *b = get_bindings(L); luaL_checkudata(L, 2, "SOKOL_BUFFER"); lua_settop(L, 2); lua_pushlightuserdata(L, &b->index_buffer); lua_call(L, 1, 0); return 0; } static int lbindings_set_ioff(lua_State *L) { sg_bindings *b = get_bindings(L); b->index_buffer_offset = luaL_checkinteger(L, 2); return 0; } struct view { sg_view view; int type; }; static int lbindings_set_view(lua_State *L) { sg_bindings *b = get_bindings(L); int index = luaL_checkinteger(L, 2); if (index < 0 || index >= SG_MAX_VIEW_BINDSLOTS) return luaL_error(L, "Invalid view slot %d", index); struct view *v = luaL_checkudata(L, 3, "SOKOL_VIEW"); b->views[index] = v->view; return 0; } static int lbindings_set_sampler(lua_State *L) { sg_bindings *b = get_bindings(L); int index = luaL_checkinteger(L, 2); if (index < 0 || index >= SG_MAX_SAMPLER_BINDSLOTS) return luaL_error(L, "Invalid sampler slot %d", index); luaL_checkudata(L, 3, "SOKOL_SAMPLER"); lua_settop(L, 3); lua_pushlightuserdata(L, &b->samplers[index]); lua_call(L, 1, 0); return 0; } static int lbindings_apply(lua_State *L) { sg_bindings *b = get_bindings(L); sg_apply_bindings(b); return 0; } #define VIEW_TYPE_INVALID 0 #define VIEW_TYPE_TEXTURE 1 #define VIEW_TYPE_STORAGE 2 #define VIEW_TYPE_COLOR_ATTACHMENT 3 #define VIEW_TYPE_DEPTH_STENCIL_ATTACHMENT 4 static inline const char * view_type_string(int type) { switch (type) { case VIEW_TYPE_TEXTURE : return "texture"; case VIEW_TYPE_STORAGE : return "storage"; case VIEW_TYPE_COLOR_ATTACHMENT : return "color_attachment"; case VIEW_TYPE_DEPTH_STENCIL_ATTACHMENT : return "depth_stencil_attachment"; default : return "invalid"; } } static inline void check_view_type(lua_State *L, struct view *v) { if (v->type != VIEW_TYPE_INVALID) luaL_error(L, "Invalid multi type set : %s", view_type_string(v->type)); } static int lview_tostring(lua_State *L) { struct view *v = (struct view *)lua_touserdata(L, 1); const char *s = view_type_string(v->type); lua_pushexternalstring(L, s, strlen(s), NULL, NULL); return 1; } static int lview_release(lua_State *L) { struct view *v = (struct view *)lua_touserdata(L, 1); if (v->type != VIEW_TYPE_INVALID) { v->type = VIEW_TYPE_INVALID; sg_destroy_view(v->view); } return 0; } static int lview_getptr(lua_State *L) { struct view *v = (struct view *)lua_touserdata(L, 1); if (v->type == VIEW_TYPE_INVALID) { return 0; } lua_pushlightuserdata(L, &v->view); return 1; } int lview_new(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); struct sg_view_desc desc; memset(&desc, 0 , sizeof(desc)); struct view *v = NULL; if (lua_getfield(L, 1, "label") == LUA_TSTRING) { v = (struct view *)lua_newuserdatauv(L, sizeof(*v), 1); lua_insert(L, -2); desc.label = lua_tostring(L, -1); lua_setiuservalue(L, 1, 1); } else { lua_pop(L, 1); v = (struct view *)lua_newuserdatauv(L, sizeof(*v), 0); } v->type = VIEW_TYPE_INVALID; if (lua_getfield(L, 1, "texture") == LUA_TUSERDATA) { check_view_type(L, v); v->type = VIEW_TYPE_TEXTURE; // image luaL_checkudata(L, -1, "SOKOL_IMAGE"); lua_pushlightuserdata(L, &desc.texture.image); lua_call(L, 1, 0); } else { lua_pop(L, 1); } if (lua_getfield(L, 1, "storage") == LUA_TUSERDATA) { check_view_type(L, v); v->type = VIEW_TYPE_STORAGE; luaL_checkudata(L, -1, "SOKOL_BUFFER"); lua_pushlightuserdata(L, &desc.storage_buffer.buffer); lua_call(L, 1, 0); } else { lua_pop(L, 1); } if (lua_getfield(L, 1, "color_attachment") == LUA_TUSERDATA) { check_view_type(L, v); v->type = VIEW_TYPE_COLOR_ATTACHMENT; luaL_checkudata(L, -1, "SOKOL_IMAGE"); lua_pushlightuserdata(L, &desc.color_attachment.image); lua_call(L, 1, 0); } else { lua_pop(L, 1); } if (lua_getfield(L, 1, "depth_stencil_attachment") == LUA_TUSERDATA) { check_view_type(L, v); v->type = VIEW_TYPE_DEPTH_STENCIL_ATTACHMENT; luaL_checkudata(L, -1, "SOKOL_IMAGE"); lua_pushlightuserdata(L, &desc.depth_stencil_attachment.image); lua_call(L, 1, 0); } else { lua_pop(L, 1); } if (v->type == VIEW_TYPE_INVALID) return luaL_error(L, "No view type"); v->view = sg_make_view(&desc); if (luaL_newmetatable(L, "SOKOL_VIEW")) { luaL_Reg l[] = { { "__index", NULL }, { "__tostring", lview_tostring }, { "__gc", lview_release }, { "__call", lview_getptr }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } int lbindings_new(lua_State *L) { struct render_bindings *b = (struct render_bindings *)lua_newuserdatauv(L, sizeof(*b), 0); memset(b, 0, sizeof(*b)); if (luaL_newmetatable(L, "SOKOL_BINDINGS")) { luaL_Reg l[] = { { "__index", NULL }, { "base", lbindings_set_base }, { "vbuffer", lbindings_set_vb }, { "voffset", lbindings_set_voff }, { "ibuffer", lbindings_set_ib }, { "ioffset", lbindings_set_ioff }, { "view", lbindings_set_view }, { "sampler", lbindings_set_sampler }, { "apply", lbindings_apply }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } ================================================ FILE: src/render_bindings.h ================================================ #ifndef soluna_render_bindings_h #define soluna_render_bindings_h #include "sokol/sokol_gfx.h" struct render_bindings { int base; sg_bindings bindings; }; #define DRAWFUNC(name) (sg_query_features().draw_base_instance ? name##_ex : name) #endif ================================================ FILE: src/render_uniform.c ================================================ #include #include #include #include "sokol/sokol_gfx.h" #define UNIFORM_TYPE_FLOAT 1 #define UNIFORM_TYPE_INT 2 static void set_uniform_float(lua_State *L, int index, uint8_t *buffer, int offset, int n) { if (n <= 1) { float v = luaL_checknumber(L, index); memcpy(buffer + offset, &v, sizeof(float)); } else { luaL_checktype(L, index, LUA_TTABLE); if (lua_rawlen(L, index) != n) { luaL_error(L, "Need table size %d", n); } int i; float * v = (float *)(buffer + offset); for (i=0;i> 28; int offset = (meta >> 14) & 0x3fff; int n = meta & 0x3fff; switch (type) { case UNIFORM_TYPE_FLOAT: set_uniform_float(L, 3, p, offset, n); break; case UNIFORM_TYPE_INT: set_uniform_int(L, 3, p, offset, n); break; default: return luaL_error(L, "Invalid uniform setter typeid %s (%d)", lua_tostring(L, 2), type); } return 0; } static void set_key(lua_State *L, int index, uint8_t *u, int size) { const char * key = lua_tostring(L, -2); if (lua_getfield(L, -1, "offset") != LUA_TNUMBER) { luaL_error(L, "Missing .%s .offset", key); } int offset = luaL_checkinteger(L, -1); lua_pop(L, 1); if (offset < 0 || offset > 0x3fff) luaL_error(L, "Invalid .%s offset = %d", key, offset); int t = lua_getfield(L, -1, "n"); int n = 1; if (t != LUA_TNUMBER && t != LUA_TNIL) { luaL_error(L, "Invalid type .%s .n (%s)", key, lua_typename(L, t)); } if (t == LUA_TNUMBER) { n = luaL_checkinteger(L, -1); } if (n < 1 || n > 0x3fff) luaL_error(L, "Invalid .%s n = %d", key, n); lua_pop(L, 1); if (lua_getfield(L, -1, "type") != LUA_TSTRING) { luaL_error(L, "Missing .%s .type", key); } const char *type = lua_tostring(L, -1); int tid = 0; int typesize = 0; if (strcmp(type, "float") == 0) { tid = UNIFORM_TYPE_FLOAT; typesize = sizeof(float); } else if (strcmp(type, "int") == 0) { tid = UNIFORM_TYPE_INT; typesize = sizeof(int); } else { luaL_error(L, "Invalid .%s .type %s", key, type); } lua_pop(L, 1); int offset_end = offset + n * typesize; if (offset_end > size) luaL_error(L, "Invalid uniform .%s (offset %d,n %d)", key, offset, n); int i; for (i=offset;i #include #include #include #include "font_define.h" // implement from image.c #include "stb/stb_image.h" #define STB_IMAGE_RESIZE_IMPLEMENTATION #include "stb/stb_image_resize2.h" #include "stb/stb_image_write.h" #include "luabuffer.h" #define MAX_SIZE 4096 #define INF 1e20 #define SDF_RADIUS 8 #define SDF_CUTOFF 0.25 // the same with font glyph size #define IMAGE_SIZE 64 // 1D squared distance transform static void edt1d(const double *f, double *d, int *v, double *z, int n) { int q,k; v[0] = 0; z[0] = -INF; z[1] = +INF; for (q = 1, k = 0; q < n; q++) { double s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); while (s <= z[k]) { k--; s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); } k++; v[k] = q; z[k] = s; z[k + 1] = +INF; } for (q = 0, k = 0; q < n; q++) { while (z[k + 1] < q) k++; d[q] = (q - v[k]) * (q - v[k]) + f[v[k]]; } } // 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/ static void edt(double *data, int width, int height, double *f, double *d, int *v, double *z) { int x,y; for (x = 0; x < width; x++) { for (y = 0; y < height; y++) { f[y] = data[y * width + x]; } edt1d(f, d, v, z, height); for (y = 0; y < height; y++) { data[y * width + x] = d[y]; } } for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { f[x] = data[y * width + x]; } edt1d(f, d, v, z, width); for (x = 0; x < width; x++) { data[y * width + x] = sqrt(d[x]); } } } struct sdf_context { double f[MAX_SIZE]; double d[MAX_SIZE]; double z[MAX_SIZE+1]; int v[MAX_SIZE]; }; static void sdf_convert(unsigned char *bytes, int x, int y, double radius, double cutoff) { int i; assert(x <= MAX_SIZE && y <= MAX_SIZE); int length = x * y; double *data = (double *)malloc(length * sizeof(double) * 3 + sizeof(struct sdf_context)); for (i = 0; i < length; i++) { // For white background, negative image data[i] = (255 - bytes[i]) / 255.0; } double *gridOuter = data + length; double *gridInner = gridOuter + length; struct sdf_context * ctx = (struct sdf_context *)(gridInner + length); for (i = 0; i < length; i++) { double a = data[i]; if (a >= 0.5) { gridOuter[i] = 0; if (a >= 1) { gridInner[i] = INF; } else { a -= 0.5; gridInner[i] = a * a; } } else if (a <= 0.5) { gridInner[i] = 0; if (a <= 0) { gridOuter[i] = INF; } else { a = 0.5 - a; gridOuter[i] = a * a; } } } edt(gridOuter, x, y, ctx->f, ctx->d, ctx->v, ctx->z); edt(gridInner, x, y, ctx->f, ctx->d, ctx->v, ctx->z); for (i = 0; i < length; i++) { double v = (gridOuter[i] - gridInner[i]) / radius + cutoff; if (v <= 0) { bytes[i] = 255; } else if (v >= 1) { bytes[i] = 0; } else { unsigned char byte = (unsigned char)(v * 255.0 + 0.5); bytes[i] = 255 - byte; } } free(data); } static void * free_image(void *ud, void *ptr, size_t osize, size_t nsize) { stbi_image_free(ptr); return NULL; } static void * free_resizeimage(void *ud, void *ptr, size_t osize, size_t nsize) { free(ptr); return NULL; } static int image_loadsdf(lua_State *L) { size_t sz; const stbi_uc *buffer = luaL_getbuffer(L, &sz); int x,y; stbi_uc * img = stbi_load_from_memory(buffer, sz, &x, &y, NULL, 1); if (img == NULL) { lua_pushnil(L); lua_pushstring(L, stbi_failure_reason()); return 2; } int target_w = luaL_optinteger(L, 2, IMAGE_SIZE); int target_h = luaL_optinteger(L, 3, target_w); if (target_w <= 0 || target_w > MAX_SIZE || target_h <= 0 || target_w > MAX_SIZE) { return luaL_error(L, "Invalid target size %d * %d", target_w, target_h); } double radius = (double)x * SDF_RADIUS / target_w; sdf_convert(img, x, y, radius, SDF_CUTOFF); if (x == target_w && y == target_h) { lua_pushexternalstring(L, (const char *)img, x * y, free_image, NULL); } else { size_t size = target_w * target_h; unsigned char * target = (unsigned char *)malloc(size + 1); if (target == NULL) { stbi_image_free(img); return luaL_error(L, "Out of memory for image"); } target[size] = 0; stbir_resize_uint8_linear(img , x , y, x, target, target_w, target_h, target_w, STBIR_1CHANNEL); stbi_image_free(img); lua_pushexternalstring(L, (const char *)target, size, free_resizeimage, NULL); } return 1; } static int image_savesdf(lua_State *L) { const char * filename = luaL_checkstring(L, 1); size_t sz; const char * buffer = luaL_checklstring(L, 2, &sz); int x = luaL_optinteger(L, 3, IMAGE_SIZE); int y = luaL_optinteger(L, 4, x); if (x * y != sz) { return luaL_error(L, "Invalid image size %d * %d", x, y); } if (!stbi_write_png(filename, x, y, 1, buffer, x)) { return luaL_error(L, "Write %s failed", filename); } return 1; } static int icon_bundle(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); int n = lua_rawlen(L, 1); size_t sz = n * FONT_MANAGER_GLYPHSIZE * FONT_MANAGER_GLYPHSIZE; void * buffer = lua_newuserdatauv(L, sz, 0); int i; for (i=0; i> 16, v >> 16); ivec2 u2 = ivec2(0 , u & 0xffff); ivec2 v2 = ivec2(0 , v & 0xffff); ivec2 off = ivec2(offset >> 16 , offset & 0xffff) - 0x8000; vec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]); vec2 pos = ((uv_offset - off) * sr[int(position.z)].m + position.xy) * framesize; gl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1); uv = (uv_base + uv_offset) * texsize; } @end @fs fs layout(binding=1) uniform texture2D tex; layout(binding=0) uniform sampler smp; layout(binding=1) uniform fs_params { float edge_mask; float dist_multiplier; int color; float unused; }; in vec2 uv; out vec4 frag_color; void main() { float dis = texture(sampler2D(tex,smp), uv).r; float smoothing = length(fwidth(uv)) * 128.0 * dist_multiplier; float alpha = smoothstep(edge_mask - smoothing, edge_mask + smoothing, dis); float color_alpha = ((color >> 24) & 0xff) / 255.0f; vec4 c = vec4( ((color >> 16) & 0xff) / 255.0f, ((color >> 8) & 0xff) / 255.0f, ((color) & 0xff) / 255.0f, color_alpha * alpha); frag_color = c; } @end @program texquad vs fs ================================================ FILE: src/service/audio.lua ================================================ local ltask = require "ltask" local audio = require "soluna.audio" local file = require "soluna.file" local datalist = require "soluna.datalist" global assert, error, ipairs, math, pairs, tonumber, tostring local device local definitions = {} local groups = {} local voices = {} local bundles = {} local next_voice_id_value = 0 local ziplist local is_quit local SOUND_FLAG_STREAM = 0x00000001 local DEFAULT_DEFINITION = { group = "sound", volume = 1.0, pan = 0.0, pitch = 1.0, loop = false, stream = false, } local function convert_value(v) if v == "true" then return true end if v == "false" then return false end return tonumber(v) or v end local function load_bundle(filename) local source = file.load(filename) local bundle = datalist.parse(assert(source, "Can't load audio bundle " .. tostring(filename))) local defs = {} for _, v in ipairs(bundle) do local def = { group = DEFAULT_DEFINITION.group, volume = DEFAULT_DEFINITION.volume, pan = DEFAULT_DEFINITION.pan, pitch = DEFAULT_DEFINITION.pitch, loop = DEFAULT_DEFINITION.loop, stream = DEFAULT_DEFINITION.stream, } for k, value in pairs(v) do def[k] = convert_value(value) end def.name = assert(def.name) def.filename = assert(def.filename) if defs[def.name] then error("Duplicate sound " .. tostring(def.name)) end defs[def.name] = def end return defs end local function merge_definition(def, opts) if opts == nil then return { filename = def.filename, group = def.group, volume = def.volume, pan = def.pan, pitch = def.pitch, loop = def.loop, stream = def.stream, } end local loop = opts.loop if loop == nil then loop = def.loop end local stream = opts.stream if stream == nil then stream = def.stream end return { filename = def.filename, group = opts.group ~= nil and opts.group or def.group, volume = opts.volume ~= nil and opts.volume or def.volume, pan = opts.pan ~= nil and opts.pan or def.pan, pitch = opts.pitch ~= nil and opts.pitch or def.pitch, loop = loop, stream = stream, } end local function release_voice(id) local voice = voices[id] if not voice then return end audio.sound_uninit(voice) voices[id] = nil end local function cleanup_voices() for id, voice in pairs(voices) do if not audio.sound_playing(voice) then release_voice(id) end end end local function next_voice_id() next_voice_id_value = next_voice_id_value + 1 return next_voice_id_value end local function seconds_to_ms(seconds) if seconds == nil then return nil end if seconds <= 0 then return 0 end return math.floor(seconds * 1000 + 0.5) end ltask.fork(function() while not is_quit do cleanup_voices() ltask.sleep(20) end end) local S = {} function S.init_device(dev) ziplist = file.ziplist and file.ziplist() if ziplist then audio.init_vfs(dev, ziplist) end device = dev end function S.init(filename) if bundles[filename] then return end local defs = load_bundle(filename) for name, def in pairs(defs) do if definitions[name] then error("Duplicate sound " .. tostring(name)) end local group = groups[def.group] if group == nil then group = assert(audio.group_init(device)) groups[def.group] = group end definitions[name] = def end bundles[filename] = true end function S.play_sound(name, opts) local def = definitions[name] if def == nil then return nil, "Unknown sound " .. tostring(name) end local final = merge_definition(def, opts) local group = groups[final.group] if group == nil then return nil, "Unknown audio bus " .. tostring(final.group) end local flags = final.stream and SOUND_FLAG_STREAM or 0 local voice, err = audio.sound_init(device, final.filename, flags, group) if not voice then return nil, err end audio.sound_set_volume(voice, final.volume) audio.sound_set_pan(voice, final.pan) audio.sound_set_pitch(voice, final.pitch) audio.sound_set_looping(voice, final.loop == true) local ok, start_err = audio.sound_start(voice) if not ok then audio.sound_uninit(voice) return nil, start_err end local id = next_voice_id() voices[id] = voice return id end function S.has_bus(name) return groups[name] ~= nil end function S.voice_stop(id, fade_seconds) local voice = voices[id] if not voice then return false end return audio.sound_stop(voice, seconds_to_ms(fade_seconds)) ~= nil end function S.voice_playing(id) local voice = voices[id] if not voice then return false end local playing = audio.sound_playing(voice) if not playing then release_voice(id) end return playing end function S.voice_set_volume(id, volume) local voice = voices[id] if not voice then return false end audio.sound_set_volume(voice, volume) return true end function S.voice_set_pan(id, pan) local voice = voices[id] if not voice then return false end audio.sound_set_pan(voice, pan) return true end function S.voice_set_pitch(id, pitch) local voice = voices[id] if not voice then return false end audio.sound_set_pitch(voice, pitch) return true end function S.voice_set_loop(id, loop) local voice = voices[id] if not voice then return false end audio.sound_set_looping(voice, loop) return true end function S.voice_seek(id, seconds) local voice = voices[id] if not voice then return false end return audio.sound_seek(voice, seconds) ~= nil end function S.voice_tell(id) local voice = voices[id] if not voice then return nil, "Voice not found" end return audio.sound_tell(voice) end function S.bus_set_volume(name, volume) local group = groups[name] if group == nil then return false end audio.group_set_volume(group, volume) return true end function S.quit() is_quit = true for id in pairs(voices) do release_voice(id) end for name, group in pairs(groups) do audio.group_uninit(group) groups[name] = nil end device = nil definitions = {} bundles = {} end return S ================================================ FILE: src/service/gamepad.lua ================================================ local ltask = require "ltask" local device = require "soluna.gamepad.device" global pairs local S = {} local listener = {} function S.register(addr, msg) listener[addr] = msg ltask.send(addr, msg) end function S.update() local change = device.update() if change then for addr, msg in pairs(listener) do ltask.send(addr, msg) end end end return S ================================================ FILE: src/service/loader.lua ================================================ local image = require "soluna.image" local spritemgr = require "soluna.spritemgr" local spritebundle = require "soluna.spritebundle" global setmetatable, ipairs, pairs, assert, type local sprite_bank -- todo: make weak table local filecache = setmetatable({ __missing = {}} , { __index = spritebundle.loadimage }) local S = {} function S.init(config) sprite_bank = spritemgr.newbank(config.max_sprite, config.texture_size) return sprite_bank:ptr() end local bundle = {} local sprite = {} local function add_list(desc) local b = {} for _, item in ipairs(desc) do local n = #item if n == 0 then local id = sprite_bank:add(item.cw, item.ch, item.x, item.y) item.id = id sprite[id] = item b[item.name] = id else local pack = {} b[item.name] = pack for i = 1, n do local s = item[i] local id = sprite_bank:add(s.cw, s.ch, s.x, s.y) sprite[id] = s pack[i] = id end end end return b end local function load_from_file(filename) local b = bundle[filename] if b then return b end local desc = spritebundle.load(filecache, filename) local b = add_list(desc) bundle[filename] = b return b end local function load_from_table(t) local desc = spritebundle.load(filecache, t, t.path) return add_list(desc) end function S.loadbundle(filename) if type(filename) == "table" then return load_from_table(filename) else return load_from_file(filename) end end function S.pack() local texid, n = sprite_bank:pack() -- upload rects into n textures, [texid, texid + n) local results = {} local texid_from = texid for i = 1, n do local r = sprite_bank:altas(texid) for id,v in pairs(r) do local x = v >> 32 local y = v & 0xffffffff local obj = sprite[id] local c = filecache[obj.filename] local data = image.canvas(c.data, c.w, c.h, obj.cx, obj.cy, obj.cw, obj.ch) local w, h, ptr = image.canvas_size(data) r[id] = { id = id, data = ptr, x = x, y = y, w = w, h = h, stride = c.w * 4, dx = obj.x, dy = obj.y } end texid = texid + 1 results[i] = r end return results, texid_from end function S.write(id, filename) local obj = sprite[id] assert(obj.cx) local c = filecache[obj.filename] local data = image.canvas(c.data, c.w, c.h, obj.cx, obj.cy, obj.cw, obj.ch) local img = image.new(obj.cw, obj.ch) image.blit(img:canvas(), data) img:write(filename) end function S.preload(filename, content, w, h) assert(#content == w * h * 4) filecache[filename] = { data = content, w = w, h = h } end return S ================================================ FILE: src/service/log.lua ================================================ local ltask = require "ltask" local writelog = require "soluna.log" local io = io global none local S = {} local sokol_log = writelog.sokol local ltask_log = writelog.ltask local function writelog() local flush while true do local ti, id, msg, sz = ltask.poplog() if ti == nil then if flush then io.flush() end break end if id == 0 then sokol_log(ti, msg) else ltask_log(ti, ltask.unpack_remove(msg, sz)) end flush = true end end ltask.fork(function() while true do writelog() ltask.sleep(20) end end) function S.quit() writelog() end return S ================================================ FILE: src/service/render.lua ================================================ local ltask = require "ltask" local render = require "soluna.render" local image = require "soluna.image" local embedsource = require "soluna.embedsource" local drawmgr = require "soluna.drawmgr" local file = require "soluna.file" global require, assert, pairs, pcall, ipairs, print, load, type local setting = require "soluna".settings() local font = {} local function create_materials(ctx) local materials = {} local function load_material(source, chunkname, id) local chunk = assert(load(source, chunkname)) local mctx = { id = id, state = ctx.state, arg = ctx.arg, tmp_buffer = ctx.tmp_buffer, settings = ctx.settings, font = ctx.font, render = ctx.render, } local material = assert(chunk(mctx), chunkname .. " : no material returned") assert(type(material.submit) == "function", chunkname .. " : missing submit function") assert(type(material.draw) == "function", chunkname .. " : missing draw function") materials[id] = material end local MATERIAL_EXTLUA_BASE = 256 do local next_id = 0 for _, name in ipairs(embedsource.material) do local id = next_id assert(id < MATERIAL_EXTLUA_BASE) next_id = id + 1 local loader = assert(embedsource.material[name]) load_material(loader(), "@src/material/" .. name .. ".lua", id) end end if setting.extlua_material then local list = setting.extlua_material if type(list) == "string" then list = { list } end local path = assert(setting.extlua_material_path) local next_id = MATERIAL_EXTLUA_BASE for _, name in ipairs(list) do local id = next_id next_id = id + 1 local fullname = assert(file.searchpath(name, path)) load_material(file.load(fullname), "@" .. fullname, id) end end return materials end do local mgr = require "soluna.font.manager" local fontapi = require "soluna.font" local texture_ptr function font.init() mgr.init(embedsource.runtime.fontmgr(), "@src/lualib/fontmgr.lua") font.texture_size = fontapi.texture_size font.cobj = fontapi.cobj() texture_ptr = fontapi.texture() end function font.shutdown() mgr.shutdown() end function font.submit(img) if fontapi.submit() then img:update(texture_ptr) end end end local batch = {}; do local thread local submit_n = 0 function batch.register(addr) local n = #batch + 1 batch[n] = { source = addr } return n end function batch.wait() if submit_n ~= #batch then thread = ltask.current_token() ltask.wait() end submit_n = 0 end function batch.submit(id, ptr, size) local q = batch[id] local token = ltask.current_token() local function func() return ptr, size, token end if q[1] == nil then submit_n = submit_n + 1 if thread and submit_n == #batch then ltask.wakeup(thread) thread = nil end q[1] = func else q[#q + 1] = func end ltask.wait() end function batch.consume(id) local q = batch[id] local r = assert(q[1]) local n = #q for i = 1, n - 1 do q[i] = q[i + 1] end q[n] = nil return r() end end local STATE local S = {} function S.app(settings) local soluna_app = require "soluna.app" for k, v in pairs(settings) do local f = soluna_app[k] if f then f(v) end end end -- todo: update mutiple images local update_image local function delay_update_image(imgmem) function update_image() local from = imgmem.from for i = 1, #imgmem do local tid = from + i local tex = STATE.textures[tid] if tex == nil then local texture_size = setting.texture_size tex = render.image { width = texture_size, height = texture_size, } STATE.textures[tid] = tex STATE.views[tid] = render.view { texture = tex } end tex:update(imgmem[i]) end update_image = nil end end local function frame(count) local batch_size = setting.batch_size -- todo: do not wait all batch commits local batch_n = #batch if update_image then update_image() end STATE.drawmgr:reset() for _, obj in pairs(STATE.materials) do if obj.reset then obj.reset() end end for i = 1, batch_n do local ptr, size = batch[i][1]() if ptr then STATE.drawmgr:append(ptr, size) end end local draw_n = #STATE.drawmgr for i = 1, draw_n do local mat, ptr, n, tex = STATE.drawmgr(i) local obj = assert(STATE.materials[mat]) obj.submit(ptr, n) end STATE.srbuffer:update(STATE.srbuffer_mem:ptr()) STATE.pass:begin() font.submit(STATE.font_texture) for i = 1, draw_n do local mat, ptr, n, tex = STATE.drawmgr(i) local obj = assert(STATE.materials[mat]) obj.draw(ptr, n, tex) end STATE.pass:finish() render.submit() end function S.frame(count) batch.wait() local ok, err = pcall(ltask.mainthread_run, frame, count) if not ok then print("RENDER ERR", err) end for i = 1, #batch do local ptr, size, token = batch.consume(i) ltask.wakeup(token) end assert(ok, err) end S.register_batch = assert(batch.register) S.submit_batch = assert(batch.submit) function S.quit() local workers = {} for _, v in ipairs(batch) do workers[v.source] = true end S.submit_batch = function() end -- prevent submit for _, v in ipairs(batch) do for _, resp in ipairs(v) do local _, _, token = resp() ltask.wakeup(token) end end for addr in pairs(workers) do ltask.call(addr, "quit") end font.shutdown() end function S.load_sprites(name) local loader = ltask.uniqueservice "loader" local spr = ltask.call(loader, "loadbundle", name) local rects, from = ltask.call(loader, "pack") local imgmems = { from = from } for i = 1, #rects do local imgmem = image.new(setting.texture_size, setting.texture_size) local canvas = imgmem:canvas() for id, v in pairs(rects[i]) do local src = image.canvas(v.data, v.w, v.h, v.stride) image.blit(canvas, src, v.x, v.y) end imgmems[i] = imgmem end delay_update_image(imgmems) return spr end local function render_init(arg) font.init() local texture_size = setting.texture_size local sr_buffer = render.buffer { type = "storage", usage = "dynamic", label = "texquad-scalerot", size = render.buffer_size("srbuffer", setting.srbuffer_size), } -- todo: don't load texture here local font_texture = render.image { width = font.texture_size, height = font.texture_size, pixel_format = "R8", } local views = { storage = render.view { storage = sr_buffer }, font = render.view { texture = font_texture }, } STATE = { pass = render.pass { color0 = setting.background, swapchain = true, }, default_sampler = render.sampler { label = "texquad-sampler" }, textures = {}, font_texture = font_texture, views = views, } STATE.srbuffer = assert(sr_buffer) STATE.srbuffer_mem = render.srbuffer(setting.srbuffer_size) STATE.drawmgr = drawmgr.new(arg.bank_ptr, setting.draw_instance) STATE.uniform = render.uniform { 12, -- size framesize = { offset = 0, type = "float", n = 2, }, tex_size = { offset = 8, type = "float", }, } STATE.uniform.framesize = { 2 / arg.width, -2 / arg.height } STATE.uniform.tex_size = 1 / texture_size local tmp_buffer = render.tmp_buffer(setting.tmpbuffer_size) STATE.materials = create_materials { state = STATE, arg = arg, tmp_buffer = tmp_buffer, settings = setting, font = font, render = render, } end function S.init(arg) ltask.mainthread_run(render_init, arg) end function S.resize(w, h) STATE.uniform.framesize = { 2 / w, -2 / h } end return S ================================================ FILE: src/service/settings.lua ================================================ local initsetting = require "soluna.initsetting" global assert local S = {} local setting function S.init(args) assert(setting == nil) setting = initsetting.init(args, true) end function S.get() return setting end return S ================================================ FILE: src/service/start.lua ================================================ local ltask = require "ltask" local file = require "soluna.file" local spritemgr = require "soluna.spritemgr" local soluna = require "soluna" local soluna_app = require "soluna.app" local util = require "soluna.util" local table = table local debug = debug global error, tostring, assert, load, type, ipairs, pairs, xpcall, print, pcall local message_unpack = soluna_app.unpackmessage local args = ... local S = {} local app = {} local app_event = {} local prehook = {} function app.cleanup() ltask.send(1, "quit_ltask") end local function skip() ltask.mainthread_run(function() end) end local init_func function app.frame() if init_func then app.frame = skip local ok , err_func = xpcall(init_func, debug.traceback) if ok then app.frame = err_func or skip else skip() ltask.log.error(err_func) soluna_app.quit() end end end local render_service = ltask.self() local pre_size function prehook.window_resize(w, h) if render_service then ltask.call(render_service, "resize", w, h) else pre_size = { width = w, height = h } end end S.event = skip -- external message from soluna host function S.external(p) local what, arg1, arg2 = message_unpack(p) local f = app[what] if f then f(arg1, arg2) return end local pre = prehook[what] if pre then pre(arg1, arg2) end local f = app_event[what] if f then f(arg1, arg2) end end local cleanup = util.func_chain() local function init(arg) if arg == nil then error "No command line args" end soluna.gamepad_init() local settings = ltask.uniqueservice "settings" ltask.call(settings, "init", arg) local setting = soluna.settings() if setting.service_path then ltask.servicepath(setting.service_path) end local audio = ltask.uniqueservice "audio" ltask.call(audio, "init_device", arg.app.audio_device) local loader = ltask.uniqueservice "loader" arg.app.bank_ptr = ltask.call(loader, "init", { max_sprite = setting.sprite_max, texture_size = setting.texture_size, }) local entry = setting.entry local source = entry and file.load(entry) if not source then error ("Can't load entry " .. tostring(entry)) end local f = assert(load(source, "@"..entry, "t")) local render = ltask.uniqueservice "render" local function init_render() ltask.call(render, "init", arg.app) render_service = render if pre_size then ltask.call(render, "resize", pre_size.width, pre_size.height) arg.app.width = pre_size.width arg.app.height = pre_size.height pre_size = nil end local batch = spritemgr.newbatch() cleanup:add(function() batch:release() end) local callback = f { batch = batch, width = arg.app.width, height = arg.app.height, table.unpack(arg), } if type(callback) ~= "table" then app.frame = skip soluna_app.close_window() return end local frame_cb = callback.frame local messages = { "mouse_move", "mouse_button", "mouse_scroll", "mouse", "touch_begin", "touch_end", "touch_moved", "touch_cancelled", "window_resize", "char", "key", } local avail = {} for _, v in ipairs(messages) do avail[v] = true end for k,v in pairs(callback) do if avail[k] then app_event[k] = v end end local batch_id = ltask.call(render, "register_batch", ltask.self()) local function frame(count) batch:reset() frame_cb(count) ltask.send(render, "submit_batch", batch_id, batch:ptr()) ltask.call(render, "frame", count) end local traceback = debug.traceback local function render_frame(count) local ok, err = xpcall(frame, traceback, count) if not ok then app.frame = skip error(err) end end return render_frame end return init_render() end function S.quit() cleanup() end ltask.fork(function() ltask.call(1, "external_forward", ltask.self(), "external") -- trigger INIT_EVENT, see main.lua skip() init_func = function() return init(args) end end) return S ================================================ FILE: src/sprite_submit.h ================================================ #ifndef soluna_sprite_submit_h #define soluna_sprite_submit_h #include "batch.h" #include #include #include static inline int32_t to_fixpoint_(float v) { v *= 256; return (int)v; } static inline void sprite_set_xy(struct draw_primitive *p, float x, float y) { p->x = to_fixpoint_(x); p->y = to_fixpoint_(y); } static inline void sprite_apply_xy(struct draw_primitive *p, float x, float y) { p->x += to_fixpoint_(x); p->y += to_fixpoint_(y); } static inline int convert_scale_(float scale) { assert(scale >= 0); if (scale >= 1.0f) { // scale is 20 bit fix point int fs = (int)((scale - 1.0f) * 256.0f); // max scale 1111, 1110, 1111,1111,1111 const int maxfs = 0xfefff; if (fs > maxfs) fs = maxfs; return fs; } // use 12 bits for [0,1) scale return 0xff000 | (int)(scale * 4096.0f); } static inline uint32_t convert_scale_part_(uint32_t scale12) { uint32_t scale_fix; if (scale12 >= 0x1000) { scale_fix = (scale12 - 0x1000) >> 4; if (scale_fix > 0xfefff) scale_fix = 0xfefff; } else { scale_fix = scale12 | 0xff000; } return scale_fix << 12; } static inline int convert_rot_(float rot) { const float pi = 3.1415927; float v = fmod(rot, 2 * pi); if (v < 0) { v += 2 * pi; } return (int)(v * (2048.0f / pi)); } static inline void sprite_set_scale(struct draw_primitive *p, float scale) { p->sr = convert_scale_(scale) << 12; } static inline void sprite_set_rot(struct draw_primitive *p, float rot) { p->sr = convert_rot_(rot); } static inline void sprite_set_sr(struct draw_primitive *p, float scale, float rot) { p->sr = convert_scale_(scale) << 12 | convert_rot_(rot); } static inline void sprite_apply_scale(struct draw_primitive *p, uint32_t scale_fix12) { uint32_t scale_fix = p->sr >> 12; if (scale_fix == 0) { scale_fix = convert_scale_part_(scale_fix12); } else { uint64_t s; if (scale_fix >= 0xff000) { s = scale_fix & 0xfff; } else { s = (scale_fix + 0x100) << 4; } s = (s * scale_fix12) >> 12; scale_fix = convert_scale_part_((uint32_t)s); } p->sr = scale_fix | (p->sr & 0xfff); } #endif ================================================ FILE: src/spritemgr.c ================================================ #include "spritemgr.h" #include "sprite_submit.h" #include "batch.h" #include "transform.h" #include #include #include #include #define DEFAULT_TEXTURE_SIZE 4096 #define INVALID_TEXTUREID 0xffff #define MAX_NODE 8192 #define STB_RECT_PACK_IMPLEMENTATION #include "stb/stb_rect_pack.h" static int lbank_add(lua_State *L) { struct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, "SOLUNA_SPRITEBANK"); if (b->n >= b->cap) { return luaL_error(L, "Too many sprite (%d)", b->n); } struct sprite_rect *r = &b->rect[b->n++]; int w = luaL_checkinteger(L, 2); int h = luaL_checkinteger(L, 3); int dx = luaL_optinteger(L, 4, 0); int dy = luaL_optinteger(L, 5, 0); if (w <= 0 || w > 0xffff || h <=0 || h >= 0xffff) return luaL_error(L, "Invalid sprite size (%d * %d)", w, h); if (dx < -0x8000 || dx > 0x7fff || dy < -0x8000 || dy > 0x7ffff) return luaL_error(L, "Invalid sprite offset (%d * %d)", dx, dy); r->u = w; r->v = h; r->off = (dx + 0x8000) << 16 | (dy + 0x8000); r->texid = INVALID_TEXTUREID; lua_pushinteger(L, b->n); return 1; } static int pack_sprite(struct sprite_bank *b, stbrp_context *ctx, stbrp_node *tmp, stbrp_rect *srect, int from, int reserved, int *reserved_n) { int last_texid = b->texture_n; stbrp_init_target(ctx, b->texture_size, b->texture_size, tmp, MAX_NODE); int i; int rect_i = reserved; for (i=from;in;i++) { struct sprite_rect *rect = &b->rect[i]; if (rect->texid == INVALID_TEXTUREID || rect->texid == last_texid) { rect->texid = last_texid; stbrp_rect * sr = &srect[rect_i++]; sr->id = i; // reserve 1 pixel border sr->w = (rect->u + 1) & 0xffff; sr->h = (rect->v + 1) & 0xffff; if (sr->w > b->texture_size || sr->h > b->texture_size) { return -1; } } } if (stbrp_pack_rects(ctx, srect, rect_i)) { // succ int j; for (j=0;jrect[sr->id]; rect->u = sr->x << 16 | (sr->w - 1); rect->v = sr->y << 16 | (sr->h - 1); rect->texid = last_texid; } *reserved_n = 0; } else { // pack a part int j; int n = 0; for (j=0;jrect[sr->id]; if (sr->was_packed) { rect->u = sr->x << 16 | (sr->w - 1); rect->v = sr->y << 16 | (sr->h - 1); rect->texid = last_texid; } else { stbrp_rect * tmp = &srect[n]; tmp->w = sr->w; tmp->h = sr->h; tmp->id = sr->id; ++n; } } *reserved_n = n; } return i; } struct tmp_context { stbrp_context ctx; stbrp_node tmp[MAX_NODE]; stbrp_rect rect[1];// = malloc(sizeof(*rect) * b->n); }; static inline struct tmp_context * alloc_context(int n) { struct tmp_context * r = (struct tmp_context *)malloc(sizeof(*r) + (n - 1) * sizeof(r->rect)); return r; } static inline void free_context(struct tmp_context *p) { free(p); } static int lbank_pack(lua_State *L) { struct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, "SOLUNA_SPRITEBANK"); struct tmp_context *ctx = alloc_context(b->n); int texture = b->texture_n; int from = 0; int reserved = 0; for (;;) { from = pack_sprite(b, &ctx->ctx, ctx->tmp, ctx->rect, from, reserved, &reserved); if (from < 0) { return luaL_error(L, "sprite image is larger than texture"); } if (reserved == 0 && from >= b->n) { break; } ++b->texture_n; } free_context(ctx); lua_pushinteger(L, texture); lua_pushinteger(L, b->texture_n - texture + 1); return 2; } static int lbank_altas(lua_State *L) { struct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, "SOLUNA_SPRITEBANK"); int tid = luaL_checkinteger(L, 2); int i; lua_newtable(L); for (i=0;in;i++) { struct sprite_rect *rect = &b->rect[i]; if (rect->texid == tid) { uint64_t x = rect->u >> 16; uint64_t y = rect->v >> 16; uint64_t v = x << 32 | y; lua_pushinteger(L, v); lua_rawseti(L, -2, i + 1); } } return 1; } static int lbank_ptr(lua_State *L) { struct sprite_bank *b = (struct sprite_bank *)luaL_checkudata(L, 1, "SOLUNA_SPRITEBANK"); lua_pushlightuserdata(L, b); return 1; } static int lsprite_newbank(lua_State *L) { int cap = luaL_checkinteger(L, 1); int texture_size = luaL_optinteger(L, 2, DEFAULT_TEXTURE_SIZE); struct sprite_bank *b = (struct sprite_bank *)lua_newuserdatauv(L, sizeof(*b) + (cap-1) * sizeof(b->rect[0]), 0); b->n = 0; b->cap = cap; b->texture_size = texture_size; b->texture_n = 0; if (luaL_newmetatable(L, "SOLUNA_SPRITEBANK")) { luaL_Reg l[] = { { "__index", NULL }, { "add", lbank_add }, { "pack", lbank_pack }, { "altas", lbank_altas }, { "ptr", lbank_ptr }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } struct layer { float s; float r; float x; float y; }; struct batch { int n; int layer; int layer_cap; struct transform trans; struct draw_batch *b; struct layer *stack; }; static int lbatch_reset(lua_State *L) { struct batch *b = (struct batch *)luaL_checkudata(L, 1, "SOLUNA_BATCH"); b->n = 0; b->layer = 0; sprite_transform_identity(&b->trans); return 0; } static int lbatch_release(lua_State *L) { struct batch *b = (struct batch *)luaL_checkudata(L, 1, "SOLUNA_BATCH"); b->n = 0; b->n = 0; batch_delete(b->b); b->b = NULL; return 0; } static struct draw_primitive * batch_add_sprite(lua_State *L, struct batch *b) { int n = b->n; struct draw_primitive * p = batch_reserve(b->b, n + 1); if (p == NULL) luaL_error(L, "batch_add_sprite : Out of memory, n = %d", n); p += n; int id = luaL_checkinteger(L, 2); if (id <= 0) luaL_error(L, "Invalid sprite id %d", id); p->x = 0; p->y = 0; p->sr = 0; p->sprite = id; b->n = n + 1; return p; } static struct draw_primitive * batch_add_material(lua_State *L, struct batch *b) { int n = b->n; struct draw_primitive * p = batch_reserve(b->b, n + 2); if (p == NULL) luaL_error(L, "batch_add_material : Out of memory, n = %d", n); p += n; if (lua_getiuservalue(L, 2, 1) != LUA_TNUMBER) luaL_error(L, "Invalid material object"); int matid = lua_tointeger(L, -1); if (matid <= 0) luaL_error(L, "Invalid material id %d", matid); lua_pop(L, 1); p->x = 0; p->y = 0; p->sr = 0; p->sprite = -matid; int sz = lua_rawlen(L, 2); if (sz > sizeof(struct draw_primitive)) luaL_error(L, "Invalid material object size (%d > %d)", sz, sizeof(struct draw_primitive)); memcpy(p+1, lua_touserdata(L, 2), sz); b->n = n + 2; return p; } static struct draw_primitive * batch_add_stream(lua_State *L, struct batch *b, int *count) { int n = b->n; size_t sz = 0; const char * data = luaL_checklstring(L, 2, &sz); if (sz == 0) return NULL; *count = sz / 2 / sizeof(struct draw_primitive); if (*count * 2 * sizeof(struct draw_primitive) != sz) luaL_error(L, "Invalid stream size (%d)", sz); struct draw_primitive * p = batch_reserve(b->b, n + 2 * *count); if (p == NULL) { luaL_error(L, "batch_add_stream : Out of memory n = %d count = %d", n, *count); } p += n; b->n = n + *count * 2; memcpy(p, data, *count * 2 * sizeof(struct draw_primitive)); return p; } static int lbatch_add(lua_State *L) { struct batch *b = (struct batch *)luaL_checkudata(L, 1, "SOLUNA_BATCH"); struct draw_primitive *p; int n = 1; switch (lua_type(L, 2)) { case LUA_TNUMBER: p = batch_add_sprite(L, b); break; case LUA_TUSERDATA: p = batch_add_material(L, b); break; case LUA_TSTRING: p = batch_add_stream(L, b, &n); if (p == NULL) return 0; break; default: return luaL_error(L, "Invalid type %s", lua_typename(L, lua_type(L, 2))); } float x = luaL_optnumber(L, 3, 0); float y = luaL_optnumber(L, 4, 0); int i; for (i=0;itrans); p+=2; } return 0; } static int lbatch_ptr(lua_State *L) { struct batch *b = (struct batch *)luaL_checkudata(L, 1, "SOLUNA_BATCH"); int offset = luaL_optinteger(L, 2, 0); struct draw_primitive * p = batch_reserve(b->b, 0); if (offset >= b->n) return 0; p += offset; int n = b->n - offset; lua_pushlightuserdata(L, p); lua_pushinteger(L, n); return 2; } static void layer_close(lua_State *L, struct batch *b) { if (b->layer <= 0) luaL_error(L, "none layer to close"); if (b->layer == 1) { b->layer = 0; sprite_transform_identity(&b->trans); } else { --b->layer; struct layer *current = &b->stack[b->layer-1]; sprite_transform_set(&b->trans, current->s, current->r, current->x, current->y); } } static struct layer * layer_new(lua_State *L, struct batch *b) { if (b->layer >= b->layer_cap) { int cap = (b->layer + 1) * 3 / 2; struct layer * tmp = (struct layer *)lua_newuserdatauv(L, cap * sizeof(struct layer), 0); memcpy(tmp, b->stack, b->layer_cap * sizeof(struct layer)); b->stack = tmp; b->layer_cap = cap; lua_setiuservalue(L, 1, 1); } struct layer * ret = &b->stack[b->layer]; ++b->layer; return ret; } static void layer_merge(struct layer *current, struct layer *last) { float x, y; if (last->r != 0) { float sinv = sinf(last->r); float cosv = cosf(last->r); y = current->y * cosv + current->x * sinv; x = current->x * cosv - current->y * sinv; current->r += last->r; } else { x = current->x; y = current->y; } if (last->s != 1) { x *= last->s; y *= last->s; current->s *= last->s; } current->x = x + last->x; current->y = y + last->y; } static int lbatch_layer(lua_State *L) { struct batch *b = (struct batch *)luaL_checkudata(L, 1, "SOLUNA_BATCH"); struct layer *new_layer = layer_new(L, b); switch (lua_gettop(L)) { case 1: // close layer --b->layer; layer_close(L, b); return 0; case 2: // rot only new_layer->s = 1; new_layer->r = luaL_checknumber(L, 2); new_layer->x = 0; new_layer->y = 0; break; case 3: // trans only new_layer->s = 1; new_layer->r = 0; new_layer->x = luaL_checknumber(L, 2); new_layer->y = luaL_checknumber(L, 3); break; case 4: // st, no rot new_layer->s = luaL_checknumber(L, 2); new_layer->r = 0; new_layer->x = luaL_checknumber(L, 3); new_layer->y = luaL_checknumber(L, 4); break; case 5: // srt new_layer->s = luaL_checknumber(L, 2); new_layer->r = luaL_checknumber(L, 3); new_layer->x = luaL_checknumber(L, 4); new_layer->y = luaL_checknumber(L, 5); break; default: luaL_error(L, "Too many arguments"); } if (new_layer->s == 0) luaL_error(L, "Scale can't be 0"); if (b->layer > 1) { layer_merge(new_layer, new_layer-1); } sprite_transform_set(&b->trans, new_layer->s, new_layer->r, new_layer->x, new_layer->y); return 0; } static int lbatch_point(lua_State *L) { struct batch *b = (struct batch *)luaL_checkudata(L, 1, "SOLUNA_BATCH"); if (b->layer == 0) { lua_settop(L, 3); return 2; } float x = luaL_checknumber(L, 2); float y = luaL_checknumber(L, 3); // to .8 fix number int fx = (int)(x * 256); int fy = (int)(y * 256); sprite_transform_point(&b->trans, &fx, &fy); lua_pushnumber(L, (float)fx / 256.0f); lua_pushnumber(L, (float)fy / 256.0f); return 2; } static int lsprite_newbatch(lua_State *L) { struct batch *b = (struct batch *)lua_newuserdatauv(L, sizeof(*b), 1); b->n = 0; b->b = batch_new(0); if (b->b == NULL) return luaL_error(L, "sprite_newbatch : Out of memory"); b->layer = 0; b->layer_cap = 0; b->stack = NULL; sprite_transform_identity(&b->trans); if (luaL_newmetatable(L, "SOLUNA_BATCH")) { luaL_Reg l[] = { { "__index", NULL }, { "reset", lbatch_reset }, { "add", lbatch_add }, { "ptr", lbatch_ptr }, { "release", lbatch_release }, { "layer", lbatch_layer }, { "point", lbatch_point }, { NULL, NULL }, }; luaL_setfuncs(L, l, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); return 1; } int luaopen_spritemgr(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "newbank", lsprite_newbank }, { "newbatch", lsprite_newbatch }, { NULL, NULL }, }; luaL_newlib(L, l); sprite_transform_init(); return 1; } ================================================ FILE: src/spritemgr.h ================================================ #ifndef soluna_spritemgr_h #define soluna_spritemgr_h #include #include #define INVALID_TEXTUREID 0xffff struct sprite_rect { uint32_t texid; uint32_t off; // (dx + 0x8000) << (dy + 0x8000) uint32_t u; // x << 16 | w uint32_t v; // y << 16 | h }; struct sprite_bank { int n; int cap; int texture_size; int texture_n; struct sprite_rect rect[1]; }; #endif ================================================ FILE: src/srbuffer.c ================================================ #include "srbuffer.h" #include #include static inline int pow2(int n) { int m = 1 << 5; while (m < n) { m *= 2; } return m; } size_t srbuffer_size(int n) { struct sr_buffer *dummy = NULL; n = pow2(n); size_t sz = sizeof(*dummy->frame) + sizeof(*dummy->cache) + sizeof(*dummy->key) + sizeof(*dummy->data); return sizeof(*dummy) + sz * n; } void srbuffer_init(struct sr_buffer *SR, int n) { n = pow2(n); uint8_t *ptr = (uint8_t *)(SR + 1); SR->data = (struct sr_mat *)ptr; ptr += n * sizeof(SR->data[0]); SR->key = (uint32_t *)ptr; ptr += n * sizeof(SR->key[0]); SR->cache = (uint16_t *)ptr; ptr += n * sizeof(SR->cache[0]); SR->frame = ptr; SR->cap = n; SR->n = 1; SR->dirty = 1; SR->current_frame = 0; SR->current_n = 1; SR->frame[0] = 0; SR->key[0] = 0; SR->cache[0] = 0; float *v = SR->data[0].v; v[0] = 1.0f; v[1] = 0; v[2] = 0; v[3] = 1.0f; } int srbuffer_add(struct sr_buffer *SR, uint32_t v) { int index = v % (SR->cap - 1); int slot = SR->cache[index]; if (slot < SR->n && v == SR->key[slot]) { if (SR->frame[slot] != SR->current_frame) { SR->frame[slot] = SR->current_frame; ++SR->current_n; } return slot; } if (SR->n >= SR->cap) { int i; for (i=1;icap;i++) { if (SR->key[i] == v) { SR->cache[index] = i; SR->frame[index] = SR->current_frame; return i; } } return -1; // full } int new_slot = 1; if (SR->current_n * 2 < SR->n) { // find an exist slot slot = v % SR->n; int i; for (i=0;in;i++) { if (SR->frame[slot] != SR->current_frame) { SR->frame[slot] = SR->current_frame; ++SR->current_n; new_slot = 0; break; } ++slot; if (slot >= SR->n) slot -= SR->n; } } if (new_slot) { slot = SR->n++; SR->frame[slot] = SR->current_frame; ++SR->current_n; } SR->dirty = 1; SR->cache[index] = slot; SR->key[slot] = v; float *mat = SR->data[slot].v; uint32_t scale_fix = v >> 12; float scale = 1.0f; if (scale_fix != 0) { if (scale_fix >= 0xff000) { scale = (float)(scale_fix & 0xfff) * (1.0f / 4096.0f); } else { scale = (float)scale_fix * (1.0f / 256.0f) + 1.0f; } } uint32_t rot_fix = v & 0xfff; if (rot_fix == 0) { mat[0] = scale; mat[1] = 0; mat[2] = 0; mat[3] = scale; } else { const float pi = 3.1415927f; float rot = (float) rot_fix * ( pi / 2048.0f ); float cosr = cosf(rot) * scale; float sinr = sinf(rot) * scale; mat[0] = cosr; mat[1] = -sinr; mat[2] = sinr; mat[3] = cosr; } return slot; } void * srbuffer_commit(struct sr_buffer *SR, int *sz) { if (SR->dirty) { *sz = SR->n * sizeof(SR->data[0]); SR->dirty = 0; SR->current_n = 1; ++SR->current_frame; SR->frame[0] = SR->current_frame; return SR->data; } *sz = 0; return NULL; } #ifdef TEST_SRBUFFER_MAIN #include #include #include "sprite_submit.h" static void test(float x, float y, float scale, float rad, uint32_t *sr, float *ox, float *oy) { struct draw_primitive tmp; const float pi = 3.1415927f; sprite_set_sr(&tmp, scale, rad * pi / 180.0f); union { struct sr_buffer buffer; uint8_t tmp[65535]; } u; assert(srbuffer_size(1024) < sizeof(u.tmp)); srbuffer_init(&u.buffer, 1024); int index = srbuffer_add(&u.buffer, tmp.sr); const float *mat = u.buffer.data[index].v; *ox = x * mat[0] + y * mat[1]; *oy = x * mat[2] + y * mat[3]; *sr = tmp.sr; } static void test_rot(float x, float y, float rot) { float ox,oy; uint32_t v; test(x, y, 1, rot, &v, &ox, &oy); printf("[%f,%f / %f] =(%x)=> [%f,%f]\n", x, y, rot, v, ox, oy); } static void test_sr(float x, float y, float scale, float rot) { float ox,oy; uint32_t v; test(x, y, scale, rot, &v, &ox, &oy); printf("[%f,%f / %f,%f] =(%x)=> [%f,%f]\n", x, y, scale, rot, v, ox, oy); } int main() { test_rot(100, 100, 45); test_rot(100, 0, 90); test_sr(100, 0, 1.5, 30); test_sr(100, 0, 0.5, -60); return 0; } #endif ================================================ FILE: src/srbuffer.h ================================================ #ifndef soluna_srbuffer_h #define soluna_srbuffer_h #include #include struct sr_mat { float v[4]; }; struct sr_buffer { int n; int cap; int current_n; uint8_t dirty; uint8_t current_frame; uint8_t *frame; uint16_t *cache; uint32_t *key; struct sr_mat *data; }; size_t srbuffer_size(int n); void srbuffer_init(struct sr_buffer *SR, int n); int srbuffer_add(struct sr_buffer *SR, uint32_t sr); void * srbuffer_commit(struct sr_buffer *SR, int *sz); #endif ================================================ FILE: src/texquad.glsl ================================================ @vs vs layout(binding=0) uniform vs_params { vec2 framesize; float texsize; }; struct sr_mat { mat2 m; }; layout(binding=0) readonly buffer sr_lut { sr_mat sr[]; }; in vec3 position; in uint offset; in uint u; in uint v; out vec2 uv; void main() { ivec2 uv_base = ivec2(u >> 16, v >> 16); ivec2 u2 = ivec2(0 , u & 0xffff); ivec2 v2 = ivec2(0 , v & 0xffff); ivec2 off = ivec2(offset >> 16 , offset & 0xffff) - 0x8000; vec2 uv_offset = vec2(u2[gl_VertexIndex & 1] , v2[gl_VertexIndex >> 1]); vec2 pos = ((uv_offset - off) * sr[int(position.z)].m + position.xy) * framesize; gl_Position = vec4(pos.x - 1.0f, pos.y + 1.0f, 0, 1); uv = (uv_base + uv_offset) * texsize; } @end @fs fs layout(binding=1) uniform texture2D tex; layout(binding=0) uniform sampler smp; in vec2 uv; out vec4 frag_color; void main() { frag_color = texture(sampler2D(tex,smp), uv); } @end @program texquad vs fs ================================================ FILE: src/tmpbuffer.h ================================================ #ifndef soluna_tmp_buffer_h #define soluna_tmp_buffer_h #include #include struct tmp_buffer { void *ptr; size_t sz; }; #define TMPBUFFER_PTR(type, obj) (type *)((obj)->ptr) #define TMPBUFFER_SIZE(type, obj) ((obj)->sz / sizeof(type)) static inline void tmp_buffer_init(lua_State *L, struct tmp_buffer *tmp, int uv_index, const char *key) { if (lua_getfield(L, 1, key) != LUA_TUSERDATA) luaL_error(L, "Invalid key .%s", key); if (lua_type(L, -1) != LUA_TUSERDATA || lua_getmetatable(L, -1)) { luaL_error(L, "Not an userdata without metatable"); } tmp->ptr = lua_touserdata(L, -1); tmp->sz = lua_rawlen(L, -1); // ud, object lua_setiuservalue(L, -2, uv_index); } #endif ================================================ FILE: src/transform.c ================================================ #include #include "batch.h" #include "transform.h" #include "sprite_submit.h" // sin(0 ~ 2 * pi) * 2^24 static int sin_lut[4096]; void sprite_transform_init() { static int init = 0; // Don't card about race condition, because sin_lut is a constant table if (init) return; int i; const float pi = 3.1415927f; const float pow2 = (float)(1<<24); for (i=0;i<4096;i++) { sin_lut[i] = (int)(sinf((float)i / 2048.0f * pi) * pow2); } init = 1; } static inline void sincos_lut(int d, int *sin, int *cos) { int x = (unsigned)d; *sin = sin_lut[x]; *cos = sin_lut[(4096 + 1024 - x) % 4096]; } void sprite_transform_apply(struct draw_primitive *p, struct transform * t) { int64_t x, y; if (t->r != 0) { int sin, cos; sincos_lut(t->r, &sin, &cos); int64_t x0 = p->x; int64_t y0 = p->y; y = y0 * cos + x0 * sin; x = x0 * cos - y0 * sin; x >>= 24; y >>= 24; int r = p->sr & 0xfff; r = (r + t->r) % 4096; p->sr = (p->sr & ~0xfff) | r; } else { x = p->x; y = p->y; } if (t->s != 0x1000) { x *= t->s; // .(12 + 12) fix number y *= t->s; x >>= 12; y >>= 12; sprite_apply_scale(p, t->s); } p->x = (int32_t)x + t->x; p->y = (int32_t)y + t->y; } void sprite_transform_set(struct transform *t, float s, float r, float x, float y) { t->s = (int32_t)(s * 4096); const float rot_scale = 2048.0 / 3.1415927; t->r = (int)(r * rot_scale) % 4096; t->x = (int32_t)(x * 256); t->y = (int32_t)(y * 256); } // returns origin point (x, y) in transformed coordinate system void sprite_transform_point(const struct transform *t, int *x, int *y) { int64_t ox = *x; int64_t oy = *y; ox -= t->x; oy -= t->y; if (t->r != 0) { int sin, cos; sincos_lut(t->r, &sin, &cos); int64_t x = ox; int64_t y = oy; ox = x * cos + y * sin; oy = y * cos - x * sin; ox >>= 24; oy >>= 24; } if (t->s != 4096) { int s = t->s; if (s == 0) s = 1; int32_t inv_s = (1 << 30) / s; // .18bits fix ox *= inv_s; oy *= inv_s; ox >>= 18; oy >>= 18; } *x = (int)ox; *y = (int)oy; } ================================================ FILE: src/transform.h ================================================ #ifndef soluna_transform_h #define soluna_transform_h #include struct transform { // ix, iy : sign bit + 23.8 fix number int32_t x; int32_t y; int32_t s; // sign bit + 19.12 fix number int r; // 12bits [0,4095] }; struct draw_primitive; static inline void sprite_transform_identity(struct transform * t) { t->x = 0; t->y = 0; t->s = 1 << 12; t->r = 0; } void sprite_transform_init(); void sprite_transform_set(struct transform *t, float s, float r, float x, float y); void sprite_transform_apply(struct draw_primitive *p, struct transform * t); void sprite_transform_point(const struct transform *t, int *x, int *y); #endif ================================================ FILE: src/truetype.c ================================================ #include #include #include #include "font_define.h" #include "truetype.h" static const unsigned char * get_ttfbuffer(lua_State *L, int index) { int t = lua_type(L, index); if (t == LUA_TSTRING) { return (const unsigned char *)lua_tostring(L, index); } else if (t == LUA_TUSERDATA || t == LUA_TLIGHTUSERDATA) { return (const unsigned char *)lua_touserdata(L, index); } luaL_error(L, "Invalid ttfbuffer type = %s", lua_typename(L, t)); return NULL; } // integer fontid (base 1) // string/userdata fontdata // integer index / string name static int lupdate_cstruct(lua_State *L) { struct truetype_font * f = truetype_cstruct(L); int fontid = luaL_checkinteger(L, 1); if (fontid < 1 || fontid > MAX_FONT_NUM) return luaL_error(L, "The font id %d is out of %d", fontid, MAX_FONT_NUM); const unsigned char * data = get_ttfbuffer(L, 2); if (data == NULL) return luaL_error(L, "Invalid font data for %d", fontid); int type = lua_type(L, 3); int offset = 0; if (type == LUA_TSTRING) { offset = stbtt_FindMatchingFont(data, lua_tostring(L, 3), STBTT_MACSTYLE_DONTCARE); if (offset < 0) return luaL_error(L, "Can't find %s in font %d", lua_tostring(L, 3), fontid); } else { int index = luaL_optinteger(L, 3, 0); offset = stbtt_GetFontOffsetForIndex(data, index); if (offset < 0) return luaL_error(L, "Invalid offset for font %d index %d", fontid, index); } --fontid; if (stbtt_InitFont(&f->fontinfo[fontid], data, offset) == 0) return luaL_error(L, "InitFont %d with failed", fontid+1); f->enable |= (uint64_t)(1 << fontid); lua_pushlightuserdata(L, &f->fontinfo[fontid]); return 1; } // integer fontid (base 1) static int lunload_cstruct(lua_State *L) { struct truetype_font * f = truetype_cstruct(L); int fontid = luaL_checkinteger(L, 1); if (fontid < 1 || fontid > MAX_FONT_NUM) return luaL_error(L, "The font id %d is out of %d", fontid, MAX_FONT_NUM); --fontid; f->enable &= ~(1 << fontid); return 0; } // string/userdata data // integer index // integer platid // integer encodeid // integer langid // return string utf-16 family, sub-family static int lnamestring(lua_State *L) { const unsigned char * data = get_ttfbuffer(L, 1); int index = luaL_checkinteger(L, 2); stbtt_fontinfo font; int offset = stbtt_GetFontOffsetForIndex(data, index); if (offset < 0) { return 0; } if (stbtt_InitFont(&font, data, offset) == 0) return luaL_error(L, "InitFont with index %d failed", index); int platid = luaL_checkinteger(L, 3); int encodeid = luaL_checkinteger(L, 4); int langid = luaL_checkinteger(L, 5); int len = 0; const char * family = stbtt_GetFontNameString(&font, &len, platid, encodeid, langid, 1); if (family) { lua_pushlstring(L, family, len); } else { lua_pushboolean(L, 0); return 1; } const char * subfam = stbtt_GetFontNameString(&font, &len, platid, encodeid, langid, 2); if (subfam) { lua_pushlstring(L, subfam, len); } else { return 1; } return 2; } static void init_cstruct(lua_State *L) { struct truetype_font *f = (struct truetype_font *)lua_newuserdatauv(L, sizeof(*f), 0); f->enable = 0; lua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_CSTRUCT); } static int ltestname(lua_State *L) { const char * name = luaL_checkstring(L, 1); int id = truetype_name(L, name); lua_pushinteger(L, id); return 1; } static int ltestinfo(lua_State *L) { int id = luaL_checkinteger(L, 1); struct truetype_font * f = truetype_cstruct(L); const stbtt_fontinfo *info = truetype_font(f, id, L); lua_pushlightuserdata(L, (void *)info); return 1; } int luaopen_font_truetype(lua_State *L) { luaL_checkversion(L); init_cstruct(L); luaL_Reg l[] = { { "update", lupdate_cstruct }, { "unload", lunload_cstruct }, { "namestring", lnamestring }, { "testname", ltestname }, // test C api : truetype_name { "testinfo", ltestinfo }, // test C api : truetype_font { "nametable", NULL }, { "idtable", NULL }, { "enum", NULL }, { NULL, NULL }, }; luaL_newlib(L, l); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_NAME); lua_setfield(L, -2, "nametable"); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_ID); lua_setfield(L, -2, "idtable"); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, TRUETYPE_ENUM); lua_setfield(L, -2, "enum"); return 1; } ================================================ FILE: src/truetype.h ================================================ #ifndef ant_truetype_h #define ant_truetype_h #include #include #include #include #include #define TRUETYPE_ID "TRUETYPE_ID" #define TRUETYPE_NAME "TRUETYPE_NAME" #define TRUETYPE_ENUM "TRUETYPE_ENUM" #define TRUETYPE_CSTRUCT "TRUETYPE_CSTRUCT" #define TRUETYPE_IMPORT "TRUETYPE_IMPORT" struct truetype_font { uint64_t enable; stbtt_fontinfo fontinfo[MAX_FONT_NUM]; }; // get global struct truetype_font static inline struct truetype_font * truetype_cstruct(lua_State *L) { if (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_CSTRUCT) != LUA_TUSERDATA) { lua_pop(L, 1); return NULL; } struct truetype_font *ret = (struct truetype_font *)lua_touserdata(L, -1); lua_pop(L, 1); return ret; } static inline int lget_fontdata(lua_State *L) { int fontid = (int)lua_tointeger(L, 1); if (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_ID) != LUA_TTABLE) { return 0; } lua_geti(L, -1, fontid); return 1; } static inline const stbtt_fontinfo * default_info(struct truetype_font *ttf) { if (ttf->enable & 1) return &ttf->fontinfo[0]; return NULL; } // font id -> font info static inline const stbtt_fontinfo * truetype_font(struct truetype_font *ttf, int fontid, lua_State *L) { if (fontid < 1 || fontid > MAX_FONT_NUM) { fontid = 0; } else { --fontid; } if (ttf->enable & (uint64_t)1 << fontid) { return &ttf->fontinfo[fontid]; } if (L == NULL) { return default_info(ttf); } lua_pushcfunction(L, lget_fontdata); lua_pushinteger(L, fontid+1); if (lua_pcall(L, 1, 1, 0) != LUA_OK) { printf("TRUETYPE_ID err: %s\n", lua_tostring(L, -1)); lua_pop(L, 1); return default_info(ttf); } const stbtt_fontinfo * info = (const stbtt_fontinfo*)lua_touserdata(L, -1); lua_pop(L, 1); if (info == NULL) { return default_info(ttf); } return info; } static inline int lget_fontid(lua_State *L) { const char *name = (const char *)lua_touserdata(L, 1); if (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_NAME) != LUA_TTABLE) { return 0; } lua_getfield(L, -1, name); return 1; } // font name -> font id static inline int truetype_name(lua_State *L, const char *name) { lua_pushcfunction(L, lget_fontid); lua_pushlightuserdata(L, (void *)name); if (lua_pcall(L, 1, 1, 0) != LUA_OK) { printf("TRUETYPE_NAME err: %s\n", lua_tostring(L, -1)); lua_pop(L, 1); return 0; } int fontid = 0; if (lua_type(L, -1) == LUA_TNUMBER) { fontid = (int)lua_tointeger(L, -1); } lua_pop(L, 1); return fontid; } static inline int lenum_fontname(lua_State *L) { int idx = lua_tointeger(L, 1); if (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_ENUM) != LUA_TTABLE) { return 0; } if (lua_geti(L, -1, idx) != LUA_TSTRING) { return 0; } return 1; } static inline int truetype_enum(lua_State *L, int idx, char buffer[], int buffer_sz) { lua_pushcfunction(L, lenum_fontname); lua_pushinteger(L, idx); if (lua_pcall(L, 1, 1, 0) != LUA_OK) { printf("TRUETYPE_ENUM err: %s\n", lua_tostring(L, -1)); lua_pop(L, 1); return -1; } if (lua_isstring(L, -1)) { size_t sz = 0; const char *name = lua_tolstring(L, -1, &sz); if (sz >= buffer_sz) { return (int)sz; } memcpy(buffer, name, sz+1); lua_pop(L, 1); return (int)sz; } else { lua_pop(L, 1); return 0; } } static inline int import_font(lua_State *L) { if (lua_getfield(L, LUA_REGISTRYINDEX, TRUETYPE_IMPORT) != LUA_TFUNCTION) { return 0; } const char * data = (const char *)lua_touserdata(L, 1); size_t sz = lua_tointeger(L, 2); lua_pushlstring(L, data, sz); lua_call(L, 1, 0); return 0; } static inline void truetype_import(lua_State *L, void* fontdata, size_t sz) { lua_pushcfunction(L, import_font); lua_pushlightuserdata(L, fontdata); lua_pushinteger(L, sz); if (lua_pcall(L, 2, 0, 0) != LUA_OK) { printf("TRUETYPE_IMPORT err: %s\n", lua_tostring(L, -1)); lua_pop(L, 1); } } #endif ================================================ FILE: src/version.h ================================================ #ifndef soluna_version_h #define soluna_version_h #define SOLUNA_API_VERSION 3 #ifndef SOLUNA_HASH_VERSION #define SOLUNA_HASH_VERSION "dev" #endif #endif ================================================ FILE: src/winfile.c ================================================ #include #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__) #include FILE * fopen_utf8(const char *filename, const char *mode) { WCHAR filenameW[FILENAME_MAX + 0x200 + 1]; int n = MultiByteToWideChar(CP_UTF8,0,(const char*)filename,-1,filenameW,FILENAME_MAX + 0x200); if (n == 0) return NULL; WCHAR modeW[128]; n = MultiByteToWideChar(CP_UTF8,0,(const char*)mode,-1,modeW, 127); if (n == 0) return NULL; return _wfopen(filenameW, modeW); } #else FILE * fopen_utf8(const char *filename, const char *mode) { return fopen(filename, mode); } #endif ================================================ FILE: src/writelog.c ================================================ #include #include #include #include #include #include #include #include #include "loginfo.h" static void write_timestamp(uint64_t ti) { time_t timer = ti / 100; int msec = ti % 100; char buffer[26]; struct tm* tm_info = localtime(&timer); strftime(buffer, 26, "%Y-%m-%d %H:%M:%S", tm_info); printf("[%s.%02d]", buffer, msec); } static int log_write_sokol(lua_State *L) { static const char *level[] = { "PANIC", "ERROR", "WARN", "INFO", }; uint64_t ti = lua_tointeger(L, 1); struct log_info *info = (struct log_info *)lua_touserdata(L, 2); if (info->log_level > 3) info->log_level = 3; write_timestamp(ti); printf("[%-5s]( %s:%d )", level[info->log_level], info->tag, info->log_item ); if (info->filename) { printf(" %s : (%d)", info->filename, info->line_nr); } printf(" %s\n", info->message); free(info); return 0; } static int log_write_ltask(lua_State *L) { uint64_t ti = lua_tointeger(L, 1); const char *level = luaL_checkstring(L, 2); size_t sz; const char *msg = luaL_checklstring(L, 3, &sz); char upper[6]; int i; for (i=0;i<5;i++) { upper[i] = toupper(level[i]); if (*level == 0) break; } upper[5] = 0; write_timestamp(ti); if (strnlen(msg, sz+1) < sz) { printf("[%-5s] ", upper); int i; for (i=0;i struct zipreader_name { const char * zipfile; const char * root; size_t root_size; }; typedef void * zipreader_file; zipreader_file zipreader_open(struct zipreader_name *names, const char * filename); void zipreader_close(zipreader_file f); int zipreader_read(zipreader_file f, void *dst, int bytes); int zipreader_seek(zipreader_file f, int64_t offset, int origin); int64_t zipreader_tell(zipreader_file f); size_t zipreader_size(zipreader_file f); #endif ================================================ FILE: test/audio.game ================================================ entry : test/audio.lua ================================================ FILE: test/audio.lua ================================================ 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 sound_bus = assert(soluna.audio_bus "sound") local music_bus = assert(soluna.audio_bus "music") local BUTTON_W = 220 local BUTTON_H = 56 local BUTTON_GAP = 18 local BUTTON_COLS = 2 local BUTTON_ROWS = 5 local SHADOW_Y = 6 local BUTTON_TEXT_COLOR = 0xff28435c local STATUS_TEXT_COLOR = 0xffdce7f2 local BUTTON_PLAY_SHOT = 1 local BUTTON_TOGGLE_LOOP = 2 local BUTTON_STOP_LAST = 3 local BUTTON_STOP_LOOP = 4 local BUTTON_SEEK_START = 5 local BUTTON_SEEK_STEP = 6 local BUTTON_SOUND_DOWN = 7 local BUTTON_SOUND_UP = 8 local BUTTON_MUSIC_DOWN = 9 local BUTTON_MUSIC_UP = 10 local pointer_x = screen_w // 2 local pointer_y = screen_h // 2 local pressed = false local pressed_button local sound_volume = 1.0 local music_volume = 1.0 local last_voice local loop_voice local loop_time = 0.0 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 fontid = font_init() local button_text = mattext.block(font.cobj(), fontid, 24, BUTTON_TEXT_COLOR, "CV") local info_text = mattext.block(font.cobj(), fontid, 20, STATUS_TEXT_COLOR, "L") local labels = { [BUTTON_PLAY_SHOT] = button_text("Play Shot", BUTTON_W, BUTTON_H), [BUTTON_TOGGLE_LOOP] = button_text("Toggle Loop", BUTTON_W, BUTTON_H), [BUTTON_STOP_LAST] = button_text("Stop Last Voice", BUTTON_W, BUTTON_H), [BUTTON_STOP_LOOP] = button_text("Stop Loop Voice", BUTTON_W, BUTTON_H), [BUTTON_SEEK_START] = button_text("Seek Loop 0.00", BUTTON_W, BUTTON_H), [BUTTON_SEEK_STEP] = button_text("Seek Loop +0.20", BUTTON_W, BUTTON_H), [BUTTON_SOUND_DOWN] = button_text("Sound -", BUTTON_W, BUTTON_H), [BUTTON_SOUND_UP] = button_text("Sound +", BUTTON_W, BUTTON_H), [BUTTON_MUSIC_DOWN] = button_text("Music -", BUTTON_W, BUTTON_H), [BUTTON_MUSIC_UP] = button_text("Music +", BUTTON_W, BUTTON_H), } local function clamp(v, lo, hi) if v < lo then return lo elseif v > hi then return hi end return v end local function playing(voice) return voice ~= nil and voice:playing() end local function update_pointer(x, y) pointer_x = x pointer_y = y end local function button_rect(index) local total_w = BUTTON_COLS * BUTTON_W + (BUTTON_COLS - 1) * BUTTON_GAP local total_h = BUTTON_ROWS * BUTTON_H + (BUTTON_ROWS - 1) * BUTTON_GAP local origin_x = (screen_w - total_w) // 2 local origin_y = (screen_h - total_h) // 2 - 40 local col = (index - 1) % BUTTON_COLS local row = (index - 1) // BUTTON_COLS return origin_x + col * (BUTTON_W + BUTTON_GAP), origin_y + row * (BUTTON_H + BUTTON_GAP) end local function button_at(x, y) for i = 1, BUTTON_ROWS * BUTTON_COLS do local bx, by = button_rect(i) if x >= bx and x <= bx + BUTTON_W and y >= by and y <= by + BUTTON_H then return i end end end local function click_button(index) if index == BUTTON_PLAY_SHOT then last_voice = assert(soluna.play_sound("bloop", { volume = 0.25, pan = clamp(pointer_x / screen_w * 2.0 - 1.0, -1.0, 1.0), pitch = 0.95, })) return end if index == BUTTON_TOGGLE_LOOP then local voice = loop_voice if playing(voice) then voice:stop(0.1) if loop_voice == voice then loop_voice = nil end else loop_voice = assert(soluna.play_sound "bloop_loop") end return end if index == BUTTON_STOP_LAST then local voice = last_voice if voice then voice:stop() if last_voice == voice then last_voice = nil end end return end if index == BUTTON_STOP_LOOP then local voice = loop_voice if voice then voice:stop(0.1) if loop_voice == voice then loop_voice = nil loop_time = 0.0 end end return end if index == BUTTON_SEEK_START then local voice = loop_voice if voice then voice:seek(0.0) end return end if index == BUTTON_SEEK_STEP then local voice = loop_voice if voice then local now = voice:tell() or 0.0 voice:seek(now + 0.2) end return end if index == BUTTON_SOUND_DOWN then sound_volume = clamp(sound_volume - 0.1, 0.0, 1.0) sound_bus:set_volume(sound_volume) return end if index == BUTTON_SOUND_UP then sound_volume = clamp(sound_volume + 0.1, 0.0, 1.0) sound_bus:set_volume(sound_volume) return end if index == BUTTON_MUSIC_DOWN then music_volume = clamp(music_volume - 0.1, 0.0, 1.0) music_bus:set_volume(music_volume) return end if index == BUTTON_MUSIC_UP then music_volume = clamp(music_volume + 0.1, 0.0, 1.0) music_bus:set_volume(music_volume) end end local callback = {} function callback.window_resize(w, h) screen_w = w screen_h = h end function callback.mouse_move(x, y) update_pointer(x, y) end function callback.mouse_button(button, key_state) if button ~= 0 then return end if key_state == 1 then pressed_button = button_at(pointer_x, pointer_y) pressed = pressed_button ~= nil return end local index = button_at(pointer_x, pointer_y) if pressed and index == pressed_button then click_button(index) end pressed = false pressed_button = nil end function callback.touch_begin(x, y) update_pointer(x, y) pressed_button = button_at(x, y) pressed = pressed_button ~= nil end function callback.touch_moved(x, y) update_pointer(x, y) if button_at(x, y) ~= pressed_button then pressed = false end end function callback.touch_end(x, y) update_pointer(x, y) local index = button_at(x, y) if pressed and index == pressed_button then click_button(index) end pressed = false pressed_button = nil end function callback.touch_cancelled() pressed = false pressed_button = nil end function callback.frame() local last = last_voice local loop = loop_voice local last_playing = playing(last) local loop_playing = playing(loop) if loop_playing then loop_time = loop:tell() or loop_time else loop_time = 0.0 end local title = info_text("Audio API Sample", 400, 28) local subtitle = info_text("Play voices, seek a stream voice, and adjust sound/music buses.", 640, 24) local status_1 = info_text( string.format("sound bus %.1f | music bus %.1f", sound_volume, music_volume), 480, 24 ) local status_2 = info_text( string.format( "last voice %s | loop voice %s | loop time %.2f", last_playing and "playing" or "idle", loop_playing and "playing" or "idle", loop_time ), 640, 24 ) batch:add(title, (screen_w - 400) // 2, 40) batch:add(subtitle, (screen_w - 640) // 2, 72) batch:add(status_1, (screen_w - 480) // 2, screen_h - 90) batch:add(status_2, (screen_w - 640) // 2, screen_h - 62) for i = 1, BUTTON_ROWS * BUTTON_COLS do local bx, by = button_rect(i) local hovered = button_at(pointer_x, pointer_y) == i local active = pressed and pressed_button == i local face_y = by + (active and 4 or 0) local face_color = active 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, face_color), bx, face_y) batch:add(labels[i], bx, face_y) end end return callback ================================================ FILE: test/bundle.lua ================================================ local sb = require "soluna.spritebundle" local filecache = setmetatable({ __missing = {} }, { __index = sb.loadimage }) print_r(sb.load(filecache, "asset/sprites.dl")) local ltask = require "ltask" local s = ltask.uniqueservice "loader" ltask.call(s, "init", { max_sprite = 65536, texture_size = 1024 }) local b = ltask.call(s, "loadbundle", "asset/sprites.dl") print_r(b) ================================================ FILE: test/extlua/material/perspective_quad.lua ================================================ local render = require "soluna.render" local pqmat = require "ext.material.perspective_quad" local ctx = ... local state = ctx.state pqmat.set_material_id(ctx.id) local inst_buffer = render.buffer { type = "vertex", usage = "stream", label = "extlua-perspective-quad-instance", size = pqmat.instance_size * ctx.settings.draw_instance, } local bindings = render.bindings() bindings:vbuffer(0, inst_buffer) bindings:sampler(0, state.default_sampler) local cobj = pqmat.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() cobj:reset() 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 ================================================ FILE: test/extlua.game ================================================ entry : extlua.lua extlua_entry : extlua_init extlua_preload : sample extlua_material : perspective_quad extlua_material_path : extlua/material/?.lua ================================================ FILE: test/extlua.lua ================================================ -- bin/soluna.exe test/extlua.game local soluna = require "soluna" local foobar = require "ext.foobar" local matpq = require "ext.material.perspective_quad" print(foobar.hello()) soluna.set_window_title "extlua perspective quad" local args = ... local batch = args.batch local callback = {} local CARD_W = 160 local CARD_H = 196 local HALF_W = CARD_W * 0.5 local HALF_H = CARD_H * 0.5 local WHITE = 0xffffffff local function rgba(color) local a = color >> 24 & 0xff local r = color >> 16 & 0xff local g = color >> 8 & 0xff local b = color & 0xff return string.pack("BBBB", r, g, b, a) end local function create_canvas(width, height) local pixels = {} local clear = rgba(0) for i = 1, width * height do pixels[i] = clear end local canvas = {} function canvas.set_pixel(x, y, color) if x < 0 or x >= width or y < 0 or y >= height then return end pixels[y * width + x + 1] = rgba(color) end function canvas.to_content() return table.concat(pixels) end return canvas end local function make_card_sprite() local canvas = create_canvas(CARD_W, CARD_H) for y = 0, CARD_H - 1 do for x = 0, CARD_W - 1 do local r = 40 + x * 120 // (CARD_W - 1) local g = 56 + y * 140 // (CARD_H - 1) local b = 224 - y * 72 // (CARD_H - 1) canvas.set_pixel(x, y, 0xff000000 | r << 16 | g << 8 | b) end end for y = 0, CARD_H - 1 do for x = 0, CARD_W - 1 do if x < 3 or x >= CARD_W - 3 or y < 3 or y >= CARD_H - 3 then canvas.set_pixel(x, y, 0xffffffff) elseif x % 32 == 0 or y % 32 == 0 then canvas.set_pixel(x, y, 0x80ffffff) end end end soluna.preload { filename = "@extlua_perspective_card", content = canvas.to_content(), w = CARD_W, h = CARD_H, } return soluna.load_sprites { { name = "card", filename = "@extlua_perspective_card", }, } end local sprites = make_card_sprite() local card = assert(sprites.card) local function card_quad(theta) local dist = 460.0 local focal = 460.0 local c = math.cos(theta) local s = math.sin(theta) local corners = { { -HALF_W, -HALF_H }, { HALF_W, -HALF_H }, { -HALF_W, HALF_H }, { HALF_W, HALF_H }, } local quad = {} local q = {} for i = 1, 4 do local x = corners[i][1] local y = corners[i][2] local rx = x * c local rz = -x * s local w = dist + rz local scale = focal / w quad[#quad + 1] = rx * scale quad[#quad + 1] = y * scale q[i] = 1.0 / w end return quad, q end function callback.frame(count) local theta = math.sin(count * 0.021) * 1.15 local quad, q = card_quad(theta) batch:add(matpq.sprite(card, { quad = quad, q = q, color = WHITE, }), args.width * 0.5, args.height * 0.5) end return callback ================================================ FILE: test/file.lua ================================================ local file = require "soluna.file" local image = require "soluna.image" local lfs = require "soluna.lfs" print_r(image.info(file.load "asset/avatar.png")) print(lfs.realpath ".") for name in lfs.dir "." do print_r(name, lfs.attributes(name)) end ================================================ FILE: test/hello.game ================================================ entry : hello.lua a.b : 1 a.c : 2 ================================================ FILE: test/hello.lua ================================================ print "Hello World" ================================================ FILE: test/icon.lua ================================================ local image = require "soluna.image" local file = require "soluna.file" local soluna = require "soluna" local c = file.load "asset/lua-logo.png" local content, w, h = image.load(c) soluna.set_icon({ data = content, w = w, h = h }) local callback = {} function callback.frame(count) end return callback ================================================ FILE: test/image.lua ================================================ local image = require "soluna.image" local file = require "soluna.file" local c = file.load "asset/avatar.png" print(image.info(c)) local content, w, h = image.load(c) local x, y, cw, ch = image.crop(content, w, h) local img = image.new(cw,ch) local src_rect = image.canvas(content, w, h, x, y, cw, ch) image.blit(img:canvas(), src_rect) --img:write("crop.png") ================================================ FILE: test/ime.lua ================================================ -- To run this sample: -- bin/soluna.exe entry=test/ime.lua local soluna = require "soluna" local app = require "soluna.app" local mattext = require "soluna.material.text" local matquad = require "soluna.material.quad" local matmask = require "soluna.material.mask" local font = require "soluna.font" local file = require "soluna.file" local utf8 = utf8 local math = math local string = string local table = table local args = ... local batch = assert(args.batch) local KEY_LEFT = 263 local KEY_RIGHT = 262 local KEY_HOME = 268 local KEY_END = 269 local KEY_BACKSPACE = 259 local KEY_DEL = 261 local KEY_ENTER = 257 local KEYSTATE_PRESS = 1 local CHAR_BACKSPACE = 8 local CHAR_DELETE = 127 local FONT_SIZE = 32 local HELP_SIZE = 18 local BOX_WIDTH = 720 local BOX_HEIGHT = 84 local BOX_PADDING_X = 18 local BOX_PADDING_Y = 8 local BOX_RADIUS = 10 local CURSOR_BLINK = 30 local function load_font() if soluna.platform == "wasm" then local bundled_name = "Source Han Sans SC Regular" local bundled_path = "asset/font/SourceHanSansSC-Regular.ttf" local bundled_data = file.load(bundled_path) if bundled_data then font.import(bundled_data) local bundled_id = font.name(bundled_name) if bundled_id then return bundled_id, bundled_name end end end local sysfont = require "soluna.font.system" local candidates = { "WenQuanYi Micro Hei", -- Linux "Microsoft YaHei", -- Windows "Yuanti SC", -- macOS "Source Han Sans SC Regular", -- WASM } for _, name in ipairs(candidates) do local ok, data = pcall(sysfont.ttfdata, name) if ok and data then font.import(data) local fontid = font.name(name) if fontid then return fontid, name end end end error "No available system font for IME sample" end local function cache(f) return setmetatable({}, { __index = function(self, k) local v = f(k) self[k] = v return v end }) end local quad_cache = cache(function(key) local w, h, c = key:match "^(%-?%d+):(%-?%d+):(%x+)$" return matquad.quad(tonumber(w), tonumber(h), tonumber(c, 16)) end) local function cached_quad(w, h, c) local key = string.format("%d:%d:%08x", w, h, c) return quad_cache[key] end local mask_cache = cache(function(key) local sprite, color = key:match "^(%d+):(%x+)$" return matmask.mask(tonumber(sprite), tonumber(color, 16)) end) local function cached_mask(sprite, color) local key = string.format("%d:%08x", sprite, color) return mask_cache[key] end local function clamp(v, lo, hi) if v < lo then return lo elseif v > hi then return hi end return v end local function rounded_box_rgba(w, h, radius) local r = math.floor(clamp(radius, 0, math.min(w, h) * 0.5)) local edge = r * r local left = r local right = w - r local top = r local bottom = h - r local opaque = "\255\255\255\255" local transparent = "\255\255\255\0" local lines = {} for y = 0, h - 1 do local py = y + 0.5 local row = {} for x = 0, w - 1 do local px = x + 0.5 local qx = clamp(px, left, right) local qy = clamp(py, top, bottom) local dx = px - qx local dy = py - qy row[x + 1] = (dx * dx + dy * dy <= edge) and opaque or transparent end lines[y + 1] = table.concat(row) end return table.concat(lines) end local rounded_box_cache = {} local function rounded_box_sprite(w, h, radius) local key = string.format("%d:%d:%d", w, h, radius) local sprite = rounded_box_cache[key] if sprite then return sprite end local filename = "@" .. "ime_round_" .. key:gsub(":", "_") soluna.preload { filename = filename, content = rounded_box_rgba(w, h, radius), w = w, h = h, } local sprites = soluna.load_sprites { { name = "box", filename = filename, cw = w, ch = h, x = 0, y = 0, } } rounded_box_cache[key] = sprites.box return sprites.box end local fontid, font_name = load_font() local fontcobj = font.cobj() local text_block, text_cursor = mattext.block(fontcobj, fontid, FONT_SIZE, 0x000000, "LV") local help_block = mattext.block(fontcobj, fontid, HELP_SIZE, 0x222222, "LV") soluna.set_window_title "soluna ime sample" app.set_ime_font(font_name, FONT_SIZE) local state = { screen_w = args.width, screen_h = args.height, mouse_x = 0, mouse_y = 0, focused = true, caret_tick = 0, text = "", cursor = 0, suppress_control_char = nil, } local function char_count(s) return utf8.len(s) or 0 end local function clamp_cursor() local n = char_count(state.text) if state.cursor < 0 then state.cursor = 0 elseif state.cursor > n then state.cursor = n end end local function byte_offset_for_char(index_1based) return utf8.offset(state.text, index_1based) or (#state.text + 1) end local function insert_text(s) if not s or s == "" then return end local byte = byte_offset_for_char(state.cursor + 1) state.text = state.text:sub(1, byte - 1) .. s .. state.text:sub(byte) state.cursor = state.cursor + char_count(s) end local function delete_backward() if state.cursor <= 0 then return end local from = byte_offset_for_char(state.cursor) local to = byte_offset_for_char(state.cursor + 1) state.text = state.text:sub(1, from - 1) .. state.text:sub(to) state.cursor = state.cursor - 1 end local function delete_forward() local n = char_count(state.text) if state.cursor >= n then return end local from = byte_offset_for_char(state.cursor + 1) local to = byte_offset_for_char(state.cursor + 2) state.text = state.text:sub(1, from - 1) .. state.text:sub(to) end local function is_control_char(codepoint) return codepoint < 32 or (codepoint >= 127 and codepoint <= 159) end local function handle_control_delete(codepoint) if codepoint ~= CHAR_BACKSPACE and codepoint ~= CHAR_DELETE then return end if state.suppress_control_char == codepoint then state.suppress_control_char = nil return end state.suppress_control_char = nil if codepoint == CHAR_BACKSPACE then delete_backward() else delete_forward() end end local function decode_char_event(value) local t = type(value) if t == "number" then if is_control_char(value) then return nil, value end return utf8.char(value), nil end if t ~= "string" or value == "" then return nil, nil end local first = utf8.codepoint(value, 1, 1) if first and is_control_char(first) then return nil, first end return value, nil end local function box_rect() local w = math.min(BOX_WIDTH, math.max(320, state.screen_w - 48)) local h = BOX_HEIGHT local x = (state.screen_w - w) // 2 local y = (state.screen_h - h) // 2 return x, y, w, h end local function in_box(x, y, bx, by, bw, bh) return x >= bx and x <= bx + bw and y >= by and y <= by + bh end local callback = {} function callback.window_resize(w, h) state.screen_w = w state.screen_h = h end function callback.mouse_move(x, y) state.mouse_x = x state.mouse_y = y end function callback.mouse_button(button, key_state) if button ~= 0 or key_state ~= KEYSTATE_PRESS then return end local bx, by, bw, bh = box_rect() state.focused = in_box(state.mouse_x, state.mouse_y, bx, by, bw, bh) if not state.focused then app.set_ime_rect(nil) end end function callback.char(value) if not state.focused then return end local text_input, control = decode_char_event(value) if control then handle_control_delete(control) return end if not text_input then return end insert_text(text_input) clamp_cursor() state.caret_tick = 0 end function callback.key(keycode, key_state) if key_state ~= KEYSTATE_PRESS or not state.focused then return end if keycode == KEY_LEFT then state.cursor = state.cursor - 1 elseif keycode == KEY_RIGHT then state.cursor = state.cursor + 1 elseif keycode == KEY_HOME then state.cursor = 0 elseif keycode == KEY_END then state.cursor = char_count(state.text) elseif keycode == KEY_BACKSPACE then delete_backward() state.suppress_control_char = CHAR_BACKSPACE elseif keycode == KEY_DEL then delete_forward() state.suppress_control_char = CHAR_DELETE elseif keycode == KEY_ENTER then insert_text "\n" end clamp_cursor() state.caret_tick = 0 end function callback.frame() clamp_cursor() local bx, by, bw, bh = box_rect() local box_sprite = rounded_box_sprite(bw, bh, BOX_RADIUS) local tx = bx + BOX_PADDING_X local ty = by + BOX_PADDING_Y local tw = bw - BOX_PADDING_X * 2 local th = bh - BOX_PADDING_Y * 2 batch:add(cached_quad(state.screen_w, state.screen_h, 0xf2f2f2ff), 0, 0) batch:add(cached_mask(box_sprite, state.focused and 0xffffffff or 0xe8e8e8ff), bx, by) batch:add( cached_quad( math.max(bw - BOX_RADIUS * 2, 2), 2, state.focused and 0x1d6ef0ff or 0x9a9a9aff ), bx + BOX_RADIUS, by + bh - 2 ) local label = text_block(state.text, tw, th) batch:add(label, tx, ty) local cx, cy, cw, ch, n, descent = text_cursor(state.text, state.cursor, tw, th) state.cursor = n descent = descent or 0 if state.focused then app.set_ime_rect { x = tx + cx, y = ty + cy - descent, width = cw, height = ch, text_color = 0xff000000, } else app.set_ime_rect(nil) end state.caret_tick = (state.caret_tick + 1) % (CURSOR_BLINK * 2) if state.focused and state.caret_tick < CURSOR_BLINK then batch:add(cached_quad(math.max(cw, 2), ch, 0x111111ff), tx + cx, ty + cy) end local help = help_block("Click box, type with CJK.", state.screen_w - 32, 24) batch:add(help, 16, 16) end return callback ================================================ FILE: test/intersect.lua ================================================ local soluna = require "soluna" local quad = require "soluna.material.quad" local args = ... local batch = args.batch local callback = {} local mx = 0 local my = 0 function callback.mouse_move(x, y) mx, my = x, y end function callback.frame(count) local rad = count * math.pi / 180 -- scale x2, move to the center of screen batch:layer(2, args.width / 2, args.height / 2) -- rotate canvas batch:layer(rad) -- use (-50, -50) , center of quad (100,100) as original point batch:layer(-50, -50) local x, y = batch:point(mx, my) local color = 0xffffff if x >=0 and x < 100 and y >=0 and y < 100 then color = 0xff0000 end batch:add(quad.quad(100,100,color)) batch:layer() batch:layer() batch:layer() end return callback ================================================ FILE: test/layout.lua ================================================ local layout = require "soluna.layout" local datalist = require "soluna.datalist" local matquad = require "soluna.material.quad" local args = ... local hud = [[ id : screen padding : 10 direction : row gap : 10 left : width : 400 background : 0x40000000 right : flex : 1 gap : 10 node : flex : 0.7 background : 0x40ffffff node : flex : 0.3 background : 0x40ffffff ]] local dom = layout.load(datalist.parse_list(hud)) local screen = dom.screen local function calc_hub() screen.width = args.width screen.height = args.height return layout.calc(dom) end local draw_list = calc_hub() local function draw_hud() for _, obj in ipairs(draw_list) do args.batch:add(matquad.quad(obj.w, obj.h, obj.background), obj.x, obj.y) end end local callback = {} function callback.frame(count) draw_hud() end function callback.window_resize(w,h) args.width = w args.height = h draw_list = calc_hub() end return callback ================================================ FILE: test/mask.lua ================================================ -- To run this sample : -- bin/soluna.exe entry=test/sprite.lua local soluna = require "soluna" local ltask = require "ltask" local mask = require "soluna.material.mask" soluna.set_window_title "soluna sprite sample" local sprites = soluna.load_sprites "asset/sprites.dl" local args = ... local batch = args.batch local callback = {} function callback.frame(count) batch:add(mask.mask(sprites.avatar, 0x40000000), args.width / 2, args.height/2, 1, 0) end return callback ================================================ FILE: test/mtex.game ================================================ # a small texture size for testing texture_size : 64 entry : mtex.lua ================================================ FILE: test/mtex.lua ================================================ -- To run this sample : -- bin/soluna.exe test/mtex.game local soluna = require "soluna" local ltask = require "ltask" soluna.set_window_title "multiple texture" local bundle = {} local function color(r,g,b) local name = string.format("%x%x%x", r,g,b) bundle[#bundle+1] = { name = r << 8 | g << 4 | b, filename = "@" .. name, } return { filename = "@" .. name, content = string.pack("BBBB", r << 4, g << 4, b << 4, 255), w = 1, h = 1, } end local function colors() local results = {} local n = 1 for r = 0, 15 do for g = 0, 15 do for b = 0, 15 do results[n] = color(r,g,b) n = n + 1 end end end return results end soluna.preload(colors()) local rects = soluna.load_sprites(bundle) local args = ... local batch = args.batch local callback = {} function callback.frame(count) batch:layer(10, 100, 100) for i = 0, 63 do for j = 0,63 do batch:add(rects[i * 64 + j], i, j) end end batch:layer() end return callback ================================================ FILE: test/setting.lua ================================================ local soluna = require "soluna" print_r(soluna.settings()) ================================================ FILE: test/sprite.lua ================================================ -- To run this sample : -- bin/soluna.exe entry=test/sprite.lua local soluna = require "soluna" local ltask = require "ltask" soluna.set_window_title "soluna sprite sample" local sprites = soluna.load_sprites "asset/sprites.dl" soluna.preload { { filename = "@red", content = "\xff\0\0\xff", w = 1, h = 1, }, { filename = "@green", content = "\0\xff\0\xff", w = 1, h = 1, }, } local rects = soluna.load_sprites { { name = "red", filename = "@red", }, { name = "green", filename = "@green", } } local args = ... local batch = args.batch local callback = {} local rot = 0 local delta = math.rad(1) function callback.frame(count) batch:layer(100, args.width/2 , args.height/2) batch:layer(rot) batch:add(rects.red) batch:layer() batch:layer(-rot) batch:add(rects.green) batch:layer() batch:layer() rot = rot + delta batch:add(sprites.avatar, args.width / 2, args.height/2) end return callback ================================================ FILE: test/spritepack.lua ================================================ local spritemgr = require "soluna.spritemgr" -- texture size = 128 local bank = spritemgr.newbank(65536, 128) bank:add(32, 16) bank:add(64, 32) bank:add(96, 96) bank:add(96, 96) local texid, n = bank:pack() print("Pack",n,"from",texid) for i = 1, n do local r = bank:altas(texid + i - 1) for k,v in pairs(r) do r[k] = { x = v >> 32, y = v & 0xffffffff } end print_r(i, r) end ================================================ FILE: test/test.lua ================================================ print_r {"Hello World"} ================================================ FILE: test/text.lua ================================================ local soluna = require "soluna" local ltask = require "ltask" local mattext = require "soluna.material.text" local matquad = require "soluna.material.quad" local font = require "soluna.font" local file = require "soluna.file" local function font_init() if soluna.platform == "wasm" then local bundled_path = "asset/font/SourceHanSansSC-Regular.ttf" local bundled_data = file.load(bundled_path) if bundled_data then font.import(bundled_data) local bundled_id = font.name "Source Han Sans SC Regular" if bundled_id then return bundled_id end end end local sysfont = require "soluna.font.system" local candidates = { "WenQuanYi Micro Hei", -- Linux "Microsoft YaHei", -- Windows "Yuanti SC", -- macOS "Source Han Sans SC Regular", -- WASM } for _, name in ipairs(candidates) do local ok, data = pcall(sysfont.ttfdata, name) if ok and data then font.import(data) local fontid = font.name(name) if fontid then return fontid end end end error "No available system font for text sample" end soluna.set_window_title "soluna text sample" local args = ... local batch = args.batch local fontid = font_init() local fontcobj = font.cobj() local callback = {} local WIDTH = 200 local HEIGHT = 200 local screen_w = args.width local screen_h = args.height function callback.window_resize(w, h) screen_w = w screen_h = h end local TEXT = "Hello, 这是一条很长的句子。它会在文本区居中。" -- size 32; color 0; alignment center local block, cursor = mattext.block(fontcobj, fontid, 32, 0, "CV") local label = block(TEXT, WIDTH, HEIGHT) local CURSOR_N = 0 function callback.frame(count) local x = (screen_w - WIDTH) / 2 local y = (screen_h - HEIGHT) / 2 batch:add(matquad.quad(WIDTH, HEIGHT, 0x400000ff), x, y) batch:add(label, x, y) -- cursor local cx, cy, cw, ch, n = cursor(TEXT, CURSOR_N, WIDTH, HEIGHT) CURSOR_N = n batch:add(matquad.quad(cw, ch, 0xffffff), cx + x, cy + y) end function callback.key(keycode, state) if state == 1 then -- press if keycode == 262 then -- right CURSOR_N = CURSOR_N + 1 elseif keycode == 263 then -- left CURSOR_N = CURSOR_N - 1 else print(keycode) end end end return callback ================================================ FILE: test/version.lua ================================================ local soluna = require "soluna" print (soluna.version) ================================================ FILE: test/window.game ================================================ entry : window.lua ================================================ FILE: test/window.lua ================================================ -- To run this sample : -- bin/soluna.exe entry=test/window.lua -- bin/soluna.exe test/window.game local soluna = require "soluna" soluna.set_window_title "Soluna Sample" local callback = {} function callback.frame(count) end return callback ================================================ FILE: website/README.md ================================================ # Website [中文](./README.zh-CN.md) This directory contains the Astro-based website for Soluna Live Examples. It is responsible for: - rendering the website pages - generating API documentation pages from `../docs/` - generating online example pages from `../test/` - using the repository `../README.md` as the homepage content - packaging `asset.zip` from `../asset/` The core WebAssembly runtime is not built here. `soluna.js`, `soluna.wasm`, and `sample.wasm` are produced by `luamake` from the repository root. ## Local development Build the web runtime from the root: ```bash luamake -compiler emcc luamake -compiler emcc sample ``` Then start the website from the `website/` directory: ```bash pnpm install pnpm run dev ``` ================================================ FILE: website/README.zh-CN.md ================================================ # Website [English](./README.md) 这个目录包含 Soluna 在线示例网站的 Astro 源码。 它负责: - 渲染网站页面 - 从 `../docs/` 生成 API 文档页面 - 从 `../test/` 生成在线示例页面 - 使用仓库根目录的 `../README.md` 作为首页内容 - 从 `../asset/` 打包 `asset.zip` 核心 WebAssembly 运行时并不在这里构建。`soluna.js`、`soluna.wasm` 和 `sample.wasm` 由仓库根目录的 `luamake` 生成。 ## 本地开发 先在仓库根目录构建 Web 运行时: ```bash luamake -compiler emcc luamake -compiler emcc sample ``` 然后进入 `website/` 目录启动站点: ```bash pnpm install pnpm run dev ``` ================================================ FILE: website/astro.config.mjs ================================================ import process from 'node:process' import { defineConfig } from 'astro/config' const site = process.env.SITE_URL || 'https://cloudwu.github.io' const configuredBase = process.env.SITE_BASE || '/' const base = configuredBase.endsWith('/') ? configuredBase : `${configuredBase}/` export default defineConfig({ site, base, output: 'static', }) ================================================ FILE: website/eslint.config.mjs ================================================ import antfu from '@antfu/eslint-config' export default antfu( { astro: true, typescript: true, stylistic: { semi: false, }, }, { rules: { 'style/semi': ['error', 'never'], }, }, ) ================================================ FILE: website/package.json ================================================ { "name": "website", "type": "module", "private": true, "packageManager": "pnpm@10.28.2", "scripts": { "prepare:runtime": "node scripts/prepare-runtime.mjs", "prepare:runtime:debug": "SOLUNA_MODE=debug node scripts/prepare-runtime.mjs", "dev": "pnpm run prepare:runtime && astro dev", "build": "pnpm run prepare:runtime && astro build", "build:pages": "SITE_BASE=/soluna/ pnpm run build", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "eslint . --fix", "preview": "astro preview" }, "dependencies": { "astro": "^5.16.8", "astro-theme-soluna": "workspace:*", "marked": "^15.0.12" }, "devDependencies": { "@antfu/eslint-config": "^4.19.0", "eslint": "^9.39.1", "eslint-plugin-astro": "^1.6.0" } } ================================================ FILE: website/packages/astro-theme-soluna/package.json ================================================ { "name": "astro-theme-soluna", "type": "module", "version": "0.1.0", "exports": { "./layouts/BaseLayout.astro": "./src/layouts/BaseLayout.astro", "./components/Footer.astro": "./src/components/Footer.astro", "./components/Hero.astro": "./src/components/Hero.astro", "./components/Menubar.astro": "./src/components/Menubar.astro", "./components/Nav.astro": "./src/components/Nav.astro", "./components/PlainList.astro": "./src/components/PlainList.astro", "./components/Section.astro": "./src/components/Section.astro", "./components/docs/DocsPage.astro": "./src/components/docs/DocsPage.astro", "./components/examples/ExampleListPage.astro": "./src/components/examples/ExampleListPage.astro", "./components/examples/ExamplePlayPage.astro": "./src/components/examples/ExamplePlayPage.astro", "./client/play": "./src/client/play.ts" }, "peerDependencies": { "astro": "^5.16.8" }, "dependencies": { "fflate": "^0.8.2" } } ================================================ FILE: website/packages/astro-theme-soluna/src/client/play.ts ================================================ import { strToU8, zipSync } from 'fflate' interface RuntimeModule { FS: { writeFile: (path: string, data: Uint8Array, options?: { canOwn?: boolean }) => void } FS_createPath: (root: string, path: string, canRead: boolean, canWrite: boolean) => void _soluna_runtime_quit?: () => void } interface PlayAppOptions { appFactory: (options: Record) => Promise appBaseUrl: string canvas: HTMLCanvasElement print: (text: string) => void printErr: (text: string) => void onAbort: (reason: unknown) => void } interface StartOptions { arguments: string[] files: Array<{ path: string data: Uint8Array canOwn?: boolean }> } interface PlayOptions { exampleSource: string exampleGameSettings?: string exampleRuntimeFiles?: RuntimeFile[] } interface RuntimeHandle { stop: () => void } interface RuntimeFile { path: string source: string } declare global { interface Window { SOLUNA_PLAY_ACTIVE?: RuntimeHandle } } function qs(selector: string, root: ParentNode = document): T | null { return root.querySelector(selector) } function normalizeBaseUrl(baseUrl: string): URL { const normalized = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/` return new URL(normalized, window.location.href) } function normalizeFileData(data: Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array { if (data instanceof Uint8Array) { return data } if (data instanceof ArrayBuffer) { return new Uint8Array(data) } return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) } function ensureAbsolutePath(path: string): string { if (!path.startsWith('/')) { throw new TypeError(`Expected an absolute FS path, got: ${path}`) } return path } function dirname(path: string): string { const normalized = ensureAbsolutePath(path) const index = normalized.lastIndexOf('/') if (index <= 0) { return '/' } return normalized.slice(0, index) } function ensureParentDirectory(runtimeModule: RuntimeModule, path: string): void { const dir = dirname(path) if (dir === '/') { return } runtimeModule.FS_createPath('/', dir.slice(1), true, true) } async function fetchArrayBuffer(url: string): Promise { const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to load ${url}`) } return response.arrayBuffer() } async function ensureCrossOriginIsolation(serviceWorkerUrl: string): Promise { if (window.crossOriginIsolated) { return true } if (!('serviceWorker' in navigator)) { return false } await navigator.serviceWorker.register(serviceWorkerUrl) if (!navigator.serviceWorker.controller) { window.location.reload() return false } return true } function installRuntimeFiles(runtimeModule: RuntimeModule, files: StartOptions['files']): void { files.forEach((file) => { ensureParentDirectory(runtimeModule, file.path) runtimeModule.FS.writeFile(file.path, file.data, { canOwn: file.canOwn }) }) } function resolveQuitApp(instance: RuntimeModule): (() => void) | undefined { if (typeof instance._soluna_runtime_quit === 'function') { return () => { instance._soluna_runtime_quit?.() } } } async function createRuntimeHandle( playOptions: PlayAppOptions, startOptions: StartOptions, ): Promise { const appBaseUrl = normalizeBaseUrl(playOptions.appBaseUrl) const instance = await playOptions.appFactory({ arguments: startOptions.arguments, canvas: playOptions.canvas, print: playOptions.print, printErr: playOptions.printErr, locateFile(path: string) { return new URL(path, appBaseUrl).toString() }, preRun: [ (runtimeModule: RuntimeModule) => { installRuntimeFiles(runtimeModule, startOptions.files) }, ], onAbort: (reason: unknown) => { playOptions.onAbort(reason) }, }) const quitApp = resolveQuitApp(instance) let stopped = false return { stop() { if (stopped) { return } stopped = true if (quitApp) { try { quitApp() } catch { // Ignore quit failures during teardown. } } }, } } function setStatus(text: string): void { const status = qs('#play-status') if (status) { status.textContent = text } } function setNote(text: string): void { const note = qs('#play-note') if (note) { note.textContent = text } } function setOverlayVisible(visible: boolean): void { const overlay = qs('#play-overlay') if (overlay) { overlay.classList.toggle('hidden', !visible) } } function resetConsole(): void { const consoleTarget = qs('#console-output') if (consoleTarget) { consoleTarget.textContent = '' } } function appendConsole(text: string, isError: boolean): void { const consoleTarget = qs('#console-output') if (!consoleTarget) { return } const line = document.createElement('div') line.textContent = text if (isError) { line.className = 'console-error' } consoleTarget.appendChild(line) consoleTarget.scrollTop = consoleTarget.scrollHeight } function createCanvas(): HTMLCanvasElement { const host = qs('#soluna-stage-host') if (!host) { throw new Error('Missing #soluna-stage-host.') } host.replaceChildren() const canvas = document.createElement('canvas') canvas.id = 'soluna-canvas' host.appendChild(canvas) return canvas } function setupCanvasResize(canvas: HTMLCanvasElement): () => void { const resize = () => { const rect = canvas.getBoundingClientRect() const ratio = window.devicePixelRatio || 1 canvas.width = Math.max(1, Math.floor(rect.width * ratio)) canvas.height = Math.max(1, Math.floor(rect.height * ratio)) } resize() return resize } function buildMainGame(exampleGameSettings = ''): string { const lines = [ 'entry : main.lua', 'high_dpi : true', 'text_sampler :', ' min_filter : linear', ' mag_filter : linear', 'extlua_entry : extlua_init', 'extlua_preload : sample', ] const settings = exampleGameSettings.trim() if (settings) { lines.push(settings) } lines.push('') return lines.join('\n') } function normalizeRuntimeFilePath(path: string): string { if (path.startsWith('/') || path.includes('\\')) { throw new Error(`Invalid runtime file path: ${path}`) } const parts = path.split('/') if (parts.some(part => part === '' || part === '.' || part === '..')) { throw new Error(`Invalid runtime file path: ${path}`) } return path } function buildMainZip(exampleSource: string, exampleGameSettings: string, runtimeFiles: RuntimeFile[]): Uint8Array { const entries: Record = { 'main.lua': strToU8(exampleSource), 'main.game': strToU8(buildMainGame(exampleGameSettings)), } runtimeFiles.forEach((file) => { const runtimePath = normalizeRuntimeFilePath(file.path) if (runtimePath === 'main.lua' || runtimePath === 'main.game') { throw new Error(`Reserved runtime file path: ${runtimePath}`) } entries[runtimePath] = strToU8(file.source) }) return zipSync(entries) } async function destroyActiveRuntime(): Promise { const runtime = window.SOLUNA_PLAY_ACTIVE window.SOLUNA_PLAY_ACTIVE = undefined runtime?.stop() } async function loadAppFactory(basePath: string): Promise { const runtimeUrl = new URL(`${basePath}runtime/soluna.js`, window.location.href).href const runtimeApi = await import(/* @vite-ignore */ runtimeUrl) if (typeof runtimeApi.default !== 'function') { throw new TypeError('soluna.js does not export createApp.') } return runtimeApi.default as PlayAppOptions['appFactory'] } async function ensureIsolation(basePath: string): Promise { if (window.crossOriginIsolated) { return true } if (!('serviceWorker' in navigator)) { setStatus('Cross-origin isolation required.') setNote('Service worker is unavailable on this browser.') return false } try { const isolated = await ensureCrossOriginIsolation(`${basePath}coi-serviceworker.min.js`) if (!isolated) { setStatus('Reloading for cross-origin isolation...') } return isolated } catch (error) { setStatus('Failed to register COI service worker.') setNote(error instanceof Error ? error.message : String(error)) return false } } async function loadRuntimeAssets( basePath: string, exampleSource: string, exampleGameSettings: string, exampleRuntimeFiles: RuntimeFile[], ) { setStatus('Preparing assets...') const assetBuffer = normalizeFileData(await fetchArrayBuffer(`${basePath}runtime/asset.zip`)) const sampleWasmPromise = fetchArrayBuffer(`${basePath}runtime/sample.wasm`) setStatus('Preparing fonts...') const fontEntries = { 'asset/font/arial.ttf': normalizeFileData(await fetchArrayBuffer(`${basePath}fonts/arial.ttf`)), 'asset/font/SourceHanSansSC-Regular.ttf': normalizeFileData( await fetchArrayBuffer(`${basePath}fonts/SourceHanSansSC-Regular.ttf`), ), } return { assetBuffer, fontZip: zipSync(fontEntries), mainZip: buildMainZip(exampleSource, exampleGameSettings, exampleRuntimeFiles), sampleWasmBuffer: normalizeFileData(await sampleWasmPromise), } } async function startRuntime( createApp: PlayAppOptions['appFactory'], basePath: string, canvas: HTMLCanvasElement, assets: Awaited>, ) { setStatus('Starting Soluna app...') return createRuntimeHandle( { appFactory: createApp, appBaseUrl: `${basePath}runtime/`, canvas, print(text) { appendConsole(String(text || ''), false) }, printErr(text) { appendConsole(String(text || ''), true) }, onAbort(reason) { setStatus('Runtime aborted.') setNote(String(reason || 'Unknown error')) }, }, { arguments: [ 'zipfile=/data/main.zip:/data/asset.zip:/data/font.zip', 'cpath=/data/?.wasm', ], files: [ { path: '/data/asset.zip', data: assets.assetBuffer, canOwn: true }, { path: '/data/main.zip', data: assets.mainZip, canOwn: true }, { path: '/data/font.zip', data: assets.fontZip, canOwn: true }, { path: '/data/sample.wasm', data: assets.sampleWasmBuffer, canOwn: true }, ], }, ) } export default async function initPlay(options: PlayOptions): Promise { const basePath = import.meta.env.BASE_URL const codeTarget = qs('#code-content') if (codeTarget) { codeTarget.textContent = options.exampleSource } setOverlayVisible(true) setStatus('Loading example source...') setNote('') resetConsole() await destroyActiveRuntime() let createApp: PlayAppOptions['appFactory'] try { createApp = await loadAppFactory(basePath) } catch (error) { setStatus('Failed to load soluna.js.') setNote(error instanceof Error ? error.message : String(error)) return } if (!(await ensureIsolation(basePath))) { return } let assets: Awaited> try { assets = await loadRuntimeAssets( basePath, options.exampleSource, options.exampleGameSettings ?? '', options.exampleRuntimeFiles ?? [], ) } catch (error) { const message = error instanceof Error ? error.message : String(error) if (message.includes('sample.wasm')) { setStatus('Failed to load external module sample.wasm.') } else if (message.includes('/fonts/')) { setStatus('Failed to load font assets.') } else if (message.includes('asset.zip')) { setStatus('Failed to load asset archive.') } else { setStatus('Failed to prepare runtime assets.') } setNote(message) return } const canvas = createCanvas() const resizeHandler = setupCanvasResize(canvas) window.addEventListener('resize', resizeHandler) try { const runtime = await startRuntime(createApp, basePath, canvas, assets) window.SOLUNA_PLAY_ACTIVE = { stop() { window.removeEventListener('resize', resizeHandler) runtime.stop() canvas.remove() }, } setOverlayVisible(false) } catch (error) { window.removeEventListener('resize', resizeHandler) canvas.remove() setStatus('Failed to start runtime.') setNote(error instanceof Error ? error.message : String(error)) } } ================================================ FILE: website/packages/astro-theme-soluna/src/components/Footer.astro ================================================
Soluna Web Lab / Live Engine Reference
================================================ FILE: website/packages/astro-theme-soluna/src/components/Hero.astro ================================================ --- interface Props { eyebrow: string title: string subtitle: string } const { eyebrow, title, subtitle } = Astro.props ---
{eyebrow}

{title}

{subtitle}

================================================ FILE: website/packages/astro-theme-soluna/src/components/Menubar.astro ================================================ ================================================ FILE: website/packages/astro-theme-soluna/src/components/Nav.astro ================================================ --- const basePath = import.meta.env.BASE_URL ---
================================================ FILE: website/packages/astro-theme-soluna/src/components/PlainList.astro ================================================ ================================================ FILE: website/packages/astro-theme-soluna/src/components/Section.astro ================================================ --- interface Props { title: string subtitle?: string subtitleHtml?: string } const { title, subtitle, subtitleHtml } = Astro.props ---

{title}

{subtitleHtml ?

: subtitle ?

{subtitle}

: null}
================================================ FILE: website/packages/astro-theme-soluna/src/components/docs/DocsPage.astro ================================================ --- import Menubar from '../Menubar.astro' interface DocBlock { signature: string | null docs: string[] annos: string[] } interface DocModule { module: string title: string blocks: DocBlock[] } interface Props { modules: DocModule[] } const { modules } = Astro.props const basePath = import.meta.env.BASE_URL function buildSegments(lines: string[]) { const segments: Array<{ type: 'paragraph' | 'list', lines: string[] }> = [] let paragraph: string[] = [] let list: string[] = [] const flushParagraph = () => { if (paragraph.length > 0) { segments.push({ type: 'paragraph', lines: [paragraph.join(' ')] }) paragraph = [] } } const flushList = () => { if (list.length > 0) { segments.push({ type: 'list', lines: list }) list = [] } } for (const line of lines) { if (line.startsWith('- ')) { flushParagraph() list.push(line.slice(2)) } else { flushList() paragraph.push(line) } } flushParagraph() flushList() return segments } ---

Docs

Soluna API reference.

home · contents · index

Contents

Index

{modules.map(module => (

{module.title}

{module.module.toLowerCase() !== module.title.toLowerCase() ?

{module.module}

: null} {module.blocks.map((block, index) => { const blockId = `${module.module}-${index + 1}` const segments = buildSegments(block.docs) return (

{block.signature ?? '@block'}

{segments.map(segment => segment.type === 'paragraph' ? ( segment.lines.map(line =>

{line}

) ) : (
    {segment.lines.map(line =>
  • {line}
  • )}
), )} {block.annos.length > 0 ? (
{block.annos.map(anno => `@${anno}`).join('\n')}
) : null}
) })}
))}
================================================ FILE: website/packages/astro-theme-soluna/src/components/examples/ExampleListPage.astro ================================================ --- import Menubar from '../Menubar.astro' interface ExampleItem { id: string title: string entry: string } interface Props { examples: ExampleItem[] } const { examples } = Astro.props const basePath = import.meta.env.BASE_URL ---

Examples

Gallery of Soluna test entries.

home ================================================ FILE: website/packages/astro-theme-soluna/src/components/examples/ExamplePlayPage.astro ================================================ --- import Menubar from '../Menubar.astro' interface ExampleItem { id: string title: string entry: string source: string gameSettings: string runtimeFiles: Array<{ path: string source: string }> } interface Props { example: ExampleItem examples: ExampleItem[] } const { example, examples } = Astro.props const basePath = import.meta.env.BASE_URL const sourceJson = JSON.stringify(example.source).replace(/

{example.title}

Entry: {example.entry}

gallery
Runtime Viewport
{example.entry}
Preparing Runtime

Loading assets...

Console

    
Example Source
main.lua
{example.source}
================================================ FILE: website/packages/astro-theme-soluna/src/layouts/BaseLayout.astro ================================================ --- import Footer from '../components/Footer.astro' import Nav from '../components/Nav.astro' import '../styles/theme.css' interface Props { title: string description?: string } const { title, description = 'Live documentation and browser-run examples for the Soluna 2D game engine.', } = Astro.props const pageTitle = title === 'Soluna' ? 'Soluna' : `${title} | Soluna` const siteUrl = Astro.site ?? new URL('https://cloudwu.github.io') const canonicalUrl = new URL(Astro.url.pathname, siteUrl) --- {pageTitle}
================================================ FILE: website/packages/astro-theme-soluna/src/styles/theme.css ================================================ :root { --page-bg: #f8f8f8; --paper: #ffffff; --border: #cccccc; --border-soft: #e2e2e2; --text: #000000; --muted: #444444; --accent: #000080; --accent-soft: #d0d0ff; --danger: #8a0000; --paper-width: 84em; --radius: 20px; --radius-small: 8px; --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; --sans: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", Helvetica, Arial, sans-serif; --heading: Verdana, Geneva, sans-serif; } * { box-sizing: border-box; } html { background-color: var(--page-bg); } body { margin: 0; min-height: 100vh; background-color: var(--page-bg); color: var(--text); font-family: var(--sans); line-height: 1.28; } a { color: var(--accent); text-decoration: none; } a:hover, a:focus-visible { background-color: var(--accent-soft); color: var(--accent); border-radius: 4px; text-decoration: none; } a:active { color: #ff0000; } p { margin: 1em 0; } ul, ol { margin: 1em 0; padding-left: 40px; } li { margin: 0.3em 0; } h1, h2, h3, h4 { color: var(--accent); font-family: var(--heading); font-weight: normal; font-style: normal; text-align: left; } h1 { margin: 0.6em 0 0.45em; font-size: 28pt; } h2 { margin: 1.25em 0 0.45em; font-size: 20pt; } h3 { margin: 1em 0 0.35em; font-size: 15pt; } h4 { margin: 1em 0 0.35em; font-size: 12pt; } code, pre { font-family: var(--mono); font-size: 12pt; } code { padding: 0 0.18em; } pre { margin: 1em 0; overflow: auto; text-align: left; white-space: pre-wrap; } pre code { padding: 0; } hr { display: none; } table hr { display: block; height: 1px; border: 0; background-color: #a0a0a0; color: #a0a0a0; } table { border: none; border-spacing: 0; border-collapse: collapse; } td, th { padding: 0; margin: 0; text-align: left; vertical-align: top; } img { max-width: 100%; background-color: var(--paper); } .page { min-height: 100vh; display: flex; flex-direction: column; } .nav, .footer { display: none; } main { flex: 1; width: 90%; max-width: var(--paper-width); margin: 16px auto; padding: 32px; border: 1px solid var(--border); border-radius: var(--radius); background-color: var(--paper); color: var(--text); line-height: 1.28; text-align: justify; } .readme-content { max-width: 72em; } .section { margin-top: 0; } .section-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 0.8em; } .section-title { margin: 0; color: var(--accent); font-family: var(--heading); font-size: 28pt; font-weight: normal; } .section-subtitle { max-width: 640px; margin: 0.65em 0 0; color: var(--muted); } .menubar { padding-bottom: 0.5em; } .menubar a:hover, .menubar a:focus-visible { margin: -3px; padding: 3px; } .plain-list { margin: 10px 0 0; padding-left: 20px; } .plain-list li { margin: 6px 0; } .grid { display: grid; gap: 10px; } .grid-3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } .grid-2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); } .card { padding: 0; border: none; background: transparent; } .card-title { margin: 0 0 6px; font-size: 1rem; } .card-meta { color: var(--muted); font-size: 0.8rem; } .card-desc { margin: 8px 0 0; color: var(--muted); } .search { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--radius-small); background-color: var(--paper); } .search-label { color: var(--muted); font-size: 0.7rem; } .search input { width: 100%; border: none; outline: none; background: transparent; color: var(--text); font-size: 1rem; } .docs-layout { display: grid; grid-template-columns: minmax(180px, 220px) 1fr; gap: 16px; margin-top: 16px; } .docs-nav { position: sticky; top: 20px; align-self: start; padding: 10px; border: 1px solid var(--border-soft); border-radius: 12px; background-color: var(--paper); } .docs-nav-title { margin-bottom: 12px; color: var(--muted); font-size: 0.85rem; } .docs-nav-list { display: flex; flex-direction: column; gap: 6px; max-height: 70vh; overflow: auto; } .docs-nav-list a { padding: 2px 0; color: var(--muted); } .docs-nav-list a:hover, .docs-nav-list a:focus-visible { color: var(--accent); } .docs-block { margin: 0 0 16px; padding-left: 10px; border-left: 2px solid #333333; } .docs-block h3 { margin: 0 0 6px; } .docs-block h3 code { font-family: inherit; font-size: inherit; } .docs-anno { margin: 1em 0; padding: 0; overflow-x: auto; color: var(--text); background: transparent; font-size: 12pt; line-height: 1.25; white-space: pre-wrap; } .example-index { margin: 1em 0; padding: 0; list-style: none; } .example-index li { display: grid; grid-template-columns: minmax(12em, 1fr) minmax(14em, 2fr); gap: 12px; align-items: baseline; margin: 0; padding: 0.35em 0; } .example-index code { color: var(--muted); font-size: 10.5pt; } .play-shell { display: grid; gap: 14px; } .play-tabs { display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 4px; } .play-tab { padding: 3px 7px; border-radius: 999px; color: var(--accent); white-space: nowrap; } .play-tab.active { background-color: var(--accent-soft); color: var(--accent); } .runtime-panel, .inspector-panel { overflow: hidden; border: 1px solid var(--border-soft); border-radius: 16px; background-color: var(--paper); } .panel-header { min-height: 34px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 12px; } .panel-title { color: var(--accent); font-family: var(--heading); font-size: 0.9rem; } .panel-meta { margin-top: 2px; color: var(--muted); font-family: var(--mono); font-size: 0.76rem; } .play-stage { position: relative; height: clamp(500px, 66vh, 760px); min-height: 420px; overflow: hidden; border-top: 1px solid var(--border-soft); background-color: var(--paper); } .play-stage canvas { width: 100%; height: 100%; display: block; } #soluna-stage-host { width: 100%; height: 100%; display: block; } .inspector-grid { display: grid; grid-template-columns: minmax(280px, 0.8fr) minmax(360px, 1.2fr); gap: 14px; align-items: start; } .console-body, .code-block { margin: 0; padding: 0 12px 12px; overflow: auto; background-color: var(--paper); color: var(--text); font-family: var(--mono); font-size: 10.5pt; line-height: 1.35; overflow-wrap: anywhere; text-align: left; white-space: pre-wrap; } .console-body { min-height: 13em; max-height: 22em; } .console-body:empty::before { content: "No output."; color: var(--muted); } .console-error { color: var(--danger); } .code-block { max-height: 38em; } .code-block code { padding: 0; } .overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 24px; background-color: rgba(255, 255, 255, 0.9); color: var(--text); text-align: center; } .overlay.hidden { opacity: 0; pointer-events: none; } .overlay-card { max-width: 420px; padding: 16px 18px; border: 1px solid var(--border-soft); border-radius: 16px; background-color: var(--paper); } .overlay-title { margin: 0 0 10px; color: var(--accent); font-family: var(--heading); font-size: 1.45rem; } .overlay-status { margin: 0; color: var(--muted); line-height: 1.6; } .note { margin-top: 8px; color: var(--danger); font-size: 0.85rem; } :target { margin: -8px; padding: 8px; border-radius: var(--radius-small); background-color: #f0f0f0; outline: none; } .is-hidden { display: none !important; } @media (max-width: 960px) { .docs-layout, .inspector-grid { grid-template-columns: 1fr; } .docs-nav { position: static; } } @media (max-width: 640px) { main { width: calc(100% - 16px); margin: 8px auto; padding: 16px 12px 40px; border-radius: 16px; } h1, .section-title { font-size: 22pt; } .section-header { align-items: flex-start; flex-direction: column; } .example-index li { grid-template-columns: 1fr; gap: 4px; } .play-stage { height: 58vh; min-height: 320px; } } ================================================ FILE: website/pnpm-workspace.yaml ================================================ packages: - packages/* ================================================ FILE: website/scripts/prepare-runtime.mjs ================================================ import { execFile } from 'node:child_process' import { copyFile, mkdir, rm, stat } from 'node:fs/promises' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import { promisify } from 'node:util' const execFileAsync = promisify(execFile) const websiteDir = path.resolve(fileURLToPath(new URL('..', import.meta.url))) const rootDir = path.resolve(websiteDir, '..') const websiteRuntimeDir = path.join(websiteDir, 'public', 'runtime') function resolveMode() { return process.env.SOLUNA_MODE || 'release' } function resolveRuntimePath(name, fallback) { const configuredPath = process.env[name] if (!configuredPath) { return fallback } if (path.isAbsolute(configuredPath)) { return configuredPath } return path.resolve(rootDir, configuredPath) } function exists(filePath) { return stat(filePath).then(() => true, () => false) } async function ensureFile(sourcePath, label) { if (!(await exists(sourcePath))) { throw new Error(`Missing ${label}: ${sourcePath}`) } } async function createAssetZip(outputPath) { await execFileAsync('zip', ['-qr', outputPath, 'asset'], { cwd: rootDir, }) } async function main() { const mode = resolveMode() const solunaJsPath = resolveRuntimePath('SOLUNA_JS_PATH', path.join(rootDir, 'bin', 'emcc', mode, 'soluna.js')) const solunaWasmPath = resolveRuntimePath('SOLUNA_WASM_PATH', path.join(rootDir, 'bin', 'emcc', mode, 'soluna.wasm')) const sampleWasmPath = resolveRuntimePath('SAMPLE_WASM_PATH', path.join(rootDir, 'bin', 'emcc', 'release', 'sample.wasm')) const solunaWasmMapPath = resolveRuntimePath( 'SOLUNA_WASM_MAP_PATH', path.join(rootDir, 'bin', 'emcc', mode, 'soluna.wasm.map'), ) await ensureFile(solunaJsPath, 'soluna.js') await ensureFile(solunaWasmPath, 'soluna.wasm') await ensureFile(sampleWasmPath, 'sample.wasm') await rm(websiteRuntimeDir, { recursive: true, force: true }) await mkdir(websiteRuntimeDir, { recursive: true }) await copyFile(solunaJsPath, path.join(websiteRuntimeDir, 'soluna.js')) await copyFile(solunaWasmPath, path.join(websiteRuntimeDir, 'soluna.wasm')) await copyFile(sampleWasmPath, path.join(websiteRuntimeDir, 'sample.wasm')) if (await exists(solunaWasmMapPath)) { await copyFile(solunaWasmMapPath, path.join(websiteRuntimeDir, 'soluna.wasm.map')) } await createAssetZip(path.join(websiteRuntimeDir, 'asset.zip')) process.stdout.write(`Prepared website runtime in ${websiteRuntimeDir}\n`) } main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`) process.exitCode = 1 }) ================================================ FILE: website/src/content.config.ts ================================================ import { defineCollection, z } from 'astro:content' import { loadDocs, loadExamples } from './lib/content' const examples = defineCollection({ loader: async () => loadExamples(), schema: z.object({ title: z.string(), entry: z.string(), source: z.string(), gameSettings: z.string(), runtimeFiles: z.array(z.object({ path: z.string(), source: z.string(), })), }), }) const docs = defineCollection({ loader: async () => loadDocs(), schema: z.object({ module: z.string(), title: z.string(), blocks: z.array( z.object({ signature: z.string().nullable(), docs: z.array(z.string()), annos: z.array(z.string()), }), ), }), }) export const collections = { docs, examples, } ================================================ FILE: website/src/lib/content.ts ================================================ import { readdir, readFile, stat } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' export interface ExampleRuntimeFile { path: string source: string } export interface ExampleEntry { id: string title: string entry: string source: string gameSettings: string runtimeFiles: ExampleRuntimeFile[] } export interface DocBlock { signature: string | null docs: string[] annos: string[] } export interface DocEntry { id: string module: string title: string blocks: DocBlock[] } const repoRoot = path.resolve(fileURLToPath(new URL('../../..', import.meta.url))) const testDir = path.join(repoRoot, 'test') const docsDir = path.join(repoRoot, 'docs') function trim(value: string): string { return value.trim() } export function titleize(name: string): string { return name .split(/[_\-\s]+/) .filter(Boolean) .map(part => part.slice(0, 1).toUpperCase() + part.slice(1)) .join(' ') } async function exists(filePath: string): Promise { return stat(filePath).then(() => true, () => false) } function normalizeGameSettings(source: string): string { const skippedKeys = new Set(['entry', 'extlua_entry', 'extlua_preload']) return source .split(/\r?\n/) .filter((line) => { const match = line.match(/^\s*([a-z_][\w.]*)\s*:/i) return !match || !skippedKeys.has(match[1]) }) .join('\n') .trim() } async function loadGameSettings(id: string): Promise { const filename = path.join(testDir, `${id}.game`) if (!(await exists(filename))) { return '' } return normalizeGameSettings(await readFile(filename, 'utf8')) } async function loadRuntimeFiles(root: string, prefix: string): Promise { if (!(await exists(root))) { return [] } const entries = await readdir(root, { withFileTypes: true }) const files = await Promise.all(entries.map(async (entry) => { const fullPath = path.join(root, entry.name) const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name if (entry.isDirectory()) { return loadRuntimeFiles(fullPath, relativePath) } if (!entry.isFile()) { return [] } return [{ path: relativePath, source: await readFile(fullPath, 'utf8'), }] })) return files.flat().sort((a, b) => a.path.localeCompare(b.path)) } export async function loadExamples(): Promise { const names = (await readdir(testDir)) .filter(name => name.endsWith('.lua')) .sort() const examples = await Promise.all( names.map(async (name) => { const id = name.slice(0, -4) const source = await readFile(path.join(testDir, name), 'utf8') const gameSettings = await loadGameSettings(id) const runtimeFiles = await loadRuntimeFiles(path.join(testDir, id), id) return { id, title: titleize(id), entry: `test/${name}`, source, gameSettings, runtimeFiles, } }), ) return examples } export async function loadDocs(): Promise { const names = (await readdir(docsDir)) .filter(name => name.endsWith('.lua')) .sort() const modules = await Promise.all( names.map(async (name) => { const moduleName = name.slice(0, -4) const fileContent = await readFile(path.join(docsDir, name), 'utf8') return { id: moduleName, module: moduleName, title: titleize(moduleName), blocks: parseDocFile(fileContent), } }), ) return modules } function parseDocFile(content: string): DocBlock[] { const blocks: DocBlock[] = [] let docLines: string[] = [] let annos: string[] = [] const flush = (signature: string | null) => { if (docLines.length === 0 && annos.every(anno => anno === 'meta' || anno.startsWith('meta '))) { docLines = [] annos = [] return } if (docLines.length === 0 && annos.length === 0) { return } blocks.push({ signature: signature ?? annotationSignature(annos), docs: docLines, annos, }) docLines = [] annos = [] } for (const line of content.split(/\r?\n/)) { if (line.startsWith('---@')) { annos.push(trim(line.replace(/^---@/, ''))) continue } if (line.startsWith('---')) { docLines.push(trim(line.replace(/^---\s?/, ''))) continue } const trimmed = trim(line) if (docLines.length > 0 || annos.length > 0) { if (trimmed === '') { flush(null) continue } flush(trimmed) } } flush(null) return blocks } function annotationSignature(annos: string[]): string | null { const anno = annos.find(anno => anno.startsWith('alias ') || anno.startsWith('class ') || anno.startsWith('type ') || anno.startsWith('field '), ) return anno ? `@${anno}` : null } ================================================ FILE: website/src/lib/readme.ts ================================================ import { readFile } from 'node:fs/promises' import { marked } from 'marked' const readmeUrl = new URL('../../../README.md', import.meta.url) const repoUrl = 'https://github.com/cloudwu/soluna' function normalizeGithubPath(pathname: string): string { if (pathname.startsWith('./')) { return pathname.slice(2) } if (pathname.startsWith('/../../')) { return pathname.slice('/../../'.length) } return pathname } function resolveReadmeHref(href: string, basePath: string): string { if ( href.startsWith('http://') || href.startsWith('https://') || href.startsWith('#') || href.startsWith('mailto:') ) { return href } if (href === './docs') { return `${basePath}docs/` } if (href === './test') { return `${basePath}examples/` } const normalized = normalizeGithubPath(href) if (normalized === 'LICENSE') { return `${repoUrl}/blob/master/LICENSE` } if (normalized.startsWith('.github/') || normalized.startsWith('docs/')) { return `${repoUrl}/tree/master/${normalized}` } if (normalized.startsWith('actions/') || normalized.startsWith('releases/')) { return `${repoUrl}/${normalized}` } return href } function rewriteReadmeLinks(markdown: string, basePath: string): string { return markdown.replace(/(!?\[[^\]]*\])\(([^)]+)\)/g, (_match, label, href) => { return `${label}(${resolveReadmeHref(href, basePath)})` }) } export async function renderReadme(basePath: string): Promise { const readme = await readFile(readmeUrl, 'utf8') const rewritten = rewriteReadmeLinks(readme, basePath) return marked.parse(rewritten) as string } ================================================ FILE: website/src/pages/docs/index.astro ================================================ --- import DocsPage from 'astro-theme-soluna/components/docs/DocsPage.astro' import BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro' import { getCollection } from 'astro:content' const modules = (await getCollection('docs')).map(entry => entry.data) --- ================================================ FILE: website/src/pages/examples/[id].astro ================================================ --- import ExamplePlayPage from 'astro-theme-soluna/components/examples/ExamplePlayPage.astro' import BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro' import { getCollection } from 'astro:content' const examples = (await getCollection('examples')) .map(entry => ({ id: entry.id, ...entry.data, })) .sort((left, right) => left.id.localeCompare(right.id)) export async function getStaticPaths() { const entries = await getCollection('examples') return entries.map(entry => ({ params: { id: entry.id }, props: { example: { id: entry.id, ...entry.data, }, }, })) } const { example } = Astro.props --- ================================================ FILE: website/src/pages/examples/index.astro ================================================ --- import ExampleListPage from 'astro-theme-soluna/components/examples/ExampleListPage.astro' import BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro' import { getCollection } from 'astro:content' const examples = (await getCollection('examples')) .map(entry => ({ id: entry.id, ...entry.data, })) .sort((left, right) => left.id.localeCompare(right.id)) --- ================================================ FILE: website/src/pages/index.astro ================================================ --- import BaseLayout from 'astro-theme-soluna/layouts/BaseLayout.astro' import { renderReadme } from '../lib/readme' const html = await renderReadme(import.meta.env.BASE_URL) ---
================================================ FILE: website/tsconfig.json ================================================ { "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": "." } }