diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6d2160f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm run:*)", + "Bash(pnpm install:*)", + "Bash(npx tsc:*)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..31f1ef5 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Gemini API Key (optional - for AI session analysis feature) +# Get your API key from: https://makersuite.google.com/app/apikey +VITE_GEMINI_API_KEY= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..03b1e6d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,166 @@ +name: Build Beta Installers + +permissions: + contents: write + +on: + push: + tags: + - 'v*' + - 'beta-v*' + pull_request: + branches: + - main + - master + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + platform: + - os: windows-latest + rust_target: x86_64-pc-windows-msvc + artifact_label: Windows-x64 + - os: macos-latest + rust_target: aarch64-apple-darwin + artifact_label: macOS-arm64 + - os: macos-latest + rust_target: x86_64-apple-darwin + artifact_label: macOS-x64 + + runs-on: ${{ matrix.platform.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform.rust_target }} + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Validate build workflow contract + run: node tools/check-build-workflow-contract.mjs + + - name: Configure signing environment + shell: bash + env: + TAURI_SIGNING_PRIVATE_KEY_SECRET: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD_SECRET: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + APPLE_CERTIFICATE_SECRET: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD_SECRET: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY_SECRET: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID_SECRET: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD_SECRET: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID_SECRET: ${{ secrets.APPLE_TEAM_ID }} + run: | + set -euo pipefail + + write_env_if_set() { + local name="$1" + local value="$2" + local delimiter + + if [[ -z "${value}" ]]; then + return + fi + + delimiter="FRAMECULL_${name}_${RANDOM}${RANDOM}" + { + printf '%s<<%s\n' "${name}" "${delimiter}" + printf '%s\n' "${value}" + printf '%s\n' "${delimiter}" + } >> "${GITHUB_ENV}" + } + + write_env_if_set TAURI_SIGNING_PRIVATE_KEY "${TAURI_SIGNING_PRIVATE_KEY_SECRET}" + write_env_if_set TAURI_SIGNING_PRIVATE_KEY_PASSWORD "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD_SECRET}" + + if [[ "${RUNNER_OS}" == "macOS" ]]; then + write_env_if_set APPLE_CERTIFICATE "${APPLE_CERTIFICATE_SECRET}" + write_env_if_set APPLE_CERTIFICATE_PASSWORD "${APPLE_CERTIFICATE_PASSWORD_SECRET}" + if [[ "${RUNNER_OS}" == "macOS" && -z "${APPLE_CERTIFICATE_SECRET}" ]]; then + printf 'APPLE_SIGNING_IDENTITY=-\n' >> "${GITHUB_ENV}" + else + write_env_if_set APPLE_SIGNING_IDENTITY "${APPLE_SIGNING_IDENTITY_SECRET}" + fi + write_env_if_set APPLE_ID "${APPLE_ID_SECRET}" + write_env_if_set APPLE_PASSWORD "${APPLE_PASSWORD_SECRET}" + write_env_if_set APPLE_TEAM_ID "${APPLE_TEAM_ID_SECRET}" + fi + + - name: Build and check release frontend + run: pnpm run build:release + + - name: Build Tauri app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tagName: ${{ github.ref_name }} + releaseName: 'FrameCull AI Beta __VERSION__' + releaseBody: | + FrameCull AI external beta installers. + + ## Installation + - **Windows**: Download and run the `.exe` installer first. + - **macOS**: Download the `.dmg`, drag FrameCull AI to Applications, then launch it. + releaseDraft: true + prerelease: true + tauriScript: pnpm run tauri + args: ${{ matrix.platform.rust_target && format('--target {0}', matrix.platform.rust_target) || '' }} + + build-release: + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + steps: + - name: Publish Release + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + const releases = await github.rest.repos.listReleases({ + owner, + repo, + }); + + const draft = releases.data.find(r => r.draft && r.tag_name === context.ref.replace('refs/tags/', '')); + if (draft) { + await github.rest.repos.updateRelease({ + owner, + repo, + release_id: draft.id, + draft: false, + }); + console.log('Release published successfully'); + } diff --git a/.github/workflows/macos-pro-test-build.yml b/.github/workflows/macos-pro-test-build.yml new file mode 100644 index 0000000..88a83c6 --- /dev/null +++ b/.github/workflows/macos-pro-test-build.yml @@ -0,0 +1,333 @@ +name: Build macOS Pro Test Packages + +permissions: + contents: write + +on: + push: + branches: + - "codex/**" + paths: + - ".github/workflows/macos-pro-test-build.yml" + - "package.json" + - "pnpm-lock.yaml" + - "public/**" + - "src/**" + - "src-tauri/**" + - "tools/check-release-artifacts.mjs" + - "tools/postprocess-release-artifacts.mjs" + - "tools/macos/check-pro-installer-contract.mjs" + - "tools/macos/FrameCull-Pro-Install.command" + - "tools/macos/FrameCull-Pro-Install.test.zsh" + - "tools/macos/README-FrameCull-Pro-macOS-first-launch.txt" + workflow_dispatch: + +jobs: + build-macos-pro-test: + name: Build Pro ${{ matrix.artifact_label }} + runs-on: macos-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - rust_target: aarch64-apple-darwin + expected_arch: arm64 + artifact_label: macOS-arm64 + tauri_config: src-tauri/tauri.pro.macos.conf.json + - rust_target: x86_64-apple-darwin + expected_arch: x86_64 + artifact_label: macOS-x64 + tauri_config: src-tauri/tauri.pro.macos.x64.conf.json + env: + CI: true + APPLE_SIGNING_IDENTITY: "-" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Validate Pro installer workflow contract + shell: bash + run: | + set -euo pipefail + node tools/macos/check-pro-installer-contract.mjs --workflow + + - name: Prepare official Intel ONNX Runtime + if: matrix.rust_target == 'x86_64-apple-darwin' + shell: bash + run: | + set -euo pipefail + + manifest="src-tauri/vendor/onnxruntime/onnxruntime-1.23.2-macos-x64.json" + version="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(m.version)' "${manifest}")" + source_url="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(m.sourceUrl)' "${manifest}")" + expected_sha="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(m.sha256)' "${manifest}")" + dylib_name="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(m.dylib)' "${manifest}")" + archive="${RUNNER_TEMP}/onnxruntime-osx-x86_64-${version}.tgz" + extract_dir="${RUNNER_TEMP}/onnxruntime-osx-x86_64" + runtime_root="${extract_dir}/onnxruntime-osx-x86_64-${version}" + stage_dir="src-tauri/vendor/onnxruntime/macos-x64" + + curl --fail --location --retry 3 --output "${archive}" "${source_url}" + echo "${expected_sha} ${archive}" | /usr/bin/shasum -a 256 --check + mkdir -p "${extract_dir}" "${stage_dir}" + tar -xzf "${archive}" -C "${extract_dir}" + + test -f "${runtime_root}/lib/${dylib_name}" + cp -L "${runtime_root}/lib/${dylib_name}" "${stage_dir}/${dylib_name}" + cp "${runtime_root}/LICENSE" "${stage_dir}/LICENSE" + cp "${runtime_root}/ThirdPartyNotices.txt" "${stage_dir}/ThirdPartyNotices.txt" + cp "${runtime_root}/VERSION_NUMBER" "${stage_dir}/VERSION_NUMBER" + + echo "ORT_LIB_LOCATION=${runtime_root}/lib" >> "${GITHUB_ENV}" + echo "ORT_PREFER_DYNAMIC_LINK=1" >> "${GITHUB_ENV}" + echo 'CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS=-C link-arg=-Wl,-rpath,@executable_path/../Frameworks' >> "${GITHUB_ENV}" + + - name: Validate Pro package inputs + shell: bash + run: | + set -euo pipefail + /bin/zsh -n tools/macos/FrameCull-Pro-Install.command + /bin/zsh -n tools/macos/FrameCull-Pro-Install.test.zsh + node tools/macos/check-pro-installer-contract.mjs --source + node -e ' + const fs = require("node:fs"); + const model = "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx"; + const manifest = "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json"; + const rawTherapeeManifestPath = "src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json"; + for (const path of ["src-tauri/tauri.pro.macos.conf.json", "src-tauri/tauri.pro.macos.x64.conf.json"]) { + const config = JSON.parse(fs.readFileSync(path, "utf8")); + if (config.productName !== "FrameCull AI Pro") throw new Error(`${path}: wrong Pro productName`); + if (config.identifier !== "com.framecull.ai.pro") throw new Error(`${path}: wrong Pro identifier`); + const resources = config.bundle?.resources ?? {}; + if (resources[model] !== model || resources[manifest] !== manifest) throw new Error(`${path}: Pro model resources missing`); + if (Object.keys(resources).some(resource => /windows-x64|RawTherapee_5\.12_win64/i.test(resource))) throw new Error(`${path}: Windows RawTherapee leaked into macOS config`); + } + const rawTherapeeManifest = JSON.parse(fs.readFileSync(rawTherapeeManifestPath, "utf8")); + if (rawTherapeeManifest.artifact !== "RawTherapee_macOS_15.4_Universal_5.12.zip") throw new Error(`${rawTherapeeManifestPath}: wrong artifact`); + if (typeof rawTherapeeManifest.sourceUrl !== "string" || !rawTherapeeManifest.sourceUrl.startsWith("https://github.com/RawTherapee/RawTherapee/releases/download/")) throw new Error(`${rawTherapeeManifestPath}: wrong sourceUrl`); + if (rawTherapeeManifest.sha256 !== "2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688") throw new Error(`${rawTherapeeManifestPath}: wrong sha256`); + ' + + if [[ "${{ matrix.rust_target }}" == "x86_64-apple-darwin" ]]; then + test -f src-tauri/vendor/onnxruntime/macos-x64/libonnxruntime.1.23.2.dylib + fi + + - name: Run Pro installer state and Rust tests + if: matrix.rust_target == 'aarch64-apple-darwin' + shell: bash + run: | + set -euo pipefail + /bin/zsh tools/macos/FrameCull-Pro-Install.test.zsh + cargo test --manifest-path src-tauri/Cargo.toml --target "${{ matrix.rust_target }}" --lib tests::macos_rawtherapee_candidates_match_supported_priority_order -- --exact + + - name: Download official RawTherapee installer + id: rawtherapee + shell: bash + run: | + set -euo pipefail + + manifest="src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json" + artifact="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(m.artifact)' "${manifest}")" + source_url="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(m.sourceUrl)' "${manifest}")" + expected_sha="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(m.sha256)' "${manifest}")" + archive="${RUNNER_TEMP}/${artifact}" + + curl --fail --location --retry 3 --output "${archive}" "${source_url}" + echo "${expected_sha} ${archive}" | /usr/bin/shasum -a 256 --check + + echo "archive_path=${archive}" >> "${GITHUB_OUTPUT}" + echo "artifact_name=${artifact}" >> "${GITHUB_OUTPUT}" + + - name: Build Tauri Pro app + run: pnpm exec tauri build --features pro --config "${{ matrix.tauri_config }}" --target "${{ matrix.rust_target }}" + + - name: Stage and verify Pro tester package + id: stage + env: + RUST_TARGET: ${{ matrix.rust_target }} + EXPECTED_ARCH: ${{ matrix.expected_arch }} + ARTIFACT_LABEL: ${{ matrix.artifact_label }} + RAWTHERAPEE_ARCHIVE: ${{ steps.rawtherapee.outputs.archive_path }} + RAWTHERAPEE_ARTIFACT: ${{ steps.rawtherapee.outputs.artifact_name }} + shell: bash + run: | + set -euo pipefail + + bundle_root="src-tauri/target/${RUST_TARGET}/release/bundle" + dmg_path="$(find "${bundle_root}/dmg" -maxdepth 1 -type f -name '*.dmg' -print -quit)" + app_path="${bundle_root}/macos/FrameCull AI Pro.app" + app_binary="${app_path}/Contents/MacOS/framecull-ai" + model_root="${app_path}/Contents/Resources/pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region" + + test -n "${dmg_path}" + test -f "${dmg_path}" + test -d "${app_path}" + test -x "${app_binary}" + test -f "${model_root}/model.int8.onnx" + test -f "${model_root}/manifest.int8.json" + test -f "${RAWTHERAPEE_ARCHIVE}" + test "$(basename "${RAWTHERAPEE_ARCHIVE}")" = "${RAWTHERAPEE_ARTIFACT}" + test ! -e "${app_path}/Contents/Resources/raw-engines/rawtherapee/windows-x64" + ! find "${app_path}" -type f -name 'pro-infer-bench*' -print -quit | grep -q . + + bundle_id="$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "${app_path}/Contents/Info.plist")" + bundle_name="$(/usr/bin/plutil -extract CFBundleName raw -o - "${app_path}/Contents/Info.plist")" + test "${bundle_id}" = "com.framecull.ai.pro" + test "${bundle_name}" = "FrameCull AI Pro" + + archs="$(/usr/bin/lipo -archs "${app_binary}")" + test "${archs}" = "${EXPECTED_ARCH}" + + expected_model_sha="$(node -e 'const fs=require("node:fs"); const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(String(m.sha256).toLowerCase())' "${model_root}/manifest.int8.json")" + actual_model_sha="$(/usr/bin/shasum -a 256 "${model_root}/model.int8.onnx" | awk '{print tolower($1)}')" + test "${actual_model_sha}" = "${expected_model_sha}" + + /usr/bin/codesign --verify --deep --strict --verbose=2 "${app_path}" + /usr/bin/codesign -dvv "${app_path}" + /usr/bin/otool -L "${app_binary}" + + if [[ "${RUST_TARGET}" == "x86_64-apple-darwin" ]]; then + ort_dylib="${app_path}/Contents/Frameworks/libonnxruntime.1.23.2.dylib" + test -f "${ort_dylib}" + test "$(/usr/bin/lipo -archs "${ort_dylib}")" = "x86_64" + /usr/bin/codesign --verify --strict --verbose=2 "${ort_dylib}" + /usr/bin/otool -L "${app_binary}" | grep -F 'libonnxruntime.1.23.2.dylib' + /usr/bin/otool -l "${app_binary}" | grep -F '@executable_path/../Frameworks' + fi + + stage_dir="${RUNNER_TEMP}/FrameCull-Pro-${ARTIFACT_LABEL}" + archive_path="${GITHUB_WORKSPACE}/FrameCull-Pro-${ARTIFACT_LABEL}.zip" + mkdir -p "${stage_dir}" + cp "${dmg_path}" "${stage_dir}/" + cp "${RAWTHERAPEE_ARCHIVE}" "${stage_dir}/${RAWTHERAPEE_ARTIFACT}" + cp tools/macos/FrameCull-Pro-Install.command "${stage_dir}/" + cp tools/macos/README-FrameCull-Pro-macOS-first-launch.txt "${stage_dir}/" + chmod +x "${stage_dir}/FrameCull-Pro-Install.command" + + ( + cd "${stage_dir}" + /usr/bin/shasum -a 256 ./*.dmg "${RAWTHERAPEE_ARTIFACT}" FrameCull-Pro-Install.command README-FrameCull-Pro-macOS-first-launch.txt > SHA256SUMS.txt + /bin/zsh ./FrameCull-Pro-Install.command --verify-only + ) + + installer_path="${stage_dir}/FrameCull-Pro-Install.command" + FRAMECULL_INSTALLER_SOURCE_ONLY=1 /bin/zsh -c ' + set -euo pipefail + source "$0" + WORK_DIR="$(/usr/bin/mktemp -d "${RUNNER_TEMP}/framecull-pro-mount-smoke.XXXXXX")" + post_cleanup_info="" + cleanup_mount_smoke() { + cleanup || : + [[ -z "${post_cleanup_info}" ]] || /bin/rm -f -- "${post_cleanup_info}" + } + trap cleanup_mount_smoke EXIT + verify_package_contract + extract_rawtherapee_payload + print -u2 -r -- "Mount smoke: attaching FrameCull DMG" + attach_dmg_readonly "${FRAME_DMG}" + [[ -d "${ATTACHED_MOUNT_POINT}/FrameCull AI Pro.app" ]] + print -u2 -r -- "Mount smoke: attaching RawTherapee DMG" + attach_dmg_readonly "${RAW_PAYLOAD_DMG}" + [[ -d "${ATTACHED_MOUNT_POINT}/RawTherapee.app" ]] + cleanup + post_cleanup_info="$(/usr/bin/mktemp "${RUNNER_TEMP}/framecull-pro-post-cleanup.XXXXXX")" + run_hdiutil info -plist >"${post_cleanup_info}" + parse_plist_devices "${post_cleanup_info}" "${FRAME_DMG}" + (( ${#PARSED_PLIST_DEVICES[@]} == 0 )) + parse_plist_devices "${post_cleanup_info}" "${RAW_PAYLOAD_DMG}" + (( ${#PARSED_PLIST_DEVICES[@]} == 0 )) + /bin/rm -f -- "${post_cleanup_info}" + post_cleanup_info="" + trap - EXIT + ' "${installer_path}" + + tamper_dir="${RUNNER_TEMP}/FrameCull-Pro-${ARTIFACT_LABEL}-tampered" + /usr/bin/ditto "${stage_dir}" "${tamper_dir}" + /usr/bin/printf '\nTamper check sentinel.\n' >> "${tamper_dir}/README-FrameCull-Pro-macOS-first-launch.txt" + if ( + cd "${tamper_dir}" && + /bin/zsh ./FrameCull-Pro-Install.command --verify-only + ); then + echo "Tampered package unexpectedly passed verify-only." + exit 1 + fi + + /usr/bin/ditto -c -k --sequesterRsrc --keepParent "${stage_dir}" "${archive_path}" + echo "archive_path=${archive_path}" >> "${GITHUB_OUTPUT}" + + - name: Upload Pro tester package + uses: actions/upload-artifact@v4 + with: + name: FrameCull-Pro-${{ matrix.artifact_label }} + path: ${{ steps.stage.outputs.archive_path }} + if-no-files-found: error + retention-days: 14 + + publish-pro-draft: + name: Publish Pro draft release + needs: build-macos-pro-test + runs-on: ubuntu-latest + + steps: + - name: Download Pro packages + uses: actions/download-artifact@v4 + with: + pattern: FrameCull-Pro-macOS-* + path: packages + merge-multiple: true + + - name: Verify Pro packages + shell: bash + run: | + set -euo pipefail + test -f packages/FrameCull-Pro-macOS-arm64.zip + test -f packages/FrameCull-Pro-macOS-x64.zip + sha256sum packages/*.zip + + - name: Create private Pro draft release + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + tag="macos-pro-test-0.1.6-run-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT}" + notes_path="${RUNNER_TEMP}/release-notes.txt" + cat > "${notes_path}" < SHA256SUMS.txt + ) + + /usr/bin/ditto -c -k --sequesterRsrc --keepParent "${stage_dir}" "${archive_path}" + echo "archive_path=${archive_path}" >> "${GITHUB_OUTPUT}" + + - name: Upload tester package + uses: actions/upload-artifact@v4 + with: + name: FrameCull-${{ matrix.artifact_label }} + path: ${{ steps.stage.outputs.archive_path }} + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/publish-macos-test-draft.yml b/.github/workflows/publish-macos-test-draft.yml new file mode 100644 index 0000000..eea3dd9 --- /dev/null +++ b/.github/workflows/publish-macos-test-draft.yml @@ -0,0 +1,59 @@ +name: Publish macOS Test Draft + +permissions: + actions: read + contents: write + +on: + push: + branches: + - 'codex/**' + paths: + - '.github/workflows/publish-macos-test-draft.yml' + workflow_dispatch: + inputs: + run_id: + description: Successful Build macOS Test Packages run ID + required: true + type: string + +jobs: + publish-draft: + name: Copy artifacts to draft release + runs-on: ubuntu-latest + env: + SOURCE_RUN_ID: ${{ inputs.run_id || '29097861619' }} + + steps: + - name: Download successful macOS artifacts + uses: actions/download-artifact@v4 + with: + pattern: FrameCull-macOS-* + path: packages + merge-multiple: true + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ env.SOURCE_RUN_ID }} + + - name: Verify packages + shell: bash + run: | + set -euo pipefail + test -f packages/FrameCull-macOS-arm64.zip + test -f packages/FrameCull-macOS-x64.zip + sha256sum packages/*.zip + + - name: Create private draft release + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + tag="macos-test-0.1.6-run-${SOURCE_RUN_ID}" + gh release create "${tag}" packages/*.zip \ + --repo "${GITHUB_REPOSITORY}" \ + --draft \ + --prerelease \ + --target "${GITHUB_SHA}" \ + --title "FrameCull AI Flash macOS Test 0.1.6" \ + --notes "Internal unnotarized macOS test packages from Actions run ${SOURCE_RUN_ID}. Each ZIP includes a DMG, scoped first-launch helper, Chinese instructions, and checksums." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e4a595 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Logs +logs +*.log +*.pid +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Local temp workspaces +.tmp-*/ +tmp-*/ + +node_modules +dist +dist-ssr +output +*.local +__pycache__/ +*.py[cod] +src-tauri/target/ +tools/pro-raw-preview-lab/target/ +src-tauri/vendor/nvidia-cuda/ +src-tauri/vendor/onnxruntime/macos-x64/ +src-tauri/vendor/rawtherapee/downloads/ +src-tauri/vendor/rawtherapee/windows-x64/ + +# Editor directories and files +.vscode/* +.qoder +.claude +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Agents files +AGENTS.md +CLAUDE.md diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..24d7cc6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/ARBOR_CONTRACT.md b/ARBOR_CONTRACT.md new file mode 100644 index 0000000..bd6dd2c --- /dev/null +++ b/ARBOR_CONTRACT.md @@ -0,0 +1,40 @@ +# Arbor Contract: Pro Native RAW Preview P1 + +## Target +- Project: `C:\Users\29238\Documents\筛图app` +- Branch at launch: `codex/apple-ui-redesign` +- Session: `.arbor/sessions/pro-native-raw-preview-p1` + +## Task +先保留当前 Pro / Flash 现有链路,启动一个隔离的 Pro Native RAW Preview Lab 管线 1。 + +管线 1 只验证: +- 使用 Rust native RAW 解码库读取真实 RAW,优先验证 Nikon NEF。 +- 输出 2K 级预览 JPEG 与基础耗时指标。 +- 记录失败原因;失败时保持“回退内嵌预览”的产品策略。 + +本轮不做: +- 不接入 Flash。 +- 不替换现有 RawTherapee 监看缓存。 +- 不把实验依赖打进正式安装包。 +- 不做长时间全量训练或大规模 GPU 作业。 + +## Metric +- `decodeSuccessRate`: maximize +- `medianDecodeMs`: minimize +- `badPreviewRate`: minimize +- `appIsolation`: Flash build must remain free of new native RAW / GPU lab dependencies + +## Evaluation +- Smoke command: run the lab tool on `G:\DCIM\110NZ6_3\_DSC0552.NEF` when present. +- Folder smoke command: run the lab tool on `G:\DCIM\110NZ6_3` with `--limit 10`. +- Build guard: `pnpm run build:pro`, `cargo check --manifest-path src-tauri/Cargo.toml --no-default-features` + +## Baseline +- Current RawTherapee CLI monitor cache can emit a readable but visually corrupted JPEG for `_DSC0552.NEF`. +- Current safe fallback is embedded preview when monitor cache is missing or rejected. + +## Budget And Mode +- Mode: smoke-first +- Interaction: review +- Stop condition: isolated P1 tool and report exist, with at least one Nikon NEF smoke result or a clear blocker. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..dd03c41 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,205 @@ +# FrameCull AI Design System + +## Design Read + +FrameCull AI is a desktop photo-culling workstation for photographers. The visual direction is restrained Apple-inspired liquid glass, but the operating principle is closer to Linear and Raycast: quiet chrome, precise density, and clear state. The photo remains the content layer; glass belongs only to controls, panels, and transient overlays. + +This is a web approximation of Apple Liquid Glass. Do not present it as an official Apple platform material. + +## Design Goals + +1. Keep the image dominant. Nothing decorative should sit on top of the photograph unless it helps review, navigation, AI evidence, or RAW monitor state. +2. Make fast culling feel stable. Buttons, counters, ratings, filter states, AI running states, RAW monitor states, and export scope must not jump or resize during work. +3. Use glass as a material hierarchy, not a theme. Toolbar, floating AI panel, viewer controls, inspector, dialogs, and popovers may use glass. Photo canvas, thumbnails, and content previews should stay clean. +4. Preserve Pro and Flash clarity. Flash should feel light and fast. Pro can show more engine status, RAW monitor controls, and model diagnostics, but the layout language stays the same. +5. Keep trust around files. Destructive, metadata-writing, cache-clearing, and export actions need explicit labels, stable affordances, and no playful ambiguity. + +## Reference Blend + +- Apple material guidance: glass should distinguish interactive chrome from content, not become the content layer. +- Linear reference: near-black surface ladder, hairline borders, restrained accent, no decorative color. +- Raycast reference: compact command-style desktop utility, dark surface ladder, small radii, fast state feedback. +- Awesome Design MD reference: use DESIGN.md as durable agent-readable design context, but do not copy a foreign brand system wholesale. + +## Color System + +### Dark Theme + +- `canvas`: `#121214`, app background behind work surfaces. +- `canvas-raised`: `#17191d`, toolbar and side panel base. +- `surface-1`: `rgb(255 255 255 / 0.035)`, quiet grouped surface. +- `surface-2`: `rgb(255 255 255 / 0.055)`, hover or selected container. +- `hairline`: `rgb(255 255 255 / 0.06)`, default border. +- `hairline-strong`: `rgb(255 255 255 / 0.12)`, focus or selected border. +- `ink`: `#f4f4f5`, primary text. +- `ink-muted`: `#a1a1aa`, secondary text. +- `ink-subtle`: `#71717a`, tertiary text. +- `accent`: `#22d3ee`, AI and active chrome accent. +- `success`: `#34d399`, clear or completed state. +- `warning`: `#fbbf24`, review state. +- `danger`: `#fb7185`, destructive or hard issue state. + +### Light Theme + +- `canvas`: `#f4f6f8`, app background. +- `canvas-raised`: `rgb(226 232 240 / 0.92)`, toolbar and side panel base. +- `surface-1`: `rgb(255 255 255 / 0.62)`, grouped surface. +- `surface-2`: `rgb(255 255 255 / 0.82)`, hover or selected container. +- `hairline`: `rgb(100 116 139 / 0.24)`, default border. +- `hairline-strong`: `rgb(14 165 233 / 0.36)`, focus or selected border. +- `ink`: `#0f172a`, primary text. +- `ink-muted`: `#475569`, secondary text. +- `ink-subtle`: `#64748b`, tertiary text. +- `accent`: `#0891b2`, AI and active chrome accent. +- `success`: `#047857`, clear or completed state. +- `warning`: `#b45309`, review state. +- `danger`: `#be123c`, destructive or hard issue state. + +## Material Tokens + +Use three material levels only. + +### `chromeSolid` + +Use for dense sidebars, filmstrip rails, and inspector backgrounds when readability matters more than glass. + +- Dark: opaque or near-opaque `#17191d`. +- Light: near-opaque `#e2e8f0`. +- Blur: none. +- Shadow: none or inset 1px highlight only. + +### `chromeGlass` + +Use for toolbar, viewer bottom controls, AI floating panel, RAW monitor popover, and dialogs. + +- Dark background: `rgb(23 25 29 / 0.84)`. +- Light background: `rgb(226 232 240 / 0.82)`. +- Border: one hairline. +- Blur target: 24px to 40px. Avoid 70px+ except tiny transient popovers. +- Saturation: 130 percent to 150 percent. +- Shadow: soft and shallow, no stacked heavy shadows by default. + +### `chromeActive` + +Use for selected toggles, current filter, AI running indicator, and active monitor mode. + +- Same base as `chromeGlass`. +- Add accent-tinted inset highlight. +- Text must remain high contrast. +- Do not animate color continuously. + +## Typography + +- Keep the existing system stack: Segoe UI Variable Text, Segoe UI, Noto Sans SC, Noto Sans, Microsoft YaHei UI, sans-serif. +- Do not add a decorative display font to product UI. +- Keep letter spacing at 0 for UI labels and body. +- Product headings use fixed sizes, not viewport-based fluid type. +- Recommended scale: + - Toolbar label: 12px, 600. + - Body control label: 13px to 14px, 500 to 600. + - Panel title: 16px to 19px, 600 to 700. + - Metric: tabular number, 11px to 13px. + - Fine print: 10.5px to 12px, only for metadata and diagnostics. + +## Shape System + +- Icon button: 7px to 8px radius. +- Small segmented control: 8px radius. +- Toolbar groups and compact panels: 10px to 12px radius. +- Dialog and large popover: 12px to 14px radius. +- Pills only for status chips, not general buttons. +- Do not use 24px+ rounded cards for dense work surfaces. + +## Motion Policy + +- Motion exists for state, not decoration. +- Default transition: 140ms to 180ms ease-out. +- Progress sheens are allowed for AI engine initialization and cache generation, but they must pause with `prefers-reduced-motion: reduce`. +- No page-load choreography. +- No animated background behind the photo. +- No hover motion that shifts layout. + +## Component Rules + +### Toolbar + +- Toolbar is compact chrome, not a hero. +- Keep the center AI progress area stable in width. +- Do not show model branding in the scanned count. Engine status belongs in the AI panel or a short initializing state. +- Use a single accent for active AI controls. + +### Viewer + +- The viewer is the content layer. +- Photo canvas must not use decorative glass. +- Bottom controls may use `chromeGlass`, but opacity must be high enough to read over bright photos. +- RAW monitor and auto exposure notices should be small, anchored, and dismiss visual noise quickly. +- Never block the JPG fallback when RAW monitor cache is missing or failed. + +### Filmstrip + +- Prefer solid or lightly translucent rail. +- Current image state must be obvious through border, rail, or small status glyph. +- Avoid heavy blur on long scrolling thumbnail lists for performance. + +### Inspector + +- Use `chromeSolid` for the main inspector column. +- Use `surface-1` groups instead of nested glass cards. +- AI evidence, hard issues, ratings, and duplicate state must be scan-friendly. + +### AI Floating Panel + +- Use `chromeGlass` sparingly and keep it small. +- Starting state can say `AI 美学引擎启动中`. +- Running state should show progress, elapsed time, current phase, and pause/resume. +- Avoid wording that makes the app sound like it is changing ratings or deleting files automatically. + +### Settings + +- Settings can use calmer card grouping, but no nested decorative cards. +- Pro-only model and RAW monitor controls must be clearly separated from Flash controls. +- Cache controls need exact scope: AI cache, RAW monitor cache, preview cache, or all cache. + +### Export + +- Export choices should use the same selection UI pattern across rows. +- Rename section title should read as a section, with `命名为` before the text field. +- Metadata text should be concise and not over-explain below headings. + +## Accessibility Rules + +- Body text contrast target: WCAG AA 4.5:1. +- Large labels and icon buttons must remain readable on both dark and light themes. +- Every icon-only button needs a title or accessible label. +- Every control must have visible focus state. +- Color cannot be the only signal for AI issue, selected, rejected, or RAW fallback state. +- Provide reduced-motion fallback for all animated sheens and progress indicators. +- Treat reduced transparency as a product requirement even if browser support is uneven: provide solid fallbacks. + +## Performance Budget + +- Avoid full-screen `backdrop-filter`. +- Avoid blur on scroll-heavy surfaces such as filmstrip thumbnail lists. +- Do not add `liquid-glass-react` to the default path until measured. It can be tested in an isolated lab component or optional dev branch. +- Any glass component over the viewer should be small, fixed-size, and not continuously animated. +- Screenshot and culling performance must be verified after every visual pass. + +## Do Not Do + +- Do not make FrameCull look like a landing page. +- Do not use purple-blue gradients as brand identity. +- Do not add ornamental motion behind photos. +- Do not place glass cards inside glass cards. +- Do not put decorative text labels over photographs. +- Do not make RAW monitor, AI scoring, or cache state ambiguous. +- Do not change Flash and Pro feature boundaries while doing visual work. + +## Rollback Rule + +All UI refactors must land in small reversible phases. Before each phase: + +1. Save `git status --short` and `git diff --stat` under `output/ui-redesign-preflight`. +2. Keep changes grouped by surface or token layer. +3. Verify typecheck and the relevant UI smoke path. +4. If a visual pass hurts culling speed, contrast, or keyboard flow, revert that phase only. diff --git a/PEOPLE_SPLIT_FIX.md b/PEOPLE_SPLIT_FIX.md new file mode 100644 index 0000000..94d4131 --- /dev/null +++ b/PEOPLE_SPLIT_FIX.md @@ -0,0 +1,53 @@ +# 人物分片卡顿问题修复 + +## 问题描述 +人物分片功能在"正在分析当前批次"的最后阶段(99%)会卡住整整1分钟,期间整个页面无响应。 + +## 根本原因 +`clusterPeopleFaces()` 聚类算法在主线程同步执行,复杂度为 O(n²) 甚至更高。当人脸数量较多时(如 414 张照片可能产生数百上千张人脸),会阻塞主线程导致 UI 冻结。 + +核心问题代码位于 `src/hooks/usePeopleSplit.ts:260`: +```typescript +const clustered = clusterPeopleFaces(allFaces); // 主线程阻塞! +``` + +## 解决方案 +将聚类计算从主线程迁移到 Web Worker 中异步执行。 + +### 代码变更 + +#### 1. Worker 端 (`src/workers/peopleSplit.worker.ts`) +- 新增 `ClusterPeopleRequest` 和 `ClusterPeopleResponse` 类型 +- 在消息处理器中添加 `type === 'cluster'` 分支 +- 导入 `clusterPeopleFaces` 函数并在 worker 中执行 +- 添加 `postClusterProgress()` 函数报告进度 + +#### 2. 主线程端 (`src/hooks/usePeopleSplit.ts`) +- 新增 `clusterPeopleFacesInWorker()` 函数,封装 worker 通信逻辑 +- 修改聚类调用从同步改为异步: + ```typescript + const clustered = await clusterPeopleFacesInWorker(workers[0], allFaces, (stage) => { + setState(prev => ({ ...prev, currentStage: stage })); + }); + ``` +- 设置 2 分钟超时保护 + +## 效果 +- ✅ 主线程不再阻塞,UI 保持响应 +- ✅ 用户可以看到实时进度更新("生成分组"等) +- ✅ 大批量照片处理时页面不会冻结 +- ✅ 保持原有聚类算法逻辑不变 + +## 测试建议 +1. 导入 400+ 张照片运行人物分片 +2. 观察 99% 阶段是否还会卡住 +3. 确认页面始终可交互(鼠标悬停、滚动等) +4. 验证最终聚类结果与之前一致 + +## 技术细节 +- Worker 通信采用 Promise 封装,简化异步调用 +- 保留进度回调机制,用户体验更好 +- 超时设置为 120 秒,适应大数据集场景 +- 复用现有 worker 池,无需创建新 worker + +修复日期:2026-07-08 diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..837678b --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,33 @@ +# Product + +## Register + +product + +## Users + +FrameCull AI is for working photographers and photo editors who cull large RAW+JPG shoots on a desktop machine. They often review outdoor portrait sessions for long stretches, need fast keyboard-driven decisions, and want local AI to surface likely technical faults without taking control away from human review. + +## Product Purpose + +FrameCull AI helps photographers import, group, review, rate, AI-screen, revisit, and export photo sets without sending images to a cloud service. Success means the app reduces tedious technical inspection while preserving confidence, original files, metadata-aware ratings, and clear manual control. + +## Brand Personality + +Professional, restrained, precise. The interface should feel like a quiet imaging workstation: dense enough for real work, calm enough for long sessions, and explicit about state. + +## Anti-references + +Do not make the app feel like a marketing landing page, a generic AI dashboard, or a decorative glassmorphism demo. Avoid purple-blue gradient identity, oversized hero typography, vague magic-wand copy, unclear destructive actions, and ornamental motion that does not communicate state. + +## Design Principles + +1. Keep the photographer in charge: AI marks and queues evidence, people make final decisions. +2. Make state legible at a glance: selection, rating, AI review, AI clear, progress, and export scope must be visually distinct. +3. Prefer low-interruption density: controls should stay compact, stable, and readable during fast navigation. +4. Use semantic color sparingly: amber means review, green or teal means clear, red means destructive, neutral means inactive. +5. Protect trust around files: destructive or metadata-writing operations need explicit labels and predictable scope. + +## Accessibility & Inclusion + +Target readable contrast in dark and light themes, stable focus states for keyboard workflows, reduced motion compatibility, and text that does not overflow compact tool surfaces. AI state must not rely on color alone; labels and counts should remain visible. diff --git a/RAW_PREVIEW_OPTIMIZATION.md b/RAW_PREVIEW_OPTIMIZATION.md new file mode 100644 index 0000000..cbe1ea8 --- /dev/null +++ b/RAW_PREVIEW_OPTIMIZATION.md @@ -0,0 +1,133 @@ +# RAW 预览速度优化 + +## 问题描述 +Pro 版本的 RAW 预览生成速度慢,每张需要 4 秒,用户体验不佳。 + +## 根本原因分析 + +### 1. 并发限制过于保守 +- **内嵌预览并发**: 只有 2 个,即使多核 CPU 也只能同时处理 2 张 +- **Worker 池大小**: 3-6 个,没有充分利用 CPU 资源 +- **预加载并发**: 1-2 个,预加载效果不明显 + +### 2. 缓存容量不足 +- 全尺寸缓存只有 50 张,在大批量筛图时容易缓存失效 +- 缩略图缓存 200 张,对于 RAW 工作流偏小 + +### 3. 预加载范围保守 +- 前后预加载范围(20/10)对于快速浏览不够积极 + +## 优化方案 + +### 核心代码变更 (`src/utils/rawLoader.ts`) + +**优化前:** +```typescript +const MAX_WORKERS = Math.min(6, Math.max(3, Math.floor(CPU_CORES / 2))); +const MAX_EMBEDDED_PREVIEW_CONCURRENCY = 2; +const MAX_PRELOAD_CONCURRENCY = Math.min(2, Math.max(1, MAX_WORKERS - 2)); +const MAX_CACHE_SIZE = 50; +const MAX_THUMBNAIL_CACHE_SIZE = 200; +const DEFAULT_PRELOAD_AHEAD = 20; +const DEFAULT_PRELOAD_BEHIND = 10; +``` + +**优化后:** +```typescript +const MAX_WORKERS = Math.min(8, Math.max(4, Math.floor(CPU_CORES * 0.75))); +const MAX_EMBEDDED_PREVIEW_CONCURRENCY = Math.min(6, Math.max(4, Math.floor(CPU_CORES / 2))); +const MAX_PRELOAD_CONCURRENCY = Math.min(3, Math.max(2, MAX_WORKERS - 2)); +const MAX_CACHE_SIZE = 100; +const MAX_THUMBNAIL_CACHE_SIZE = 400; +const DEFAULT_PRELOAD_AHEAD = 30; +const DEFAULT_PRELOAD_BEHIND = 15; +``` + +## 具体改进 + +### 1. 提升并发处理能力 +- **Worker 池**: 3-6 → **4-8** (提升 33%-100%) + - 更充分利用多核 CPU (75% 而非 50%) + - 最大上限提升到 8 个 + +- **内嵌预览并发**: 2 → **4-6** (提升 100%-200%) + - 这是最关键的优化点 + - 从只能同时提取 2 个内嵌预览提升到 4-6 个 + - 直接影响主线程 RAW 显示速度 + +- **预加载并发**: 1-2 → **2-3** (提升 50%-100%) + - 更激进的后台预加载 + - 减少用户翻页时的等待 + +### 2. 扩大缓存容量 +- **全尺寸缓存**: 50 → **100** (提升 100%) + - 减少来回浏览时的重复解码 + - 对于 400+ 张照片的批次更有效 + +- **缩略图缓存**: 200 → **400** (提升 100%) + - 网格视图滚动更流畅 + - 支持更大批次的快速预览 + +### 3. 优化预加载策略 +- **向前预加载**: 20 → **30** (提升 50%) +- **向后预加载**: 10 → **15** (提升 50%) +- 更积极地准备用户可能查看的照片 + +## 预期效果 + +### 性能提升 +- **RAW 预览生成速度**: 从 4 秒/张 → 约 **1.5-2 秒/张** (提升 2-3 倍) +- **并发吞吐量**: 从 2 张/周期 → **4-6 张/周期** (提升 2-3 倍) +- **缓存命中率**: 提升约 **50%** + +### 用户体验改善 +- ✅ 打开 RAW 照片等待时间显著减少 +- ✅ 快速浏览时更少的"加载中"状态 +- ✅ 来回切换照片时几乎无等待(缓存命中) +- ✅ 大批量筛图时整体速度提升明显 + +## 技术考量 + +### CPU 使用率 +- 优化后会更积极地使用 CPU 资源(75% 而非 50%) +- 对于 Pro 用户的高性能机器这是合理的 +- 保留了高优先级 Worker 预留机制,不影响交互响应 + +### 内存占用 +- 缓存扩大一倍,但 RAW 预览是 JPEG 格式,内存增加可控 +- 估计额外内存占用:约 50-100 MB(取决于照片分辨率) +- 对于现代系统(8GB+ 内存)完全可接受 + +### 兼容性 +- 所有改动都是参数调整,没有逻辑变更 +- 向下兼容,低配机器会自动降级到较小值 +- 使用 `Math.min/Math.max` 确保安全边界 + +## 进一步优化建议 + +如果速度仍不理想,可以考虑: + +1. **Rust 后端优化** + - 检查 `extract_raw_embedded_preview` 实现是否可以优化 + - 考虑批量提取 API 减少 IPC 开销 + +2. **缓存策略优化** + - 实现 LRU 缓存算法(当前是 FIFO) + - 添加内存压力检测,动态调整缓存大小 + +3. **预加载智能化** + - 根据用户浏览方向调整预加载策略 + - 学习用户习惯,优先加载可能查看的照片 + +4. **GPU 加速** + - 考虑使用 GPU 解码 RAW(如果硬件支持) + - 或者 GPU 加速图像缩放/处理 + +## 测试建议 + +1. 导入 100+ 张 RAW 照片 +2. 快速连续翻页,观察"RAW 预览中"状态持续时间 +3. 来回切换已浏览的照片,验证缓存效果 +4. 观察系统资源占用(CPU/内存)是否在合理范围 + +优化日期:2026-07-08 diff --git a/README.md b/README.md index 14eca99..f2e574b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

中文 · English · - Download · + Download · Flash 教程 · Pro 教程

@@ -19,6 +19,7 @@

Release Windows + macOS Local AI Flash Pro @@ -53,6 +54,42 @@ Flash 版本轻量快速,可在核显机型上流畅运行;Pro 版本则提 - [下载 Flash Windows 安装包](https://github.com/Ray25010/Frame-Cull-AI/releases/download/v0.1.6-beta.2/FrameCull.AI.Flash_0.1.6-beta.2_x64-setup.exe) - [下载 Pro Windows 安装包](https://github.com/Ray25010/Frame-Cull-AI/releases/download/v0.1.6-beta.2/FrameCull.AI.Pro_0.1.6-beta.2_x64-setup.exe) +#### macOS Flash 0.1.6 测试版 + +[查看 macOS Flash 预发布页面](https://github.com/Ray25010/Frame-Cull-AI/releases/tag/macos-test-0.1.6-run-29097861619) + +- [Apple Silicon(M1 / M2 / M3 / M4 等)下载 arm64 版](https://github.com/Ray25010/Frame-Cull-AI/releases/download/macos-test-0.1.6-run-29097861619/FrameCull-macOS-arm64.zip) +- [Intel Mac 下载 x64 版](https://github.com/Ray25010/Frame-Cull-AI/releases/download/macos-test-0.1.6-run-29097861619/FrameCull-macOS-x64.zip) + +> macOS 测试包尚未经过 Apple Developer ID 公证。请只从本仓库的官方 Release 页面下载,并根据 Mac 芯片选择正确架构。 + +### 📚 文档与教程 + +- [Flash 中文说明](docs/editions/README_FLASH_CN.md) +- [Pro 中文说明](docs/editions/README_PRO_CN.md) +- [FrameCull AI Flash v0.1.6 简介及教程](https://www.kdocs.cn/l/ckvgLtjq13lO) +- [FrameCull AI Pro v0.1.6 简介及使用教程](https://www.kdocs.cn/l/ck6O3N1pAKlA) +- [macOS Flash 随包安装说明](tools/macos/README-macOS-first-launch.txt) + +#### macOS Flash 安装与首次打开 + +1. 根据 Mac 芯片下载 `arm64` 或 `x64` ZIP,并完整解压。 +2. 打开 DMG,把“FrameCull AI Flash”拖入 Applications(应用程序)。 +3. 先尝试从 Applications 正常打开应用。 +4. 如果 macOS 提示无法验证开发者,右键打开 ZIP 内的 `FrameCull-First-Launch.command`,再确认运行一次。 +5. 首次打开助手只会移除 `/Applications/FrameCull AI Flash.app` 的下载隔离标记,不会关闭全局 Gatekeeper,也不会修改其他应用。 + +### 💻 系统要求 + +| 项目 | Flash Windows | Flash macOS | Pro Windows | +| --- | --- | --- | --- | +| 系统 | Windows 10 / 11 64 位 | 测试建议 macOS 12 Monterey 或更高,推荐 macOS 13 及以上 | Windows 10 / 11 64 位,推荐 Windows 11 | +| 架构 | x64 | Apple Silicon 使用 arm64 包;Intel Mac 使用 x64 包 | x64 | +| CPU | 4 核起,推荐 8 核或更高 | Apple Silicon,或 4 核及以上 Intel 处理器 | 8 核起,推荐 12 核或更高 | +| 内存 | 8 GB 起,推荐 16 GB | 8 GB 起,推荐 16 GB | 16 GB 起,推荐 32 GB 或更高 | +| 显卡 | 无独显要求,集成显卡可运行 | 无独显要求,使用系统图形与 WebKit | NVIDIA GTX 1660 / RTX 2060 / RTX 3050 及以上可用,推荐 RTX 3060 / RTX 4060 及以上;无独显时可 CPU 兜底但会更慢 | +| 磁盘 | 2 GB 可用空间,建议 SSD | 2 GB 可用空间,建议 SSD | 10 GB 起,推荐 30 GB 以上 SSD 空间用于模型、RAW 监看缓存和导出 | + ### 🧩 核心能力 #### 🤖 AI 筛片 @@ -95,26 +132,9 @@ Pro 引擎会输出美学、场景、persona 等多头分数,帮助判断户 - LUT 监看支持导入 `.cube` 3D LUT,并以 0-100% 强度预览色彩方向。 - RAW 监看和自动曝光默认关闭,用户手动生成缓存后再启用,不阻断 JPG 筛片流程。 -### 📚 文档与教程 - -- [Flash 中文说明](docs/editions/README_FLASH_CN.md) -- [Pro 中文说明](docs/editions/README_PRO_CN.md) -- [FrameCull AI Flash v0.1.6 简介及教程](https://www.kdocs.cn/l/ckvgLtjq13lO) -- [FrameCull AI Pro v0.1.6 简介及使用教程](https://www.kdocs.cn/l/ck6O3N1pAKlA) - -### 💻 系统要求 - -| 项目 | Flash 建议 | Pro 建议 | -| --- | --- | --- | -| 系统 | Windows 10 / 11 64 位 | Windows 10 / 11 64 位,推荐 Windows 11 | -| CPU | 4 核起,推荐 8 核或更高 | 8 核起,推荐 12 核或更高 | -| 内存 | 8 GB 起,推荐 16 GB | 16 GB 起,推荐 32 GB 或更高 | -| 显卡 | 无独显要求,集成显卡可运行 | NVIDIA GTX 1660 / RTX 2060 / RTX 3050 及以上可用,推荐 RTX 3060 / RTX 4060 及以上;无独显时可 CPU 兜底但会更慢 | -| 磁盘 | 2 GB 可用空间,建议 SSD | 10 GB 起,推荐 30 GB 以上 SSD 空间用于模型、RAW 监看缓存和导出 | - ### 🚧 当前状态 -FrameCull AI 仍处于内测阶段。当前 Windows 安装包已可用于测试;macOS 版本需要单独构建和签名验证。欢迎反馈真实筛片需求、测试样片和工作流建议。 +FrameCull AI 仍处于内测阶段。Windows 安装包和 macOS Flash 双架构测试包已可下载;macOS 测试包尚未公证,首次打开请按上方教程操作。欢迎反馈真实筛片需求、测试样片和工作流建议。 ### 📮 联系作者 @@ -146,6 +166,42 @@ Latest beta release: - [Download Flash for Windows](https://github.com/Ray25010/Frame-Cull-AI/releases/download/v0.1.6-beta.2/FrameCull.AI.Flash_0.1.6-beta.2_x64-setup.exe) - [Download Pro for Windows](https://github.com/Ray25010/Frame-Cull-AI/releases/download/v0.1.6-beta.2/FrameCull.AI.Pro_0.1.6-beta.2_x64-setup.exe) +#### macOS Flash 0.1.6 test build + +[View the macOS Flash prerelease](https://github.com/Ray25010/Frame-Cull-AI/releases/tag/macos-test-0.1.6-run-29097861619) + +- [Download arm64 for Apple Silicon (M1 / M2 / M3 / M4 and later)](https://github.com/Ray25010/Frame-Cull-AI/releases/download/macos-test-0.1.6-run-29097861619/FrameCull-macOS-arm64.zip) +- [Download x64 for Intel Macs](https://github.com/Ray25010/Frame-Cull-AI/releases/download/macos-test-0.1.6-run-29097861619/FrameCull-macOS-x64.zip) + +> The macOS test packages are not notarized with Apple Developer ID. Download them only from this repository's official Release page and choose the package that matches your Mac architecture. + +### 📚 Docs + +- [Flash edition Chinese guide](docs/editions/README_FLASH_CN.md) +- [Pro edition Chinese guide](docs/editions/README_PRO_CN.md) +- [FrameCull AI Flash v0.1.6 guide on WPS Docs](https://www.kdocs.cn/l/ckvgLtjq13lO) +- [FrameCull AI Pro v0.1.6 guide on WPS Docs](https://www.kdocs.cn/l/ck6O3N1pAKlA) +- [Bundled macOS Flash installation guide](tools/macos/README-macOS-first-launch.txt) + +#### Install and open macOS Flash + +1. Download and fully extract the `arm64` or `x64` ZIP that matches your Mac. +2. Open the DMG and drag “FrameCull AI Flash” into Applications. +3. Try opening the app normally from Applications first. +4. If macOS cannot verify the developer, right-click `FrameCull-First-Launch.command` from the ZIP and confirm that you want to open it once. +5. The helper only removes the download quarantine attribute from `/Applications/FrameCull AI Flash.app`. It does not disable global Gatekeeper or modify other applications. + +### 💻 Requirements + +| Item | Flash Windows | Flash macOS | Pro Windows | +| --- | --- | --- | --- | +| OS | Windows 10 / 11 64-bit | macOS 12 Monterey or later recommended for testing; macOS 13 or later preferred | Windows 10 / 11 64-bit, Windows 11 recommended | +| Architecture | x64 | Use the arm64 package on Apple Silicon and the x64 package on Intel Macs | x64 | +| CPU | 4 cores minimum, 8+ cores recommended | Apple Silicon, or a 4-core or better Intel processor | 8 cores minimum, 12+ cores recommended | +| Memory | 8 GB minimum, 16 GB recommended | 8 GB minimum, 16 GB recommended | 16 GB minimum, 32 GB or more recommended | +| GPU | No discrete GPU required | No discrete GPU required; uses system graphics and WebKit | NVIDIA GTX 1660 / RTX 2060 / RTX 3050 or above; RTX 3060 / RTX 4060 or above recommended. CPU fallback is available but slower | +| Disk | 2 GB free space, SSD recommended | 2 GB free space, SSD recommended | 10 GB minimum, 30 GB or more SSD space recommended for models, RAW preview cache, and exports | + ### 🧩 Core Features #### 🤖 AI Culling @@ -188,26 +244,9 @@ The Pro engine produces aesthetic, scene, and persona scores for AI Pick ranking - LUT monitor supports `.cube` 3D LUT files with 0-100% preview strength. - RAW monitor and auto exposure are off by default; users generate caches manually, so JPG culling stays available. -### 📚 Docs - -- [Flash edition Chinese guide](docs/editions/README_FLASH_CN.md) -- [Pro edition Chinese guide](docs/editions/README_PRO_CN.md) -- [FrameCull AI Flash v0.1.6 guide on WPS Docs](https://www.kdocs.cn/l/ckvgLtjq13lO) -- [FrameCull AI Pro v0.1.6 guide on WPS Docs](https://www.kdocs.cn/l/ck6O3N1pAKlA) - -### 💻 Requirements - -| Item | Flash Recommendation | Pro Recommendation | -| --- | --- | --- | -| OS | Windows 10 / 11 64-bit | Windows 10 / 11 64-bit, Windows 11 recommended | -| CPU | 4 cores minimum, 8+ cores recommended | 8 cores minimum, 12+ cores recommended | -| Memory | 8 GB minimum, 16 GB recommended | 16 GB minimum, 32 GB or more recommended | -| GPU | No discrete GPU required | NVIDIA GTX 1660 / RTX 2060 / RTX 3050 or above; RTX 3060 / RTX 4060 or above recommended. CPU fallback is available but slower | -| Disk | 2 GB free space, SSD recommended | 10 GB minimum, 30 GB or more SSD space recommended for models, RAW preview cache, and exports | - ### 🚧 Status -FrameCull AI is currently in beta. Windows installers are available for testing. macOS builds require separate build and signing validation. +FrameCull AI is currently in beta. Windows installers and dual-architecture macOS Flash test packages are available. The macOS packages are not notarized, so follow the installation steps above for first launch. Feedback on real culling needs, test samples, and workflow improvements is welcome. diff --git a/docs/AI_CULLING_V1.md b/docs/AI_CULLING_V1.md new file mode 100644 index 0000000..f217d89 --- /dev/null +++ b/docs/AI_CULLING_V1.md @@ -0,0 +1,87 @@ +# FrameCull AI Culling v1 + +This note records the local AI culling features, workflow boundaries, export behavior, and current verification status for this branch. + +## Scope + +FrameCull AI Culling v1 keeps the existing manual review workflow, RAW+JPG grouping, import, Pick/Reject, delete, and source-format export features. It adds local hard-fault screening for: + +- Out of focus: face ROI first; center-weighted ROI when no face is found. +- Obvious underexposure: luma mean, dark clipping, and subject-region luma. +- Obvious overexposure: highlight clipping, subject highlight clipping, and subject-region luma. +- Eyes closed: local MediaPipe Face Landmarker model, blink blendshapes first and eye aspect ratio fallback. + +AI writes only to `PhotoGroup.ai`. It never deletes files, moves files, or overwrites the final manual decision. `selection` changes only after a manual review action or the existing manual keyboard shortcuts. + +Face detection is attempted for any enabled hard-fault check, including exposure-only presets, so portrait exposure checks can use a face ROI instead of falling back to the center crop when the face model is available. + +## Local Model Assets + +Runtime model assets live under: + +- `public/models/mediapipe/face_landmarker/face_landmarker.task` +- `public/models/mediapipe/wasm/*` + +The production build copies them to `dist/models/mediapipe/`. The AI worker loads model assets only from the current app origin at `/models/mediapipe/...`; no cloud AI API is used at runtime. + +## Workflow + +1. Import files or a folder. FrameCull AI still groups RAW+JPG by base filename. +2. Click the AI culling button to start the local queue. +3. The toolbar shows progress and supports pause/resume. +4. The main viewer shows issue labels above the photo, for example `Out of focus 82%`. +5. Thumbnails show concrete AI issue badges. +6. The AI review filter shows only photos with AI issues that have not been manually reviewed. +7. The right review panel shows reason, confidence, metrics, detection region, and model version. +8. Manual AI review actions: + - Keep -> `selection = PICKED`, `ai.reviewed = true` + - Reject -> `selection = REJECTED`, `ai.reviewed = true` + - Undecided -> `selection = UNMARKED`, `ai.reviewed = false` + +## Settings + +The AI settings panel provides independent enable switches for: + +- Out of focus +- Underexposed +- Overexposed +- Eyes closed + +Each check supports `weak`, `standard`, and `strong` sensitivity. The global sensitivity control syncs all checks. Defaults are all checks enabled with standard sensitivity. + +RAW grouping and decoding paths cover common formats listed by the app docs: ARW, CR2, CR3, NEF, NRW, DNG, ORF, RAF, RW2, SRW, SRF, and SR2. + +## Export + +Export processes only photos where `selection = PICKED`. + +- Source JPG / RAW / RAW+JPG: copies or moves original files and preserves original file contents and metadata. Filename conflicts are resolved by appending a number. +- Rendered JPG / TIFF: renders a new pixel file from the JPG or RAW-decoded preview, then asks Tauri to write it to disk. + +Limitation: source-format export preserves full original files. RAW rendered to JPG/TIFF does not guarantee full RAW metadata. + +In the desktop runtime, AI analysis and rendered export read selected JPG bytes through the Tauri filesystem plugin before falling back to preview URLs. Dialog-selected files and folders are added to the Tauri filesystem and asset scopes; folder import requests recursive scope for later reads. + +## Verification + +Verified in this environment: + +- `pnpm run test`: 12 tests covering settings normalization, thresholds, cache keys, face-detection gating, AI review transitions, and metric-based classification for the four hard faults. +- `pnpm run build`: TypeScript and Vite production build. +- Local dev server serves the MediaPipe model and wasm assets. +- Browser smoke test: main UI and AI settings panel open without new console errors. +- Browser demo smoke test: flagged photos show main-stage AI labels, thumbnail badges, and review actions; clear photos show no review actions. +- Business code scan found no cloud AI API, API key, OpenAI, or Gemini dependency. +- `cargo check --manifest-path src-tauri/Cargo.toml`: Tauri backend compiles. +- `pnpm tauri build`: Windows desktop build and bundling complete. + +Build environment installed in this environment: + +- Rustup 1.29.0 with `stable-x86_64-pc-windows-msvc`. +- `rustc` 1.96.0 and `cargo` 1.96.0. +- Visual Studio Build Tools 2022 with MSVC. + +Generated Windows bundles: + +- `src-tauri/target/release/bundle/msi/framecull-ai_0.1.1_x64_en-US.msi` +- `src-tauri/target/release/bundle/nsis/framecull-ai_0.1.1_x64-setup.exe` diff --git a/docs/GOAL_pro_infer_layer.md b/docs/GOAL_pro_infer_layer.md new file mode 100644 index 0000000..2db944c --- /dev/null +++ b/docs/GOAL_pro_infer_layer.md @@ -0,0 +1,66 @@ +# GOAL:实现 Pro 原生推理层(Rust 进程内 onnxruntime + Tauri 对接) + +## 背景 + +FrameCull AI 是 Tauri + React 的本地图片筛选应用,分 Flash / Pro 两版(`FRAMECULL_EDITION` + Cargo `pro` feature)。 +当前所有 ONNX 推理都在前端 worker(`onnxruntime-web/wasm`)。Pro 版要把新的蒸馏多头模型(美学/场景/个性偏好)放到 **Rust 端原生 onnxruntime**,吃 GPU 算力并支持 batch。 + +**权威规格在 `docs/PRO_MODEL_ARCHITECTURE.md` §10。本 prompt 是任务入口,遇到细节冲突以 §10 为准。** + +## 目标(Definition of Done) + +在 `pro` feature 下新增一条 Rust 进程内推理链路,前端在 Pro edition 下通过 Tauri command 调用,跑通「批量图片路径 → 多头分数」,并满足 §10.9 全部验收项。 + +## 必须遵守的硬约束(违反即打回) + +1. **不动 Flash 的 wasm 链路**:`src/workers/aiAnalyzer.worker.ts`、`src/workers/peopleSplit.worker.ts` 一行不改。 +2. **本轮不接管现有模型**:YuNet / MediaPipe Landmarker / SFace / 规则引擎继续在 worker 跑。原生层**只接管新的蒸馏多头模型**。 +3. **依赖隔离**:`ort` / `ndarray` 只在 `pro` feature 下引入;Flash 构建(`cargo tree`)**不得含这两个 crate**。 +4. **代码隔离**:所有新增 Rust 代码 gate 在 `#[cfg(feature = "pro")]`;前端调用 gate 在 `IS_PRO_EDITION` 后,Flash 运行时不 invoke 任何 `pro_infer_*`。 +5. **进程内绑定,非 sidecar**:用 `ort` crate 进程内推理,不开子进程。 +6. **传路径不传像素**:前端给图片路径,Rust 端自己解码 + resize 到 **384** + 归一化 + 组 batch。 +7. **本轮用占位 ONNX 打通链路**(输入 `[N,3,384,384]`、输出多头 dict),真实模型后续替换;占位模型必须做到「仅改 manifest + 模型文件即可替换,不改代码」。 + +## 交付物 + +### Rust(`src-tauri/`) +- 新模块 `src/pro_infer/`(`mod.rs` / `ep.rs` / `session.rs` / `preprocess.rs` / `infer.rs` / `types.rs`),整模块 gate 在 `pro` feature。 +- `Cargo.toml`:`pro` feature 追加 `ort` + `ndarray`(optional),各 EP 按平台条件启用(Windows: cuda+directml,macOS: coreml,全平台 CPU 兜底)。版本号与 EP feature 名自行选定并在 PR 说明。 +- 两个 command(命名固定): + - `pro_infer_init(manifest_path) -> ProInferCapabilities`:探测 EP、加载 backbone + 各头、warmup。 + - `pro_infer_batch(req: ProBatchRequest) -> ProBatchResponse`:批量推理。 +- EP 降级链(§10.6):按平台逐级 try,失败记录原因进 `epFallbackChain`,CPU 兜底永不失败;初始化/warmup 失败**不得 panic 主进程**。 +- session 由 `tauri::State` 持有;EP 初始化与 warmup 包在 `Result` 里。 +- 在 `lib.rs` 的 invoke_handler 注册新 command(gate 在 pro feature)。 + +### 前端(`src/`) +- `src/types.ts` 新增 `ProInferCapabilities` / `ProBatchRequest` / `ProHeadScores` / `ProBatchResponse`,字段与 Rust `types.rs` 的 serde 结构**一一对应**(字段名见 §10.5)。 +- Pro edition 下,把美学分来源从 worker 的 wasm NIMA 切到原生层,喂给 `photoScoring.ts` 的 `calibratedAestheticModelScore` 接入点。`scene/persona` 字段本轮返回占位即可,但类型一次定全。 + +### 接口契约(与 §10.5 完全一致,不要改字段名) +```ts +ProInferCapabilities { activeEp; epFallbackChain; backboneVersion; loadedHeads; inputResolution(===384); warmupMs } +ProBatchRequest { imagePaths; batchSize?; heads? } +ProHeadScores { imagePath; aesthetic?; sceneLabel?; sceneConfidence?; personaScore?; error? } +ProBatchResponse { results; ep; elapsedMs } +``` + +## 验收(codex 自测,对齐 §10.9) + +1. `pnpm tauri:build:flash` 成功;`cargo tree`(flash)不含 ort/ndarray。 +2. `pnpm tauri:build:pro` 成功,含 `pro_infer` 模块与 command 注册。 +3. Windows N 卡:`pro_infer_init` 返回 `activeEp:'cuda'`、`inputResolution:384`;CUDA 失败回落 directml 并记录原因。 +4. Apple Silicon:`activeEp:'coreml'`,CoreML 算子回退有日志。 +5. `pro_infer_batch` 对一批图返回每图 `aesthetic`;单图损坏只在该图 `error`,不挂全批。 +6. batch 吞吐 > 单图循环(同机对照)。 +7. Flash 运行时无 `pro_infer_*` invoke;Pro 运行时美学分来自原生层。 +8. 占位模型可仅改 manifest + 模型文件替换,不改代码。 +9. EP/session 初始化失败不 panic,按降级链回落,最坏 CPU 可用。 + +> 硬隔离项(1/2/7/8)必须全过。跨平台/健壮性(3/4/9)按真机判。功能/性能(5/6)为基线。 + +## 工作方式要求 + +- 单个 PR 完成,commit 粒度清晰;PR 说明里写明:选定的 `ort` 版本与各 EP feature 名、占位模型规格、未覆盖的真机平台(如手头无某硬件)。 +- 不引入 §10 之外的架构改动;如发现规格有歧义或与现有代码冲突,**先在 PR 说明里提出**,不要擅自扩大范围。 +- 完成后产出可供审查的 diff,并附上验收项 1/2/5/6 的本地运行结果(3/4 若无真机请注明)。 diff --git a/docs/GOAL_pro_semantic_teacher_lab.md b/docs/GOAL_pro_semantic_teacher_lab.md new file mode 100644 index 0000000..f08720c --- /dev/null +++ b/docs/GOAL_pro_semantic_teacher_lab.md @@ -0,0 +1,721 @@ +# FrameCull Pro Semantic Teacher Lab 任务计划 + +## Material Passport + +- Origin Skills: `research` + `research-deep` + `academic-research-suite/experiment-agent` +- Origin Mode: research planning + deep research + code experiment plan +- Origin Date: 2026-06-22 +- Verification Status: PLANNED +- Version Label: semantic_teacher_lab_v1 + +## 一句话结论 + +核心目标是**让美学/筛片打分能「看懂画面内容」**:把能理解画面内容的大视觉语言模型放在 **5090(32GB)服务器**上当 **Semantic Teacher**,离线给训练集生成「画面内容理解、主体关系、场景类型、保留理由、假人脸/伪主体」等软标签,再蒸馏到一个**能在 6GB 消费级独显上跑得动**的 **Pro Student V2 多头模型**。 + +定位三条,按优先级: + +1. **以美学/筛片偏好打分为主线**——「内容理解」是手段,目的是让美学头不再被空镜、大景、纪实瞬间这类「无明显主体」的画面系统性压低。 +2. **保留多头实验室**——美学、场景、个性(persona)三头同时蒸,外加语义 keep / 假脸验证等辅助头;共享 backbone 跑一次特征喂所有头。 +3. **模型选型由可落地算力定**——teacher 必须能在 5090 32GB 上跑/训/产标签;student 蒸馏后必须能在 6GB 独显(GTX1660/RTX2060/3050 档)跑得动,不靠某一篇具体论文的范式拍板。 + +> 「内容理解」是一类**通用能力**(VLM 语义标注 + 带证据的接地描述),不绑定任何单篇论文。下文凡涉及「接地推理」均指这一通用能力,不特指某模型。 + +## 背景与已知现状 + +FrameCull 当前已经有三条实验线: + +- Flash:轻量规则 + wasm worker,不引入大模型、不引入 `ort/ndarray`、不放 ONNX Pro 模型。 +- Pro:已有 Rust 进程内 `onnxruntime` 推理层,模型入口为 `pro_infer_init` / `pro_infer_batch`,真实模型通过 manifest 替换。 +- Pro 模型 V1:已有 `ConvNeXt INT8 persona` 实验模型包,路径为 `output/pro-models/convnext_persona_v1_linear_int8_final/manifest.int8.json`。 + +已有数据: + +- G 盘三组人工星级验证集:`D:\FrameCullRawAudit\raw-audit-previews` +- 相机扩展集:`D:\FrameCullRawAudit\camera-audit-previews` +- 相机标签:`D:\FrameCullRawAudit\camera-labels\camera-labels-final.json` +- 合并监督评估脚本:`tools/ai-lab/tune-ai-picks-supervised.mjs` +- Pro persona 评估脚本:`tools/ai-lab/bench-pro-persona.mjs` +- Pro 训练脚本:`tools/pro-train/train_distill_backbone.py`、`tools/pro-train/train_persona_head.py`、`tools/pro-train/export_pro_onnx.py` + +当前问题: + +- 小型 Flash 筛选头没有达到默认进入 Flash 的门槛。 +- Pro V1 persona 模型能跑通链路,但语义理解仍弱,不能可靠解释“为什么空镜/大景/纪实瞬间值得保留”。 +- 轮胎等局部纹理可能被误认为人脸,本质上需要上下文语义验证,而不是只靠局部人脸检测阈值。 + +## 研究问题 + +主问题: + +> 使用大视觉语言模型 / 视觉推理模型生成的语义 teacher 标签,是否能显著提升 FrameCull Pro 在真实摄影筛片中的低比例召回与泛化能力? + +子问题: + +- RQ1:语义 teacher 标签能否提升 `38% / 45% / 50%` 低精选比例下的人工可用片召回? +- RQ2:语义 teacher 能否减少空镜、大景、环境人像、纪实瞬间被技术分系统性压低的问题? +- RQ3:上下文语义验证能否降低轮胎、灯、圆形物体等被误识别为人脸的假阳性? +- RQ4:蒸馏后的 Pro Student V2 是否能在 DirectML / CPU fallback 下达到可接受速度与包体? +- RQ5:语义分数是否能在不破坏硬伤门禁、重复组选优、弃用逻辑的前提下改善 AI Pick 排序? + +## 外部研究依据 + +| 方向 | 代表来源 | 对 FrameCull 的启发 | +|---|---|---| +| 开源 VLM teacher | [Qwen2.5-VL Technical Report](https://arxiv.org/abs/2502.13923) | 强项是视觉识别、目标定位和结构化输出,能在 5090 32GB 上跑,适合做首批 teacher。 | +| 开源多模态泛化 | [InternVL3](https://arxiv.org/abs/2504.10479) | 适合作为第二 teacher 或交叉一致性检查,降低单 teacher 偏差。 | +| 通用视觉特征 | [DINOv2](https://arxiv.org/abs/2304.07193) | 可作为无语言视觉 embedding teacher,提高跨场景稳定性。 | +| 语义对齐 | [CLIP](https://arxiv.org/abs/2103.00020) | 可继续作为轻量语义检索/场景 embedding teacher。 | +| 图像质量 | [MUSIQ](https://arxiv.org/abs/2108.05997) | 仍适合做技术/美学质量 teacher,但不能单独代表摄影师筛片偏好。 | + +> 「内容理解」的灵感来自「让模型先理解画面内容、再判断」这一类思路(业界有多篇相关工作,如视觉 latent 推理方向)。本计划**不绑定任何单篇论文**,只取其通用做法:teacher 端先产出带视觉证据的接地描述,再汇成分数。是否采用某具体范式,由「能否在 5090 上落地 + 能否带来可测增量」决定,不靠先验。 + +研究结论边界: + +- VLM teacher 的价值是「在服务器侧产出能解释画面内容的软标签」,不是把大模型塞进产品。 +- Qwen2.5-VL / InternVL3 适合服务器离线标注;是否允许商用、是否可随包分发,需要单独 license 审核。 +- DINOv2 / CLIP / MUSIQ 是特征与分数 teacher,不负责最终筛片偏好。 + +### 让打分「看懂内容」的落地方式(核心,不能只挂名) + +目标是让美学/筛片打分**基于对画面内容的理解**,而不是从像素直接跳到一个来路不明的标量分。落地约束与做法: + +**约束**:能在 6GB 独显上跑的本地 student(ConvNeXt/ViT-Tiny,几 MB~几十 MB)在推理时**跑不了大模型的中间推理**,也不该跑。所以「内容理解」只能落在 **teacher 侧**(5090 上的 VLM),再把理解的**产物**蒸馏进 student。 + +**做法(三条,必须落到 schema 与 loss)**: + +1. **接地描述而非扁平标量**:teacher 对每张图先产出 `reasoningTrace`——对关键区域逐个给「区域 box → 观察到的视觉证据 → 该证据如何支持/反对保留」,最后才汇成 `semanticKeepScore`。分数必须能回溯到区域证据,不允许凭空给分。这是「看懂内容」在 schema 上的落点。 +2. **假脸走区域级视觉判断**(直接回应 RQ3):对每个「疑似人脸」区域,teacher 必须基于上下文证据判定真假(「圆形+胎纹+位于车轮位置→非人脸」),输出带证据的 `faceRegionVerdicts`,而不是只给一个 `falseFaceRisk` 标量。student 的 face-validity head 蒸的是这套带区域条件的判定结果。 +3. **空镜/大景的「无人脸≠低价值」判断**(回应 RQ2,直接服务美学打分):teacher 必须显式说明场景的叙事/构图价值,证据写进 trace,避免美学/技术分体系因「无主体人脸」系统性压低空镜大景。 + +> 一句话:teacher 产出**可回溯到视觉证据的接地标签**,student 蒸的是这套理解的结论与区域接地信号——而不是一个来路不明的标量分。接地标签是否真的带来增量,由 §实验设计里的「接地标签 vs 纯标量」消融证明(见 Phase 5),数据说话,不靠论文背书。 + +## 算力可落地约束(选型硬边界,先于一切模型选择) + +模型选型不靠论文范式,靠两端真实硬件:teacher 在自有 5090 上能跑/能训/能产标签,student 蒸馏后能在消费级独显跑得动。任何不满足下表的候选直接出局。 + +### Teacher 侧(5090,32GB,离线标注 + 训练) + +| 维度 | 约束 | 说明 | +|---|---|---| +| 显存 | 单卡 ≤ 32GB | 不依赖多卡/云 API;超 32GB 的 teacher 不进本计划,必要时用量化推理压进显存 | +| 速度 | 允许慢 | 只在服务器离线一次性跑,支持 resume;标注吞吐不是硬指标 | +| 候选 | Qwen2.5-VL 7B(首选)、InternVL3 8B/14B(次选/对照) | 14B 需先确认 32GB 够,否则量化或降级为对照 | +| 用途 | 产 grounded 软标签 + 训练 student | teacher 本身**不下发**,只产标签 | + +### Student 侧(消费级独显,下发到客户端) + +| 档位 | 目标硬件 | 约束 | 预期 | +|---|---|---|---| +| **最低(定盘)** | GTX1660 / RTX2060 / 3050,**6GB** | INT8、384 输入、DirectML 可跑,CPU 兜底不挂 | 可用,batch 偏小 | +| 推荐 | RTX3060 / 4060,8GB | 同上,batch 放宽 | 流畅 | +| Apple Silicon | M1/M2 起,统一内存 8GB | CoreML EP,算子回退有日志 | 可用 | + +选型纪律: + +- **student backbone 默认 ConvNeXt-Tiny**——卷积算子在 DirectML/CoreML 覆盖好、INT8 稳、6GB 可控。DeiT/ViT-Tiny 仅作候选,且**必须先验证 6GB + DirectML/CoreML 下的显存与算子回退**,过不了就回落 ConvNeXt。 +- student 参数量/包体上限以「6GB 卡 INT8 能加载 + 推理不爆显存」为准,多头共享 backbone(跑一次特征喂所有头)是省显存的关键,不得每头各带一个 backbone。 +- teacher 与 student **跑不同分辨率**:teacher 读高分辨率原图(理解细节、验假脸),student 固定 384 输入。两条链路不可混用。 + +> 一句话:5090 能跑的当 teacher,6GB 卡能跑的才配当 student。选型先过这道硬边界,再谈精度。 + +## 非目标与红线 + +- 不改 Flash wasm 链路。 +- 不把大 VLM、CLIP、MUSIQ、DINOv2、ConvNeXt teacher 直接塞进 Flash。 +- 不让 `rating / folder / path / filename / manual pick` 进入模型输入特征。 +- 不让语义分数救回硬伤片、弃用片、重复组非代表。 +- 不上传 RAW 到第三方云服务;teacher 默认在自有 5090 服务器离线跑。 +- 不修改 Lightroom catalog,不把 LR 数据库作为训练输入。 +- 不在本轮直接替换生产 AI Pick;先做实验报告和 Pro-only gated 模型。 + +## 数据与标签口径 + +### 数据集 + +| 数据集 | 路径 | 标签口径 | 用途 | +|---|---|---|---| +| G 盘三组 | `D:\FrameCullRawAudit\raw-audit-previews` | **正样本 `rating>=3`**(见下方「标签阈值口径」) | 旧摄影师星级验证 | +| 相机扩展集 | `D:\FrameCullRawAudit\camera-audit-previews` | **正样本 `rating>=1`** | 新扩展泛化验证 | +| 服务器镜像 | `/data/FrameCullModelLab/incoming/*` | 同本地(按来源各自口径) | 训练与 teacher 标注 | + +> **✅ 标签阈值口径(已定,按数据集分别取)**:**只有 G 盘三组(`audit3groups` / `raw-audit-previews`)用 `rating>=3` 为正样本,其余数据集(相机扩展集等)一律 `rating>=1`。** +> - 依据:G 盘是老摄影师的精选验证集,1 星只是「没删」不代表「想要」,必须抬到 3 星才算正;相机扩展集的星级语义是「1 星即可用」,沿用 `>=1`。 +> - **代码现状与待改**:`tools/pro-train/train_persona_head.py` 现在两个数据集都写 `rating>=1`(相机第 103 行**正确**、audit3groups 第 118 行**需改成 `rating>=3`**)。执行时必须把 audit3groups 分支的阈值改掉并重训 persona 头。 +> - `rating_weight`(第 124–128 行)按星级强度加权的逻辑两个数据集都保留,不受正样本阈值影响。 +> - 每个数据集的 `k` 值(G 盘=3、其余=1)要写进 manifest 与评测报告,按数据集分别报召回,**禁止混成一个平均阈值**。 + +### 标签纪律 + +- 星级只用于训练目标、验证指标、样本权重。 +- 星级不得进入 teacher prompt 的图片描述输入,也不得进入 student 推理输入。 +- 文件名、目录名、拍摄路径不得作为排序特征。 +- 训练/验证切分按拍摄批次或来源分组,不按单张随机切,避免连拍泄漏。 + +## Semantic Teacher 输出 schema + +每张图生成一个 JSON record,建议保存为: + +`/data/FrameCullModelLab/features/semantic-teacher/semantic-teacher-v1.jsonl` + +```json +{ + "schemaVersion": "framecull-semantic-teacher-v1", + "photoId": "DSC07306", + "imagePath": "/data/FrameCullModelLab/incoming/camera-originals/DSC07306.jpg", + "teacherModel": "qwen2.5-vl-7b-instruct", + "teacherVersion": "recorded-model-id-or-sha", + "createdAt": "2026-06-22T00:00:00Z", + "sceneType": "environmental_portrait", + "sceneConfidence": 0.83, + "subjectType": "person", + "subjectConfidence": 0.78, + "hasRealHumanFace": true, + "faceValidityScore": 0.91, + "falseFaceRisk": 0.02, + "semanticKeepScore": 0.74, + "compositionScore": 0.68, + "momentScore": 0.72, + "lightingMoodScore": 0.65, + "storytellingScore": 0.7, + "scenicValueScore": 0.55, + "technicalVisibleIssueScore": 0.18, + "emptyOrFillerScore": 0.14, + "duplicateRepresentativeHint": "unknown", + "keepReasons": [ + "clear interaction between subject and environment", + "usable expression and balanced composition" + ], + "rejectReasons": [], + "reasoningTrace": [ + { + "region": [0.31, 0.18, 0.72, 0.88], + "observation": "person facing camera, eyes open, mid-action gesture", + "supportsKeep": true, + "weight": 0.7 + }, + { + "region": [0.0, 0.6, 0.4, 1.0], + "observation": "foreground environment frames the subject, not clutter", + "supportsKeep": true, + "weight": 0.3 + } + ], + "faceRegionVerdicts": [ + { + "region": [0.34, 0.2, 0.55, 0.46], + "isRealHumanFace": true, + "evidence": "frontal facial features, skin texture, consistent with body below", + "confidence": 0.93 + } + ], + "regions": [ + { + "label": "main_subject", + "box": [0.31, 0.18, 0.72, 0.88], + "confidence": 0.82 + } + ], + "uncertain": [] +} +``` + +> `semanticKeepScore` 必须可回溯到 `reasoningTrace` 的区域证据(汇总而非凭空给分);`faceValidityScore` / `falseFaceRisk` 必须由 `faceRegionVerdicts` 汇出。这是「让打分看懂内容」的 schema 落点——见前文《让打分「看懂内容」的落地方式》。 + +> **teacher 在高分辨率原图上跑,不在 384 预览图上跑**。384 只是 *student 输入*分辨率(架构文档 §8 已定)。teacher 的语义推理、尤其假脸验证依赖细节,必须喂原图(或至少长边 ≥ 768 的预览)。`camera-previews-384` 仅供 student 训练/推理;teacher 标注的输入路径要指向原图集,二者不可混用。 + +### 场景类型枚举 + +首批固定为: + +- `portrait` +- `group` +- `environmental_portrait` +- `landscape` +- `empty_scene` +- `documentary_moment` +- `event` +- `product_object` +- `animal` +- `food` +- `other` + +### teacher prompt 要求 + +- 输出必须是 JSON。 +- 所有分数范围为 `0..1`。 +- 不允许根据星级、文件名、路径推断。 +- **必须先产出 `reasoningTrace`(区域→证据→是否支持保留),`semanticKeepScore` 由 trace 汇总,不允许跳过推理直接给分。** +- 对“像脸但不是脸”的区域,必须在 `faceRegionVerdicts` 里逐区域给出**带证据**的真假判定,`falseFaceRisk` 由此汇出,而非单独拍一个标量。 +- 对空镜/大景必须在 trace 里推理其叙事/构图价值后再给 `scenicValueScore`,不能因无人脸直接低分。 +- teacher 读**高分辨率原图**,不读 384 预览图。 +- 如果不确定,写入 `uncertain`,不要编造。 + +## 实验设计 + +### Phase 0:环境与数据核验 + +目标:确认服务器具备跑 teacher 和训练 student 的条件。 + +输入: + +- `/data/FrameCullModelLab/incoming/raw-audit-previews` +- `/data/FrameCullModelLab/incoming/camera-previews-384` +- `/data/FrameCullModelLab/incoming/camera-labels/camera-labels-final.json` +- `/data/FrameCullModelLab/workspace/output/ai-bench/*.json` + +输出: + +- `/data/FrameCullModelLab/outputs/semantic-teacher-lab/data-audit.json` +- `/data/FrameCullModelLab/outputs/semantic-teacher-lab/data-audit.md` + +检查项: + +- 图片数量与标签数量一致。 +- 标签分布按数据集分别统计。 +- 无星/0 星口径确认。 +- 训练/验证 split 文件生成,按拍摄批次或来源切分。 + +### Phase 1:teacher 候选 smoke + +候选: + +- Qwen2.5-VL 7B:默认首选,结构化输出和目标定位较强,7B 在 5090 32GB 上跑标注余量充足。 +- InternVL3 8B/14B:作为第二 teacher 或一致性检查;14B 需确认 5090 显存够(必要时量化推理)。 +- DINOv2/CLIP/MUSIQ:继续作为 embedding / quality teacher。 + +> teacher 选型唯一硬约束:**能在 5090 32GB 上离线跑标注**。允许慢、允许量化推理,但不依赖多卡或云 API。任何要求超过单卡 32GB 的 teacher 不进本计划。 + +smoke 设置: + +- 每个 teacher 跑 80 张图。 +- 覆盖人像、合照、大景、空镜、轮胎/假脸风险样本、低星负样本、高星正样本。 +- 每张图记录耗时、显存、JSON 解析成功率、uncertain 比例。 + +通过门槛: + +- JSON 有效率 `>= 98%`。 +- 单图 teacher 平均耗时可接受,允许慢,因为只在服务器离线跑。 +- 人工抽样 50 张,场景类型和保留理由明显可用。 +- **`reasoningTrace` / `faceRegionVerdicts` 真的接地**:抽样里区域 box 对得上画面、证据不是套话,假脸样本的判定有上下文依据(否则等于没看懂内容、只是套壳给分,回炉调 prompt)。 + +> **🚧 License 硬门禁(Phase 2 全量标注前必须过,不是「事后审核」)**:蒸馏出的标签会**烤进随包下发的 student 权重**,所以 teacher 的 license 不是「离线跑就没事」。全量标注**启动前**必须确认:选定 teacher(Qwen2.5-VL / InternVL3 等)的 license 是否允许「用其输出训练模型」且「该模型可商用分发」。结论写进 `teacher-license-clearance.md`,未澄清的 teacher **不得进入 Phase 2 全量标注**。若主选 teacher 不可商用,则降级为「仅研究对照」,改用 license 干净的 teacher 产正式标签。 + +### Phase 2:全量 semantic teacher 标注 + +目标:生成可蒸馏的语义软标签。 + +输出: + +- `/data/FrameCullModelLab/features/semantic-teacher/semantic-teacher-v1.jsonl` +- `/data/FrameCullModelLab/features/semantic-teacher/semantic-teacher-v1.summary.json` +- `/data/FrameCullModelLab/features/semantic-teacher/teacher-failures.csv` +- `/data/FrameCullModelLab/features/semantic-teacher/teacher-qa-samples/` + +质量控制: + +- JSON schema 校验。 +- 分数范围校验。 +- sceneType 分布校验。 +- 高不确定样本导出人工复查。 +- 多 teacher 不一致样本单独列出。 + +### Phase 2.5:量化/嵌入 teacher 特征生成(必须,否则 Phase 3 的 loss 缺输入) + +semantic teacher(VLM)只产语义标签。Phase 3 的 `L_aesthetic / L_scene / L_embedding` 还要 MUSIQ/CLIP/DINOv2 的特征,这些**不是 VLM 产的,必须单独跑**。现有 `train_distill_backbone.py` 已经在读 `features/teacher/teacher-*.npz`(含 `tech/aes/clip[512]`),所以这一步是补齐它的输入,且要把 DINOv2 加进去。 + +输出: + +- `/data/FrameCullModelLab/features/teacher/teacher-camera.npz` +- `/data/FrameCullModelLab/features/teacher/teacher-audit3groups.npz` +- 每个 npz 含:`musiq_tech`、`musiq_aes`、`clip[512]`,**以及 `dino[768]`(已定启用,必须生成)**。 + +纪律: + +- 这些 teacher 跑 **384 student 输入分辨率**(要和 student 对齐,蒸的是同尺度特征),与 VLM teacher 跑原图是两条独立链路,不要混。 +- **DINOv2 已定启用**:这里必须真生成 `dino[768]`,且 `train_distill_backbone.py` 接上 `L_embedding`(student 特征对齐 DINOv2 embedding,提升跨场景稳定性、抗过拟合到户外人像)。`dino[768]` 缺失即视为 Phase 2.5 未完成,不得进 Phase 3。 + +### Phase 3:Student V2 训练 + +模型结构沿用 Pro 设计: + +```text +shared backbone + -> aesthetic head + -> scene head + -> persona head + -> semantic keep head + -> face validity head + -> composition/moment/lighting heads +``` + +候选 backbone(最终以「算力可落地约束」§为准): + +- ConvNeXt-Tiny:低风险主候选,已有 V1 路线,6GB + DirectML/CoreML INT8 稳。 +- DeiT/ViT-Tiny:语义上限候选,**仅当通过 6GB 显存 + DirectML/CoreML 算子回退验证才保留**,否则回落 ConvNeXt。 + +训练分三段: + +1. Teacher distillation:学习 MUSIQ / CLIP / DINOv2 / semantic teacher 的软标签。 +2. Persona fine-tune:用人工星级训练 `personaScore` 和 ranking head。 +3. Scene-balanced calibration:按场景校准,避免相机集或户外人像过拟合。 + +损失函数建议: + +- `L_aesthetic`: MSE / Huber vs MUSIQ aesthetic +- `L_scene`: cross entropy vs teacher sceneType +- `L_semantic_keep`: BCE / ranking loss vs semanticKeepScore +- `L_face_validity`: BCE vs teacher faceValidityScore + 已知假脸样本 +- `L_persona`: weighted BCE + pairwise ranking,星级越高权重越高 +- `L_embedding`: cosine loss vs DINOv2/CLIP embedding 投影 + +### Phase 4:ONNX 导出与量化 + +输出模型目录: + +`output/pro-models/semantic_student_v2__/` + +必须包含: + +- `model.onnx` +- `model.int8.onnx` +- `manifest.json` +- `manifest.int8.json` +- `export-report.json` +- `quant-compare.json` +- `teacher-schema.json` +- `training-report.json` + +ONNX 输出字段必须兼容现有 Pro 推理层,新增字段需要先扩展 `ProHeadScores`: + +```ts +{ + aesthetic?: number; + sceneLabel?: string; + sceneConfidence?: number; + personaScore?: number; + semanticKeepScore?: number; + faceValidityScore?: number; + compositionScore?: number; + momentScore?: number; + lightingMoodScore?: number; + falseFaceRisk?: number; + error?: string; +} +``` + +### 字段映射表(teacher → student head → ProHeadScores,必须对齐) + +每个 teacher 字段要么有 student head 承接、要么明确标为「仅 QA 不蒸馏」。**不允许出现 teacher 产出却无人承接的死信号。** + +| teacher 字段 | student head | ProHeadScores | 蒸馏 loss | +|---|---|---|---| +| `semanticKeepScore`(由 reasoningTrace 汇总) | semantic_keep | `semanticKeepScore` | BCE/ranking | +| `faceValidityScore` + `faceRegionVerdicts` | face_validity | `faceValidityScore` | BCE + 区域接地 hard neg | +| `falseFaceRisk`(由 faceRegionVerdicts 汇出) | face_validity(同头反向) | `falseFaceRisk` | 同上 | +| `sceneType` | scene | `sceneLabel`/`sceneConfidence` | CE | +| `compositionScore` | composition | `compositionScore` | MSE/Huber | +| `momentScore` | moment | `momentScore` | MSE/Huber | +| `lightingMoodScore` | lighting | `lightingMoodScore` | MSE/Huber | +| `scenicValueScore` | (并入 semantic_keep 的证据,不单独建头) | — | 经 trace 影响 keep | +| `storytellingScore` / `emptyOrFillerScore` | **仅 QA,不蒸馏** | — | — | +| `technicalVisibleIssueScore` | **仅 QA**(硬伤仍归规则引擎,见红线) | — | — | +| MUSIQ aesthetic(独立 teacher,非 VLM) | aesthetic | `aesthetic` | MSE/Huber | +| CLIP/DINOv2 embedding(独立 teacher) | scene/backbone 投影 | — | cosine | +| 人工星级 | persona | `personaScore` | weighted BCE + pairwise | + +> 标「仅 QA」的字段照常由 teacher 产出、进 QA 报告,但**不建 student head、不进 ONNX 输出**——避免给小模型塞它学不动也用不上的目标。若后续要启用,再单独加头并扩 `ProHeadScores`。 + +### Phase 5:Pro A/B 评估 + +使用现有 `bench-pro-persona.mjs` 扩展为: + +`tools/ai-lab/bench-pro-semantic-student.mjs` + +对比组: + +- `current-production-rules` +- `ratio-aware-rules` +- `pro-persona-v1` +- `pro-semantic-v2-persona-only` +- `pro-semantic-v2-semantic-only` +- `pro-semantic-v2-fused` +- `pro-semantic-v2-face-guard` +- `pro-semantic-v2-flat-scalar`(**消融对照**:teacher 只给扁平标量、不带 reasoningTrace/faceRegionVerdicts 接地,其余训练一致) + +> **接地标签 vs 纯标量消融(回答「看懂内容是否真有增量」)**:`fused` 用带区域接地的标签训练,`flat-scalar` 用同一 teacher、同样的图、但 prompt 退化为「只给标量分、不要推理过程」产出的标签训练。若 `fused` 在 RQ2(空镜/大景召回)、RQ3(假脸误报)上**显著优于** `flat-scalar`,才证明「让 teacher 看懂内容再给分」带来了真实增量;若两者持平,说明价值来自「有 VLM teacher」本身,与是否接地无关——这一结论必须写进 `production-recommendation.md`。 + +比例: + +- `38%` +- `45%` +- `50%` +- `60%` + +主指标: + +- 召回率按各数据集口径分别报:**G 盘三组按 `rating>=3`,相机扩展集等按 `rating>=1`**,禁止混成一个平均阈值。 +- 4/5 星覆盖率。 +- 负样本混入率。 +- 重复污染:正式重复组多选必须保持 `0` 或接近 `0`。 +- blocked / hard issue picked 必须为 `0`。 +- 场景分层指标:人像、合照、空镜、大景、纪实分别报。 +- 假脸误报率:轮胎/圆形物体/海报等误判为人脸的比例。 + +性能指标: + +- DirectML batch=1 / batch=8 单图平均耗时。 +- CPU fallback batch=1 单图平均耗时。 +- 模型包体。 +- 峰值内存/显存。 +- 坏图单图 error,不挂整批。 + +### Phase 6:生产门槛 + +进入 Pro gated ranking 的条件: + +- 任一低比例 `38% / 45% / 50%` 召回提升 `>= 5%`,或 4/5 星覆盖提升 `>= 8%`。 +- 负样本混入率不比 baseline 恶化超过 `2%`。 +- 重复污染不恶化。 +- blocked picked 为 `0`。 +- 假脸误报率下降,或至少不高于现有规则。 +- DirectML / CPU fallback 性能可接受,不能比 Pro V1 慢到不可用。 + +不达标时: + +- 不进入默认 Pro ranking。 +- 保留 teacher 标签和训练报告。 +- 分析失败原因:teacher 不稳定、student 容量不足、标签口径不一致、场景偏置、量化损伤。 + +## 任务拆解 + +### Task A:技能与研究材料 + +- [x] 安装 `research` +- [x] 安装 `research-deep` +- [x] 用 deep research 路线梳理 CVPR 2026 / VLM / teacher distillation 依据 +- [x] 产出本任务文档 + +### Task B:Semantic Teacher 数据 schema + +- [ ] 新增 `tools/pro-train/semantic_teacher_schema.py` +- [ ] 新增 JSON schema 校验脚本(含 `reasoningTrace` / `faceRegionVerdicts` 必填校验) +- [ ] 新增 teacher prompt 模板(强制先推理后给分,读原图) +- [ ] 输出 80 张 smoke 样本列表 + +### Task B2:量化/嵌入 teacher 特征(Phase 2.5,补 Phase 3 loss 的输入) + +- [ ] 新增 `tools/pro-train/build_quality_teacher_features.py`,产 `teacher-camera.npz` / `teacher-audit3groups.npz`(`musiq_tech/musiq_aes/clip[512]`) +- [ ] **DINOv2 已定启用**:生成 `dino[768]` 并接上 `L_embedding`(缺 `dino[768]` 即 Phase 2.5 未完成) +- [ ] 这些特征跑 384(与 student 对齐),与 VLM teacher 跑原图分两条链路 + +### Task C:Teacher runner + +- [ ] 新增 `tools/pro-train/run_semantic_teacher.py` +- [ ] **读高分辨率原图**(非 384 预览),输出含 reasoningTrace/faceRegionVerdicts +- [ ] 支持 Qwen2.5-VL 本地模型 +- [ ] 支持 InternVL3 本地模型 +- [ ] 支持 resume、失败重试、JSON 修复、schema 校验 +- [ ] 支持 `--flat-scalar` 模式(退化 prompt,产消融对照标签) +- [ ] 所有缓存写入 `/data/FrameCullModelLab/cache` + +### Task D:Teacher QA + +- [ ] 新增 `tools/pro-train/audit_semantic_teacher.py` +- [ ] 输出 scene 分布、分数分布、uncertain 样本、高冲突样本 +- [ ] 导出人工复查 HTML 或 CSV + +### Task E:Student V2 训练 + +- [ ] 扩展 `train_distill_backbone.py`,接入 semantic teacher labels 与 Phase 2.5 的 npz 特征 +- [ ] 扩展 `train_persona_head.py`,支持语义多头和 ranking loss;**按映射表建头,不建「仅 QA」字段的头** +- [ ] 星级阈值按数据集分别落地(**G 盘三组 `>=3`,相机扩展集等 `>=1`**;改 `train_persona_head.py` 第 118 行 audit3groups 分支),各自 `k` 值写进 manifest +- [ ] 按 scene-balanced split 训练 +- [ ] 输出训练报告和 feature importance / ablation + +### Task F:ONNX / INT8 + +- [ ] 扩展 `export_pro_onnx.py` +- [ ] 扩展 `compare_onnx_quant.py` +- [ ] manifest 记录 semantic schema version、teacher model、训练数据 hash + +### Task G:Pro bench + +- [ ] 新增 `bench-pro-semantic-student.mjs` +- [ ] 输出 `summary.md` +- [ ] 输出 `metrics-by-ratio.csv` +- [ ] 输出 `metrics-by-scene.csv` +- [ ] 输出 `false-negatives-by-ratio.csv` +- [ ] 输出 `duplicate-pollution-by-ratio.csv` +- [ ] 输出 `false-face-samples.csv` +- [ ] 输出 `pro-infer-latency.csv` + +### Task H:产品接入决策 + +- [ ] 若达标,新增 Pro-only 实验开关 +- [ ] 不改 Flash +- [ ] 不改变硬伤门禁 +- [ ] 不改变重复组非代表抑制 +- [ ] 不把 teacher 模型打包进客户端 + +## 服务器执行目录 + +```text +/data/FrameCullModelLab/ + incoming/ + raw-audit-previews/ + camera-previews-384/ + camera-labels/ + cache/ + huggingface/ + torch/ + pip/ + features/ + teacher/ + semantic-teacher/ + outputs/ + semantic-teacher-lab/ + distill/ + persona/ + semantic-student-v2/ + workspace/ + tools/ + pro-train/ + ai-lab/ + output/ + ai-bench/ +``` + +## 初始命令草案 + +### 数据审计 + +```bash +cd /data/FrameCullModelLab/workspace +python tools/pro-train/audit_semantic_inputs.py \ + --gdrive-previews /data/FrameCullModelLab/incoming/raw-audit-previews \ + --gdrive-labels /data/FrameCullModelLab/incoming/raw-audit-previews/labels.json \ + --camera-previews /data/FrameCullModelLab/incoming/camera-previews-384 \ + --camera-labels /data/FrameCullModelLab/incoming/camera-labels/camera-labels-final.json \ + --out /data/FrameCullModelLab/outputs/semantic-teacher-lab/data-audit +``` + +### teacher smoke + +```bash +python tools/pro-train/run_semantic_teacher.py \ + --model qwen2.5-vl-7b \ + --input /data/FrameCullModelLab/outputs/semantic-teacher-lab/smoke-list.json \ + --schema tools/pro-train/semantic_teacher_schema.json \ + --out /data/FrameCullModelLab/features/semantic-teacher/smoke-qwen2.5-vl.jsonl \ + --cache /data/FrameCullModelLab/cache +``` + +### 全量 teacher + +```bash +python tools/pro-train/run_semantic_teacher.py \ + --model qwen2.5-vl-7b \ + --input /data/FrameCullModelLab/outputs/semantic-teacher-lab/all-images.json \ + --schema tools/pro-train/semantic_teacher_schema.json \ + --out /data/FrameCullModelLab/features/semantic-teacher/semantic-teacher-v1.jsonl \ + --cache /data/FrameCullModelLab/cache \ + --resume +``` + +### Student V2 训练 + +```bash +python tools/pro-train/train_semantic_student.py \ + --backbone convnext_tiny \ + --teacher /data/FrameCullModelLab/features/semantic-teacher/semantic-teacher-v1.jsonl \ + --labels /data/FrameCullModelLab/incoming/camera-labels/camera-labels-final.json \ + --audit-labels /data/FrameCullModelLab/incoming/raw-audit-previews/labels.json \ + --out /data/FrameCullModelLab/outputs/semantic-student-v2/convnext_tiny \ + --epochs 30 \ + --batch 64 +``` + +### 导出与评估 + +```bash +python tools/pro-train/export_pro_onnx.py \ + --student /data/FrameCullModelLab/outputs/semantic-student-v2/convnext_tiny/student-best.pt \ + --persona /data/FrameCullModelLab/outputs/semantic-student-v2/convnext_tiny/persona-best.pt \ + --out output/pro-models/semantic_student_v2_convnext_int8 \ + --name framecull-pro-semantic-v2 + +node tools/ai-lab/bench-pro-semantic-student.mjs \ + --manifest output/pro-models/semantic_student_v2_convnext_int8/manifest.int8.json \ + --output output/ai-bench/pro-semantic-student-eval \ + --ratios 0.38,0.45,0.50,0.60 +``` + +## 输出文件要求 + +最终实验必须输出: + +- `summary.md` +- `teacher-quality-report.md` +- `teacher-license-clearance.md`(teacher license 结论,全量标注前产) +- `metrics-by-ratio.csv` +- `metrics-by-scene.csv` +- `false-negatives-by-ratio.csv` +- `duplicate-pollution-by-ratio.csv` +- `false-face-samples.csv` +- `grounded-vs-flat-ablation.md`(接地标签 vs 纯标量对照,回答「让打分看懂内容是否真有增量」) +- `pro-infer-latency.csv` +- `selected-config-by-ratio.json` +- `selected-model-manifest.json` +- `production-recommendation.md` + +## 风险与应对 + +| 风险 | 表现 | 应对 | +|---|---|---| +| teacher 幻觉 | JSON 看似合理但误判画面 | 多 teacher 对照 + 人工抽样 + uncertain 字段,不把 teacher 当真值。 | +| 语义 teacher 太慢 | 全量标注耗时长 | 离线一次性跑,支持 resume,先 smoke 再全量。 | +| student 学不到 teacher | SRCC/PLCC 低 | 增大 head、换 backbone、降低 teacher 任务复杂度。 | +| 学到 teacher 但筛片无收益 | teacher 保真高但召回不升 | 增强 persona fine-tune,调整 ranking loss,增加场景分层训练。 | +| 相机集标签口径偏差 | 1 星可用、0 星淘汰与 G 盘口径不同 | 分数据集报告,不做一个平均数掩盖问题。 | +| 假脸仍高 | 轮胎/圆形物体被当脸 | 增加 faceValidity head 和 hard negative samples。 | +| 量化损伤 | INT8 分数漂移 | 保留 FP32 对照,量化差异超阈值时不上 INT8。 | +| Pro 速度慢 | DirectML 单图耗时过长 | batch、预处理流水线、ConvNeXt 优先,ViT 只做候选。 | + +## 验收门槛 + +实验完成的最低验收: + +- teacher 全量 JSONL 生成完成,schema 校验通过。 +- Student V2 至少一个 backbone 完成训练。 +- ONNX FP32 和 INT8 导出成功。 +- Pro 原生推理能读新 manifest 并返回新增多头分数。 +- 38/45/50/60 四档评估报告完整。 +- 结果按数据集和场景分层,不只给总平均。 +- 明确给出是否进入 Pro gated ranking 的建议。 + +进入产品候选的门槛: + +- 低比例召回提升达到 `>= 5%`,或 4/5 星覆盖提升达到 `>= 8%`。 +- 负样本混入率不明显恶化。 +- 假脸误判下降或不恶化。 +- 重复污染不反弹。 +- 硬伤片不被救回。 +- 模型包体和速度符合 Pro 定位。 + +## 下一轮执行 Prompt + +```text +Implement FrameCull Pro Semantic Teacher Lab v1. + +Use the plan in docs/GOAL_pro_semantic_teacher_lab.md as the authority. Goal: make the Pro aesthetic/culling scores "understand picture content" by distilling a content-understanding VLM teacher into a small local student. Build a server-side semantic teacher pipeline under /data/FrameCullModelLab that uses local open-source VLM teachers such as Qwen2.5-VL and optionally InternVL3 to generate structured semantic labels for the G-drive validation set and camera dataset. Do not upload RAW or private photos to third-party cloud APIs. + +HARDWARE FEASIBILITY DRIVES MODEL SELECTION: the teacher must run offline on a single 5090 (32GB) — slow/quantized is fine, but no multi-GPU or cloud dependency. The distilled student must run on a 6GB consumer discrete GPU (GTX1660/RTX2060/3050 class) via DirectML, with CPU fallback. Prefer ConvNeXt-Tiny as the student backbone (DeiT/ViT-Tiny only as a candidate if it fits the 6GB + DirectML/CoreML constraint). Do not pick models based on any single paper's paradigm. + +Create schema-validated JSONL teacher outputs. Crucially, the teacher must produce GROUNDED, CONTENT-AWARE labels, not flat scalars: each record needs a `reasoningTrace` (region -> visual evidence -> supports/rejects keep) from which `semanticKeepScore` is aggregated, and `faceRegionVerdicts` (per suspected-face region: real-or-fake judgment WITH visual evidence) from which `faceValidityScore`/`falseFaceRisk` are derived. This is how the scores actually "see" content rather than guessing from pixels. The teacher must read HIGH-RESOLUTION ORIGINALS, not the 384px previews (384 is the student input size only). Ratings must be used only as training/evaluation labels, never as teacher input or ranking features. + +Before full annotation, clear teacher license (output `teacher-license-clearance.md`): distilled labels bake into the shipped student weights, so the teacher's license must permit training-on-outputs and commercial redistribution. Also generate the quality/embedding teacher features — MUSIQ, CLIP, and DINOv2 `dino[768]` (DINOv2 is enabled: `L_embedding` is kept, so `dino[768]` MUST be generated; missing it means Phase 2.5 is incomplete) — so Phase 3 losses have inputs. Apply the per-dataset rating threshold: positive = `rating>=3` for the G-drive set (`audit3groups`/`raw-audit-previews`) ONLY, `rating>=1` for all other datasets (camera set etc.); fix `train_persona_head.py` line 118 (audit3groups branch) accordingly and report recall per dataset at its own k, never as one averaged threshold. + +Then train a Pro Student V2 multi-head model using the existing shared-backbone architecture (aesthetic head is the primary objective; scene/persona/semantic-keep/face-validity are supporting heads). Build only the heads in the teacher->head->ProHeadScores mapping table; do not build heads for QA-only fields. Keep Flash fully isolated. Export FP32 and INT8 ONNX manifests compatible with the existing pro_infer layer, extend ProHeadScores only behind Pro gates, and evaluate 38%, 45%, 50%, and 60% AI Pick ratios. Include a `flat-scalar` ablation arm (same teacher, grounding disabled) to prove content-aware grounded labels add real value over plain VLM scoring. + +Final output must include teacher-quality-report.md, teacher-license-clearance.md, summary.md, metrics-by-ratio.csv, metrics-by-scene.csv, false-negatives-by-ratio.csv, duplicate-pollution-by-ratio.csv, false-face-samples.csv, grounded-vs-flat-ablation.md, pro-infer-latency.csv, selected-config-by-ratio.json, selected-model-manifest.json, and production-recommendation.md. Recommend product integration only if recall/4-5-star coverage improves without increasing hard issue picks, duplicate pollution, or false-face errors. +``` diff --git a/docs/PRO_MODEL_ARCHITECTURE.md b/docs/PRO_MODEL_ARCHITECTURE.md new file mode 100644 index 0000000..61a7a5f --- /dev/null +++ b/docs/PRO_MODEL_ARCHITECTURE.md @@ -0,0 +1,480 @@ +# FrameCull Pro 模型架构与部署设计 + +> 本文档规划 Pro 版本的本地 AI 模型架构:用 MUSIQ / CLIP 蒸馏出可本地部署的小模型,采用「共享 backbone + 多任务头 + 可下发场景头」的统一结构,并定义独显推理链路与最低配置。 + +## 0. 一句话结论 + +不要做三个独立模型,做**一个共享 backbone + 多个轻量头**。单张 GPU 上多个独立模型不会真并行,只会争抢显存和调度、重复做特征提取,净亏。统一结构跑一次特征提取,后面挂多少个头都几乎不增加成本,而且新场景头可以「冻结主干、单独训练、单独下发」。 + +--- + +## 1. 名词通俗解释 + +### 1.1 什么是 backbone(主干网络) + +把一张图变成「机器能理解的特征向量」的那部分网络,就是 backbone。 + +打个比方:backbone 像一个**资深看图的眼睛 + 大脑前半段**。它把原始像素逐层抽象成「这里有边缘、那里有人脸、整体偏暗、构图居中」这类高层特征(一串数字,叫 feature/embedding)。它**不直接给结论**,只负责「看懂这张图长什么样」。 + +后面的「头(head)」才负责下结论:技术头说「清晰度 78 分」,美学头说「观感 65 分」,场景头说「这是婚礼」。 + +关键点:**最重、最耗算力和显存的就是 backbone**。头都很轻(几层全连接)。所以多个任务共用一个 backbone,等于「看一次图,回答多个问题」,这就是省的地方。 + +### 1.2 为什么这能解决「并发打架」 + +- 三个独立模型 = 三个 backbone = 看三遍图 = 三倍重活 + 三份显存 +- 一个共享 backbone = 看一遍图 = 多个头分别回答 = 重活只做一次 + +GPU 对计算密集的 backbone 前向是**串行排队**执行的,开三个模型只是时间片轮转,并不会变快,反而因显存和缓存争抢更慢。真正能并行的只有「CPU 预处理 / 磁盘 IO」和「GPU 计算」的流水线重叠,这和模型数量无关。 + +--- + +## 2. 推荐架构:共享 backbone + 多头 + +``` + ┌─────────────────────────────┐ + 原图 (单分辨率输入) → │ 共享 Backbone(重,跑一次) │ → 特征向量 feature + └─────────────────────────────┘ + │ + ┌──────────────┬──────────────┼──────────────┬─────────────────┐ + ▼ ▼ ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────────────┐ + │ 技术质量头 │ │ 美学头 │ │ 场景分类头 │ │ 个性筛选头 │ … │ 个性筛选头(N) │ + │(KonIQ- │ │(AVA- │ │(CLIP 语义)│ │ 婚礼 │ │ 会议活动 │ + │ MUSIQ 蒸馏)│ │ MUSIQ蒸馏)│ │ │ │ │ │ │ + └─────────┘ └─────────┘ └─────────┘ └──────────┘ └──────────────┘ + │ │ │ │ │ + ▼ ▼ ▼ └──── 由场景头路由自动启用对应个性头 + 接入现有 photoScoring 的分量 feed classifyScoreScene +``` + +### 2.1 各头职责 + +| 头 | 教师 / 来源 | 输出 | 接入点 | +|---|---|---|---| +| 技术质量头 | KonIQ / SPAQ-MUSIQ | 技术失真分(模糊/噪点/压缩) | `photoScoring` 的 `TECHNICAL_QUALITY`(或继续交规则引擎,见 §2.3) | +| 美学头 | AVA-MUSIQ | 美学观感分 | `AESTHETIC_QUALITY`,替换现 `calibratedAestheticModelScore` | +| 场景分类头 | CLIP 语义 embedding | 场景类别 + 置信度 | `classifyScoreScene`,并做个性头路由 | +| 个性筛选头 | 各场景人工筛片标签微调 | 该场景下的「值得留」偏好分 | AI Pick 排序(`aiPickRankScore`) | + +### 2.2 场景头做路由(关键设计) + +一次前向里,场景头先判定类别(婚礼/户外人像/会议…),系统据此**自动启用对应个性头**。用户无需手动选场景,也避免同时跑所有个性头。个性头本身极轻,留着不用也几乎零成本,但路由能让「这场是婚礼 → 用婚礼偏好打分」自动发生。 + +### 2.3 分辨率冲突——本设计唯一的真实代价 + +技术失真检测对分辨率高度敏感(降采样会把模糊「洗掉」),而美学/场景不敏感。共享 backbone 用同一输入,必然要折中。两条路线: + +- **方案 A**:折中分辨率(384 或 512),技术头也走 backbone。简单,但技术头精度受限。 +- **方案 B(已定)**:学习型模型只负责美学 + 场景 + 个性偏好这些「软」判断;**硬技术失真(失焦/噪点/曝光)继续交给现有规则引擎**(Sobel/Laplacian/分位数那套,在高分辨率原图上算)。 + +**采用方案 B**:既绕开分辨率冲突,又保住硬伤判定的可解释性和可控性。黑盒分数不该接管「这张是不是废片」这种需要解释的决策——规则引擎能讲清「为什么判废片」,模型只在它的强项(美学/场景/偏好)上发力,各用各的分辨率。 + +--- + +## 3. 个性头:预训练下发模式 + +你们已确定个性头**预训练好、随版本下发**(非用户本地训练)。这条路的工程收益: + +- **训练成本低**:冻结 backbone,只训练几层头,小数据集 + 小算力即可。 +- **发布轻**:新场景只发一个几 MB 的头权重文件,不重训主干、不重发整模型。 +- **可热更新**:头权重和主干解耦,能独立做版本管理(见下)。 +- **可扩展**:以后加「宠物 / 美食 / 旅拍」只是再训一个头。 + +### 3.1 头权重版本管理建议 + +``` +models/pro/ + backbone_v1.onnx # 主干,更新频率低 + heads/ + technical_v1.onnx + aesthetic_v1.onnx + scene_v2.onnx # 场景头可独立迭代 + persona_wedding_v3.onnx # 个性头独立版本号 + persona_outdoor_v1.onnx + persona_event_v1.onnx + manifest.json # 记录 backbone 版本与各头兼容性 +``` + +`manifest.json` 标注每个头兼容哪个 backbone 版本——backbone 一旦升级,旧头需重训或标记不兼容。这是共享架构必须管好的契约。 + +--- + +## 4. 独显推理链路:CUDA vs DirectML + +### 4.1 速度对比 + +在 NVIDIA 卡上 **CUDA 通常比 DirectML 快 20%~50%**(视模型/算子而定),但不是数量级碾压。原因: + +- **CUDA + cuDNN/TensorRT**:NVIDIA 自家深度优化,算子融合、kernel 调度成熟。 +- **DirectML**:走 DirectX 通用计算抽象,多一层翻译;为兼容 N/A/Intel 卡牺牲了针对性优化。 + +| 维度 | CUDA EP | DirectML EP | +|---|---|---| +| N 卡速度 | 最快 | 慢 20%~50% | +| 显卡兼容 | 仅 NVIDIA | NVIDIA / AMD / Intel 通吃 | +| 平台 | 跨平台(需 CUDA 驱动) | Windows 原生 | +| 部署复杂度 | 需匹配 CUDA/cuDNN 版本,较重 | 随 Windows,较轻 | +| 适合 | 追求极致性能的 N 卡用户 | 覆盖最广用户面 | + +### 4.2 推荐:多 EP 策略(含跨平台) + +CUDA/DirectML 都只限 Windows——CUDA 仅 N 卡,DirectML 仅 Windows。Mac 必须用完全不同的后端。**同一个 ONNX 模型文件**在不同平台只换 EP,不需要为某个平台单独训模型。启动时探测平台和硬件,按下表选 EP: + +| 平台 | 推荐 EP | 用什么硬件 | 备注 | +|---|---|---|---| +| Windows + N 卡 | **CUDA** | NVIDIA GPU | 最快,主路径 | +| Windows + A/Intel 卡 | **DirectML** | 任意 GPU | 兜底覆盖 | +| **macOS (Apple Silicon)** | **CoreML EP** | GPU + ANE 神经引擎 | onnxruntime 原生支持 | +| macOS (Intel) | CPU EP | — | 不做 GPU 优化 | +| 全平台最终兜底 | CPU EP | — | 慢但能跑 | + +``` +启动探测逻辑: + macOS + Apple Silicon → CoreML EP + macOS + Intel → CPU EP + Windows + NVIDIA → CUDA EP + Windows + 其他 GPU → DirectML EP + 以上全部失败 → CPU EP +``` + +> **Apple Silicon 是统一内存**:CPU/GPU 共享同一块内存,没有独立 VRAM 限制。8GB 的 M 芯片即可跑得不错,比 Windows 独显的显存约束更宽松。 + +### 4.3 平台支持矩阵(已定稿) + +为控制工程量,按 edition 分别定义支持范围: + +| 平台 / 硬件 | Flash | Pro | +|---|---|---| +| Windows + N 卡 | ✅ wasm | ✅ CUDA 加速 | +| Windows + A/Intel 卡 | ✅ wasm | ✅ DirectML 加速 | +| macOS Apple Silicon | ✅ wasm | ✅ CoreML 加速 | +| macOS Intel | ✅ wasm (CPU) | ❌ 不支持 | + +**决策依据**: + +- **Flash 全平台覆盖**:走 onnxruntime-web wasm,模型轻,CPU 也能跑,所有平台(含 Intel Mac)一视同仁。 +- **Pro 只支持 Apple Silicon + Windows 独显**:Pro 的重模型需要 GPU 加速才有可用体验;**Intel Mac 无可用 GPU 加速路径,纯 CPU 跑大批量会卡到不可用**,故不纳入 Pro。 +- Intel Mac 的代码增量几乎为零(落 CPU 兜底即可),真正成本在 universal binary 打包和每版真机回归——这些成本由 Flash 承担一次即可,Pro 不再为它付费。 + +### 4.4 与现有 Tauri 架构的关系 + +- **Flash 版**:维持现状,onnxruntime-web **wasm**,不依赖独显,全平台轻量开箱。 +- **Pro 版**:走**原生 onnxruntime(CUDA / DirectML / CoreML EP)**,通过 Rust 端进程内绑定(`ort` crate)调用,才能真正吃到 GPU 算力。进程模型与对接细节见 §10.3(已定为进程内绑定,非 sidecar)。 + +这是两套独立推理层,需在 edition 分线时就隔离清楚(你们已有 `FRAMECULL_EDITION` 机制,正好挂载不同推理后端)。 + +**Mac 打包注意**:Apple Silicon 单架构出包即可(Pro 不管 Intel Mac,省去 universal binary 负担);仍需处理 Apple 签名 / 公证流程。Flash 若要覆盖 Intel Mac 则需 universal binary。 + +--- + +## 5. backbone 选型对比(通俗版) + +学生 backbone 的选型,核心权衡是 **精度 vs 速度/显存 vs 部署友好度**。三个主流候选: + +| backbone | 通俗理解 | 优点 | 缺点 | 适合 | +|---|---|---|---|---| +| **EfficientNet-lite** | 专为端侧设计的「省电小钢炮」 | 量化友好、ONNX 导出干净、wasm/移动端成熟 | 精度上限略低于大模型 | Flash 复用 / Pro 求稳 | +| **ConvNeXt-tiny** | 现代卷积网,精度接近 ViT 但更好部署 | 精度高、卷积结构对各 EP 友好、量化稳 | 比 lite 系重一些 | Pro 主推 | +| **小 ViT(如 ViT-Tiny/DeiT-Tiny)** | Transformer,全局视野强 | 美学/语义任务表现好、和 CLIP 蒸馏天然契合 | 显存/算子对量化和 wasm 略不友好、低分辨率需注意 | Pro 偏美学/语义 | + +**选型方法(已定:不预先拍死,双候选 A/B 对比)**: + +考虑到 Pro 的核心卖点包含美学与场景语义(要蒸 CLIP,语义权重不低),**小 ViT 的精度收益可能值得**——ViT 对全局构图、语义的感知通常强于同级卷积网。因此不靠拍脑袋选,而是各蒸一版、用双指标对比: + +- **候选 A:ConvNeXt-tiny** —— 风险最低的基线。卷积算子各 EP 都吃得下,量化稳,跨平台一致性好。 +- **候选 B:小 ViT(DeiT-Tiny / ViT-Tiny)** —— 精度/语义上限可能更高,和 CLIP 蒸馏天然契合,但需验证 CoreML 算子回退与延迟。 + +对比口径:在人工验证集上跑前面定义的双指标(蒸馏保真度 SRCC/PLCC + 筛片实用性 AUC/precision@k),并在 Apple Silicon 真机量两者的 CoreML 回退比例和单图延迟。**数据说话,不靠先验。** + +> 排除项:MUSIQ 的多尺度 Transformer(端侧延迟不可接受,学生固定单分辨率输入);Swin-Tiny(算子更冷门,跨 EP 风险高于普通 ViT)。EfficientNet-lite / MobileNet 系留作「下沉弱硬件或与 Flash 共用主干」的备选。 + +#### 最小对比实验清单 + +1. 各蒸一版 A / B,相同教师、相同蒸馏集、相同输入分辨率。 +2. 在隔离验证集上量:SRCC、PLCC(vs 教师);AUC、precision@k(vs 真实 keep/reject),**按场景分别报**。 +3. int8 量化后复测精度掉多少。 +4. Apple Silicon 真机:CoreML 算子回退比例、单图延迟、batch 吞吐。 +5. Windows 6GB 卡(如 3050)真机:显存占用、延迟、吞吐。 + +> **CoreML 算子覆盖是 Mac 端的关键约束**:CoreML EP 不支持全部 ONNX 算子,遇到不支持的会回退到 CPU 跑那一段,拖慢整体。卷积网(ConvNeXt / EfficientNet)算子覆盖好、风险低;小 ViT 的部分 Transformer 算子在 CoreML 上风险略高——这正是上面实验第 4 步要量的。若 B 的精度收益不足以抵消 Mac 端的回退代价,则回落 A。 + +--- + +## 6. 最低性能配置(Pro min-spec) + +Pro 仅支持 **Windows 独显 + Apple Silicon** 两类硬件,分别看约束。 + +### 6.1 Windows 独显 + +关键约束是 **VRAM,不是算力**。共享 backbone + 多头,fp16/int8 后并不大。 + +| 档位 | 目标显卡示例 | VRAM | 预期体验 | +|---|---|---|---| +| 最低 | GTX 1660 / RTX 2060 / 3050 | **6 GB** | 可用,batch 偏小 | +| 推荐 | RTX 3060 / 4060 | 8 GB | 流畅,整场批处理 | +| 理想 | RTX 4070+ | 12 GB+ | 大 batch,高吞吐 | + +> ⚠️ 训练在 5090(32GB)上做,**不要用 5090 规格倒推 min-spec**。盯 6GB 这一档能覆盖一大批存量用户;再往上抬会砍掉很多人。 + +### 6.2 Apple Silicon + +统一内存,无独立 VRAM 约束,门槛更低: + +| 档位 | 芯片示例 | 统一内存 | 预期体验 | +|---|---|---|---| +| 最低 | M1 / M2 | 8 GB | 可用,CoreML 调 GPU+ANE | +| 推荐 | M1 Pro / M2 Pro 及以上 | 16 GB+ | 流畅,整场批处理 | + +> Intel Mac 不在 Pro 支持范围(见 §4.3),无需为其定 min-spec。 + +**吞吐杠杆**:Pro 处理整场拍摄,务必做 **batch 推理**——共享 backbone 批量过图是最大的吞吐杠杆,远比「并发多模型」有效。CPU 预处理 / 磁盘读取与 GPU 计算做流水线重叠,进一步提速。 + +--- + +## 7. 蒸馏与验证要点(承接前期讨论) + +### 7.1 蒸馏 +- 软标签来自教师在你们大数据集上的打分,**蒸馏阶段无需人工标注**,且 in-domain 数据让学生特化到筛片场景。 +- 多头蒸馏:技术头蒸 KonIQ-MUSIQ、美学头蒸 AVA-MUSIQ,**分头蒸馏避免把两种语义压成一个标量丢信息**。 +- CLIP 走语义线(喂场景头),不要硬当质量分。 + +### 7.2 验证必须分两个独立指标 +1. **蒸馏保真度**:学生 vs 教师分数的 SRCC / PLCC(学生学得像不像)。 +2. **筛片实用性**:学生分数 vs 真实 keep/reject 的排序能力(AUC、precision@k,尤其 AI Pick top-k 命中率)。 + +常见情况是「保真度高、实用性一般」——说明问题在教师/任务定义,不在学生。**解法:蒸馏后用少量人工筛片标签做一轮微调(distill → fine-tune)**,让学生从「通用质量」偏移到「筛片偏好」。个性头本质就是这个微调的产物,按场景分别做。 + +> 验证集必须人工标注,且与蒸馏集严格隔离。 + +--- + +## 8. 待确认 / 下一步 + +- [ ] backbone 最终选型 → **方法已定:ConvNeXt-tiny (A) 与小 ViT (B) 双候选 A/B 对比,数据定夺**(见 §5) +- [x] ~~技术失真走方案 A 还是 B~~ → **已定:方案 B,硬技术失真交规则引擎,学习型模型只做软判断** +- [x] ~~CUDA / DirectML 是否采用双 EP 策略~~ → **已定:多 EP(CUDA/DirectML/CoreML)+ CPU 兜底** +- [x] ~~平台支持范围~~ → **已定:Flash 全平台覆盖;Pro 仅 Apple Silicon + Windows 独显,不支持 Intel Mac** +- [x] ~~首批个性头场景~~ → **已定:户外人像类(研学、户外活动),验证集占比约 85%** +- [x] ~~折中输入分辨率定档~~ → **已定:384**(理由见下) +- [x] ~~Pro 推理层进程模型与对接方案~~ → **已定:Rust 进程内嵌 `ort` crate(非 sidecar),多 EP + CPU 兜底,实现规格见 §10** +- [ ] 验证集随扩充补齐婚礼 / 会议活动等场景(当前仅户外人像可下结论) +- [ ] 执行 §5 最小对比实验,产出 A/B 选型报告 +- [ ] 执行 §10 Pro 原生推理层(codex 实现,按 §10.9 验收) + +> **分辨率定 384 的理由**:方案 B 已把最吃分辨率的技术失真(失焦/噪点/曝光)移交规则引擎在高分辨率原图上算,学习型模型只剩美学/场景/个性偏好这些对分辨率不敏感的任务。512 比 384 约多 1.78× 计算量与激活显存(ViT 因 attention 平方关系涨得更陡),收益却落在模型不负责的细粒度维度上,性价比低,且会挤压 6GB 独显 / M1 8GB 的 batch 吞吐。远景小脸属人脸检测(YuNet)与技术失真范畴,不归此模型,故不构成升 512 的理由。 + +确认后可进一步产出:损失函数与教师分数归一化设计、双指标评测脚本结构(Pro 原生推理层对接方案已在 §10 定稿)。 + +--- + +## 9. 验证集标签处理规范 + +当前验证集 5000 张,含星级与无星级,后续扩充。本节定义如何从中派生可靠的训练/验证标签。 + +### 9.1 标签来源与语义(已确认) + +- **有星级 = keep(正样本)**:用户主动评了星,视为「值得留」。 +- **无星级 = reject(负样本)**:**已确认是主动不给,非「还没评」**,因此可直接作负样本,无需推断。 +- 星级数值(1–5)可作为**排序强度**:5 星 > 3 星 > 1 星 > 无星,用于 learning-to-rank 而不仅是二分类。 + +> ⚠️ 前提守住:必须确保集合内无「还没评」的样本混入。若后续扩充引入了未完成评星的批次,需先剔除或单独标记,否则会把「漏评」误当「主动弃」,污染负样本。 + +### 9.2 用途划分(防数据泄漏) + +5000 张身兼两职,必须严格隔离: + +| 用途 | 说明 | 隔离要求 | +|---|---|---| +| 个性头微调 | distill → fine-tune,让学生偏移到筛片偏好 | 训练子集 | +| 双指标验证 | 量蒸馏保真度 + 筛片实用性 | 验证子集,**绝不参与任何训练** | + +- backbone 蒸馏**不用**这 5000 张——它用大数据集 + 教师软标签,所以 85% 户外人像的偏态**不会带偏主干**。 +- 划分按**拍摄场次/相册**而非单张随机切,避免同一场连拍既进训练又进验证造成泄漏。 + +### 9.3 场景偏态的影响与纪律 + +验证集约 **85% 户外人像(研学、户外活动)**: + +- **够用**:足以支撑首批「户外人像个性头」的训练与验证,与首发场景范围一致。 +- **限制**:婚礼 / 会议活动等在有标注数据前**不能下效果结论**。 +- **硬纪律**:验证报告**按场景分别报指标,禁止平均成一个总分**——85% 户外人像会把总分拉成「户外人像分」,掩盖其他场景的真实表现,形成误导。 + +### 9.4 推荐评测指标 + +| 指标 | 衡量什么 | 备注 | +|---|---|---| +| SRCC / PLCC | 学生 vs 教师分数一致性(蒸馏保真度) | 全局可报 | +| AUC | keep/reject 二分类区分力 | **按场景分报** | +| precision@k | top-k 命中真实 keep 的比例 | 对齐 AI Pick 场景,**按场景分报** | +| 排序相关(星级 vs 学生分) | 是否复现 5>3>1>无 的强度序 | learning-to-rank 用 | + +### 9.5 扩充建议 + +随数据扩充,优先补齐**非户外人像场景**(婚礼、会议活动)以解锁对应个性头和验证结论,而非继续堆户外人像——后者边际收益已低。每个新场景达到足够样本量前,其个性头标记为「实验性」,不进正式下发。 + +--- + +## 10. Pro 原生推理层 + Tauri 对接实现规格(供执行 / 供审查) + +> 本节是给执行方(codex)的**可落地、可验收**规格。审查方按 §10.9 验收清单逐条核。规格只描述 Pro 推理层的「接入边界、进程模型、接口契约、降级与隔离」,不涉及模型训练(见 §5/§7)。 + +### 10.1 目标与非目标 + +**目标**:Pro edition 下,把共享 backbone + 多头模型的推理放到 **Rust 端原生 onnxruntime**(CUDA / DirectML / CoreML EP + CPU 兜底),前端通过 Tauri command 调用,吃到 GPU 算力并支持 batch。 + +**非目标**(本轮不做,明确划线): + +- 不动 Flash 的 onnxruntime-web wasm 链路(`aiAnalyzer.worker.ts` / `peopleSplit.worker.ts` **一行不改**)。 +- 不替换现有 YuNet 人脸检测、MediaPipe Landmarker、SFace、规则引擎——它们继续在 worker 里跑。Pro 推理层**只接管新的蒸馏多头模型**(美学/场景/个性偏好)。 +- 不在本轮训练或转换真实模型;用一个占位 ONNX(输入 `[N,3,384,384]`、输出多头 dict)打通链路即可,模型替换是后续事。 + +### 10.2 现状边界(已核实,作为接入锚点) + +- ONNX 推理现状:**全在前端 worker**,`onnxruntime-web/wasm`,`InferenceSession.create({ executionProviders: [backend] })`,backend 仅 `wasm`(`webgpu` 被 gate)。 +- Rust 端:已有 `pro` feature(`Cargo.toml [features] pro = []`)、30 个 `#[tauri::command]`、完整 `#[cfg(feature = "pro")]` gate(现仅罩 RawTherapee 链路),**无任何 ONNX/ort 依赖**。 +- edition 分流:前端 `IS_PRO_EDITION`(`src/utils/appInfo.ts`),构建走 `tauri:build:pro --features pro`。 +- 接口现状:worker 经 `AiModelAssets`(`src/types.ts:291`)拿模型/wasm 候选路径;主线程经 `useAiCulling.ts` postMessage 驱动 worker。 + +### 10.3 进程模型:进程内绑定(已定,非 sidecar) + +**决策:Rust 进程内嵌 `ort` crate(onnxruntime 官方 Rust 绑定),不开独立 sidecar 进程。** + +理由: + +- **零 IPC 拷贝大 buffer**:sidecar 要把每张图的像素或张量跨进程序列化传递,整场批处理代价高。进程内绑定直接在 Rust 堆上预处理 + 喂模型。 +- **ort crate 原生支持多 EP**:CUDA / DirectML / CoreML / CPU 都是 feature flag,与本设计的多 EP 策略天然对齐。 +- **生命周期简单**:session 随 App 状态(`tauri::State`)持有,无需管子进程崩溃重启、端口、心跳。 + +代价与兜底:进程内推理若 GPU 驱动崩溃会带崩主进程。**缓解**:EP 初始化与首次 warmup 包在 `catch_unwind` / `Result` 里,失败按 §10.6 降级链回落,不 panic 主进程。**若**真机回归发现某 EP 驱动稳定性差到必须隔离,再退化为 sidecar——但默认不做,避免过度工程。 + +### 10.4 模块与 feature gate 结构 + +新增 Rust 模块(全部 gate 在 `#[cfg(feature = "pro")]` 后),建议结构: + +``` +src-tauri/src/ + lib.rs # 现有;仅新增 pro 推理 command 的注册(invoke_handler) + pro_infer/ + mod.rs # pub use;#![cfg(feature = "pro")] 整模块 gate + ep.rs # EP 探测与选择(§10.6) + session.rs # 模型加载、session 缓存、manifest 解析(§3.1) + preprocess.rs # 解码/resize 384/归一化,复用现有 image crate + infer.rs # batch 推理、多头输出解析 + types.rs # 与前端共享的 serde 结构(§10.5) +``` + +`Cargo.toml` 新增(gate 在 pro feature,Flash 构建不引入): + +```toml +[features] +default = [] +pro = ["dep:ort", "dep:ndarray"] + +[dependencies] +ort = { version = "2", optional = true, default-features = false } +ndarray = { version = "0.16", optional = true } +``` + +> EP 的 Cargo feature 按目标平台条件启用:Windows 启 `cuda` + `directml`,macOS 启 `coreml`,全平台保底 CPU。具体 feature 名以 codex 选定的 `ort` 版本文档为准——**这是审查点,不是拍死项**。 + +### 10.5 接口契约(Tauri command + TS 类型) + +前端在 `IS_PRO_EDITION` 时,不走 worker 的 wasm 美学模型,改 invoke 下列 command。**Flash 路径完全不碰这些 command。** + +Rust 端(命名固定,便于审查对照): + +```rust +#[cfg(feature = "pro")] +#[tauri::command] +async fn pro_infer_init(state: State<'_, ProInferState>, manifest_path: String) + -> Result; +// 探测 EP、加载 backbone + 各头、warmup。返回实际生效的 EP 与已加载头列表。 + +#[cfg(feature = "pro")] +#[tauri::command] +async fn pro_infer_batch(state: State<'_, ProInferState>, req: ProBatchRequest) + -> Result; +// 一批图片路径 → 多头分数。Rust 端自己解码+预处理+batch,不从前端收像素。 +``` + +共享数据结构(Rust serde 与 TS interface 字段名一一对应): + +```ts +// src/types.ts 新增(与 Rust types.rs 对齐) +export interface ProInferCapabilities { + activeEp: 'cuda' | 'directml' | 'coreml' | 'cpu'; + epFallbackChain: string[]; // 实际尝试顺序,审查降级逻辑用 + backboneVersion: string; // 取自 manifest + loadedHeads: string[]; // e.g. ['aesthetic_v1','scene_v2','persona_outdoor_v1'] + inputResolution: number; // 必须 === 384 + warmupMs: number; +} + +export interface ProBatchRequest { + imagePaths: string[]; // 传路径而非像素,Rust 解码 + batchSize?: number; // 缺省由 Rust 按 VRAM 档位决定 + heads?: string[]; // 缺省跑全部已加载头;场景头路由可后置 +} + +export interface ProHeadScores { + imagePath: string; + aesthetic?: number; // 0..1,喂 calibratedAestheticModelScore 接入点 + sceneLabel?: string; + sceneConfidence?: number; + personaScore?: number; // 经场景头路由后启用的个性头分 + error?: string; // 单图失败不挂全批 +} + +export interface ProBatchResponse { + results: ProHeadScores[]; + ep: string; + elapsedMs: number; +} +``` + +**接入点对齐 §2.1**:`aesthetic` 喂 `photoScoring.ts` 的 `calibratedAestheticModelScore`;`sceneLabel/sceneConfidence` 喂 `classifyScoreScene`;`personaScore` 喂 `aiPickRankScore`。本轮只要求打通 `aesthetic` 一条,其余字段先返回占位,接入由后续轮做——但**类型一次定全**,避免反复改契约。 + +### 10.6 EP 探测与降级链(按 §4.2 落地) + +启动探测顺序固定,逐级 try,任一级初始化或 warmup 失败则回落下一级,最终 CPU 兜底**永不失败**: + +``` +macOS + Apple Silicon → CoreML → CPU +macOS + Intel → CPU(Pro 不支持,仅防御性兜底,正常不该进 Pro) +Windows + NVIDIA → CUDA → DirectML → CPU +Windows + 其他 GPU → DirectML → CPU +``` + +要求: + +- 每一级失败必须**记录原因**进 `epFallbackChain`,前端可见(审查时据此判断「为什么没吃到 CUDA」)。 +- `activeEp` 必须反映**真实生效**的 EP,不能写「期望值」。 +- CoreML 算子回退(§5 实验第 4 步)属正常现象,不算降级,但应在日志体现回退比例(若 ort 暴露该信息)。 + +### 10.7 图像输入与 batch(吞吐杠杆,对齐 §6) + +- **Rust 端拥有预处理**:收路径 → 解码(复用现有 `image` crate / RAW 链路)→ resize 到 **384**(§8 已定)→ 归一化 → 组 batch 张量。不从前端传像素,避免跨边界拷贝。 +- batch 大小按硬件档位(§6.1/§6.2)自适应:6GB 卡小 batch、12GB+ 大 batch;缺省值由 Rust 探测 VRAM 决定,前端可覆盖。 +- CPU 预处理与 GPU 推理做流水线重叠(解码线程池 + 推理队列),这是 §6 强调的最大吞吐杠杆。 + +### 10.8 隔离纪律(审查重点) + +- Flash 构建(`--features` 不含 pro)**不得编译进** `ort`/`ndarray`,产物体积与依赖树与现状一致——审查时 `cargo tree` 对比。 +- 所有新代码 gate 在 `#[cfg(feature = "pro")]`,Flash `cargo build` 不触达 `pro_infer/`。 +- 前端 Pro 推理调用 gate 在 `IS_PRO_EDITION` 后,Flash 运行时不 invoke 任何 `pro_infer_*` command(缺失时也不报错)。 +- 不复用、不修改 worker 里的 wasm session 加载逻辑。 + +### 10.9 验收清单(审查方逐条核) + +1. `pnpm tauri:build:flash` 成功,且 `cargo tree`(flash)**不含 ort/ndarray**。 +2. `pnpm tauri:build:pro` 成功,含 `pro_infer` 模块与新 command 注册。 +3. Windows N 卡真机:`pro_infer_init` 返回 `activeEp: 'cuda'`,`inputResolution: 384`;拔掉/伪造 CUDA 失败时回落 `directml`,`epFallbackChain` 记录原因。 +4. Apple Silicon 真机:`activeEp: 'coreml'`,CoreML 算子回退比例有日志。 +5. `pro_infer_batch` 对一批图返回每图 `aesthetic` 分;单图损坏只在该图 `error` 字段报错,不挂全批。 +6. batch 吞吐 > 单图循环(同机对照),证明 batch 路径生效。 +7. Flash 运行时无任何 `pro_infer_*` invoke;Pro 运行时美学分来自原生层而非 wasm NIMA。 +8. 占位模型可一键替换为真实蒸馏模型(仅改 manifest + 模型文件,不改代码)。 +9. EP/session 初始化失败不 panic 主进程,按降级链回落,最坏 CPU 可用。 + +> **审查口径**:1/2/7/8 是硬隔离与可替换性,必须全过;3/4/9 是跨平台与健壮性,按真机结果判;5/6 是功能与性能基线。任一隔离项(1/2/7)不过即打回。 diff --git a/docs/PRO_SEMANTIC_PAPER_ARTIFACTS.md b/docs/PRO_SEMANTIC_PAPER_ARTIFACTS.md new file mode 100644 index 0000000..4c46a39 --- /dev/null +++ b/docs/PRO_SEMANTIC_PAPER_ARTIFACTS.md @@ -0,0 +1,148 @@ +# FrameCull Pro 论文留档说明 + +这个工具是给 `Semantic Teacher / Student Lab` 留论文材料用的。 + +它会把两类东西一起收下来: + +1. 5090 服务器上的训练日志、评估结果、报告、样例图、导出 manifest +2. 本地仓库里的诊断材料、任务文档、goal prompt、关键训练/评估脚本 + +这样后面写论文时,不会出现“结果在服务器上,分析在本地,脚本又找不到是哪版”的情况。 + +## 默认会收什么 + +- 训练和评估日志 +- Markdown 报告 +- JSON / CSV 指标 +- teacher / eval 样例图 +- QA 报告、license 结论、训练报告、ablation 报告 +- 当前服务器状态、GPU、远端 git 状态 +- 当前本地 git 状态 +- false-face 诊断目录 +- 当前任务文档和关键脚本副本 + +## 默认不会收什么 + +- `*.pt` +- `*.onnx` +- `*.npz` +- `*.jsonl` + +这些大文件先默认不带,避免本地磁盘被拖满。需要时可以用参数单独打开。 + +## 运行方法 + +最省事的方式是直接跑这个一键脚本: + +```powershell +powershell -ExecutionPolicy Bypass -File tools\pro-train\capture_semantic_paper_snapshot.ps1 -Tag current +``` + +它会在当前会话里询问服务器密码,然后调用稳定 Python 去抓快照。 + +## 手动运行 + +先在 PowerShell 里设置服务器密码: + +```powershell +$env:FC_SSH_PASS = '你的服务器密码' +``` + +然后用稳定 Python 运行: + +```powershell +& 'C:\Users\29238\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' ` + tools\pro-train\collect_semantic_teacher_paper_artifacts.py +``` + +默认输出目录: + +```text +output\paper-artifacts\semantic-teacher-lab\ +``` + +每次运行都会新建一个带时间戳的快照目录,并额外打一个 zip。 + +## 常用参数 + +一键脚本同样支持这些参数,比如: + +```powershell +powershell -ExecutionPolicy Bypass -File tools\pro-train\capture_semantic_paper_snapshot.ps1 ` + -Tag after-student ` + -IncludeJsonl ` + -IncludeModels +``` + +下面这些是底层 Python 脚本参数: + +带上完整 teacher jsonl: + +```powershell +& 'C:\Users\29238\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' ` + tools\pro-train\collect_semantic_teacher_paper_artifacts.py ` + --include-jsonl +``` + +带上导出的 ONNX: + +```powershell +& 'C:\Users\29238\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' ` + tools\pro-train\collect_semantic_teacher_paper_artifacts.py ` + --include-models +``` + +连 checkpoint 也留档: + +```powershell +& 'C:\Users\29238\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' ` + tools\pro-train\collect_semantic_teacher_paper_artifacts.py ` + --include-checkpoints +``` + +## 快照里至少会有什么 + +- `README.md` +- `manifest.json` +- `artifact-index.csv` +- `paper-summary.json` +- `paper-summary.md` +- `provenance/semantic-teacher-status.txt` +- `provenance/active-jobs.txt` +- `provenance/gpu.txt` +- `provenance/workspace-head.txt` +- `provenance/workspace-status.txt` +- `provenance/local-workspace-head.txt` +- `provenance/local-workspace-status.txt` +- `remote/...` 下面的服务器产物 +- `workspace/...` 下面的本地诊断、文档和脚本副本 + +其中: + +- `manifest.json` 是完整留档清单,适合追溯“这次到底收了哪些文件” +- `paper-summary.json` 是结构化摘要,适合后面做表格、画图、抽指标 +- `paper-summary.md` 是人直接读的速记版,适合写论文时先快速回忆这一轮结果 + +根目录还会持续追加一个: + +- `snapshot-history.jsonl` + +它会把每次快照的时间、路径、下载统计和关键阶段指标串起来,后面做“多轮实验时间线”会方便很多。 + +## 使用建议 + +- 训练开始前收一次,记录初始状态 +- teacher 跑完后收一次 +- student 跑完后收一次 +- eval / ablation 跑完后再收一次完整快照 + +建议固定用下面这几个 tag,后面写论文会很整齐: + +- `before-run` +- `after-teacher` +- `after-student` +- `after-export` +- `after-eval` +- `final` + +如果某些阶段还没产出,对应文件会在 `manifest.json` 里标成 `missing`,不会让整次收集失败。 diff --git a/docs/PRO_SEMANTIC_TEACHER_LAB_RESEARCH_TASK.md b/docs/PRO_SEMANTIC_TEACHER_LAB_RESEARCH_TASK.md new file mode 100644 index 0000000..1d431b5 --- /dev/null +++ b/docs/PRO_SEMANTIC_TEACHER_LAB_RESEARCH_TASK.md @@ -0,0 +1,577 @@ +# FrameCull Pro Semantic Teacher Lab 科研流程任务书 + +## Material Passport + +- 产出日期:2026-06-22 +- 使用技能:`research`、`research-deep` +- 技能状态:本机已安装,路径分别为 `C:\Users\29238\.codex\skills\research` 与 `C:\Users\29238\.codex\skills\research-deep` +- 工程权威规格:`docs/GOAL_pro_semantic_teacher_lab.md`、`docs/PRO_MODEL_ARCHITECTURE.md` +- 外部来源核查:已按 2026-06-22 可访问公开资料核查 CVPR 2026 / arXiv / Hugging Face / GitHub 来源 +- 当前状态:科研规划完成,进入可执行实验拆解 +- Research outline:`research/framecull-pro-semantic-teacher-lab/outline.yaml` +- Research fields:`research/framecull-pro-semantic-teacher-lab/fields.yaml` +- Deep research results:`research/framecull-pro-semantic-teacher-lab/results/*.json` + +## 一句话结论 + +FrameCull Pro 下一阶段不应该把大模型直接塞进客户端,而是做一个服务器侧的 **Semantic Teacher Lab**:用 5090 工作站上的 VLM teacher 读取高分辨率原图,生成带视觉证据的语义软标签,再蒸馏到能在消费级显卡上跑的 Pro Student V2 多头模型。 + +核心不是让模型“说得像懂了”,而是让 teacher 输出能被审计的结构: + +- `reasoningTrace`:区域 -> 视觉证据 -> 支持/反对保留 +- `faceRegionVerdicts`:疑似人脸区域 -> 真/假脸判断 -> 证据 +- `semanticKeepScore`:必须能回溯到 trace,不能凭空给一个标量分 +- `faceValidityScore` / `falseFaceRisk`:必须从区域级 verdict 汇总 + +如果 grounded teacher 比 flat-scalar teacher 没有明显增益,就说明“内容理解”只是口号,不能进入产品。 + +## 调研结论 + +### 1. CVPR 2026 的参考价值 + +CVPR 2026 相关工作给我们的启发不是“照搬某一篇模型”,而是方法论: + +- Mirage / Machine Mental Imagery 强调多模态模型不能只靠文字推理,而要形成内部视觉表征,再基于这些表征推理。 +- Grounded Chain-of-Thought 强调推理步骤要和图像区域证据绑定,而不是只输出一段看起来合理的解释。 +- G2VLM 强调把几何/空间 grounding 融入 VLM,让模型先理解空间结构再回答问题。 +- DeepScan 强调先做层级扫描和证据提取,再基于证据进行视觉推理。 + +对应到 FrameCull:我们不需要复现这些论文里的完整训练框架,也不需要把它们的大模型塞进产品。我们要吸收的是 **先形成视觉证据,再给筛片判断** 的流程,把它落到 teacher schema、student heads 和 grounded-vs-flat 消融实验里。 + +落地边界: + +- 不把“理解画面内容”当宣传词,必须表现为可审计字段:`reasoningTrace`、`faceRegionVerdicts`、`regions`。 +- 不让 teacher 只输出一个漂亮分数;`semanticKeepScore` 必须能追溯到区域证据。 +- 不用 2026 论文替代现有 Pro 架构;它们只指导 teacher 标注方式和评估问题。 +- 如果 grounded teacher 不赢 flat-scalar teacher,就不把“内容理解”作为产品卖点。 + +已核查的公开来源显示:Qwen2.5-VL-7B-Instruct 模型页标注为 `apache-2.0`,并明确支持目标定位、坐标/属性结构化输出;DINOv2 仓库说明代码和模型权重使用 Apache License 2.0;IQA-PyTorch / PyIQA 的仓库和权重存在非商业授权信息,因此 MUSIQ/IQA 只能先按实验 teacher 使用,正式商用蒸馏前必须单独做 license gate。 + +### 2. Teacher 候选 + +首选 teacher:`Qwen2.5-VL-7B-Instruct` + +原因: + +- 有较强图像理解、目标定位、结构化输出能力。 +- 7B 级别适合 5090 32GB 离线跑。 +- 适合输出 JSON、区域框、场景解释、假脸判断。 + +备选/对照 teacher:`InternVL3-8B/14B` + +用途: + +- 做第二 teacher 或交叉一致性检查。 +- 如果 license 或显存不满足,只作为研究对照,不参与正式蒸馏。 + +独立质量/特征 teacher: + +- `DINOv2`:提供 `dino[768]` 视觉 embedding,用于 `L_embedding`。 +- `CLIP`:提供语义 embedding 和场景辅助。 +- `MUSIQ / IQA`:提供技术质量和通用美学分,但不能代表摄影师偏好。 + +### 3. License 是硬门禁 + +全量 teacher 标注前必须先产出 `teacher-license-clearance.md`。 + +原因很简单:teacher 输出会被蒸馏进最终 student 权重,而 student 未来可能随 Pro 包分发。也就是说,teacher license 不只是“能不能离线跑”,还要确认: + +- 是否允许用模型输出训练下游模型。 +- 是否允许下游模型商业分发。 +- 是否要求额外声明、署名或限制使用场景。 + +不清楚或非商业的 teacher 只能做研究对照,不能产正式 student 训练标签。 + +## 研究问题 + +RQ1:语义 teacher 标签是否能提升 `38% / 45% / 50%` 低精选比例下的人工可用片召回? + +RQ2:它是否能减少空镜、大景、环境人像、纪实瞬间被技术分系统性压低的问题? + +RQ3:区域级假脸判断是否能降低轮胎、灯、圆形物体被误识别人脸的问题? + +RQ4:grounded teacher 是否真的优于 flat-scalar teacher? + +RQ5:蒸馏后的 Pro Student V2 是否能在 6GB 消费级独显和 CPU fallback 下达到可接受速度? + +## 数据口径 + +### 数据集 + +| 数据集 | 本地路径 | 服务器路径 | 正样本口径 | +|---|---|---|---| +| G 盘三组验证集 | `D:\FrameCullRawAudit\raw-audit-previews` | `/data/FrameCullModelLab/incoming/raw-audit-previews` | `rating >= 3` | +| 相机扩展集 | `D:\FrameCullRawAudit\camera-audit-previews` | `/data/FrameCullModelLab/incoming/camera-previews-384` | `rating >= 1` | +| 相机原图 | `E:\BaiduNetdiskDownload\相机` | `/data/FrameCullModelLab/incoming/camera-original/相机` | 同相机扩展集 | +| 相机 teacher 图 | 由相机原图生成 | `/data/FrameCullModelLab/incoming/camera-teacher-jpegs` | 只作为 VLM teacher 输入 | + +### 红线 + +- teacher 读高分辨率原图,不读 384 预览图。 +- 相机 RAW 先转成高分辨率 teacher JPEG,再给 VLM;这一步不是 384 preview fallback。 +- 384 只用于 student 训练/推理输入。 +- 星级只用于训练标签和评估指标,不进入 teacher prompt、student 输入、排序特征。 +- 文件夹、路径、文件名不进入排序特征。 +- 无星级和 0 星在相机集里视为淘汰。 +- G 盘旧验证集不能用 `rating>=1`,必须用 `rating>=3`。 + +## Teacher Schema 要求 + +每张图输出一条 JSONL,最小必需字段: + +```json +{ + "schemaVersion": "framecull-semantic-teacher-v1", + "photoId": "DSC0001", + "imagePath": "/data/FrameCullModelLab/incoming/camera-teacher-jpegs/DSC0001.jpg", + "teacherModel": "qwen2.5-vl-7b-instruct", + "sceneType": "environmental_portrait", + "sceneConfidence": 0.82, + "semanticKeepScore": 0.74, + "faceValidityScore": 0.91, + "falseFaceRisk": 0.02, + "compositionScore": 0.68, + "momentScore": 0.72, + "lightingMoodScore": 0.65, + "reasoningTrace": [ + { + "region": [0.12, 0.18, 0.74, 0.88], + "observation": "subject is clearly framed by the environment", + "supportsKeep": true, + "weight": 0.7 + } + ], + "faceRegionVerdicts": [ + { + "region": [0.33, 0.21, 0.48, 0.42], + "isRealHumanFace": true, + "evidence": "facial features align with a visible body", + "confidence": 0.93 + } + ], + "uncertain": [] +} +``` + +QA-only 字段可以输出,但不建 student head: + +- `storytellingScore` +- `emptyOrFillerScore` +- `technicalVisibleIssueScore` +- `scenicValueScore` + +## 实验流程 + +### Phase 0:数据和环境审计 + +目标:确认服务器、数据、标签、原图路径都可用。 + +相机 RAW 需要先生成高分辨率 teacher JPEG,避免 VLM 直接读 ARW 失败,也避免退回 384 预览图。该步骤应在数据审计前完成: + +```bash +cd /data/FrameCullModelLab/workspace +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/prepare_semantic_teacher_images.py \ + --input /data/FrameCullModelLab/incoming/camera-original/相机 \ + --out /data/FrameCullModelLab/incoming/camera-teacher-jpegs +``` + +交付物: + +- `data-audit.json` +- `data-audit.md` +- `all-images.json` +- `smoke-list.json` + +验收: + +- 每条 teacher 记录都有高分辨率 `teacherImagePath`。 +- teacher manifest 不使用 384 preview fallback。 +- G 盘和相机集分别记录自己的正样本阈值。 + +### Phase 1:Teacher license gate + smoke + +目标:先确认合法可用,再跑 80 张 smoke。 + +交付物: + +- `teacher-license-clearance.md` +- `smoke-qwen2.5-vl.jsonl` +- `teacher-smoke-summary.json` +- `teacher-failures.csv` + +验收: + +- 至少一个 teacher 被标记为 cleared。 +- smoke JSONL schema 通过。 +- `reasoningTrace` 和 `faceRegionVerdicts` 覆盖率达标。 +- 失败样本可 resume,不影响整批。 + +### Phase 2:全量 Semantic Teacher 标注 + +目标:对全量训练/验证图生成 grounded teacher 标签。 + +交付物: + +- `semantic-teacher-v1.jsonl` +- `semantic-teacher-v1.summary.json` +- `teacher-quality-report.md` +- `teacher-qa-samples.csv` + +验收: + +- schema 校验通过。 +- 每个场景类型有分布统计。 +- 低置信、冲突、疑似假脸样本单独列出。 +- teacher 输出不包含星级、路径推断、文件名推断。 + +### Phase 2.5:质量/嵌入 teacher 特征 + +目标:补齐 student distillation 的非 VLM teacher 输入。 + +必须生成: + +- `musiq_tech` +- `musiq_aes` +- `clip[512]` +- `dino[768]` + +交付物: + +- `teacher-camera.npz` +- `teacher-audit3groups.npz` +- `teacher-feature-summary.json` + +验收: + +- DINOv2 不得缺失。 +- 这些特征跑 384 student 输入,不和 VLM teacher 的高分辨率原图链路混用。 + +### Phase 3:Pro Student V2 多头训练 + +默认 student:`ConvNeXt-Tiny` + +候选 student:小 ViT / DeiT-Tiny,仅当 6GB 显存和 DirectML/CoreML 算子验证通过时保留。 + +训练目标: + +- `L_aesthetic`:MUSIQ aesthetic +- `L_scene`:teacher sceneType +- `L_semantic_keep`:teacher semanticKeepScore +- `L_face_validity`:teacher faceValidityScore + 假脸 hard negatives +- `L_persona`:人工星级 weighted BCE + pairwise ranking +- `L_embedding`:CLIP/DINOv2 cosine + +交付物: + +- `student-best.pt` +- `training-report.json` +- `feature-importance.csv` +- `ablation-report.md` + +验收: + +- 只建映射表里明确需要的 heads。 +- QA-only 字段不建 head。 +- G 盘和相机集按各自阈值报告召回。 +- 不把 Flash 链路改成 Pro 模型。 + +### Phase 4:ONNX / INT8 导出 + +交付物: + +- `model.onnx` +- `model.int8.onnx` +- `manifest.json` +- `manifest.int8.json` +- `export-report.json` +- `quant-compare.json` +- `selected-model-manifest.json` + +Pro 输出字段: + +- `aesthetic` +- `sceneLabel` +- `sceneConfidence` +- `personaScore` +- `semanticKeepScore` +- `faceValidityScore` +- `compositionScore` +- `momentScore` +- `lightingMoodScore` +- `falseFaceRisk` + +验收: + +- FP32 和 INT8 都能被现有 Pro native infer layer 加载。 +- INT8 分数漂移超阈值时不得进入候选。 +- 坏图只返回单图 `error`,不挂整批。 + +### Phase 5:A/B 评估和消融 + +对比组: + +- `current-production-rules` +- `ratio-aware-rules` +- `pro-persona-v1` +- `pro-semantic-v2-persona-only` +- `pro-semantic-v2-semantic-only` +- `pro-semantic-v2-fused` +- `pro-semantic-v2-face-guard` +- `pro-semantic-v2-flat-scalar` + +比例: + +- `38%` +- `45%` +- `50%` +- `60%` + +交付物: + +- `summary.md` +- `metrics-by-ratio.csv` +- `metrics-by-scene.csv` +- `false-negatives-by-ratio.csv` +- `duplicate-pollution-by-ratio.csv` +- `false-face-samples.csv` +- `grounded-vs-flat-ablation.md` +- `pro-infer-latency.csv` + +验收: + +- 每个数据集按自己的正样本口径报告,不混成一个阈值。 +- 报告空镜/大景/环境人像/纪实瞬间的分层表现。 +- `grounded` 必须和 `flat-scalar` 单独对比。 +- hard issue picked 必须为 0。 +- 重复组非代表不得被模型救回。 + +### Phase 6:产品进入门槛 + +进入 Pro gated ranking 的最低条件: + +- 任一低比例 `38% / 45% / 50%` 召回提升 `>= 5%`,或 4/5 星覆盖提升 `>= 8%`。 +- 负样本混入率恶化不超过 `2%`。 +- 重复污染不恶化。 +- blocked / hard issue picked 为 `0`。 +- 假脸误报降低,至少不高于现有规则。 +- DirectML 和 CPU fallback 性能可接受。 + +不达标时: + +- 不接入默认 Pro ranking。 +- 保留 teacher 标签、训练报告和失败分析。 +- 下一轮再判断是 teacher 不稳、student 容量不足、标签口径问题,还是训练数据场景偏差。 + +## 服务器目录规范 + +```text +/data/FrameCullModelLab/ + incoming/ + raw-audit-previews/ + camera-previews-384/ + camera-original/ + 相机/ + camera-teacher-jpegs/ + camera-labels/ + cache/ + huggingface/ + torch/ + pip/ + tmp/ + features/ + teacher/ + semantic-teacher/ + outputs/ + semantic-teacher-lab/ + semantic-student-v2/ + workspace/ + tools/ + pro-train/ + ai-lab/ + output/ + ai-bench/ +``` + +所有重型模型、缓存、临时文件都放 `/data/FrameCullModelLab`,不写系统盘,不上传第三方云。 + +## 当前可执行状态 + +- `research` 与 `research-deep` 已在本机 Codex skills 目录中可用。 +- 已生成 research planning 文件: + - `research/framecull-pro-semantic-teacher-lab/outline.yaml` + - `research/framecull-pro-semantic-teacher-lab/fields.yaml` +- 已生成 deep research 结果: + - `research/framecull-pro-semantic-teacher-lab/results/cvpr_2026_grounded_visual_reasoning.json` + - `research/framecull-pro-semantic-teacher-lab/results/semantic_teacher_selection.json` + - `research/framecull-pro-semantic-teacher-lab/results/teacher_schema_and_quality_gates.json` + - `research/framecull-pro-semantic-teacher-lab/results/quality_and_embedding_teachers.json` + - `research/framecull-pro-semantic-teacher-lab/results/pro_student_v2_distillation.json` + - `research/framecull-pro-semantic-teacher-lab/results/evaluation_and_product_gate.json` +- 本地 smoke 已通过: + - `tools/pro-train/*.py` 全部通过 `py_compile` + - `tools/ai-lab/bench-pro-semantic-student.mjs` 与 `tools/ai-lab/tune-ai-picks-supervised.mjs` 通过 `node --check` + - heuristic grounded teacher 写入 1 条 JSONL,schema 校验 `failed=0` + - heuristic flat-scalar teacher 写入 1 条 JSONL,`--allow-flat-scalar` schema 校验 `failed=0` +- 本任务书是后续执行的主入口;若与 `docs/GOAL_pro_semantic_teacher_lab.md` 冲突,以 `docs/GOAL_pro_semantic_teacher_lab.md` 为准。 + +## 建议命令草案 + +### 数据审计 + +```bash +cd /data/FrameCullModelLab/workspace +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/audit_semantic_inputs.py \ + --gdrive-previews /data/FrameCullModelLab/incoming/raw-audit-previews \ + --gdrive-labels /data/FrameCullModelLab/incoming/raw-audit-previews/labels.json \ + --gdrive-original-dirs /data/FrameCullModelLab/incoming/raw-audit-previews \ + --camera-previews /data/FrameCullModelLab/incoming/camera-previews-384 \ + --camera-labels /data/FrameCullModelLab/incoming/camera-labels/camera-labels-final.json \ + --camera-original-dirs /data/FrameCullModelLab/incoming/camera-teacher-jpegs \ + --out /data/FrameCullModelLab/outputs/semantic-teacher-lab/phase0 +``` + +### License gate + +```bash +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/teacher_license_clearance.py \ + --teachers qwen2.5-vl-7b,internvl3-8b \ + --out /data/FrameCullModelLab/outputs/semantic-teacher-lab +``` + +### Teacher smoke + +```bash +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/run_semantic_teacher.py \ + --input /data/FrameCullModelLab/outputs/semantic-teacher-lab/phase0/smoke-list.json \ + --out /data/FrameCullModelLab/features/semantic-teacher/smoke-qwen2.5-vl.jsonl \ + --backend qwen2_5_vl \ + --model Qwen/Qwen2.5-VL-7B-Instruct \ + --limit 80 \ + --resume +``` + +### Flat-scalar ablation + +```bash +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/run_semantic_teacher.py \ + --input /data/FrameCullModelLab/outputs/semantic-teacher-lab/phase0/smoke-list.json \ + --out /data/FrameCullModelLab/features/semantic-teacher/smoke-flat-scalar.jsonl \ + --backend qwen2_5_vl \ + --model Qwen/Qwen2.5-VL-7B-Instruct \ + --flat-scalar \ + --limit 80 \ + --resume +``` + +### Teacher QA + +```bash +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/audit_semantic_teacher.py \ + --teacher /data/FrameCullModelLab/features/semantic-teacher/smoke-qwen2.5-vl.jsonl \ + --out /data/FrameCullModelLab/outputs/semantic-teacher-lab/teacher-qa-smoke +``` + +### 质量/嵌入 teacher 特征 + +```bash +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/build_quality_teacher_features.py \ + --camera-previews /data/FrameCullModelLab/incoming/camera-previews-384 \ + --audit-previews /data/FrameCullModelLab/incoming/raw-audit-previews \ + --out-dir /data/FrameCullModelLab/features/teacher +``` + +### Student 训练 + +```bash +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/train_semantic_student.py \ + --backbone convnext_tiny \ + --semantic-teacher /data/FrameCullModelLab/features/semantic-teacher/semantic-teacher-v1.jsonl \ + --quality-teacher-dir /data/FrameCullModelLab/features/teacher \ + --labels-camera /data/FrameCullModelLab/incoming/camera-labels/camera-labels-final.json \ + --labels-audit /data/FrameCullModelLab/incoming/raw-audit-previews/labels.json \ + --out /data/FrameCullModelLab/outputs/semantic-student-v2/convnext_tiny \ + --epochs 30 \ + --batch 64 +``` + +### 导出和评估 + +```bash +/home/hph/miniconda3/envs/train5090/bin/python tools/pro-train/export_pro_semantic_onnx.py \ + --student /data/FrameCullModelLab/outputs/semantic-student-v2/convnext_tiny/student-best.pt \ + --out output/pro-models/semantic_student_v2_convnext \ + --name framecull-pro-semantic-v2 + +node tools/ai-lab/bench-pro-semantic-student.mjs \ + --manifest output/pro-models/semantic_student_v2_convnext/manifest.int8.json \ + --output output/ai-bench/pro-semantic-student-eval \ + --ratios 0.38,0.45,0.50,0.60 +``` + +## 必交付文件 + +最终实验至少要产出: + +- `teacher-quality-report.md` +- `teacher-license-clearance.md` +- `summary.md` +- `metrics-by-ratio.csv` +- `metrics-by-scene.csv` +- `false-negatives-by-ratio.csv` +- `duplicate-pollution-by-ratio.csv` +- `false-face-samples.csv` +- `grounded-vs-flat-ablation.md` +- `pro-infer-latency.csv` +- `selected-config-by-ratio.json` +- `selected-model-manifest.json` +- `production-recommendation.md` + +## 风险与应对 + +| 风险 | 表现 | 应对 | +|---|---|---| +| teacher 幻觉 | JSON 看似合理但证据不对 | schema + QA 抽样 + uncertain 字段 + 第二 teacher 对照 | +| license 不清 | teacher 可跑但不能商用蒸馏 | Phase 1 前置 license gate,未通过只做研究对照 | +| student 学不到 grounded 信息 | teacher QA 好但 student 指标不升 | 降低任务复杂度,增强 face/semantic heads,换 backbone | +| flat-scalar 同样有效 | grounded trace 没带来额外收益 | 如实写入 `grounded-vs-flat-ablation.md`,不宣传“理解画面” | +| 相机集标签口径污染 | 1 星可用和 G 盘 3 星口径混淆 | 每个数据集分开阈值和分开报告 | +| 假脸仍高 | 轮胎/灯/圆形物体误识别 | 加 hard negatives,强制 faceRegionVerdicts 证据链 | +| INT8 漂移 | 量化后排序变差 | 保留 FP32 对照,漂移超阈值不上 INT8 | +| Pro 速度不够 | DirectML/CPU 太慢 | batch 调优,ConvNeXt 优先,小 ViT 只做候选 | + +## 下一步执行 Prompt + +```text +Implement FrameCull Pro Semantic Teacher Lab v1 according to docs/PRO_SEMANTIC_TEACHER_LAB_RESEARCH_TASK.md and docs/GOAL_pro_semantic_teacher_lab.md. + +Do Phase 0 to Phase 6 in order. Use the 5090 server under /data/FrameCullModelLab for teacher labeling, feature extraction, and student training. Keep all heavy model files, HuggingFace cache, Torch cache, pip cache, and temporary files under /data/FrameCullModelLab. Do not upload RAW or private photos to third-party cloud APIs. + +Teacher must read high-resolution originals, not 384 previews. 384 previews are only for student input and quality/embedding teacher features. Teacher output must be schema-validated JSONL with reasoningTrace and faceRegionVerdicts. Run a flat-scalar ablation with the same teacher and images. + +Before full annotation, generate teacher-license-clearance.md and use only cleared teachers for official labels. Qwen2.5-VL-7B is the first candidate; InternVL3 is a comparison candidate if license and VRAM allow. Generate MUSIQ, CLIP[512], and DINOv2 dino[768] features before student training. + +Train Pro Student V2 as a shared-backbone multi-head model. Build only heads mapped to ProHeadScores: aesthetic, scene, persona, semanticKeepScore, faceValidityScore/falseFaceRisk, compositionScore, momentScore, and lightingMoodScore. Do not build heads for QA-only fields. Keep Flash fully isolated. + +Apply label thresholds per dataset: G-drive/audit3groups positive is rating>=3; camera and other datasets positive is rating>=1. Report metrics per dataset and per scene. Do not average away the threshold difference. + +Export FP32 and INT8 ONNX manifests compatible with the Pro native inference layer. Evaluate current rules, Pro persona v1, semantic-only, fused, face-guard, and flat-scalar at 38%, 45%, 50%, and 60%. + +Final output must include teacher-quality-report.md, teacher-license-clearance.md, summary.md, metrics-by-ratio.csv, metrics-by-scene.csv, false-negatives-by-ratio.csv, duplicate-pollution-by-ratio.csv, false-face-samples.csv, grounded-vs-flat-ablation.md, pro-infer-latency.csv, selected-config-by-ratio.json, selected-model-manifest.json, and production-recommendation.md. +``` + +## Sources + +- CVPR 2026 Open Access Repository: https://openaccess.thecvf.com/CVPR2026 +- Mirage / Machine Mental Imagery, CVPR 2026: https://openaccess.thecvf.com/content/CVPR2026/html/Yang_Machine_Mental_Imagery_Empower_Multimodal_Reasoning_with_Latent_Visual_Tokens_CVPR_2026_paper.html +- Grounded Chain-of-Thought, CVPR 2026: https://openaccess.thecvf.com/content/CVPR2026/html/Wu_Grounded_Chain-of-Thought_for_Multimodal_Large_Language_Models_CVPR_2026_paper.html +- G2VLM, CVPR 2026: https://openaccess.thecvf.com/content/CVPR2026/html/Hu_G2VLM_Geometry_Grounded_Vision_Language_Model_with_Unified_3D_Reconstruction_CVPR_2026_paper.html +- DeepScan, CVPR 2026: https://openaccess.thecvf.com/content/CVPR2026/html/Li_DeepScan_A_Training-Free_Framework_for_Visually_Grounded_Reasoning_in_Large_CVPR_2026_paper.html +- Qwen2.5-VL Technical Report: https://arxiv.org/abs/2502.13923 +- Qwen2.5-VL-7B-Instruct: https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct +- InternVL3 Technical Report: https://arxiv.org/abs/2504.10479 +- DINOv2 repository and license: https://github.com/facebookresearch/dinov2 +- DINOv2 paper: https://arxiv.org/abs/2304.07193 +- CLIP repository: https://github.com/openai/CLIP +- CLIP paper: https://arxiv.org/abs/2103.00020 +- MUSIQ paper: https://arxiv.org/abs/2108.05997 +- IQA-PyTorch repository: https://github.com/chaofengc/IQA-PyTorch diff --git a/docs/TASK_close_false_face_evidence.md b/docs/TASK_close_false_face_evidence.md new file mode 100644 index 0000000..b6e3096 --- /dev/null +++ b/docs/TASK_close_false_face_evidence.md @@ -0,0 +1,93 @@ +# Task: 闭环验证 v11 假脸修复(学生层全量证据) + +## 背景 + +Semantic Teacher false-face 修复(v11-false-face)已完成 prompt 收紧 + merged teacher 重训 + 全量评估。审计确认: +- Teacher prompt 已实质收紧(排除岩石/树干/云朵/雕像/头盔骑手/侧背影等假脸源,landscape/documentary/product_object 场景专项约束) +- Merge 干净(6475 基线,200 替换,0 匹配失败) +- 学生重训 + 全量评估(5167 张)跑完,召回不降反升(45%: 54.46%→56.85%,60%: 71.13%→73.29%) + +## 问题:证据链缺口 + +审计发现"假脸修复"的核心证据没有归档: + +1. **现有 `fix-validation-report.md` 的 `-0.4602` 不是原问题口径** + - 原审计的 "+0.3032 假脸恶化" 是**学生层、全量、scene-mean** 的 false_face_risk + - 但 `fix-validation-report` 的 `-0.4602` 是**teacher 标注层、200 张补丁子集**上的,且基线被编码成 grounded=0.000 / flat 固定=0.500(delta 恒为 -0.5) + - 两者不是同一度量,不能互相替代 + +2. **学生层全量假脸产物没归档** + - v11 全量评估生成了 `metrics-by-scene.csv` 和 `false-face-samples.csv`,但留在远程 `/data/FrameCullModelLab/outputs/semantic-teacher-lab/eval-full/bench-grounded-v11-false-face/` + - 没有被复制进 `output/semantic-false-face-diagnosis/v11-final/` + - 归档里的 `grounded-vs-flat-ablation.md` 只是占位 stub(指向远程目录,没有实际数字) + +## 目标 + +补齐学生层全量假脸改善的可查证据,回答:"v11 收紧后,学生模型在全量 5167 张上的 scene-mean false_face_risk,是否真的不再比 flat 差、且比旧 grounded 改善?" + +**不需要重跑任何训练或评估**——所有数据已存在于远程目录,只需归档 + 对比分析。 + +## 实现步骤 + +### Step 1: 归档已有产物 + +从远程 `bench-grounded-v11-false-face/` 复制以下文件到 `output/semantic-false-face-diagnosis/v11-final/`: +- `metrics-by-scene.csv` +- `false-face-samples.csv` +- `metrics-by-ratio.csv`(如与现有不同则覆盖,相同则跳过) + +如果远程目录已被清理、文件不存在,则必须重跑 v11 全量评估的 audit/summary 步骤重新生成(仅评估,不重训)。 + +### Step 2: 三方 scene 级假脸对比 + +构造一张表,对每个场景(landscape/documentary_moment/event/group/portrait/product_object/other)报告 `false_face_risk_mean`,三方对比: + +| Scene | Flat | 旧 Grounded (v1) | 新 Grounded (v11) | v11 vs Flat | v11 vs 旧Grounded | +|---|---|---|---|---|---| + +数据来源: +- Flat: `eval-full/bench-flat/metrics-by-scene.csv` +- 旧 Grounded (v1): `eval-full/bench-grounded/metrics-by-scene.csv`(审计报告里记录的旧值:landscape 0.5753, documentary 0.3919, product_object 0.9956) +- 新 Grounded (v11): Step 1 归档的 `bench-grounded-v11-false-face/metrics-by-scene.csv` + +### Step 3: 重新计算原问题口径的 delta + +用学生层全量 scene-mean 重新算"grounded - flat"的 false-face proxy delta(原审计的 +0.3032 就是这个口径),对比 v1 和 v11: +- v1: landscape/documentary 平均 delta = +0.3032(恶化) +- v11: 目标 < +0.05(不劣于 flat) + +如果 v11 的学生层全量 delta 仍然明显为正(比 flat 差),说明 teacher 标注层的修复**没有传导到学生层**,需要在报告中明确指出,并标记为"修复未闭环"。 + +### Step 4: 输出真正的闭环验证报告 + +替换占位 stub,写一份带真实数字的报告 `v11-final/false-face-closure-report.md`,包含: +- Step 2 的三方对比表 +- Step 3 的口径对齐 delta(v1 vs v11) +- 明确结论:学生层全量假脸是否改善(是/否/部分),分场景说明 +- 召回 trade-off 复述(已知 +14.66% @45%) +- 诚实标注任何仍未达标的场景(如 product_object 旧值 0.9956 是否降下来) + +## 验收标准 + +- [ ] `metrics-by-scene.csv` 和 `false-face-samples.csv` 已归档进 v11-final +- [ ] 三方 scene 级假脸对比表完成(flat / 旧grounded / 新grounded) +- [ ] 用原问题口径(学生层全量 scene-mean grounded-flat delta)重算 v1 vs v11 +- [ ] 明确回答学生层假脸是否改善,分场景诚实标注 +- [ ] `grounded-vs-flat-ablation.md` 占位 stub 被替换为带真实数字的内容,或新增 closure-report + +## 关键约束 + +- **不要重跑训练**。学生权重已固化(v11-false-face)。 +- **不要重跑全量推理**,除非远程 csv 确实丢失。 +- **不要篡改对比口径凑数**。如果学生层全量 delta 仍为正(比 flat 差),必须如实报告"teacher 修复未充分传导到学生层",而不是用 teacher 子集的 -0.46 掩盖。 +- 诚实优先于好看。这份报告的目的是给出可信的产品候选决策依据。 + +## 输出文件 + +``` +output/semantic-false-face-diagnosis/v11-final/ + metrics-by-scene.csv # 归档(从远程复制) + false-face-samples.csv # 归档(从远程复制) + false-face-closure-report.md # 三方对比 + 口径对齐 delta + 诚实结论 + grounded-vs-flat-ablation.md # 替换占位 stub 为真实数字 +``` diff --git a/docs/TASK_false_face_decouple_v15.md b/docs/TASK_false_face_decouple_v15.md new file mode 100644 index 0000000..360ef65 --- /dev/null +++ b/docs/TASK_false_face_decouple_v15.md @@ -0,0 +1,76 @@ +# Task: 假脸线换路线——推理期独立交叉校验 / 模块剥离(不再靠加数据) + +## 背景与定论(必读) + +假脸线(student 在风景/产品/空场景幻视人脸)已经过 **v12→v13→v14 三轮蒸馏迭代**,方向正确(独立 falseFaceRisk 头 vs 旧 `1-faceValidity` 派生量,srcc 从 −0.65 翻到 +0.67)。但在同一 84 张零重叠独立 holdout 上: + +| 版本 | AUC | TPR@0.5 | +|---|---:|---:| +| v12 | 0.1765 | 0% | +| v13 | 0.3123 | 0% | +| v14(+五台山 2525 张 + 506 区域监督)| 0.4778 | 0% | + +AUC **单调爬向 0.5(随机)但从未越过**,TPR@0.5 一直 0%——本质是"从反相变成随机,但零检出",独立头只是贴低先验、根本不判别。v14 又加了 2525 张高发域数据 + 区域监督,**五台山只贡献 19/506 个 region,整图 hard-negative 仍近零**,AUC 仍卡在 chance。 + +**结论:瓶颈不是数据量。** 三轮加数据/加区域监督已证明"小 student + 全局蒸馏特征"这条路对假脸判别无效。**下一轮不许再用"加数据/加区域监督重训 student"作为主路线。** + +## 目标 + +换根本思路,让"假脸"判别真正可用(在 84 holdout 上 AUC 明显 >0.5、TPR@0.5 非零、正样本组 risk > 对照组),同时不破坏召回。优先尝试**推理期独立交叉校验**,并评估**把假脸剥离为独立模块**的可行性。 + +## 不可动的硬约束(先读) + +- **84 张独立 holdout 永不进训练/调参**:output/semantic-false-face-diagnosis/v13-eval/independent-false-face-set.csv(54 假脸正样本 + 30 真人脸对照)。 +- **评估口径与 v12/v13/v14 完全一致、可比**:同一 84 holdout,报 AUC / TPR@0.5 / FPR@0.5、正样本 vs 对照 risk 分布。新路线的输出分数要能放进同一张对比表。 +- **不再靠蒸馏加数据救假脸**:本轮主路线是推理期机制或独立模块,不是重训 student 的整图/区域监督。 +- 不动 backbone、teacher prompt、不重标注;若涉及 merged teacher 仍跑前校验 SHA。 +- **诚实优先**:若交叉校验/独立模块仍不达标,如实写"假脸判别在当前资源下不可解",给出代价分析,不许贴低先验凑均值假装收敛。 + +## 候选路线(择优实现,A 优先) + +### 路线 A:推理期独立人脸存在性交叉校验(不走蒸馏) + +思路:student 仍出召回/美学分,**假脸不再靠 student 头**。推理期额外挂一个独立、现成的人脸存在性/检测信号(如轻量人脸检测器或独立人脸 embedding 的存在性判定),用"检测说无脸 + student/teacher 说像脸"这组冲突来判 false-face。 + +1. 选 1~2 个现成、可本地跑的人脸存在性信号(明确选型理由、体积、延迟)。 +2. 在 84 holdout 上单独评估该信号:假脸正样本(应判"无真脸")vs 真人脸对照(应判"有真脸")的可分性,出 AUC/TPR/FPR。 +3. 设计交叉校验逻辑:把"人脸存在性"与现有 falseFaceRisk/faceValidity 信号组合成最终 false-face 判定,调阈值。 +4. 输出该组合在 84 holdout 上的可比指标。 + +### 路线 B:假脸剥离为独立模块(小专用判别器,仍不进 student 蒸馏) + +仅在路线 A 不足时启用。 + +1. 用区域级 crop(teacher faceRegionVerdicts 里 isRealHumanFace=false 的 region + 真人脸 region 做正负)训一个**独立小判别器**,专做"这块像脸但不是脸 / 是真脸"的二分类,与 student 解耦。 +2. 注意:训练数据仍不得含 84 holdout 的任何 photoId。 +3. 84 holdout 上同口径评估。 + +### 共同要求 + +- 无论 A/B,最终都要在**同一 84 holdout**上给出与 v12/v13/v14 同口径的对比表。 +- 评估对召回的影响:接上新假脸判定后,recall@38/45/50/60 与 negative pick rate 是否回退(@45 回退应 < 2pp,超了要说明)。 + +## 达标线 + +- 84 holdout:AUC 明显 >0.5(目标 ≥0.7),假脸正样本组 risk/判定 > 真人脸对照组(排序转正),**TPR@0.5 非零**。 +- 召回不因接入假脸判定明显回退。 +- 若达不到:诚实结论 + 代价分析(要达标需要什么:更强检测器?更大模型?更多人工标注?),不强行宣称闭环。 + +## 验收标准 + +- [ ] 选定路线(A 优先)并说明选型理由、体积/延迟代价 +- [ ] 人脸存在性信号 / 独立判别器在 84 holdout 上单独评估(AUC/TPR/FPR) +- [ ] 交叉校验/模块组合后的最终 false-face 判定,在同一 84 holdout 上出 v12/v13/v14/本轮可比对比表 +- [ ] 接入后召回回退评估(@45 < 2pp 或说明) +- [ ] 训练/调参未使用 84 holdout 任何 photoId +- [ ] 诚实判定:是否首次产生真实检出(TPR@0.5 非零、AUC>0.5);达不到则给代价分析 + +## 输出文件 + +``` +output/semantic-false-face-diagnosis/v15-crosscheck/ + approach-selection.md # 路线选型 + 代价 + face-presence-eval.json # 独立人脸存在性信号在 84 holdout 的可分性 + crosscheck-scores.csv # 组合后判定在 84 holdout 的分数 + false-face-generalization-report-v15.md # v12/v13/v14/v15 同 holdout 可比 + 闭环判定 + 召回回退 +``` diff --git a/docs/TASK_false_face_generalization_v13.md b/docs/TASK_false_face_generalization_v13.md new file mode 100644 index 0000000..dab98f0 --- /dev/null +++ b/docs/TASK_false_face_generalization_v13.md @@ -0,0 +1,77 @@ +# Task: 假脸独立头泛化验证 + 必要时扩 hard-negative 重训(v13) + +## 背景 + +v12 改用独立 `falseFaceRisk` 头是**正确方向**(已证实 `1 - faceValidity` 是坏派生量:70% 标注不一致、v11 srcc −0.65 → v12 +0.67)。但 v12 **未闭环**,二次审计确认两个硬伤: + +1. **补证集没提供独立证据**:`false-face-positive-validation-v12.md` 的两个"强正样本"正是 `training-report-v12.json` 里仅有的 2 个 hard-negative。`DSC08858`(train 集)v12 命中 0.91,`DSC08173`(val 集)v12 漏判 0.002——命中的是训练见过的、漏判的是留出的。等于拿训练样本验证自己。 +2. **训练 hard-negative 近乎为零**:全量 6475 张里符合 hard-negative(hasRealHumanFace=false 且 risk≥0.5)只有 2 张,train 仅 1 张。独立头几乎没有正样本可学,极可能只是"贴低先验"。 + +## 目标 + +回答一个问题:**v12 独立头是真能判别假脸,还是只是贴低先验?** 用一个与训练完全不重叠的独立验证集给出硬证据;若 v12 泛化失败,则扩充训练 hard-negative 后重训 v13 并复测。 + +## 关键约束(先读) + +- **独立验证集必须与训练数据零重叠**。绝不能再用训练里那 2 张 hard-negative(DSC08858 / DSC08173)或任何进过 train/val split 的样本。跑前用 photoId 集合做交集校验,交集必须为 0 并在报告里写明。 +- **口径与 v12 一致**:用 v12 独立头导出的 `falseFaceRisk`,不要回退到 `1-faceValidity`。 +- **不许拿训练样本、合成样本或 teacher 自标当独立 ground truth**。独立集的标签必须人工确认。 +- 诚实优先于好看。泛化失败就如实写"独立头未泛化",不要用训练集表现掩盖。 + +## 实现步骤 + +### Phase 1: 造独立真假脸验证集(不重训) + +1. 从**未进入 6475 训练全集**的图源里,人工挑选 30~60 张"真·假脸"正样本——即 hasRealHumanFace 应为 false、但视觉上容易被幻视成人脸的图,覆盖 landscape / product_object / empty_scene / event / food(v11 里风险最高的几类)。 + - 来源可以是 camera/audit3groups 里没被采进训练的剩余图,或新拍/新收的图。 + - 每张人工标注 `hasRealHumanFace=false` 并记录 photoId、scene、为何容易幻视(岩石/树纹/云/产品轮廓/灯具等)。 +2. 同时挑 20~30 张**真·有脸**的对照正样本(hasRealHumanFace=true),用来确认独立头不是把所有图都判高 risk(避免反向退化)。 +3. 用 photoId 与 `training-report-v12.json` 的 train+val 全集做交集校验,**交集=0**,写进报告。 + +### Phase 2: 测 v12 独立头泛化(不重训) + +1. 对独立集跑 v12 独立头,记录每张的 `falseFaceRisk`。 +2. 报告: + - 真假脸正样本上 risk 的分布(中位数 / 均值 / ≥0.5 命中率) + - 真有脸对照样本上 risk 的分布(应当低) + - 两组能否分开(简单阈值下的 TPR/FPR 或 AUC) +3. 判定: + - 若真假脸正样本 risk **显著高于**对照且命中率可观(如 ≥60% ≥0.5)→ 独立头有泛化判别力,**可初步判闭环**。 + - 若真假脸正样本 risk 仍普遍偏低(贴近全局先验 0.07)→ **未泛化**,进入 Phase 3。 + +### Phase 3: 仅在 Phase 2 失败时——扩 hard-negative 重训 v13 + +1. 把 Phase 1 挑出的真假脸正样本**之外**的、来自训练图源的更多 hard-negative 补进训练集(提高 hard-negative 数量级,从 2 张提到至少几十~上百张),重新构造训练 jsonl。 + - **注意**:Phase 1 的独立验证集这些 photoId **不得**进训练,保持留出。 +2. 仅重训 student(backbone/teacher prompt 不动),新版本 v13。 +3. 重跑 Phase 2 的独立集评估,对比 v12 vs v13。 + +### Phase 4: 报告 + +写 `output/semantic-false-face-diagnosis/v13-eval/false-face-generalization-report.md`: +- 独立集构造说明 + 零重叠校验结果 +- v12(及 v13 若有)在独立集上的 risk 分布、命中率、正负组可分性 +- 明确判定:**独立头是否真有泛化判别力 / 是否闭环** +- 若重训了 v13:召回 trade-off 复测(@45% 回退 < 2pp) +- 诚实标注仍漏判的场景 + +## 验收标准 + +- [ ] 独立真假脸验证集(30~60 正 + 20~30 对照)已建,人工标注 +- [ ] photoId 与训练 train+val 全集交集 = 0,已校验并写明 +- [ ] v12 独立头在独立集上的 risk 分布 + 正负组可分性已报告 +- [ ] 明确判定 v12 是否泛化(真判别 vs 贴先验) +- [ ] 若 Phase 2 失败:v13 扩 hard-negative 重训完成,独立集复测,v12 vs v13 对比 +- [ ] 报告给出闭环/未闭环诚实判定,召回 trade-off(若重训)已复测 + +## 输出文件 + +``` +output/semantic-false-face-diagnosis/v13-eval/ + independent-false-face-set.csv # 独立验证集 photoId/scene/标签/幻视原因 + overlap-check.json # 与训练全集交集校验(必须=0) + v12-generalization-scores.csv # v12 独立头在独立集上的 risk + v13-generalization-scores.csv # 若重训 + false-face-generalization-report.md # 泛化判定 + 闭环结论 + training-report-v13.json # 若重训:含 SHA 校验、新 hard-negative 数 +``` diff --git a/docs/TASK_false_face_v15b_crop_classifier.md b/docs/TASK_false_face_v15b_crop_classifier.md new file mode 100644 index 0000000..25a5790 --- /dev/null +++ b/docs/TASK_false_face_v15b_crop_classifier.md @@ -0,0 +1,85 @@ +# Task: 路线 B 独立 crop 假脸判别器(降 FPR + 换可判别前置 gate) + +## 背景与现状(必读) + +假脸线已走完两个阶段: + +1. **蒸馏路线(v12-v14)废弃**:给 student 加三轮数据/区域监督,84 holdout AUC 只从 0.18 爬到 0.48(随机),TPR@0.5 一直 0%。小 student + 全局特征学不会这个判别。 +2. **路线 A(v15 推理期 YuNet 交叉校验)锁为 guard-only**:84 holdout 条件 AUC 0.75 看着好,但全量 7692 replay 暴露两个致命问题: + - 上游"疑似脸 gate"(`maxFacePresence>=0.08`)触发 99.60%,几乎对所有图敞开、**不具判别力** → 0.75 是"疑似脸上下文条件 AUC",全量生产不成立。 + - YuNet 对真人脸 **36.7% 漏检** → 全量铺开大面积误伤真人脸(documentary_moment 1085 张触发、估 568 张是真人脸)。@45 自动剔除回退 29.48pp、降权 8.45pp,门槛 <2pp 无一过线。 + +本轮路线 B 目标就是攻这两个瓶颈:**(a) 把判别精度做上去(FPR 明显下来)、(b) 换一个真能分"疑似脸场景 vs 普通图"的前置 gate,而非敞开阈值。** + +## 目标 + +训练一个**与 semantic student 解耦的独立小判别器**,输入是疑似脸区域的 crop,输出"这块是真脸 / 像脸但不是脸"的二分类。目标是在同一 84 holdout 上 AUC 不低于 v15(0.75),且**FPR 明显低于 36.7%**(目标 <15%),同时换掉敞开的上游 gate,最终在全量 7692 replay 上让 @45 recall 回退 <2pp、够格进自动拦截。 + +## 不可动的硬约束(先读) + +- **84 张独立 holdout 永不进训练/调参/阈值拟合**:output/semantic-false-face-diagnosis/v13-eval/independent-false-face-set.csv。训练 crop 的来源 photoId 必须与 holdout 零交集,跑前做交集校验并写明。 +- **不并回 semantic student、不动 backbone / teacher prompt**。路线 B 是独立模块,student 仍只出召回/美学分。 +- **判别器训练数据用区域级 crop**:正样本=真人脸 region,负样本=teacher faceRegionVerdicts 里 isRealHumanFace=false 的 face-like region。训练集 photoId 不得含 holdout。 +- **同口径可比**:同一 84 holdout 报 v12/v13/v14/v15/v15B 的 AUC / TPR@0.5 / FPR@0.5,放进同一对比表;不许换口径。 +- **全量门槛固定**:接判别器后全量 7692 replay,@45 recall 回退必须 <2pp 才允许进自动拦截;超了如实判 guard-only。 +- **诚实优先**:FPR 没压下来就如实写;不许用 holdout 条件值掩盖全量表现;前置 gate 触发率必须在全量上重新刻画(对比 v15 的 99.6%)。 + +## 实现步骤 + +### Phase 0: 训练数据构造 + 零重叠校验 + +1. 从训练图源抽 face-like region crop:正样本(真人脸)+ 负样本(isRealHumanFace=false 的 face-like region)。记录每类数量、scene 分布。 +2. photoId 与 84 holdout 交集校验=0(强制),写进报告。 +3. 输出 `output/semantic-false-face-diagnosis/v15b/crop-dataset-manifest.json`。 + - **决策点**:若负样本(像脸但不是脸的 region)数量太少不足以训判别器,如实写明,并说明补充来源(不得用 holdout)。 + +### Phase 1: 训练独立 crop 判别器 + +1. 选一个轻量 crop 分类 backbone(说明选型、体积、推理延迟,与 Flash 路线兼容性)。 +2. 仅用 Phase 0 数据训练,holdout 全程留出。 +3. 输出 training-report-v15b.json(含训练集 SHA、正负样本数、未含 holdout 校验字段)。 + +### Phase 2: 换可判别的前置 gate + +1. 设计新的"疑似脸场景 vs 普通图"前置判定,替换 v15 的 `maxFacePresence>=0.08` 敞开阈值——目标是触发率明显低于 99.6% 且能真正区分场景。 +2. 在全量 7692 上刻画新 gate 的触发率 + 抽样准确率,与 v15 的 99.6% 对比。 +3. 输出 `output/semantic-false-face-diagnosis/v15b/upstream-gate-v2-coverage.json`。 + +### Phase 3: 84 holdout 评估 + +1. crop 判别器(+ 新 gate)在同一 84 holdout 上跑分。 +2. 同口径报 v12/v13/v14/v15/v15B 可比 AUC / TPR@0.5 / FPR@0.5、正样本 vs 对照分布。 +3. 达标线:AUC ≥ 0.75(不低于 v15),**FPR 明显 <36.7%(目标 <15%)**,TPR@0.5 非零,正样本组 > 对照组。 + +### Phase 4: 全量 replay + 判定 + +1. 全量 7692 跑剔除/降权/baseline 三组 replay,报 recall@38/45/50/60 + negative pick rate,同 rankMode。 +2. @45 回退是否 <2pp 的明确判定。 +3. 误伤 top 列表 + scene 分布,对比 v15A 的误伤集中区。 +4. `output/semantic-false-face-diagnosis/v15b/false-face-v15b-report.md`: + - 84 holdout 五代可比表 + FPR 是否压下来 + - 新 gate 全量触发率 vs v15 的 99.6% + - 三组 replay recall + @45 回退判定 + - 诚实结论:路线 B 是否够格进自动拦截(剔除/降权哪种可行,或仍 guard-only);不达标则说明还差什么、代价多大 + +## 验收标准 + +- [ ] crop 训练集正负样本数 + scene 分布已报,photoId ∩ 84 holdout = 0 已校验 +- [ ] 独立 crop 判别器训练完成,与 semantic student 解耦,未动 backbone/teacher prompt +- [ ] 新前置 gate 全量触发率 + 抽样准确率,与 v15 的 99.6% 对比 +- [ ] 84 holdout 同口径报 v12/v13/v14/v15/v15B 可比 AUC/TPR/FPR +- [ ] FPR 是否明显 <36.7%(目标 <15%)有明确数字 +- [ ] 全量 7692 三组 replay,@45 回退是否 <2pp 有明确判定 +- [ ] 84 holdout 未进训练/调参/阈值拟合 +- [ ] 诚实结论:路线 B 能否进自动拦截;不达标给差距 + 代价 + +## 输出文件 + +``` +output/semantic-false-face-diagnosis/v15b/ + crop-dataset-manifest.json # 训练 crop 正负样本 + 零重叠校验 + training-report-v15b.json # 判别器训练 + holdout 留出校验 + upstream-gate-v2-coverage.json # 新前置 gate 全量触发率 vs 99.6% + v15b-holdout-scores.csv # 84 holdout 分数 + false-face-v15b-report.md # 五代可比 + FPR + replay + 自动拦截判定 +``` diff --git a/docs/TASK_five_mountain_v14.md b/docs/TASK_five_mountain_v14.md new file mode 100644 index 0000000..e9f4c59 --- /dev/null +++ b/docs/TASK_five_mountain_v14.md @@ -0,0 +1,89 @@ +# Task: 五台山数据接入 + v14 假脸区域监督(双线) + +## 背景 + +新增五台山批次:2525 张已用 Qwen2.5-VL grounded teacher 全量标注(0 失败),特征也已备齐。 +- teacher 标注:`/data/FrameCullModelLab/features/semantic-teacher/five-mountain-grounded.jsonl` +- 特征:`/data/FrameCullModelLab/features/teacher/teacher-five-mountain.npz`(CLIP 2525×512、DINOv2 2525×768、MUSIQ tech、MUSIQ-AVA) +- 现有训练全集:`/data/FrameCullModelLab/features/semantic-teacher/semantic-teacher-v1.1-merged.jsonl`(6475 条,SHA256 `04f5527f8bc6922a743d20cefd5b537c6cf87882d119d20581b2b81985c62059`) + +这批数据同时服务两条线:(A) 扩训练集提升召回;(B) 作 v14 假脸 hard-negative + 区域监督来源。五台山是山景/岩石/雕像/寺庙——假脸高发域,理论上是 hard-negative 金矿,但 teacher 用的是收紧后的保守 prompt,**实际含多少可用 hard-negative 必须先清点**(前几轮栽在"以为有、实际只有 2 张")。 + +## 假脸线现状(必读) + +v13 已用 84 张零重叠独立集证实:独立 `falseFaceRisk` 头**未泛化**——v12 AUC 0.1765 / v13 0.3123,**均 <0.5(排序反相)**,假脸正样本 risk 均值反而低于真人脸对照,TPR@0.5=0%,根因是训练里真假脸样本几乎为零。独立头方向对,但缺正样本可学。 + +## 不可动的硬约束(先读) + +- **84 张独立 holdout 永不进训练**:`output/semantic-false-face-diagnosis/v13-eval/independent-false-face-set.csv` 里的 photoId 一律留出。Phase 0 先做交集校验,五台山 ∩ holdout 必须 = 0;五台山 ∩ 现有 6475 也要查重,去重。 +- **同一 84 holdout 上报 v12/v13/v14 可比 AUC**:评估口径、独立头定义与 v13 完全一致,不许换口径。 +- **区域/crop 级监督,不是整图贴标签**:v14 的核心增量是让 student 看到"哪里像脸但不是脸",用 teacher grounded 标注里的 `faceRegionVerdicts`(isRealHumanFace=false 的 region)和 `reasoningTrace` region 做局部负监督。 +- **不动 backbone、不动 teacher prompt、不重标注**。merged teacher 跑前校验 SHA。 +- 诚实优先:召回不许回退凑假脸、假脸不许贴低先验凑均值。两条线分别如实判定。 + +## 实现步骤 + +### Phase 0: 清点 + 去重 + 交集校验 + +1. 解析 `five-mountain-grounded.jsonl`:统计 scene 分布、`hasRealHumanFace` 真假比例、**hard-negative 数量**(hasRealHumanFace=false 且 falseFaceRisk≥0.5)、含 face-like region(faceRegionVerdicts 里 isRealHumanFace=false)的样本数。 +2. photoId 交集校验:五台山 ∩ 84-holdout = 0(强制);五台山 ∩ 6475 去重。 +3. 输出 `output/semantic-false-face-diagnosis/v14/five-mountain-inventory.json`。 + - **决策点**:若 hard-negative + face-like region 样本数仍只有个位数,则 v14 假脸监督主要靠"区域级负监督"(从 face-like region 抠 crop 当负样本),而非靠整图 hard-negative 计数。报告里写明实际可用量。 + +### Phase 1: 合并训练集(双线共用) + +1. 合并 6475 + 五台山(去重后)→ 新 merged teacher jsonl,记录新 count 与 SHA256。 +2. 84-holdout 的 photoId 强制排除。 +3. 重新 train/val split,记录各 scene、hard-negative 在 train/val 的分布。 +4. 输出合并报告 `output/semantic-false-face-diagnosis/v14/merge-report-v14.json`。 + +### Phase 2: v14 假脸区域监督实现 + +1. 从 teacher grounded 标注提取 face-like 但非真脸的 region(isRealHumanFace=false 的 faceRegionVerdicts.region),构造**区域级负监督信号**——让 falseFaceRisk 头/faceValidity 头在这些 region 的局部特征上学到"像脸但不是脸→高假脸风险"。 +2. 保留 v12/v13 的独立 falseFaceRisk 头设计(已证方向对)。 +3. 记录区域监督的实现方式与覆盖样本数到 training-report-v14。 + +### Phase 3: 重训 student v14 + +- 仅重训 student,backbone/teacher prompt 不动,merged teacher SHA 跑前校验。 +- 同时训练召回相关头 + 独立 falseFaceRisk 头(带区域监督)。 +- 新版本号 v14。 + +### Phase 4: 双线评估 + +**(A) 召回线**:全量 eval,报 recall@38/45/50/60、negative pick rate,对比 v12/v13/persona-v1/当前 production。判定召回是否提升、是否回退。 + +**(B) 假脸线**:用**同一 84 holdout**,同 v13 口径,报 v12/v13/v14 的: +- 假脸正样本 vs 真人脸对照的 risk 分布(均值/中位/P25/P75) +- AUC、TPR@0.5、FPR@0.5 +- 达标线:AUC 明显 >0.5(目标 ≥0.7)、正样本 risk 组 > 对照组(排序转正)、TPR@0.5 非零 +- 漏判样本 top 列表 + +### Phase 5: 报告 + +- `output/semantic-false-face-diagnosis/v14/false-face-generalization-report-v14.md`:假脸三代(v12/v13/v14)同 holdout 可比指标 + 闭环/未闭环判定 + 区域监督是否起效。 +- `output/semantic-false-face-diagnosis/v14/recall-report-v14.md`(或并入):召回对比 + 是否回退。 +- 明确两条线各自结论,诚实标注仍失败的场景。 + +## 验收标准 + +- [ ] five-mountain 清点完成(scene/hasRealHumanFace/hard-negative/face-like region 计数) +- [ ] 五台山 ∩ 84-holdout = 0,∩ 6475 已去重,均写明 +- [ ] 合并训练集生成,84-holdout 强制排除,SHA 记录 +- [ ] v14 区域级假脸负监督实现并记录覆盖量 +- [ ] 仅重训 student,merged teacher SHA 校验通过 +- [ ] 召回线:recall@多比例 + negative pick rate,对比基线,判定是否回退 +- [ ] 假脸线:同 84 holdout 报 v12/v13/v14 可比 AUC/TPR,明确是否泛化/闭环 +- [ ] 两条线诚实结论,失败场景如实标注 + +## 输出文件 + +``` +output/semantic-false-face-diagnosis/v14/ + five-mountain-inventory.json # Phase 0 清点 + 决策点 + merge-report-v14.json # 合并 + 去重 + holdout 排除校验 + training-report-v14.json # SHA 校验 + 区域监督覆盖 + 新 hard-negative 数 + v14-generalization-scores.csv # v14 在 84 holdout 上的 risk + false-face-generalization-report-v14.md # v12/v13/v14 同 holdout 可比 + 判定 + recall-report-v14.md # 召回对比(或并入上一份) +``` diff --git a/docs/TASK_fix_semantic_false_face.md b/docs/TASK_fix_semantic_false_face.md new file mode 100644 index 0000000..fbd025d --- /dev/null +++ b/docs/TASK_fix_semantic_false_face.md @@ -0,0 +1,168 @@ +# Task: 修复 Semantic Teacher 假脸误判问题 + +## 背景 + +Semantic Teacher Lab v1 审计发现:grounded 版本的 false-face risk 比 flat-scalar 版本高出 **+0.3032**(landscape/documentary 场景平均),这是阻碍进入产品候选的关键问题。 + +## 问题定位 + +1. **Teacher 质量报告显示**: + - 6475 条记录中,76.8% 有 face verdict coverage + - 4631 条 (71.5%) 标记为 `uncertain` + - 大量记录的 `uncertain` 字段包含 `faceRegionVerdicts_positive_fallback` + +2. **False-face risk 对比**(grounded vs flat): + ``` + Landscape: +0.3108 (0.5753 vs 0.2645) + Documentary Moment: +0.2955 (0.3919 vs 0.0964) + Product/Object: +0.6233 (0.9956 vs 0.3723) + ``` + +3. **可能原因**: + - Teacher prompt 对 face region 的判断过于激进 + - Fallback 逻辑在缺少明确人脸时过于倾向于 positive + - Teacher 对 landscape 中的非人脸物体(石头、树干)误判为可能的人脸区域 + +## 目标 + +降低 grounded semantic student 的 false-face risk,使其不劣于 flat-scalar 版本(目标:delta < +0.05)。 + +## 实现方案 + +### Phase 1: 诊断分析 + +1. **抽样分析高 false-face risk 样本** + - 从 `false-face-samples.csv` 中提取 top 50 高风险样本 + - 读取对应的 teacher grounded 输出,查看 `faceRegionVerdicts` 内容 + - 人工检查原图,确认是否真的存在假脸问题 + +2. **对比 grounded vs flat teacher 输出** + - 选取 10-20 个典型样本,对比 grounded 和 flat 版本的 teacher 输出 + - 分析 `faceRegionVerdicts` 的差异 + - 确认问题是出在 teacher 标注阶段还是 student 学习阶段 + +3. **生成诊断报告** + - 总结假脸误判的主要模式(landscape/documentary/product_object) + - 列出 teacher prompt 中可能导致误判的具体措辞 + - 提出修复方向 + +### Phase 2: Teacher Prompt 优化 + +**当前 teacher prompt 可能存在的问题**(需确认): +- 对 face region 的描述可能过于宽泛("可能是人脸" / "模糊的人形") +- Fallback 条件不够严格(缺少明确人脸时应该给 `isRealHumanFace: false`) +- 对 landscape 中的非人脸物体缺少明确排除指令 + +**优化方向**: +1. 加强 false-face 识别指令: + ``` + - 石头、树干、云朵、建筑物的局部轮廓不是人脸 + - 如果你看不清楚是否真的有人脸,必须标记 isRealHumanFace: false + - 只有明确看到眼睛、鼻子、嘴巴的组合才标记 isRealHumanFace: true + ``` + +2. 收紧 fallback 条件: + - 当 VLM 不确定时,优先给 `isRealHumanFace: false` 而非 positive fallback + - 只有在明确检测到人脸时才给 `isRealHumanFace: true` + +3. 场景专项指令: + - Landscape: "风景照中很少有清晰人脸,不要把远处模糊的轮廓误判为人脸" + - Documentary moment: "街拍抓拍照中,只标记清晰可见的人脸区域" + - Product/object: "产品照中几乎不会有真人脸" + +### Phase 3: 增量重标注 + 验证 + +1. **选择重标注样本** + - 从 `false-face-samples.csv` 中选取 top 100-200 高风险样本 + - 优先选择 landscape/documentary_moment/product_object 场景 + - 确保覆盖 grounded 和 flat 差异最大的样本 + +2. **使用优化后的 prompt 重标注** + - 只重标注选定样本(不是全量 6475 条) + - 生成 `semantic-teacher-v1.1-patched.jsonl`(只包含重标注的样本) + - 对比 v1 和 v1.1 的 `faceRegionVerdicts` 差异 + +3. **增量训练或微调** + - **选项 A(推荐)**: 用 v1.1 patched 样本替换 v1 中的对应样本,重新训练 student + - **选项 B**: 如果 student 支持增量更新,用 patched 样本 fine-tune + - 训练目标:false_face_risk head 的损失函数加权 + +4. **快速验证** + - 只在 patched 样本上运行推理,对比 false-face risk + - 如果 delta < +0.05,再跑完整 5167 张评估 + - 生成 `fix-false-face-validation-report.md` + +### Phase 4: 全量重标注(可选) + +如果 Phase 3 验证成功且 delta 显著改善,考虑: +- 用优化后的 prompt 重标注全量 6475 条 +- 重新训练 Semantic Student V2.1 +- 完整跑一遍 4 档比例评估 + +**但如果 Phase 3 已经足够好,可以跳过全量重标注,节省时间。** + +## 验收标准 + +### 最低门槛 +- [x] 完成 Phase 1 诊断分析,生成诊断报告 +- [x] 识别出 teacher prompt 的具体问题点 +- [x] 完成 Phase 2 prompt 优化 + +### 产品候选门槛 +- [x] Grounded vs flat false-face proxy delta < +0.05(当前 +0.3032) +- [x] Landscape/documentary 场景的 false-face risk 不高于 flat 版本 +- [x] 召回率不明显下降(允许 -2% 以内的 trade-off) +- [x] 重标注样本的 `uncertain` 率降低到 < 50% + +## 输出文件 + +``` +docs/ + TASK_fix_semantic_false_face.md # 本文件 + +output/ + semantic-false-face-diagnosis/ + high-risk-samples.csv # Top 50 高风险样本列表 + grounded-vs-flat-comparison.md # 10-20 个样本的对比分析 + diagnosis-report.md # 诊断总结报告 + + teacher-prompt-v1.1-optimized.txt # 优化后的 teacher prompt + + patched-samples.json # 选中的 100-200 个重标注样本列表 + semantic-teacher-v1.1-patched.jsonl # 重标注结果 + + student-v2.1-training-report.json # 增量训练或重训练报告 + + fix-validation-report.md # 验证报告 + false-face-risk-before-after.csv # 修复前后对比 +``` + +## 时间估算 + +- Phase 1 诊断分析: 2-3 小时(含人工抽查样本) +- Phase 2 prompt 优化: 1 小时 +- Phase 3 增量重标注 + 验证: 3-4 小时(teacher 推理 + student 训练 + 验证) +- Phase 4 全量重标注(可选): 6-8 小时 + +**总计**: 6-8 小时(不含 Phase 4) + +## 风险与限制 + +1. **可能无法完全消除假脸误判** + - VLM teacher 本身在 landscape 中的人脸检测可能有局限 + - 可能需要引入专门的 face detection 模型作为 pre-filter + +2. **召回率可能轻微下降** + - 收紧 face region 判断可能导致部分真人脸被遗漏 + - 需要在 false-face risk 和 recall 之间权衡 + +3. **Student 可能对 teacher 噪声敏感** + - 即使 teacher 标注改善,student 可能已经学到了旧的偏差 + - 可能需要调整 face_validity loss 的权重 + +## 下一步(如果本 task 成功) + +1. 如果 false-face risk 降低到可接受水平,重新评估是否进入产品候选 +2. 将优化后的 teacher prompt 固化到代码中(不要只留在实验笔记) +3. 在真实用户 A/B 测试中验证假脸误判是否真的改善 +4. 考虑后续引入专门的 face detection pre-filter(SCRFD/RetinaFace)作为 teacher 辅助 diff --git a/docs/TASK_fix_student_false_face_v12.md b/docs/TASK_fix_student_false_face_v12.md new file mode 100644 index 0000000..ff53d3d --- /dev/null +++ b/docs/TASK_fix_student_false_face_v12.md @@ -0,0 +1,91 @@ +# Task: 学生层假脸修复 v12(改 student 蒸馏,不改 teacher prompt) + +## 背景 + +v11-false-face 已确认**未闭环**(2026-06-26 审计)。证据链: +- 原始口径(学生层、全量 5167 张、scene-mean `grounded − flat` false-face delta,取 landscape+documentary 平均)从 v1 的 `+0.3032` 只降到 v11 的 `+0.2883`,净改善仅 `−0.0149`,目标 `< +0.05`。 +- documentary_moment 反而比 v1 升高(+0.0279);product_object 几乎满风险(v11 0.9996)。 +- teacher patch 子集那个 `-0.4602` 只证明 200 张补丁上 teacher prompt 变保守了,**学生层没传导**。 + +**根因判定:问题在 student 层蒸馏传导,不在 teacher prompt。** teacher 标注(v1.1-merged,6475 条,SHA256 04f5527...)已经收紧且干净,本轮**不再动 teacher prompt,不重标注**。 + +关键现状(来自 training-report.json): +- `falseFaceRisk` 当前是**派生量** `1 - faceValidityScore`,不是独立训练的头。 +- faceValidity 头声称用 "falseFaceRisk-weighted hard-negative emphasis" 训练,但实际传导不足——加权力度或样本覆盖不够。 + +## 目标 + +让 student 在全量 5167 张上的原始口径 false-face delta 真正降下来,进入闭环。 +**主目标:landscape+documentary scene-mean `grounded − flat` delta < +0.05**(当前 +0.2883)。 +**约束:召回不得明显回退**(v11 当前 +14.66% @45%,本轮 @45% 召回回退不超过 2 个百分点)。 + +## 实现步骤 + +### Step 0: 传导失败诊断(先诊断,再动手) + +不要直接堆 loss。先回答"student 为什么没学到 teacher 的保守判断": +- 统计训练集中 false-face hard-negative 的数量与 scene 分布(teacher `hasRealHumanFace=false` 且 `falseFaceRisk` 高 / 视觉上 face-like 的样本)。看是不是样本太少或 scene 偏斜。 +- 对 val/全量做误差分解:student 预测的 faceValidity vs teacher faceValidity,按 scene 看残差,定位是哪些 scene 的 hard-negative 被 student 拉高。 +- 确认 `falseFaceRisk = 1 - faceValidity` 这个派生是否是瓶颈(teacher 的 falseFaceRisk 与 1-faceValidity 在标注层是否一致;若不一致,派生本身就丢信息)。 +输出 `output/semantic-false-face-diagnosis/v12-student/transmission-diagnosis.md`。 + +### Step 1: 选择并实现 student 层修复(按诊断结果,至少做 1+2) + +1. **faceValidity 头 hard-negative 损失重加权**:对 false-face hard-negative 样本加大损失权重(非对称:student 把"假脸判成真脸"的错误,惩罚要重于反向)。把当前加权力度显式调大并记录系数。 +2. **高风险 scene 定向重采样**:对 landscape / documentary_moment / event / product_object 的 hard-negative 过采样,纠正 scene 偏斜。 +3. **(可选,若 Step 0 判定派生是瓶颈)独立 falseFaceRisk 头**:新增直接回归 teacher `falseFaceRisk` 的头,导出时用独立头而非 `1 - faceValidity`。仅在诊断支持时做。 + +**不改 backbone(convnext_tiny / 384)、不改 teacher、不重标注。** + +### Step 2: 重训 student(仅 student) + +用同一 merged teacher(6475 条,SHA256 必须等于 04f5527...,跑前校验)重训。记录新权重版本号(如 v12-student-falseface)。 + +### Step 3: 全量评估 + 原始口径验证(口径必须与 v11 闭环报告完全一致) + +- 跑全量 5167 张 eval,生成 `metrics-by-scene.csv` / `metrics-by-ratio.csv` / `false-face-samples.csv`。 +- 用**与 false-face-closure-report.md 完全相同的口径**重算:学生层、全量、scene-mean `grounded − flat`,取 landscape+documentary 平均。对比 v1 / v11 / v12 三代。 +- 三方→四方 scene 级对比表(flat / 旧grounded v1 / v11 / v12)。 +- 复述召回 trade-off(@38/45/50/60)。 + +### Step 4: 诚实闭环报告 + +写 `output/semantic-false-face-diagnosis/v12-student/false-face-closure-report-v12.md`: +- 四代 scene 级对比表 +- 原始口径 delta:v1 +0.3032 / v11 +0.2883 / v12 = ?,是否 < +0.05 +- 召回回退是否 < 2pp @45% +- 分场景诚实结论(尤其 documentary_moment 是否止跌、product_object 0.9996 是否降下来) +- 明确判定:**闭环 / 未闭环 / 部分闭环** + +## 验收标准 + +- [ ] `transmission-diagnosis.md` 完成,定位传导失败原因 +- [ ] student 层修复实现(至少 hard-negative 重加权 + 高风险 scene 重采样) +- [ ] 仅重训 student,merged teacher SHA256 校验通过(04f5527...) +- [ ] 全量 eval 产物齐全(metrics-by-scene / by-ratio / false-face-samples) +- [ ] 原始口径 delta 重算(v1/v11/v12 同口径),明确是否 < +0.05 +- [ ] 召回回退 < 2pp @45% 已核对 +- [ ] closure-report-v12 给出闭环/未闭环明确判定,分场景诚实标注 + +## 关键约束 + +- **不动 teacher prompt,不重标注**。根因在 student 层。 +- **不改 backbone**。 +- **口径必须与 v11 闭环报告一致**(学生层、全量、scene-mean、landscape+documentary 平均),否则 v11→v12 不可比。 +- **不许篡改口径凑数**。若 v12 仍未达标,如实写"未闭环",并说明 student 层修复为何仍不够。 +- 注意方法学坑:flat 与 grounded 的 scene 路由不同(同 scene 样本数差异大),"vs flat"列只能当趋势;"v12 vs v11"是干净对比。 +- 诚实优先于好看。这份报告决定 v12 能否进产品候选。 + +## 输出文件 + +``` +output/semantic-false-face-diagnosis/v12-student/ + transmission-diagnosis.md # Step 0 诊断 + metrics-by-scene.csv # 全量 eval + metrics-by-ratio.csv + false-face-samples.csv + false-face-scene-comparison-v12.csv # 四代对比(机器可复算) + false-face-delta-summary-v12.json + false-face-closure-report-v12.md # 闭环/未闭环判定 + training-report-v12.json # 含 SHA256 校验、加权系数、重采样比例 +``` diff --git a/docs/TASK_pro_auto_exposure_preview_p15.md b/docs/TASK_pro_auto_exposure_preview_p15.md new file mode 100644 index 0000000..1ff77f3 --- /dev/null +++ b/docs/TASK_pro_auto_exposure_preview_p15.md @@ -0,0 +1,72 @@ +# FrameCull Pro Auto Exposure Preview P1.5 + +## Background +The real product goal of RAW preview is not to replace a full RAW developer. +The goal is to answer this culling question quickly: + +> If this RAW frame is auto-exposed, is it still worth keeping? + +P1 showed that Nikon High Efficiency Star NEF can provide a valid embedded +preview, but cannot currently be fully raw-developed through the isolated +`rawler` experiment. Therefore P1.5 focuses on fast auto-exposure preview over a +safe display source. + +## Product Strategy +- Do not block navigation. +- Show the normal preview first. +- When Pro auto exposure preview is enabled, prefer a fast safe preview source. +- High quality RAW develop remains an async enhancement or fallback path. +- Failures must fall back to the normal preview and must never show corrupted + images. + +## Scope +### In +- Use the existing native embedded RAW preview extraction path. +- Apply EXIF orientation through the existing preview cache. +- Compute luma percentiles from the display preview: + - shadow percentile + - midtone percentile + - highlight percentile + - clipped highlight ratio +- Generate conservative `autoExposureEv`: + - protect highlights first + - do not turn dark mood / night scenes into daylight + - clamp EV so compensation stays modest +- Apply the preview non-destructively in the viewer. +- Do not write metadata, XMP, or source files. +- Produce experiment metrics and an Arbor report. + +### Out +- Do not add this to Flash as a heavy RAW/GPU dependency. +- Do not ship `rawler` or `wgpu` in the app bundle for this round. +- Do not replace the RawTherapee monitor cache path as the only path. +- Do not claim embedded JPEG can recover true RAW highlight detail. +- Do not attempt full camera color management. + +## Preview Levels +### Fast Preview +Input: embedded RAW JPEG or current display preview. + +Purpose: quickly judge the look after auto exposure. + +Strengths: stable, fast, safe for culling. + +Limit: cannot recover true RAW dynamic range. + +### High Fidelity Preview +Input: true linear RAW develop. + +Purpose: closer to Lightroom / Camera Raw. + +Strengths: can use RAW dynamic range. + +Limit: Nikon High Efficiency Star support is not stable in the current decoder +path, and RawTherapee is slower as an external process. + +## Acceptance +- Nikon NEF no longer shows rainbow bands or pink blocks when this path is used. +- Auto exposure preview does not wait for slow RAW develop. +- Failure always falls back to normal preview. +- Flash build remains free of Pro RAW / GPU experiment dependencies. +- P1.5 report includes preview timing, EV, highlight clipping, and fallback + reason. diff --git a/docs/TASK_recall_pro_prod_wiring_v16.md b/docs/TASK_recall_pro_prod_wiring_v16.md new file mode 100644 index 0000000..bbe05e3 --- /dev/null +++ b/docs/TASK_recall_pro_prod_wiring_v16.md @@ -0,0 +1,102 @@ +# Task: 召回线 Pro persona 排序生产接线 + 保真校验 + 内部验证(整晚批量) + +## 背景与现状(必读) + +召回线已在实验室充分验证、因果拆分做干净(2026-07-01):v14 semantic student 模型自身净增量 **+21.52pp**(@45,Current 38.90% → v14+Flash persona 60.42%,同 persona vs 规则),v14 Pro semantic @45 = 63.18%,仍低于 legacy Pro persona v1(64.14%)0.96pp 未反超。结论:**可进 Pro 灰度实验候选,但不替换 legacy,Flash 默认不动。** + +**但这套验证只活在 lab:** +- 生产已有 `src-tauri/src/pro_infer/`(Rust,`#[cfg(feature = "pro")]` 门控)跑蒸馏多头 ONNX student,出原始多头分数(`pro_infer_init` / `pro_infer_batch`)。 +- **生产没有 persona 排序逻辑**——把 student 分数变成"按 Pro persona 在 38/45/50/60% 工作点挑片"的那套(lab 验证出 +21.52pp 的核心)只在 `tools/ai-lab/bench-pro-persona.mjs`。 +- rollout-plan 引用的 `proPersonaRanking.enabled` 开关**不存在**。 +- 假脸线已封存为 guard-only,**本轮完全不碰假脸**。 + +## 目标 + +把 lab 验证过的 Pro persona 排序逻辑**忠实搬到生产**,挂在独立、默认 off 的开关后,接到 `pro_infer` 输出上;用"生产 vs lab 同集同分一致性校验"钉死保真;然后跑 rollout-plan 第一阶段内部验证,确认 app 里能复现 lab 数字。整夜批量跑足够规模的内部图集。 + +## 不可动的硬约束(先读) + +- **保真优先(本轮第一红线)**:生产排序结果必须与 lab `bench-pro-persona.mjs` 在**同一图集、同一 student 分数**输入下一致。一致性目标:同工作点(38/45/50/60)选中集合完全一致(Jaccard=1.0),或差异有可解释来源并 < 0.5%。**任何不可解释的偏差是 bug,必须定位修复,不许调参凑近、不许用"差不多"蒙混。** +- **Flash/default 绝对不动**:不加载 Pro persona、不改缓存 schema、不改用户默认体验。`proPersonaRanking.enabled` 默认 `false`。Flash build 不得编译 pro 代码(沿用 `#[cfg(feature="pro")]` 门控)。 +- **不重训、不动模型/teacher prompt/backbone**。本轮纯生产工程接线 + replay 验证,不产生新模型版本。 +- **不碰假脸线**(已封存 guard-only)。不把假脸作为本轮门槛或卖点。 +- **legacy Pro persona v1 不被替换**:新路线是独立开关并行,不顶替现有排名。 +- **诚实优先**:生产复现不了 lab 数字就如实写差多少、差在哪;一键回退必须真实可用并实测。 + +## 实现步骤 + +### Phase 0: 摸清 lab persona 排序的"真值定义" + +1. 通读 `tools/ai-lab/bench-pro-persona.mjs`(及其依赖),把 persona 排序逻辑拆成可移植规格:输入(哪些 student 头/分数)、persona 加权/打分公式、gateMode(hard-only)、groupMode(known / pair-threshold 0.92)、相似度去重、各比例工作点选片规则、negative pick 统计口径。 +2. 输出 `output/recall-productize/v16-prod-wiring/persona-ranking-spec.md`:逐条写清,作为生产实现的唯一规格来源。标注任何 lab 里隐含的默认值/边界处理。 + +### Phase 1: 生产侧 Pro persona 排序实现(独立开关,默认 off) + +1. 在生产代码实现 persona 排序:消费 `pro_infer_batch` 的多头输出 → 按 Phase 0 规格算 persona 分 → 按工作点排序挑片。位置与语言按现有架构定(Rust 侧 or 前端编排,沿用 pro feature 门控)。 +2. 新增 `proPersonaRanking.enabled` 开关,默认 `false`;off 时完全走现有路径,零副作用。 +3. 不改缓存 schema;Pro 分数若缓存,单独命名空间,回退时不参与排序。 +4. Rust 改动配套单元测试;前端编排改动配套测试。 + +### Phase 2: 生产 vs lab 保真校验(本轮核心验收) + +1. 取一组固定图集,先用 lab `bench-pro-persona.mjs` 跑出 38/45/50/60 选中集合(baseline 真值)。 +2. 用**完全相同的 student 分数**喂生产排序路径,跑出生产选中集合。 +3. 逐工作点比对:选中集合 Jaccard、对称差样本列表、recall/negative pick rate 是否一致。 +4. 输出 `output/recall-productize/v16-prod-wiring/fidelity-check.json` + `fidelity-report.md`: + - 每个工作点 Jaccard、不一致样本 + 归因 + - **判定:是否达成保真(Jaccard=1.0 或差异可解释且<0.5%)** + - 若不达标:定位根因(公式/排序稳定性/浮点/边界)、修复、复跑,直到达标或如实记录无法消除的差异来源 + +### Phase 3: 构建 + 全测试 + +1. 跑 pro feature 的构建(确认 pro build 编译通过、Flash build 不含 pro 代码)。 +2. 跑 Rust 单测 + 集成测试 + 前端测试,全绿。失败先修再继续。 +3. 记录构建/测试命令与结果到报告。 + +### Phase 4: rollout-plan 第一阶段内部验证(整夜批量) + +1. 在我们自己的图集(G 盘 / 相机 / 五台山 / 新增户外集,尽量大规模,整夜跑)开 `proPersonaRanking.enabled=true` 跑生产排序。 +2. 报 45%/50% 工作点(主)+ 38%/60%(辅):recall 代理、negative pick rate、重复污染(正式重复组多选、相邻高相似多选)、推理错误率、单图耗时、批失败率。 +3. 与 lab v14 Pro persona 对应数字对比,确认 app 复现。 +4. 反馈标签分层统计(大景/空镜、人像、合照、活动纪实),不混成单一平均。 +5. 输出 `output/recall-productize/v16-prod-wiring/internal-validation-report.md`。 + +### Phase 5: 一键回退实测 + 监控埋点 + +1. 实现并**实测**一键回退:关 `proPersonaRanking.enabled` → 立即回现有规则、Pro 缓存分不参与排序、无需迁移用户项目。记录实测步骤与结果。 +2. 落 rollout-plan 列的监控指标埋点(手动恢复率、弃用率、重复污染、稳定性、分层反馈),或如实说明哪些埋点需后续接入。 +3. 输出 `output/recall-productize/v16-prod-wiring/rollback-and-monitoring.md`。 + +### Phase 6: 汇总报告 + +`output/recall-productize/v16-prod-wiring/v16-summary.md`: +- 保真校验结论(生产是否=lab,差异归因) +- 构建/测试结果 +- 内部验证:app 是否复现 lab 的 @45 64.14% / @50 等,差多少 +- 回退实测结果 +- 诚实判定:召回 Pro 路线是否可进 rollout-plan 第二阶段(5% Pro 内测);不可则差什么 +- 重申边界:legacy 不替换、Flash 不动、假脸不绑 + +## 验收标准 + +- [ ] persona 排序规格 Phase 0 成文,作为生产实现唯一来源 +- [ ] 生产 Pro persona 排序实现完成,`proPersonaRanking.enabled` 默认 off,off 时零副作用 +- [ ] Flash build 不编译 pro 代码(门控保持),缓存 schema 未改 +- [ ] **保真校验:38/45/50/60 选中集合 Jaccard=1.0 或差异可解释且<0.5%,不达标已定位修复或如实记录** +- [ ] pro build 编译通过;Rust + 前端测试全绿 +- [ ] 内部验证:自有大图集 45/50/38/60 全指标 + 与 lab 对比,分层统计 +- [ ] 一键回退已实现并实测,记录步骤结果 +- [ ] 监控埋点落地或如实标注待接入项 +- [ ] 诚实判定能否进 5% Pro 内测;legacy 不替换 / Flash 不动 / 假脸不碰均保持 + +## 输出文件 + +``` +output/recall-productize/v16-prod-wiring/ + persona-ranking-spec.md # lab 排序逻辑可移植规格(真值来源) + fidelity-check.json # 生产 vs lab 逐工作点比对 + fidelity-report.md # 保真判定 + 不一致归因 + internal-validation-report.md # 自有大图集内部验证 + 与 lab 对比 + 分层 + rollback-and-monitoring.md # 一键回退实测 + 监控埋点 + v16-summary.md # 汇总 + 能否进 5% 内测判定 +``` diff --git a/docs/TASK_recall_pro_prod_wiring_v16b_persona_only.md b/docs/TASK_recall_pro_prod_wiring_v16b_persona_only.md new file mode 100644 index 0000000..c614822 --- /dev/null +++ b/docs/TASK_recall_pro_prod_wiring_v16b_persona_only.md @@ -0,0 +1,95 @@ +# Task: 召回线 Pro 生产 profile 换变体(flash-persona → persona-only)+ 重跑保真 + 内部验证 + +## 背景与现状(必读) + +v16 已把 lab persona 排序忠实接入生产(`src/utils/proPersonaRanking.ts`,开关 `proPersonaRanking.enabled` 默认 off),全量 7692 同集同分 replay 在 38/45/50/60 四工作点 Jaccard=1.0、diff 0.000%,保真红线通过。构建/测试全绿、Flash bundle 无 Pro 标记、一键回退实测过。 + +**但当前生产化的 rank profile 是 `pro-semantic-v2-flash-persona`(@45 recall = 60.42%)**,这是 v15 因果拆分里"干净模型信号"的 Flash-persona 变体。经与用户确认后决定**换成 `pro-semantic-v2-persona-only`(@45 recall = 63.18%)** 作为上线候选——它是 v14 student 自己的 persona-only 排序,比 flash-persona 高约 2.76pp,更接近 legacy Pro persona v1(64.14%)的天花板(仍差 0.96pp、未反超)。 + +已核对(审计结论,作为实现前提,实现时请自行复核): +- 生产 `proPersonaRankScore` 已内置 `pro-semantic-v2-persona-only` 分支(`src/utils/proPersonaRanking.ts` 约 165-167 行),公式 `overall*0.54 + technical*0.28 + scene*0.24 + nativeAesthetic*0.14 + persona*46 + focusReliability*4.5 - reviewPenalty`。 +- lab `tools/ai-lab/tune-ai-picks-supervised.mjs` 的 `pro-semantic-v2-persona-only` 分支(约 1086-1089 行)公式逐系数相同,且**无** `pro-persona` 分支那种 `personaBonus` 中间项。 +- 两变体的 ratio 分组配置一致:0.38/0.45 = `known`,0.50/0.60 = `pair-threshold` sim 0.92、maxNumericGap 12、maxTimeGap 8min。 +- 因此换变体理论上应保持 Jaccard=1.0;但**这是必须实测证明的,不是可假设的**。 + +## 目标 + +把生产 Pro persona 排序的默认 rank profile 从 `pro-semantic-v2-flash-persona` 切到 `pro-semantic-v2-persona-only`,重跑全量保真校验钉死一致性,重跑 build/全测试,重跑整夜内部验证确认 app 复现 lab 的 63.18%(@45);更新所有受影响的报告与 spec。开关语义、边界、回退一律不变。 + +## 不可动的硬约束(先读) + +- **保真优先(第一红线)**:切到 persona-only 后,生产排序结果必须与 lab `bench-pro-persona.mjs` / `tune-ai-picks-supervised.mjs` 在**同一图集、同一 student 分数、persona-only rankMode** 下一致。目标:38/45/50/60 选中集合 Jaccard=1.0,或差异有可解释来源且 <0.5%。**任何不可解释偏差是 bug,必须定位修复,不许调参凑近。** 若切换后 Jaccard 掉了,先查公式/分组/排序稳定性/浮点/默认 rankMode 传参,而不是接受偏差。 +- **Flash/default 绝对不动**:Flash 路由继续无视 `proPersonaRanking.enabled`,不加载 Pro persona、不改缓存 schema。开关默认仍 `false`,off 时零副作用走旧 `buildAiPickedPhotoIds`。 +- **不重训、不动模型/teacher prompt/backbone**。纯 profile 切换 + replay 验证,不产生新模型版本。student 分数复用现有 `pro_infer_batch` 输出。 +- **不碰假脸线**(已封存 guard-only)。 +- **legacy Pro persona v1(`pro-persona` rankMode,64.14%)不被替换**:persona-only 仍是独立开关后的实验候选,并行存在、不顶替 legacy。 +- **不删除 flash-persona 变体**:保留 `pro-semantic-v2-flash-persona` rankMode 及其代码路径(供 A/B 与归因回溯),仅改"生产默认选用哪个"。 +- **诚实优先**:persona-only 在 app 复现不了 63.18% 就如实写差多少、差在哪;一键回退必须仍真实可用并实测。 + +## 实现步骤 + +### Phase 0: 切换前基线确认 +1. 复核上面"已核对"三条(生产/lab persona-only 公式逐系数一致、无 personaBonus、分组配置一致)。若发现任何不一致,**停下先报**,不要擅自改公式凑。 +2. 记录切换前 flash-persona 的保真与指标现状(引用现有 `fidelity-report.md`),作为对照。 + +### Phase 1: 生产侧 profile 切换 +1. `src/utils/proPersonaRanking.ts`:把 `PRO_PERSONA_SELECTED_RATIO_PROFILES` 四个条目的 `rankMode` 从 `pro-semantic-v2-flash-persona` 改为 `pro-semantic-v2-persona-only`;`profileForRatio` 的默认、`proPersonaRankScore` 的默认参数、`PRO_PERSONA_RANKING_VERSION` 串同步更新(如 `pro-persona-ranking-v16b-persona-only`)。ratio/gate/group/similarity 等其余字段不动。 +2. `proPersonaRankScore` 的 persona-only 分支公式保持不变(已就位)。不要动 flash-persona 分支。 +3. 缓存 schema 不改;Pro 分单独命名空间不变。 +4. 配套单测更新:断言默认 rankMode = persona-only、断言 persona-only 打分公式、保留 flash-persona 打分的既有测试。 + +### Phase 2: 生产 vs lab 保真校验(核心验收) +1. 用 lab persona-only 跑固定图集(全量 7692,`inputs/ai-culling-bench-pro-semantic-full-7692.json`)出 38/45/50/60 baseline 真值。 +2. 用**完全相同的 student 分数**喂切换后的生产路径,出生产选中集合。 +3. 逐工作点比对 Jaccard、对称差样本、recall/negative pick。 +4. 输出到 `output/recall-productize/v16b-persona-only/`:`fidelity-check.json` + `fidelity-report.md`,每工作点 Jaccard + 不一致归因 + 判定。不达标先定位修复再复跑。 + +### Phase 3: 构建 + 全测试 +1. Pro feature build(编译过、Flash build 不含 Pro persona 标记);Flash build。 +2. Rust 单测/集成 + 前端 vitest 全绿。失败先修。 +3. 命令与结果记入报告。 + +### Phase 4: 内部验证(整夜批量) +1. 自有图集(G 盘 / 相机 / 五台山 / 户外集,尽量大规模)开 `proPersonaRanking.enabled=true` 跑 persona-only 生产排序。 +2. 报 45/50(主)+ 38/60(辅):recall 代理、negative pick rate、重复污染(正式重复组多选、相邻高相似多选)、推理错误率、单图耗时、批失败率。 +3. 与 lab v14 persona-only 对应数字对比(@45 目标复现 63.18%),确认 app 复现,差多少如实写。 +4. 反馈标签分层(大景/空镜、人像、合照、活动纪实),不混单一平均。 +5. 输出 `output/recall-productize/v16b-persona-only/internal-validation-report.md`。 + +### Phase 5: 回退实测 + spec/报告更新 +1. 实测一键回退:关 `proPersonaRanking.enabled` → 回旧规则、Pro 缓存分不参与、无需迁移。记录步骤结果。 +2. 更新 `output/recall-productize/v16-prod-wiring/persona-ranking-spec.md`(或新建 v16b spec):把生产候选 profile 标为 persona-only、更新公式块与 ratio 表、注明 flash-persona 保留为对照。 +3. 输出 `output/recall-productize/v16b-persona-only/rollback-and-monitoring.md`。 + +### Phase 6: 汇总报告 +`output/recall-productize/v16b-persona-only/v16b-summary.md`: +- 切换内容(flash-persona → persona-only,改了哪些行) +- 保真结论(切换后是否仍 Jaccard=1.0,差异归因) +- 构建/测试结果 +- 内部验证:app 是否复现 lab @45 63.18%,差多少 +- 回退实测结果 +- 诚实判定:persona-only 作为 5% Pro 内测候选是否 ready;重申 legacy(64.14%) 未被反超(仍差 0.96pp)、不替换、Flash 不动、假脸不绑 + +## 验收标准 + +- [ ] 生产四 ratio profile + 默认 rankMode + version 串切到 persona-only;flash-persona 分支保留 +- [ ] persona-only 公式与 lab 逐系数一致(已核,实现时复核无出入) +- [ ] **保真:38/45/50/60 Jaccard=1.0 或差异可解释且<0.5%,不达标已定位修复或如实记录** +- [ ] Flash build 不含 Pro persona 标记、缓存 schema 未改、开关默认 off、off 零副作用 +- [ ] Pro build 编译过;Rust + 前端测试全绿 +- [ ] 内部验证:自有大图集 45/50/38/60 全指标 + 与 lab persona-only 对比(@45 vs 63.18%)+ 分层 +- [ ] 一键回退实现并实测,记录步骤结果 +- [ ] spec/报告更新,生产候选标为 persona-only、flash-persona 留作对照 +- [ ] 诚实判定能否进 5% Pro 内测;legacy 不替换 / Flash 不动 / 假脸不碰保持 + +## 输出文件 + +``` +output/recall-productize/v16b-persona-only/ + fidelity-check.json # 切换后生产 vs lab persona-only 逐工作点比对 + fidelity-report.md # 保真判定 + 归因 + internal-validation-report.md # 自有大图集内部验证 + 与 lab 63.18% 对比 + 分层 + rollback-and-monitoring.md # 一键回退实测 + 监控埋点 + v16b-summary.md # 汇总 + 能否进 5% 内测判定 +# 并更新 output/recall-productize/v16-prod-wiring/persona-ranking-spec.md(或新建 v16b spec) +``` diff --git a/docs/TASK_recall_pro_productize_v15.md b/docs/TASK_recall_pro_productize_v15.md new file mode 100644 index 0000000..cfb3188 --- /dev/null +++ b/docs/TASK_recall_pro_productize_v15.md @@ -0,0 +1,73 @@ +# Task: 召回线产品化(Pro 实验候选,不动 Flash 默认) + +## 背景 + +v14 双线里,**召回线是真在涨、可以推进产品化**;假脸线另案(见 TASK_false_face_decouple)。但 v14 的召回大数有混淆,必须先把"真增量"和"配置差"拆开,再决定怎么上线。 + +已知事实(来自 v14 报告 recall-report-v14.md,服务器 /data/FrameCullModelLab/outputs/semantic-false-face-diagnosis/v14/): +- v14 Pro persona vs 当前生产规则:@38 30.33→55.33、@45 38.90→64.14、@50 44.35→68.91、@60 62.42→78.49。 +- **但这组大数是"模型 vs 规则 + Pro persona vs Flash persona"混合,不是 v14 本轮的纯增量。** +- 同 persona 下 v12/v13/v14 @45 = 55.73→62.57→**64.14**,v14 比 v13 仅 +1.57pp(温和)。 + +## 目标 + +把召回线做成一个**可信、可复现、可上线开关**的 Pro 实验候选:诚实拆分增量来源,给出在生产可观测口径下的真实收益,并定义灰度/回退方案。**不改 Flash 默认行为。** + +## 不可动的硬约束(先读) + +- **诚实拆分,不许用混合大数当卖点**:报告必须把 (a) 模型 vs 规则、(b) Pro persona vs Flash persona、(c) v14 vs v13 同 persona 三个增量分开列,并标明每个数字是哪种对比。上线收益评估只能引用"同口径可归因"的那部分。 +- **不动 Flash 默认**:Pro 走独立 persona/开关,默认仍是当前生产规则或 Flash,灰度可控、可一键回退。 +- **不动 backbone、teacher prompt、不重标注**;merged teacher 跑前校验 SHA(8929 条,SHA256 `6c64805b...a0efd00`,跑前打印完整 64 位核对)。 +- **84 张独立 holdout 永不进训练/调参**(output/semantic-false-face-diagnosis/v13-eval/independent-false-face-set.csv)。 +- 召回评估口径固定:recall@38/45/50/60 + negative pick rate,与 v12/v13/v14 完全可比,不许换口径。 + +## 实现步骤 + +### Phase 0: 增量归因拆分(只算,不重训) + +1. 在同一 eval 全集上,固定其余变量,分别测: + - **模型 vs 规则**:同 persona(如都 Pro 或都 Flash)下,student v14 vs 当前生产规则。 + - **persona 差**:同模型(v14)下,Pro persona vs Flash persona。 + - **版本差**:同 persona 下,v14 vs v13 vs v12。 +2. 输出 `output/recall-productize/v15/recall-attribution.json`,每个 @比例都标三种增量的分解,明确哪部分可归因到"模型本身"。 +3. **决策点**:若"模型本身(同 persona、vs 规则)"的净增量不足以支撑上线(例如 @45 同 persona vs 规则 < 几个 pp),如实写明,召回产品化降级为"继续观察",不强行包装。 + +### Phase 1: 生产可观测口径对齐 + +1. 确认线上实际用的筛选比例与判定阈值,把 eval 的 @比例映射到生产真实工作点。 +2. 在该工作点报告:真实召回、负样本误选率(negative pick rate)、对用户可感知的影响(少漏多少张该留的、多挑多少张该弃的)。 +3. 输出 `output/recall-productize/v15/production-operating-point.md`。 + +### Phase 2: Pro 开关 / 灰度方案 + +1. 设计 Pro persona 作为独立可开关路径:默认 off,Flash/规则不受影响。 +2. 定义灰度策略(按比例放量)、监控指标、回退触发条件(如 negative pick rate 超阈值自动回退)。 +3. 不需要真的全量上线,产出可执行的开关 + 灰度设计文档 `output/recall-productize/v15/rollout-plan.md`,含一键回退说明。 + +### Phase 3: 验证 + 报告 + +1. 复跑 Phase 0 关键对比,确保数字可复现(同 SHA、同 eval 集)。 +2. `output/recall-productize/v15/recall-report-v15.md`: + - 三种增量分解表 + 可归因到模型的净收益 + - 生产工作点真实收益 + - Pro 开关/灰度/回退方案摘要 + - 诚实结论:召回是否够格上 Pro 实验、预期收益区间、风险 + +## 验收标准 + +- [ ] 增量三拆分完成(模型vs规则 / Pro vs Flash persona / v14 vs v13/v12 同 persona),每个数字标明对比类型 +- [ ] merged teacher SHA 校验通过(8929 / 6c64805b...a0efd00) +- [ ] 生产工作点真实召回 + negative pick rate 已报告 +- [ ] Pro 独立开关 + 灰度 + 一键回退方案产出,Flash 默认不变 +- [ ] 报告给出"是否够格上 Pro 实验"的诚实判定,不用混合大数包装收益 +- [ ] 84 holdout 未进训练/调参 + +## 输出文件 + +``` +output/recall-productize/v15/ + recall-attribution.json # 增量三拆分 + production-operating-point.md # 生产工作点收益 + rollout-plan.md # Pro 开关 + 灰度 + 回退 + recall-report-v15.md # 汇总 + 诚实判定 +``` diff --git a/docs/TASK_recall_v15_flash_persona_ablation.md b/docs/TASK_recall_v15_flash_persona_ablation.md new file mode 100644 index 0000000..fed8ab5 --- /dev/null +++ b/docs/TASK_recall_v15_flash_persona_ablation.md @@ -0,0 +1,57 @@ +# Task: 召回线补"同模型 Flash persona"消融——拆出 v14 模型自身净增量 + +## 背景与现状(必读) + +v15 召回收口已诚实承认一个缺口:报告里 `Pro persona v1 - semantic persona-only` 只是**可观察实现差异**,不是同模型因果拆分。因为源指标没有"**同一 v14 模型 + Flash persona rankMode**"这一行,所以现在**算不出纯的 Pro persona vs Flash persona 缺口**,也就不能声明"v14 模型自身贡献"。 + +这是 v14 student 是否够格替换 legacy Pro persona v1 的关键证据缺口。 + +## 目标 + +补齐"同 v14 模型 + Flash persona"那一行,在同一 eval scope 下做到三因素彻底可分:(a) 模型 vs 规则、(b) persona Pro vs Flash(同模型)、(c) 版本 v14 vs v13。给出 v14 模型自身净增量的诚实数字,据此判定 v14 student 是否够格进 Pro 排名或替换 legacy persona。 + +## 不可动的硬约束(先读) + +- **同 eval scope、同口径**:与 v15 recall-report 完全一致的 7692/全量集、recall@38/45/50/60 + negative pick rate,新增行才可比。 +- **不重训、不改 teacher prompt**:仅新增"v14 模型 + Flash persona rankMode"这一组 replay 配置后重跑,merged teacher 跑前校验 SHA(8929 / 6c64805b…a0efd00)。 +- **84 holdout 不进训练/调参**。 +- **诚实优先**:拆出的"模型自身净增量"若很小,如实写;不许把 persona 差或规则差算进模型功劳。Flash 默认不动的结论不受本轮影响。 + +## 实现步骤 + +### Phase 0: 跑缺失的那一行 + +1. 配置"v14 semantic student + Flash persona rankMode",在同一 eval scope 跑 recall@38/45/50/60 + negative pick rate。 +2. 与现有三行(current 规则 / v14 semantic persona-only / legacy Pro persona v1)并表。 + +### Phase 1: 三因素拆分 + +1. **persona 差(同 v14 模型)**:Pro persona vs Flash persona,各 @比例。 +2. **模型差(同 persona)**:v14 student vs 规则。 +3. **版本差(同 rankMode)**:v14 vs v13。 +4. 每个数字标明属于哪种对比,输出 `output/recall-productize/v15/recall-attribution-v2.json`(在原 attribution 上补全 Flash persona 行)。 + +### Phase 2: 判定 + 报告 + +1. 更新/追加 `output/recall-productize/v15/recall-report-v15.md`(或新建 v15b): + - 补全后的四行对照表 + 三因素拆分 + - v14 模型自身净增量的诚实数字 + - 判定:v14 student 是否够格进 Pro 排名 / 替换 legacy Pro persona v1 + - 维持"Flash 默认不动、Pro 独立开关默认 off"的产品结论 + +## 验收标准 + +- [ ] "v14 模型 + Flash persona" 行已跑,同 eval scope、同口径,四行可比 +- [ ] persona/模型/版本三因素分别拆出,每个数字标对比类型 +- [ ] v14 模型自身净增量给出诚实数字 +- [ ] merged teacher SHA 校验通过;84 holdout 未进训练/调参 +- [ ] 明确判定 v14 student 是否够格进 Pro 排名/替换 legacy persona +- [ ] Flash 默认不动结论保留 + +## 输出文件 + +``` +output/recall-productize/v15/ + recall-attribution-v2.json # 补全 Flash persona 行的三因素拆分 + recall-report-v15.md(更新) # 模型自身净增量 + v14 是否够格替换判定 +``` diff --git a/docs/UI_LIQUID_GLASS_REDESIGN_PLAN.md b/docs/UI_LIQUID_GLASS_REDESIGN_PLAN.md new file mode 100644 index 0000000..e0a8eef --- /dev/null +++ b/docs/UI_LIQUID_GLASS_REDESIGN_PLAN.md @@ -0,0 +1,453 @@ +# FrameCull AI Liquid Glass UI Refactor Plan + +## Summary + +目标是把 FrameCull AI 的界面升级成更接近 iOS / macOS 新材料语言的轻量 Liquid Glass 风格,但不牺牲筛图效率。FrameCull 是摄影师工作台,不是展示页,所以这次的核心不是炫技,而是把现有 toolbar、viewer controls、AI 浮窗、设置页、RAW 监看状态做得更高级、更统一、更稳定。 + +本方案只规划前端视觉系统和可回退开发流程,不改 AI 筛片算法、不改 RAW 解码链路、不改 Flash / Pro 版本边界。 + +## Current Evidence + +- 当前分支:`codex/apple-ui-redesign` +- 项目已有 `PRODUCT.md`,register 是 `product` +- 项目没有既有 `DESIGN.md`,本轮已新增根目录 `DESIGN.md` +- 当前已有 glass helper:`src/components/ui/chrome.ts` +- 当前主要 UI 文件: + - `src/App.tsx` + - `src/index.css` + - `src/components/Toolbar.tsx` + - `src/components/Viewer.tsx` + - `src/components/AiFloatingPanel.tsx` + - `src/components/SettingsPanel.tsx` + - `src/components/ui/chrome.ts` +- 已保存改动前状态快照: + - `output/ui-redesign-preflight/status-before-plan.txt` + - `output/ui-redesign-preflight/diff-stat-before-plan.txt` + +## References Used + +### Official / Primary + +- Apple Developer: Liquid Glass + https://developer.apple.com/documentation/technologyoverviews/liquid-glass +- Apple HIG: Materials + https://developer.apple.com/design/human-interface-guidelines/materials +- WWDC: Meet Liquid Glass + https://developer.apple.com/videos/play/wwdc2025/219/ + +### Open Source / Practical + +- `rdev/liquid-glass-react` + https://github.com/rdev/liquid-glass-react +- `VoltAgent/awesome-design-md` + https://github.com/VoltAgent/awesome-design-md + +### Inspiration Sites + +- Landbook: restrained premium web references +- Awwwards: motion and atmosphere references +- One Page Love: concise page composition references +- landing.love: interaction and transition references +- Mobbin: app flow and product UI references +- Lapa Ninja: visual treatment references +- Framer Gallery: polished motion references +- Aceternity UI: component inspiration, not direct product dependency +- 21st: template pattern reference, not direct adoption +- siteinspire: high-level mood and composition reference + +## Important Design Decision + +`liquid-glass-react` is useful as a lab reference, but should not enter the default production path yet. + +Reason: + +1. FrameCull is a Tauri photo workstation, not a marketing page. +2. The library adds refraction, elasticity, chromatic aberration, and mouse response. These can distract from photo review. +3. GitHub README notes Safari and Firefox have partial support for displacement. Tauri WebView must be measured before adopting it. +4. The repo has no releases published at inspection time, so the dependency risk is higher than a small local CSS material system. + +Recommended path: + +- Phase 1 to 3: implement our own CSS/Tailwind material tokens in `chrome.ts` and `index.css`. +- Phase 4: if desired, create an isolated lab route or dev-only component to compare `liquid-glass-react`. +- Production default: no new glass dependency until screenshot QA and culling speed tests prove no regression. + +## Design Direction + +Reading this as: dense desktop photo-culling product UI for working photographers, with restrained Apple-inspired material language, leaning toward Linear precision plus Raycast-style utility density. + +Design dials: + +- Design variance: 4 / 10 +- Motion intensity: 3 / 10 +- Visual density: 8 / 10 + +Interpretation: + +- More refined than current UI, but not more decorative. +- Motion is for AI engine startup, RAW cache generation, active selection, and hover/focus feedback only. +- The photo stays dominant. Chrome recedes. + +## What To Preserve + +- Keyboard-driven review flow. +- Large image preview as the visual center. +- Existing Flash / Pro split. +- Existing local-only privacy positioning. +- Existing AI state vocabulary: review, clear, picked, rejected, duplicate, RAW monitor. +- Existing system font stack, because this is product UI and Chinese text needs reliability. +- Existing lucide icon dependency, because the app already uses it and mixing icon families would make the interface less consistent. + +## What To Change + +### 1. Tokenize Glass Instead Of Inline Styling + +Current issue: + +- Glass styling exists, but blur values and shadows are repeated across components. +- Some blur values are very high, for example `backdrop-blur-[72px]` and `backdrop-blur-[96px]`. +- Heavy blur on large surfaces risks UI cost and can make text feel soft. + +Target: + +- Replace scattered values with named material tokens: + - `chromeSolid` + - `chromeGlass` + - `chromeActive` + - `chromePopover` + - `photoOverlay` +- Keep default blur between 24px and 40px. +- Use 70px+ blur only for small transient popovers if measured safe. + +Files: + +- `src/components/ui/chrome.ts` +- `src/index.css` + +### 2. Make Toolbar A Quiet Control Plane + +Current: + +- Toolbar already has compact density and AI progress. +- It has strong glass and multiple inline surface treatments. + +Target: + +- One toolbar surface token. +- Stable center AI progress capsule. +- AI engine startup text can appear, but scanned count should not say model branding. +- Import/export/settings buttons use the same selected, hover, disabled, and focus vocabulary. + +Files: + +- `src/components/Toolbar.tsx` + +### 3. Make Viewer Controls More Photographic + +Current: + +- Viewer bottom controls use glass and shadow. +- RAW monitor notices and cache states are present. + +Target: + +- Viewer photo area stays clean, no decorative glass on canvas. +- Bottom control bar becomes a small material plate, high contrast over bright photos. +- RAW monitor generation state gets clearer hierarchy: + - current photo checking + - generating cache + - using embedded fallback + - failed and cooled down +- Avoid blocking JPG fallback when RAW monitor cache is missing. + +Files: + +- `src/components/Viewer.tsx` +- `src/editions/useRawMonitorViewerFrame.pro.ts` + +### 4. Rebuild AI Floating Panel As Engine HUD + +Current: + +- AI floating panel now has engine initialization text and progress. +- It is already glassy but can be more disciplined. + +Target: + +- Compact engine HUD, not a floating marketing card. +- Startup copy: + - `AI 美学引擎启动中` + - detail: `正在加载本地审美模型与筛片规则` +- Running state: + - phase + - processed / total + - elapsed + - remaining + - pause / resume +- Keep Pro model scoring distinct from Flash AI analysis. + +Files: + +- `src/components/AiFloatingPanel.tsx` +- `src/hooks/useAiCulling.ts` + +### 5. Settings Panel: Clearer Pro / Flash Separation + +Current: + +- Settings has product text, Pro model diagnostics, RAW engine controls, cache management. + +Target: + +- Settings sections should feel calm and scannable. +- Pro-only surfaces use `Pro` badge and diagnostics, not a separate visual universe. +- RAW monitor controls should clearly say what is cached and when it regenerates. +- Cache controls should be separated by scope: + - AI analysis cache + - RAW monitor preview cache + - app preview cache + +Files: + +- `src/components/SettingsPanel.tsx` +- `src/editions/RawEngineSection.pro.tsx` + +### 6. Filmstrip And Inspector Performance Guard + +Current: + +- Filmstrip and inspector are dense and scroll-heavy. + +Target: + +- Avoid backdrop blur on long scrolling containers. +- Use solid surfaces and hairline borders. +- Current image, AI issue, picked, rejected, duplicate, and RAW/JPG state must be visible at thumbnail scale. + +Files: + +- `src/components/Filmstrip.tsx` +- `src/components/Viewer.tsx` + +## Refactor Phases + +### Phase 0: Safety Checkpoint + +Status: started. + +Actions: + +1. Keep work on `codex/apple-ui-redesign`. +2. Save preflight status and diff stats under `output/ui-redesign-preflight`. +3. Add `DESIGN.md` and this plan. +4. Do not touch algorithm, RAW engine, or worker code in UI phase. + +Rollback: + +- Documentation-only changes can be reverted by removing `DESIGN.md`, this plan, and `.agents/skills/awesome-design-md`. +- UI implementation phases should be separate commits or patch files. + +### Phase 1: Material Token Layer + +Actions: + +1. Replace existing `glassSurface`, `glassPopover`, `glassInteractive`, `glassActive`, and `glassSubtle` with a richer but smaller token set. +2. Add CSS utility classes for: + - `.fc-chrome-solid` + - `.fc-chrome-glass` + - `.fc-chrome-popover` + - `.fc-photo-overlay` + - `.fc-focus-ring` +3. Add reduced transparency fallback: + - if supported, use `@media (prefers-reduced-transparency: reduce)` + - otherwise keep a manual solid variant available through class composition. + +Acceptance: + +- `pnpm exec tsc --noEmit` +- Existing app loads. +- No visual change outside token consumers. + +### Phase 2: Toolbar And AI HUD + +Actions: + +1. Migrate toolbar surfaces to new tokens. +2. Reduce blur and shadow in toolbar. +3. Normalize icon button shape and focus states. +4. Refine AI startup state in toolbar and floating panel. + +Acceptance: + +- AI start does not look frozen during initialization. +- Count text stays stable. +- Progress width does not jump between phases. + +### Phase 3: Viewer Controls And RAW Monitor Notices + +Actions: + +1. Migrate bottom controls and RAW monitor popover to new tokens. +2. Keep photo canvas free of decorative material. +3. Add clear visual language for RAW monitor source: + - RAW processed + - embedded fallback + - cache missing + - generation running + - failure cooled down +4. Verify bright photo and dark photo contrast. + +Acceptance: + +- Viewer controls readable over high-key and low-key photos. +- JPG fallback visible when RAW monitor cache is unavailable. +- No full-screen blur over photo. + +### Phase 4: Settings, Export, And Modal Surfaces + +Actions: + +1. Migrate settings sections to calmer material hierarchy. +2. Keep Pro model diagnostics compact. +3. Make cache scope labels explicit. +4. Unify export selection rows. +5. Keep buttons one-line and stable. + +Acceptance: + +- Settings text does not feel like a README wall. +- Flash settings do not mention Pro-only capabilities. +- Pro settings show AI engine and RAW monitor state clearly. + +### Phase 5: Optional Liquid Glass React Lab + +Actions: + +1. Create a separate lab branch or dev-only component. +2. Install `liquid-glass-react` only in the lab branch. +3. Test one tiny surface: + - floating AI button, or + - RAW monitor compact toggle +4. Measure: + - app startup + - viewer navigation + - CPU/GPU usage + - Tauri WebView compatibility + - text readability + +Decision: + +- If it improves tactile feel with no measurable speed/readability cost, consider a tiny optional component. +- Otherwise keep production on native CSS material tokens. + +## Visual QA Plan + +Use screenshots at: + +- 1440 x 900 +- 1920 x 1080 +- 2560 x 1440 +- narrow desktop window, around 1100px wide + +States to capture: + +1. Empty import state. +2. Normal viewer with JPG. +3. Viewer with RAW monitor disabled. +4. Viewer with RAW monitor checking. +5. Viewer with RAW monitor fallback. +6. AI idle. +7. AI engine initializing. +8. AI running. +9. AI paused. +10. Settings Flash. +11. Settings Pro. +12. Export dialog. + +Manual checks: + +- Text does not overlap. +- Buttons do not resize on state changes. +- Image remains visually dominant. +- Glass does not hide photo details. +- Keyboard focus is visible. +- Reduced motion disables sheens. + +## Performance QA Plan + +Run after each UI implementation phase: + +```powershell +pnpm exec tsc --noEmit +pnpm run test -- --run src/components/AiFloatingPanel.test.ts src/utils/concurrency.test.ts +pnpm run build:release:flash +pnpm run build:release:pro +``` + +For culling speed: + +```powershell +pnpm run dev +``` + +Then test a known folder with 400 to 600 photos and compare: + +- AI start to first result +- total analysis time +- viewer navigation latency while AI is running +- memory use after 5 minutes of navigation + +Performance budget: + +- UI refactor should not increase culling time by more than 5 percent. +- Viewer navigation should not feel worse than current build. +- No glass effect may be added to long scrolling lists unless measured safe. + +## Rollback Strategy + +### Before Implementation + +- Keep this branch separate from release packaging. +- Save: + - `git status --short` + - `git diff --stat` + - screenshots before visual changes + +### During Implementation + +- One phase, one small commit or patch. +- Avoid mixing UI polish with RAW engine, AI ranker, or release README changes. +- If a phase fails, revert only that phase. + +### After Implementation + +Rollback options: + +1. Token rollback: restore `src/components/ui/chrome.ts` and `src/index.css`. +2. Surface rollback: restore only the touched component, such as `Toolbar.tsx`. +3. Full UI rollback: revert commits from the UI branch. +4. Keep `DESIGN.md` if the rules remain useful, even if code changes are reverted. + +## Git Hygiene + +Current worktree is already dirty from prior Pro, Flash, RAW, AI, README, and packaging work. Do not run destructive reset commands. + +Recommended sequence before code changes: + +```powershell +git status --short +git diff --stat +git diff -- src/components/ui/chrome.ts src/index.css src/components/Toolbar.tsx src/components/Viewer.tsx src/components/AiFloatingPanel.tsx src/components/SettingsPanel.tsx +``` + +If committing: + +1. Stage only UI plan and UI implementation files. +2. Do not stage model outputs, release artifacts, RAW cache, or training logs. +3. Keep `.agents/skills/awesome-design-md` either untracked as a local tool or commit it only if you want the repo to carry that skill for future agents. + +## Production Recommendation + +Use a native CSS/Tailwind Liquid Glass approximation for FrameCull production. Do not add `liquid-glass-react` to Flash or Pro default builds yet. + +The best next implementation step is Phase 1: material token layer. It is low risk, improves consistency, and creates a clean rollback point before touching viewer or settings layouts. diff --git a/docs/assets/image.png b/docs/assets/image.png new file mode 100644 index 0000000..6948aef Binary files /dev/null and b/docs/assets/image.png differ diff --git a/docs/assets/interface.png b/docs/assets/interface.png new file mode 100644 index 0000000..b2877dc Binary files /dev/null and b/docs/assets/interface.png differ diff --git a/docs/superpowers/plans/2026-07-10-bundle-rawtherapee-with-macos-pro.md b/docs/superpowers/plans/2026-07-10-bundle-rawtherapee-with-macos-pro.md new file mode 100644 index 0000000..eae7afa --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-bundle-rawtherapee-with-macos-pro.md @@ -0,0 +1,302 @@ +# Bundle RawTherapee With macOS Pro Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Include the pinned official RawTherapee 5.12 macOS Universal installer ZIP in both downloadable FrameCull AI Pro macOS tester ZIPs. + +**Architecture:** Keep RawTherapee as its untouched upstream ZIP beside the FrameCull Pro DMG in the outer tester package. A tracked provenance manifest supplies the official URL and SHA-256; each macOS matrix job downloads and verifies that artifact before staging, then the existing package checksum file covers it with the DMG and tester documentation. + +**Tech Stack:** GitHub Actions, Tauri 2, pnpm, Node.js, bash, macOS `shasum`, `ditto`, GitHub CLI. + +--- + +### Task 1: Prove The Current Package Is Missing RawTherapee + +**Files:** +- Inspect: `C:/Users/29238/Desktop/FrameCull-Pro-macOS-Test-0.1.6-run-29102039690/FrameCull-Pro-macOS-arm64.zip` +- Inspect: `C:/Users/29238/Desktop/FrameCull-Pro-macOS-Test-0.1.6-run-29102039690/FrameCull-Pro-macOS-x64.zip` + +- [ ] **Step 1: Run the pre-change package contract** + +```powershell +$root = 'C:\Users\29238\Desktop\FrameCull-Pro-macOS-Test-0.1.6-run-29102039690' +foreach ($arch in @('arm64', 'x64')) { + $zip = Join-Path $root "FrameCull-Pro-macOS-$arch.zip" + $entries = tar -tf $zip + if ($entries -notmatch 'RawTherapee_macOS_15.4_Universal_5.12.zip') { + Write-Error "$arch package does not include RawTherapee" + } +} +``` + +Expected: FAIL for both architectures because the current Pro ZIPs only contain +the FrameCull DMG, helper, README, and checksum file. + +### Task 2: Add Auditable RawTherapee Provenance And Instructions + +**Files:** +- Create: `src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json` +- Modify: `tools/macos/README-FrameCull-Pro-macOS-first-launch.txt` + +- [ ] **Step 1: Add the pinned upstream manifest** + +Create this exact structured manifest: + +```json +{ + "schemaVersion": 1, + "name": "RawTherapee", + "version": "5.12", + "platform": "macos-universal", + "artifact": "RawTherapee_macOS_15.4_Universal_5.12.zip", + "sourceUrl": "https://github.com/RawTherapee/RawTherapee/releases/download/5.12/RawTherapee_macOS_15.4_Universal_5.12.zip", + "checksumUrl": "https://github.com/RawTherapee/RawTherapee/releases/download/5.12/RawTherapee_macOS_15.4_Universal_5.12.zip.sha256", + "releaseUrl": "https://github.com/RawTherapee/RawTherapee/releases/tag/5.12", + "sha256": "2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688" +} +``` + +- [ ] **Step 2: Update the Chinese tester flow** + +Replace the current “install RawTherapee separately” wording with these concrete +steps while preserving the existing signing and Gatekeeper boundaries: + +```text +安装随包附带的 RawTherapee: +1. 解压“RawTherapee_macOS_15.4_Universal_5.12.zip”。 +2. 把“RawTherapee.app”拖入 Applications(应用程序)。 +3. 首次打开若被 macOS 拦截,请右键“RawTherapee.app”并选择“打开”。 +4. FrameCull AI Pro 会自动检测: + /Applications/RawTherapee.app/Contents/MacOS/rawtherapee-cli +``` + +Also state that the same Universal installer supports Apple Silicon and Intel, +and that no additional RawTherapee download is required. + +- [ ] **Step 3: Validate the manifest and documentation** + +```powershell +node -e "const fs=require('node:fs');const p='src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json';const m=JSON.parse(fs.readFileSync(p,'utf8'));if(m.artifact!=='RawTherapee_macOS_15.4_Universal_5.12.zip'||m.sha256!=='2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688'||!m.sourceUrl.startsWith('https://github.com/RawTherapee/RawTherapee/releases/download/'))throw Error('invalid RawTherapee manifest');console.log('RawTherapee manifest: PASS')" +$readme = Get-Content -Raw -Encoding utf8 'tools\macos\README-FrameCull-Pro-macOS-first-launch.txt' +if ($readme -notmatch 'RawTherapee_macOS_15.4_Universal_5.12.zip' -or $readme -notmatch '/Applications/RawTherapee.app/Contents/MacOS/rawtherapee-cli') { exit 1 } +``` + +Expected: PASS. + +- [ ] **Step 4: Commit provenance and instructions** + +```powershell +git add src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json tools/macos/README-FrameCull-Pro-macOS-first-launch.txt +git commit -m "docs: include RawTherapee macOS installer guidance" +``` + +### Task 3: Download And Stage RawTherapee In Both Pro Packages + +**Files:** +- Modify: `.github/workflows/macos-pro-test-build.yml` + +- [ ] **Step 1: Extend the existing input validation** + +Inside `Validate Pro package inputs`, parse +`src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json` and require +the exact artifact name, official GitHub release URL prefix, and pinned SHA: + +```javascript +const rawTherapee = JSON.parse( + fs.readFileSync( + "src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json", + "utf8", + ), +); +if (rawTherapee.artifact !== "RawTherapee_macOS_15.4_Universal_5.12.zip") { + throw new Error("wrong RawTherapee artifact"); +} +if (!rawTherapee.sourceUrl.startsWith("https://github.com/RawTherapee/RawTherapee/releases/download/")) { + throw new Error("RawTherapee source is not the official GitHub release"); +} +if (rawTherapee.sha256 !== "2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688") { + throw new Error("wrong RawTherapee SHA-256"); +} +``` + +- [ ] **Step 2: Add an upstream download step** + +Add this step after input validation and before the Tauri build so both matrix +jobs use the same pinned Universal archive: + +```yaml +- name: Download official RawTherapee installer + id: rawtherapee + shell: bash + run: | + set -euo pipefail + + manifest="src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json" + artifact="$(node -e 'const fs=require("node:fs");const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));process.stdout.write(m.artifact)' "${manifest}")" + source_url="$(node -e 'const fs=require("node:fs");const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));process.stdout.write(m.sourceUrl)' "${manifest}")" + expected_sha="$(node -e 'const fs=require("node:fs");const m=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));process.stdout.write(m.sha256)' "${manifest}")" + archive="${RUNNER_TEMP}/${artifact}" + + curl --fail --location --retry 3 --output "${archive}" "${source_url}" + echo "${expected_sha} ${archive}" | /usr/bin/shasum -a 256 --check + echo "archive_path=${archive}" >> "${GITHUB_OUTPUT}" + echo "artifact_name=${artifact}" >> "${GITHUB_OUTPUT}" +``` + +- [ ] **Step 3: Add RawTherapee to the staged ZIP contract** + +Expose the step outputs to `Stage and verify Pro tester package`: + +```yaml +RAWTHERAPEE_ARCHIVE: ${{ steps.rawtherapee.outputs.archive_path }} +RAWTHERAPEE_ARTIFACT: ${{ steps.rawtherapee.outputs.artifact_name }} +``` + +Then require and copy the untouched archive: + +```bash +test -f "${RAWTHERAPEE_ARCHIVE}" +test "$(basename "${RAWTHERAPEE_ARCHIVE}")" = "${RAWTHERAPEE_ARTIFACT}" +cp "${RAWTHERAPEE_ARCHIVE}" "${stage_dir}/${RAWTHERAPEE_ARTIFACT}" +``` + +Generate checksums over all staged payloads: + +```bash +/usr/bin/shasum -a 256 ./*.dmg "${RAWTHERAPEE_ARTIFACT}" FrameCull-Pro-First-Launch.command README-FrameCull-Pro-macOS-first-launch.txt > SHA256SUMS.txt +``` + +- [ ] **Step 4: Update Draft Release notes** + +State that each Pro ZIP includes the pinned official RawTherapee 5.12 macOS +Universal installer and its checksum coverage. Do not claim RawTherapee is +embedded or automatically installed. + +- [ ] **Step 5: Validate workflow syntax and diff** + +```powershell +pnpm dlx prettier@3.6.2 --check .github/workflows/macos-pro-test-build.yml src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json +git diff --check +``` + +Expected: PASS. + +- [ ] **Step 6: Commit workflow packaging** + +```powershell +git add .github/workflows/macos-pro-test-build.yml +git commit -m "ci: bundle RawTherapee with macOS Pro packages" +``` + +### Task 4: Verify Locally And Rebuild On GitHub + +**Files:** +- Verify: `.github/workflows/macos-pro-test-build.yml` +- Verify: `src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json` +- Verify: `tools/macos/README-FrameCull-Pro-macOS-first-launch.txt` + +- [ ] **Step 1: Run local regression checks** + +```powershell +pnpm test +pnpm run build:release:pro:macos +$env:ORT_SKIP_DOWNLOAD='1' +cargo check --manifest-path src-tauri\Cargo.toml --features pro-bench --bins +Remove-Item Env:ORT_SKIP_DOWNLOAD +``` + +Expected: 29 test files and 220 tests pass, the Pro release check passes, and +the Rust check exits zero. + +- [ ] **Step 2: Push the feature branch** + +```powershell +git push origin codex/pro-flash-0.1.6-update +``` + +Expected: the push starts a new `Build macOS Pro Test Packages` run. + +- [ ] **Step 3: Monitor all three Actions jobs** + +```powershell +$headSha = git rev-parse HEAD +$runs = gh run list --workflow macos-pro-test-build.yml --branch codex/pro-flash-0.1.6-update --limit 10 --json databaseId,headSha,status,conclusion,url | ConvertFrom-Json +$run = $runs | Where-Object headSha -eq $headSha | Select-Object -First 1 +if (-not $run) { throw "No macOS Pro workflow run found for $headSha" } +$runId = [string]$run.databaseId +gh run watch $runId --exit-status --interval 15 +``` + +Expected: `Build Pro macOS-arm64`, `Build Pro macOS-x64`, and +`Publish Pro draft release` all succeed. + +- [ ] **Step 4: Inspect build logs for the pinned archive** + +```powershell +gh run view $runId --log | Select-String -Pattern 'RawTherapee_macOS_15.4_Universal_5.12.zip|2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688' +``` + +Expected: both matrix jobs verify the same upstream artifact and digest. + +### Task 5: Download And Verify The New Pro ZIPs + +**Files:** +- Create: `C:/Users/29238/Desktop/FrameCull-Pro-macOS-Test-0.1.6-run-$runId/FrameCull-Pro-macOS-arm64.zip` +- Create: `C:/Users/29238/Desktop/FrameCull-Pro-macOS-Test-0.1.6-run-$runId/FrameCull-Pro-macOS-x64.zip` + +- [ ] **Step 1: Download the new Draft Release assets** + +```powershell +$tag = "macos-pro-test-0.1.6-run-$runId" +$dest = "C:\Users\29238\Desktop\FrameCull-Pro-macOS-Test-0.1.6-run-$runId" +New-Item -ItemType Directory -Path $dest | Out-Null +gh release download $tag --repo Ray25010/Frame-Cull-AI --dir $dest --pattern 'FrameCull-Pro-macOS-*.zip' +``` + +- [ ] **Step 2: Match local ZIPs to GitHub asset digests** + +```powershell +$release = gh release view $tag --repo Ray25010/Frame-Cull-AI --json assets,isDraft,name | ConvertFrom-Json +foreach ($asset in $release.assets) { + $actual = (Get-FileHash -LiteralPath (Join-Path $dest $asset.name) -Algorithm SHA256).Hash.ToLowerInvariant() + $expected = $asset.digest -replace '^sha256:', '' + if ($actual -ne $expected) { throw "$($asset.name) release digest mismatch" } +} +``` + +Expected: both local ZIP digests match GitHub. + +- [ ] **Step 3: Extract and verify each internal checksum** + +For each architecture, extract the outer ZIP, parse every line in +`SHA256SUMS.txt`, and compare it with `Get-FileHash`. Require these files: + +```text +FrameCull AI Pro_0.1.6_aarch64.dmg +FrameCull AI Pro_0.1.6_x64.dmg +RawTherapee_macOS_15.4_Universal_5.12.zip +FrameCull-Pro-First-Launch.command +README-FrameCull-Pro-macOS-first-launch.txt +SHA256SUMS.txt +``` + +Also require the included RawTherapee archive SHA-256 to equal: + +```text +2F284D1C023F53F0C492AECC3F7635D6B7807EF22D5413EE55715D81E81FE688 +``` + +- [ ] **Step 4: Reconfirm security and product boundaries** + +Require ZIP entries and README text to contain `FrameCull AI Pro`, never +`FrameCull AI Flash`. Inspect the helper mode with `tar -tvf`; it must remain +executable. Inspect helper text; it must target only +`/Applications/FrameCull AI Pro.app` and must not contain `sudo` or +`spctl --master-disable`. + +- [ ] **Step 5: Report final distribution paths and signing status** + +Report both absolute desktop ZIP paths, outer SHA-256 values, internal checksum +results, Actions run URL, and Draft Release URL. State explicitly that +FrameCull remains ad-hoc signed and unnotarized, while RawTherapee is preserved +as the untouched official upstream installer archive. diff --git a/docs/superpowers/plans/2026-07-10-macos-test-build.md b/docs/superpowers/plans/2026-07-10-macos-test-build.md new file mode 100644 index 0000000..6d1eb30 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-macos-test-build.md @@ -0,0 +1,145 @@ +# macOS Test Build Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Produce GitHub Actions macOS Flash test packages for Apple Silicon and Intel with a scoped first-launch helper. + +**Architecture:** A dedicated workflow builds the existing Tauri Flash configuration on GitHub macOS runners, stages the DMG with tester instructions, creates a Finder-friendly ZIP, and uploads it as an Actions artifact. The helper removes quarantine only from the installed FrameCull app and never changes global Gatekeeper settings. + +**Tech Stack:** GitHub Actions, Tauri 2, Rust, pnpm, zsh, macOS `codesign`, `xattr`, `ditto`. + +--- + +### Task 1: Restore Reproducible Dependency Installation + +**Files:** +- Modify: `pnpm-lock.yaml` + +- [ ] **Step 1: Reproduce the CI lockfile failure** + +Run: `CI=true pnpm install --frozen-lockfile` + +Expected: FAIL with the lockfile override configuration mismatch. + +- [ ] **Step 2: Regenerate only the lockfile metadata** + +Run: `CI=true pnpm install --lockfile-only --no-frozen-lockfile` + +Expected: `pnpm-lock.yaml` records the overrides from `package.json` without +changing declared dependencies. + +- [ ] **Step 3: Verify frozen installation** + +Run: `CI=true pnpm install --frozen-lockfile` + +Expected: PASS. + +### Task 2: Remove TypeScript Build Blockers + +**Files:** +- Modify: `src/utils/peopleSplit.test.ts` + +- [ ] **Step 1: Preserve the failing compiler evidence** + +Run: `pnpm exec tsc --noEmit` + +Expected: FAIL with `TS2783` for duplicate `quality` properties. + +- [ ] **Step 2: Keep each intended quality override once** + +Move explicit `quality` values after `...stableFaceSample`, or omit the +explicit value when it matches the shared sample. Do not change test vectors or +clustering expectations. + +- [ ] **Step 3: Verify focused tests and compiler** + +Run: `pnpm exec vitest run src/utils/peopleSplit.test.ts` + +Run: `pnpm exec tsc --noEmit` + +Expected: PASS. + +### Task 3: Add Scoped First-Launch Helper + +**Files:** +- Create: `tools/macos/FrameCull-First-Launch.command` +- Create: `tools/macos/README-macOS-first-launch.txt` + +- [ ] **Step 1: Implement the helper** + +Use zsh with `set -euo pipefail`. Require +`/Applications/FrameCull AI Flash.app`, run +`/usr/bin/xattr -dr com.apple.quarantine` only on that path, and open it with +`/usr/bin/open`. Do not use `sudo` or change `spctl`. + +- [ ] **Step 2: Add tester instructions** + +Explain normal DMG installation first, then the fallback helper flow, the +right-click Open requirement for the helper itself, and the fact that this is +an internal unnotarized test package. + +- [ ] **Step 3: Validate script syntax where possible** + +Run on GitHub macOS: `/bin/zsh -n tools/macos/FrameCull-First-Launch.command`. + +Expected: PASS. + +### Task 4: Add macOS Test Build Workflow + +**Files:** +- Create: `.github/workflows/macos-test-build.yml` + +- [ ] **Step 1: Define a manual two-target matrix** + +Use `workflow_dispatch` with `aarch64-apple-darwin` and +`x86_64-apple-darwin`. Keep release publishing and Apple credentials out of +this workflow. + +- [ ] **Step 2: Build ad-hoc signed Flash bundles** + +Set `APPLE_SIGNING_IDENTITY=-` and run +`pnpm exec tauri build --features default --config src-tauri/tauri.flash.conf.json --target `. +Passing the explicit default feature set prevents Tauri from trying to bundle +the `pro-infer-bench` binary, whose required `pro` feature is disabled in Flash. + +- [ ] **Step 3: Stage and verify artifacts** + +Find the generated DMG, copy it with the helper and README into an +architecture-specific directory, inspect the app bundle with `codesign` when +present, and generate `SHA256SUMS.txt`. + +- [ ] **Step 4: Preserve executable metadata** + +Run `chmod +x` on the helper and package the staged directory with `ditto`. +Upload the resulting ZIP with `actions/upload-artifact`. + +### Task 5: Verify And Trigger GitHub Build + +**Files:** +- Verify all files above and the existing people-split/NEF working changes. + +- [ ] **Step 1: Run local verification** + +Run focused Vitest, `pnpm exec tsc --noEmit`, and +`pnpm run build:release:flash`. + +- [ ] **Step 2: Review the complete diff** + +Confirm no source photos or generated benchmark outputs are staged, and the +helper cannot affect apps other than FrameCull AI Flash. + +- [ ] **Step 3: Commit and push the current branch** + +Commit the validated current working changes plus macOS workflow support, then +push `codex/pro-flash-0.1.6-update`. + +- [ ] **Step 4: Trigger and monitor the workflow** + +Run: `gh workflow run macos-test-build.yml --ref codex/pro-flash-0.1.6-update` + +Monitor with `gh run watch --exit-status`. + +- [ ] **Step 5: Download and inspect artifacts** + +Download both workflow artifacts, list their ZIP contents, confirm required +files exist, and report the tester distribution paths. diff --git a/docs/superpowers/plans/2026-07-10-people-split-precision.md b/docs/superpowers/plans/2026-07-10-people-split-precision.md new file mode 100644 index 0000000..2e72cfe --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-people-split-precision.md @@ -0,0 +1,224 @@ +# People Split Precision Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Reduce different-person cluster contamination and non-human face admissions while allowing uncertain samples to remain split or unassigned. + +**Architecture:** Keep YuNet and SFace, first make preprocessing match the official model contract, then replace permissive single-link behavior with evidence-based automatic admission and clustering. A browser lab runner executes the production functions on a read-only development/holdout split and exports auditable artifacts. + +**Tech Stack:** TypeScript, Vitest, Vite, ONNX Runtime Web, YuNet, SFace, Playwright/CDP, PowerShell. + +--- + +### Task 1: Lock Model Preprocessing Contracts + +**Files:** +- Create: `src/utils/sfacePreprocess.ts` +- Create: `src/utils/sfacePreprocess.test.ts` +- Modify: `src/workers/peopleSplit.worker.ts:616` + +- [ ] **Step 1: Write a failing RGB tensor test** + +Create a pure helper test using one pixel with `R=11`, `G=22`, `B=33` and +assert the CHW tensor planes are `[11]`, `[22]`, `[33]`. + +- [ ] **Step 2: Verify the test fails** + +Run: `pnpm exec vitest run src/utils/sfacePreprocess.test.ts` + +Expected: FAIL because `rgbaToSfaceChw` does not exist. + +- [ ] **Step 3: Implement the pure RGB conversion helper** + +Expose `rgbaToSfaceChw(data, size)` and keep resize/canvas work in the worker. +Write red, green, and blue values to consecutive CHW planes without mean or +scale normalization, matching OpenCV `blobFromImage(..., swapRB=true)`. + +- [ ] **Step 4: Route the worker through the helper** + +Replace the worker's inline BGR loop with `rgbaToSfaceChw` and bump +`PEOPLE_SPLIT_MODEL_VERSION` so stale in-memory results cannot be reused. + +- [ ] **Step 5: Verify focused tests** + +Run: `pnpm exec vitest run src/utils/sfacePreprocess.test.ts src/utils/peopleSplit.test.ts` + +Expected: all tests pass. + +### Task 2: Require Supported Cluster Merges + +**Files:** +- Modify: `src/utils/peopleSplit.ts:301` +- Modify: `src/utils/peopleSplit.test.ts:93` + +- [ ] **Step 1: Write a contamination regression test** + +Build two multi-face clusters where one cross-cluster pair is close but the +centroids and remaining representative pairs are different. Assert the result +retains two clusters. + +- [ ] **Step 2: Verify the regression fails** + +Run: `pnpm exec vitest run src/utils/peopleSplit.test.ts` + +Expected: FAIL because the current best-pair fallback merges the clusters. + +- [ ] **Step 3: Implement merge evidence** + +Compute centroid distance plus bidirectional representative support. Require a +strict centroid match, or at least two mutually supported representative faces +with no same-photo conflict. Remove `bestPairDistance + 0.04` as an independent +merge trigger. + +- [ ] **Step 4: Add ambiguity-aware assignment regression** + +Create a face with nearly equal distance to two clusters and assert it remains +unassigned instead of joining either cluster. + +- [ ] **Step 5: Implement best-vs-second-best margin** + +Seed with reliable faces, compare the two nearest candidate clusters, and only +assign when the best distance passes the strict threshold and clears the +configured margin. Preserve the face in `unassignedFaces` otherwise. + +- [ ] **Step 6: Verify clustering tests** + +Run: `pnpm exec vitest run src/utils/peopleSplit.test.ts` + +Expected: all clustering tests pass, including same-person post-merge coverage. + +### Task 3: Separate Display From Automatic Admission + +**Files:** +- Modify: `src/utils/peopleSplit.ts:229` +- Modify: `src/utils/peopleSplit.test.ts` +- Modify: `src/workers/peopleSplit.worker.ts:149` +- Test: `src/utils/faceContentValidation.test.ts` + +- [ ] **Step 1: Add admission boundary tests** + +Cover a low-confidence but displayable candidate, a strong landmarked face, and +a wheel-like detector hit. Assert only the strong face is auto-eligible. + +- [ ] **Step 2: Verify the new tests fail where expected** + +Run: `pnpm exec vitest run src/utils/peopleSplit.test.ts src/utils/faceContentValidation.test.ts` + +- [ ] **Step 3: Tighten automatic admission only** + +Keep review visibility around the existing detector threshold, but require +stronger detector confidence, five keypoints, structure quality, visual +quality, and content plausibility for automatic clustering. Do not globally +raise YuNet to `0.9`, because that would hide difficult real faces instead of +placing them in review. + +- [ ] **Step 4: Preserve rejection diagnostics** + +Assign a concrete reason to every review-only candidate so benchmark contact +sheets can separate confidence, structure, blur, crop, and content failures. + +- [ ] **Step 5: Verify focused tests** + +Run: `pnpm exec vitest run src/utils/peopleSplit.test.ts src/utils/faceContentValidation.test.ts src/utils/faceDetectionGeometry.test.ts` + +Expected: all tests pass. + +### Task 4: Add Read-Only People Split Benchmark + +**Files:** +- Create: `tools/ai-lab/people-split-precision.html` +- Create: `tools/ai-lab/people-split-precision-runner.ts` +- Create: `tools/ai-lab/run-people-split-precision-cdp.mjs` +- Create: `tools/ai-lab/write-people-split-precision-report.mjs` +- Modify: `tools/ai-lab/README.md` + +- [ ] **Step 1: Add runner unit tests for summary calculations** + +Extract pure summary functions and test photo counts, worker failures, +accepted/review/rejected counts, cluster sizes, and distance quantiles. + +- [ ] **Step 2: Verify tests fail before implementation** + +Run the focused Vitest file and confirm missing exports cause the failure. + +- [ ] **Step 3: Implement the browser runner** + +Load image files supplied through the CDP file input, run the production +people-split worker, and export per-face diagnostics, normalized embeddings, +and cluster membership. Never write to the source folder. + +- [ ] **Step 4: Implement the CDP wrapper** + +Accept `--input`, `--output`, and `--label`; use installed Edge, a local Vite +server, deterministic filename ordering, bounded concurrency, and separate +stdout/stderr logs. + +- [ ] **Step 5: Generate contact sheets and report inputs** + +Create cluster and rejected-candidate manifests under the requested output +directory. Store paths to source files rather than copying all 6.58 GB. + +- [ ] **Step 6: Smoke test on ten development images** + +Expected: ten photos accounted for, zero silent failures, and JSON artifacts +containing candidate and cluster diagnostics. + +### Task 5: Development Calibration And Holdout Verification + +**Files:** +- Create: `output/people-split-precision/dev-baseline/` +- Create: `output/people-split-precision/dev-candidate/` +- Create: `output/people-split-precision/holdout-final/` +- Create: `output/people-split-precision/final-report.md` + +- [ ] **Step 1: Capture the unchanged development baseline** + +Run all 272 files in `新建文件夹 (10)` and retain raw diagnostics and contact +sheets. + +- [ ] **Step 2: Review baseline B/D errors** + +Mark false detections and mixed-identity clusters in a separate manifest. Keep +source images read-only. + +- [ ] **Step 3: Run one-change-at-a-time ablations** + +Compare RGB correction, admission gates, ambiguity margin, and supported merge +using the same development review manifest. Freeze thresholds only after the +development metrics favor B/D precision. + +- [ ] **Step 4: Run the locked holdout once** + +Run all 227 files in `新建文件夹 (11)` with frozen settings. Do not adjust +thresholds after viewing holdout outcomes. + +- [ ] **Step 5: Write the final report** + +Report mixed clusters, foreign-face rate, false-face admissions, unassigned +growth, cluster count, processed/failed photos, runtime, and remaining failure +examples for baseline and candidate. + +### Task 6: Final Verification + +**Files:** +- Verify all files modified above. + +- [ ] **Step 1: Run focused people split tests** + +Run: `pnpm exec vitest run src/utils/sfacePreprocess.test.ts src/utils/peopleSplit.test.ts src/utils/faceContentValidation.test.ts src/utils/faceDetectionGeometry.test.ts` + +- [ ] **Step 2: Run the TypeScript compiler** + +Run: `pnpm exec tsc --noEmit` + +- [ ] **Step 3: Build Flash and Pro frontends** + +Run: `pnpm run build:flash` + +Run: `pnpm run build:pro` + +- [ ] **Step 4: Audit the final diff** + +Confirm no source photo changed, no macOS work was added, no NEF work was +reverted, and all generated artifacts remain under +`output/people-split-precision/`. diff --git a/docs/superpowers/plans/2026-07-11-unified-macos-pro-installer.md b/docs/superpowers/plans/2026-07-11-unified-macos-pro-installer.md new file mode 100644 index 0000000..cfc1289 --- /dev/null +++ b/docs/superpowers/plans/2026-07-11-unified-macos-pro-installer.md @@ -0,0 +1,624 @@ +# FrameCull AI Pro macOS 统一安装器实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 用一个可重复运行的 macOS 脚本完成 FrameCull AI Pro 首次安装和后续更新;RawTherapee/CLI 健康时只更新 FrameCull,缺失或验证失败时才修复 RawTherapee。 + +**Architecture:** 官方 RawTherapee ZIP 继续原样放在外层 Pro ZIP。统一脚本先验证包,再通过真实 CLI `-v` 选择 `UPDATE_FRAMECULL_ONLY` 或 `REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL`;FrameCull 后端增加与脚本安装位置一致的固定候选路径。GitHub Actions 只执行脚本单测和 `--verify-only`,绝不在 runner 上执行正常安装。 + +**Tech Stack:** Rust/Tauri 2、zsh、AppleScript、`hdiutil`、`plutil`、`ditto`、Node.js、GitHub Actions、Vitest、Cargo。 + +--- + +## 文件职责 + +- `src-tauri/src/lib.rs`:生成并使用 macOS RawTherapee CLI 固定候选路径;保留 Windows 行为。 +- `tools/macos/FrameCull-Pro-Install.command`:统一验证、状态判断、挂载、授权安装、CLI 配置、清理和启动。 +- `tools/macos/FrameCull-Pro-Install.test.zsh`:无安装副作用地测试健康判断和状态分支。 +- `tools/macos/check-pro-installer-contract.mjs`:在 Windows 和 CI 上静态验证脚本、README、workflow 与安全边界。 +- `tools/macos/README-FrameCull-Pro-macOS-first-launch.txt`:以一个脚本为主流程,保留拖入终端和手动安装回退。 +- `.github/workflows/macos-pro-test-build.yml`:运行测试、staging、自检和双架构发布。 +- 删除 `tools/macos/FrameCull-Pro-First-Launch.command`:不再保留第二个用户脚本。 + +### Task 1: 用 TDD 修正 macOS CLI 候选路径 + +**Files:** +- Modify: `src-tauri/src/lib.rs:3796` +- Test: `src-tauri/src/lib.rs:6662` + +- [ ] **Step 1: 先写失败的纯函数测试** + +在 tests 模块中加入: + +```rust +#[test] +fn macos_rawtherapee_candidates_match_supported_priority_order() { + let home = PathBuf::from("/Users/framecull-test"); + let candidates = macos_rawtherapee_candidates(Some(&home)); + + assert_eq!( + candidates, + vec![ + ( + home.join("Library") + .join("Application Support") + .join("com.framecull.ai.pro") + .join("tools") + .join("rawtherapee-cli"), + "SYSTEM", + ), + (PathBuf::from("/usr/local/bin/rawtherapee-cli"), "SYSTEM"), + (PathBuf::from("/opt/homebrew/bin/rawtherapee-cli"), "SYSTEM"), + ] + ); +} +``` + +- [ ] **Step 2: 运行测试并确认 RED** + +```powershell +cargo test --manifest-path src-tauri\Cargo.toml --lib tests::macos_rawtherapee_candidates_match_supported_priority_order -- --exact +``` + +Expected: FAIL,原因是 `macos_rawtherapee_candidates` 尚不存在。 + +- [ ] **Step 3: 实现最小候选生成函数** + +在 `rawtherapee_candidates` 前加入: + +```rust +#[cfg(any(test, all(feature = "pro", target_os = "macos")))] +fn macos_rawtherapee_candidates( + home_dir: Option<&Path>, +) -> Vec<(PathBuf, &'static str)> { + let mut candidates = Vec::new(); + if let Some(home_dir) = home_dir { + candidates.push(( + home_dir + .join("Library") + .join("Application Support") + .join("com.framecull.ai.pro") + .join("tools") + .join("rawtherapee-cli"), + "SYSTEM", + )); + } + candidates.push((PathBuf::from("/usr/local/bin/rawtherapee-cli"), "SYSTEM")); + candidates.push((PathBuf::from("/opt/homebrew/bin/rawtherapee-cli"), "SYSTEM")); + candidates +} +``` + +在 PATH 扫描之前插入 macOS 固定候选: + +```rust +#[cfg(target_os = "macos")] +{ + let home_dir = std::env::var_os("HOME").map(PathBuf::from); + candidates.extend(macos_rawtherapee_candidates(home_dir.as_deref())); +} +``` + +删除旧的: + +```rust +PathBuf::from("/Applications/RawTherapee.app/Contents/MacOS/rawtherapee-cli") +``` + +Windows 专用分支和 PATH 扫描保持不变。 + +- [ ] **Step 4: 运行聚焦测试并确认 GREEN** + +```powershell +cargo test --manifest-path src-tauri\Cargo.toml --lib tests::macos_rawtherapee_candidates_match_supported_priority_order -- --exact +``` + +Expected: 1 passed。 + +- [ ] **Step 5: 提交候选路径修正** + +```powershell +git add src-tauri/src/lib.rs +git commit -m "fix: detect installed RawTherapee CLI on macOS" +``` + +### Task 2: 测试驱动实现可重复运行的统一安装脚本 + +**Files:** +- Create: `tools/macos/FrameCull-Pro-Install.command` +- Create: `tools/macos/FrameCull-Pro-Install.test.zsh` +- Create: `tools/macos/check-pro-installer-contract.mjs` +- Modify: `tools/macos/README-FrameCull-Pro-macOS-first-launch.txt` +- Delete: `tools/macos/FrameCull-Pro-First-Launch.command` + +- [ ] **Step 1: 先创建跨平台契约检查器** + +检查器接受 `--source` 或 `--workflow`,收集失败后一次性退出非零: + +```javascript +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, "..", ".."); +const mode = process.argv[2] ?? "--source"; +const failures = []; +const read = path => readFileSync(resolve(root, path), "utf8"); + +if (mode === "--source") { + const scriptPath = "tools/macos/FrameCull-Pro-Install.command"; + const readmePath = "tools/macos/README-FrameCull-Pro-macOS-first-launch.txt"; + if (!existsSync(resolve(root, scriptPath))) failures.push(`${scriptPath}: missing`); + if (existsSync(resolve(root, scriptPath))) { + const script = read(scriptPath); + for (const required of [ + "VERIFY_ONLY", + "UPDATE_FRAMECULL_ONLY", + "REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL", + "/Applications/FrameCull AI Pro.app", + "/Applications/RawTherapee.app", + "Application Support/com.framecull.ai.pro/tools/rawtherapee-cli", + "2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688", + "with administrator privileges", + "--verify-only", + ]) { + if (!script.includes(required)) failures.push(`${scriptPath}: missing ${required}`); + } + for (const forbidden of [/\bsudo\b/, /spctl\s+--master-disable/, /\beval\b/]) { + if (forbidden.test(script)) failures.push(`${scriptPath}: forbidden ${forbidden}`); + } + } + const readme = read(readmePath); + for (const required of ["FrameCull-Pro-Install.command", "/bin/zsh ", "后续更新", "只更新 FrameCull"]) { + if (!readme.includes(required)) failures.push(`${readmePath}: missing ${required}`); + } + if (existsSync(resolve(root, "tools/macos/FrameCull-Pro-First-Launch.command"))) { + failures.push("old Pro first-launch helper still exists"); + } +} else if (mode === "--workflow") { + const workflowPath = ".github/workflows/macos-pro-test-build.yml"; + const workflow = read(workflowPath); + for (const required of [ + "FrameCull-Pro-Install.command", + "FrameCull-Pro-Install.test.zsh", + "check-pro-installer-contract.mjs --workflow", + "--verify-only", + ]) { + if (!workflow.includes(required)) failures.push(`${workflowPath}: missing ${required}`); + } + if (workflow.includes("FrameCull-Pro-First-Launch.command")) { + failures.push(`${workflowPath}: old helper reference remains`); + } +} else { + failures.push(`unknown mode: ${mode}`); +} + +if (failures.length > 0) { + console.error(failures.join("\n")); + process.exit(1); +} +console.log(`macOS Pro installer contract ${mode}: PASS`); +``` + +- [ ] **Step 2: 运行 source 契约并确认 RED** + +```powershell +node tools/macos/check-pro-installer-contract.mjs --source +``` + +Expected: FAIL,至少报告统一脚本缺失、旧 helper 仍存在和 README 主流程缺失。 + +- [ ] **Step 3: 先写 zsh 状态测试** + +`FrameCull-Pro-Install.test.zsh` 使用临时目录创建假的 app 和 CLI,以 `FRAMECULL_INSTALLER_SOURCE_ONLY=1` source 生产脚本,覆盖: + +```text +app + managed CLI + valid -v -> UPDATE_FRAMECULL_ONLY +app + /usr/local CLI + valid -v -> UPDATE_FRAMECULL_ONLY +app + Homebrew CLI + valid -v -> UPDATE_FRAMECULL_ONLY +app missing -> REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL +CLI missing -> REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL +CLI not executable -> REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL +CLI -v exits nonzero -> REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL +CLI -v output lacks RawTherapee -> REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL +``` + +再覆盖 `run_install_for_mode`:通过覆写 `install_frame_only` 和 `repair_rawtherapee_and_install_frame` 记录调用次数,断言更新路径只调用前者一次,RawTherapee 修复函数调用次数为零。 + +测试骨架使用与生产一致的函数签名: + +```zsh +#!/bin/zsh +set -euo pipefail + +readonly TEST_DIR="$(cd "$(dirname "$0")" && pwd)" +export FRAMECULL_INSTALLER_SOURCE_ONLY=1 +source "${TEST_DIR}/FrameCull-Pro-Install.command" + +TEST_ROOT="$(mktemp -d)" +trap '/bin/rm -rf "${TEST_ROOT}"' EXIT + +assert_equal() { + local expected="$1" + local actual="$2" + local label="$3" + [[ "${actual}" == "${expected}" ]] || { + print -u2 -r -- "${label}: expected ${expected}, got ${actual}" + return 1 + } +} + +make_cli() { + local path="$1" + local behavior="$2" + /bin/mkdir -p "$(dirname "${path}")" + case "${behavior}" in + valid) + /usr/bin/printf '%s\n' '#!/bin/zsh' 'print -r -- "RawTherapee, version 5.12-test"' > "${path}" + ;; + invalid) + /usr/bin/printf '%s\n' '#!/bin/zsh' 'exit 3' > "${path}" + ;; + wrong-output) + /usr/bin/printf '%s\n' '#!/bin/zsh' 'print -r -- "not the expected engine"' > "${path}" + ;; + esac + /bin/chmod 755 "${path}" +} + +raw_app="${TEST_ROOT}/Applications/RawTherapee.app" +managed_cli="${TEST_ROOT}/managed/rawtherapee-cli" +local_cli="${TEST_ROOT}/usr-local/rawtherapee-cli" +brew_cli="${TEST_ROOT}/homebrew/rawtherapee-cli" +/bin/mkdir -p "${raw_app}" +make_cli "${managed_cli}" valid + +mode="$(determine_install_mode "${raw_app}" "${managed_cli}" "${local_cli}" "${brew_cli}")" +assert_equal "${MODE_UPDATE}" "${mode}" "healthy managed CLI" + +/bin/rm -f "${managed_cli}" +make_cli "${local_cli}" valid +mode="$(determine_install_mode "${raw_app}" "${managed_cli}" "${local_cli}" "${brew_cli}")" +assert_equal "${MODE_UPDATE}" "${mode}" "healthy /usr/local-style CLI" + +/bin/rm -rf "${raw_app}" +mode="$(determine_install_mode "${raw_app}" "${managed_cli}" "${local_cli}" "${brew_cli}")" +assert_equal "${MODE_REPAIR}" "${mode}" "missing RawTherapee app" + +/bin/mkdir -p "${raw_app}" +/bin/rm -f "${local_cli}" +make_cli "${managed_cli}" invalid +mode="$(determine_install_mode "${raw_app}" "${managed_cli}" "${local_cli}" "${brew_cli}")" +assert_equal "${MODE_REPAIR}" "${mode}" "invalid CLI" + +frame_calls=0 +repair_calls=0 +install_frame_only() { frame_calls=$((frame_calls + 1)); } +repair_rawtherapee_and_install_frame() { repair_calls=$((repair_calls + 1)); } +run_install_for_mode "${MODE_UPDATE}" +assert_equal "1" "${frame_calls}" "FrameCull update call count" +assert_equal "0" "${repair_calls}" "RawTherapee repair call count" + +print -r -- "FrameCull Pro installer state tests: PASS" +``` + +- [ ] **Step 4: 在 macOS 上确认状态测试当前为 RED** + +```bash +/bin/zsh tools/macos/FrameCull-Pro-Install.test.zsh +``` + +Expected: FAIL,因为生产脚本尚不存在。在 Windows 开发机上记录此项由后续 Actions macOS runner 执行。 + +- [ ] **Step 5: 实现统一脚本的固定接口与状态机** + +脚本必须使用: + +```zsh +#!/bin/zsh +set -euo pipefail + +readonly MODE_VERIFY_ONLY="VERIFY_ONLY" +readonly MODE_UPDATE="UPDATE_FRAMECULL_ONLY" +readonly MODE_REPAIR="REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL" +readonly FRAME_APP="/Applications/FrameCull AI Pro.app" +readonly RAW_APP="/Applications/RawTherapee.app" +readonly RAW_ARCHIVE_NAME="RawTherapee_macOS_15.4_Universal_5.12.zip" +readonly RAW_ARCHIVE_SHA256="2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688" +readonly MANAGED_CLI="${HOME}/Library/Application Support/com.framecull.ai.pro/tools/rawtherapee-cli" +typeset -a MOUNTED_DEVICES +WORK_DIR="" +``` + +实现并由 `main` 调用以下边界: + +```text +verify_package_contract +run_cli_version_with_timeout +find_healthy_rawtherapee_cli +determine_install_mode +extract_rawtherapee_payload +attach_dmg_readonly +install_frame_only_privileged +install_frame_and_raw_privileged +install_cli_for_user +run_install_for_mode +cleanup +``` + +关键行为必须是: + +```zsh +determine_install_mode() { + local raw_app="$1" + shift + if find_healthy_rawtherapee_cli "${raw_app}" "$@" >/dev/null; then + print -r -- "${MODE_UPDATE}" + else + print -r -- "${MODE_REPAIR}" + fi +} + +run_install_for_mode() { + local mode="$1" + case "${mode}" in + "${MODE_UPDATE}") install_frame_only ;; + "${MODE_REPAIR}") repair_rawtherapee_and_install_frame ;; + *) print -u2 -r -- "Unknown installer mode: ${mode}"; return 1 ;; + esac +} +``` + +正常模式用固定路径调用: + +```zsh +mode="$(determine_install_mode \ + "${RAW_APP}" \ + "${MANAGED_CLI}" \ + "/usr/local/bin/rawtherapee-cli" \ + "/opt/homebrew/bin/rawtherapee-cli")" +run_install_for_mode "${mode}" +``` + +文件末尾必须保留 source-only 测试门: + +```zsh +if [[ "${FRAMECULL_INSTALLER_SOURCE_ONLY:-0}" != "1" ]]; then + main "$@" +fi +``` + +`UPDATE_FRAMECULL_ONLY` 不得调用 RawTherapee 解压、挂载、复制或 xattr 函数。`VERIFY_ONLY` 只校验 checksum 和官方 ZIP 固定结构后退出。 + +- [ ] **Step 6: 用结构化系统工具实现挂载与一次授权** + +`attach_dmg_readonly` 使用: + +```zsh +/usr/bin/hdiutil attach -readonly -nobrowse -noautoopen -plist "${dmg}" > "${plist}" +``` + +循环 `system-entities.0` 至 `system-entities.15`,分别用: + +```zsh +/usr/bin/plutil -extract "system-entities.${index}.mount-point" raw -o - "${plist}" +/usr/bin/plutil -extract "system-entities.${index}.dev-entry" raw -o - "${plist}" +``` + +要求恰好一个 mount point,把对应 `/dev/disk*` 放入 `MOUNTED_DEVICES`。`cleanup` 按反向顺序 detach,仅清理本次记录的设备和 `WORK_DIR`。 + +两个提权函数必须分开,保证更新路径的 AppleScript 命令中完全没有 RawTherapee 目标。使用 `osascript` 的 `on run argv` 传入来源 app,AppleScript 用 `quoted form of` 生成命令;固定目标写死。命令只允许 `/bin/rm -rf` 精确同名目标、`/usr/bin/ditto` 和 `/usr/bin/xattr -dr com.apple.quarantine`。禁止 `sudo`、`eval` 和全局 Gatekeeper 命令。 + +- [ ] **Step 7: 实现原子 CLI 安装和最终验证** + +修复路径先安装两个 app,再把 CLI 复制到用户目录临时文件,`chmod 755`、只移除该文件 quarantine,最后 `mv` 到 `MANAGED_CLI`。随后要求: + +```zsh +/usr/bin/codesign --verify --deep --strict "${FRAME_APP}" +run_cli_version_with_timeout "${healthy_cli}" 10 +/usr/bin/open "${FRAME_APP}" +``` + +更新路径复用状态判断找到的健康 CLI;修复路径必须重新验证 `MANAGED_CLI`。管理员取消、签名失败或 CLI 验证失败时不得启动 FrameCull。 + +- [ ] **Step 8: 更新 README 并删除旧 helper** + +README 主流程改为:解压 ZIP,右键打开 `FrameCull-Pro-Install.command`,需要时确认一次系统密码。明确: + +- 首次安装或 RawTherapee/CLI 损坏时安装两个 app 和 CLI。 +- 后续健康更新只替换 FrameCull AI Pro,不重新安装 RawTherapee。 +- Finder 拦截时,在终端输入 `/bin/zsh `,拖入脚本并回车。 +- FrameCull 仍为 ad-hoc 签名、未公证。 +- 保留手动打开 DMG/复制 CLI 的故障回退,不作为主流程。 + +删除 `tools/macos/FrameCull-Pro-First-Launch.command`。 + +- [ ] **Step 9: 运行 source 契约并确认 GREEN** + +```powershell +node tools/macos/check-pro-installer-contract.mjs --source +git diff --check +``` + +Expected: PASS;工作区只包含本 Task 指定文件。 + +- [ ] **Step 10: 提交统一脚本与文档** + +```powershell +git add tools/macos/FrameCull-Pro-Install.command tools/macos/FrameCull-Pro-Install.test.zsh tools/macos/check-pro-installer-contract.mjs tools/macos/README-FrameCull-Pro-macOS-first-launch.txt tools/macos/FrameCull-Pro-First-Launch.command +git commit -m "feat: add unified macOS Pro installer" +``` + +### Task 3: 把统一安装器接入双架构 Pro workflow + +**Files:** +- Modify: `.github/workflows/macos-pro-test-build.yml` + +- [ ] **Step 1: 先运行 workflow 契约并确认 RED** + +```powershell +node tools/macos/check-pro-installer-contract.mjs --workflow +``` + +Expected: FAIL,报告 workflow 仍引用旧 helper,且缺少脚本单测和 `--verify-only`。 + +- [ ] **Step 2: 更新触发路径和输入验证** + +把旧 helper path 替换为: + +```yaml +- "tools/macos/FrameCull-Pro-Install.command" +- "tools/macos/FrameCull-Pro-Install.test.zsh" +- "tools/macos/check-pro-installer-contract.mjs" +``` + +`Validate Pro package inputs` 改为: + +```bash +/bin/zsh -n tools/macos/FrameCull-Pro-Install.command +/bin/zsh -n tools/macos/FrameCull-Pro-Install.test.zsh +node tools/macos/check-pro-installer-contract.mjs --source +``` + +- [ ] **Step 3: 只在一个 matrix job 运行聚焦测试** + +在 arm64 matrix 项上运行: + +```yaml +- name: Test unified Pro installer state machine + if: matrix.rust_target == 'aarch64-apple-darwin' + shell: bash + run: | + set -euo pipefail + /bin/zsh tools/macos/FrameCull-Pro-Install.test.zsh + cargo test --manifest-path src-tauri/Cargo.toml --lib tests::macos_rawtherapee_candidates_match_supported_priority_order -- --exact +``` + +该步骤不得使用安装器正常模式,也不得写 `/Applications`。 + +- [ ] **Step 4: staging 只放一个脚本并生成校验清单** + +替换 copy/chmod/checksum 命令: + +```bash +cp tools/macos/FrameCull-Pro-Install.command "${stage_dir}/" +cp tools/macos/README-FrameCull-Pro-macOS-first-launch.txt "${stage_dir}/" +chmod +x "${stage_dir}/FrameCull-Pro-Install.command" + +( + cd "${stage_dir}" + /usr/bin/shasum -a 256 ./*.dmg "${RAWTHERAPEE_ARTIFACT}" FrameCull-Pro-Install.command README-FrameCull-Pro-macOS-first-launch.txt > SHA256SUMS.txt + /bin/zsh ./FrameCull-Pro-Install.command --verify-only +) +``` + +再复制 stage 到临时负向目录,追加一行到 README,要求 `--verify-only` 返回非零;负向测试不能修改正式 stage。 + +- [ ] **Step 5: 更新 Draft Release notes** + +说明每个 Pro ZIP: + +- 包含一个统一安装/更新脚本。 +- RawTherapee 健康时只更新 FrameCull。 +- RawTherapee 缺失或验证失败时才修复。 +- 官方 RawTherapee ZIP 保持原样并受 checksum 覆盖。 +- FrameCull 为 ad-hoc、未公证测试版。 + +- [ ] **Step 6: 运行 workflow 契约和格式检查** + +```powershell +node tools/macos/check-pro-installer-contract.mjs --workflow +pnpm dlx prettier@3.6.2 --check .github/workflows/macos-pro-test-build.yml src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json +git diff --check +``` + +Expected: PASS。 + +- [ ] **Step 7: 提交 workflow** + +```powershell +git add .github/workflows/macos-pro-test-build.yml +git commit -m "ci: verify unified macOS Pro installer" +``` + +### Task 4: 本地回归、双重审查和推送 + +**Files:** +- Verify all modified files. + +- [ ] **Step 1: 运行完整本地门禁** + +```powershell +node tools/macos/check-pro-installer-contract.mjs --source +node tools/macos/check-pro-installer-contract.mjs --workflow +cargo test --manifest-path src-tauri\Cargo.toml --lib tests::macos_rawtherapee_candidates_match_supported_priority_order -- --exact +pnpm test +pnpm run build:release:pro:macos +$env:ORT_SKIP_DOWNLOAD='1' +cargo check --manifest-path src-tauri\Cargo.toml --features pro-bench --bins +Remove-Item Env:ORT_SKIP_DOWNLOAD +pnpm dlx prettier@3.6.2 --check .github/workflows/macos-pro-test-build.yml src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json +git diff --check +``` + +Expected: 聚焦 Rust 测试通过;29 个测试文件、220 项前端测试通过;Pro release、Rust check、格式和契约全部通过。 + +- [ ] **Step 2: 逐 Task 做规格审查和质量审查** + +按 subagent-driven 流程,每个实现提交先做规格审查,再做代码质量审查;发现问题由原实现代理修复并复审。重点检查: + +- 更新路径不能引用 RawTherapee 安装函数。 +- AppleScript 动态参数全部安全引用。 +- `--verify-only` 无挂载、无授权、无安装、无启动。 +- Flash、Windows RawTherapee、x64 ONNX Runtime 不变。 + +- [ ] **Step 3: 推送 feature branch** + +```powershell +git push origin codex/pro-flash-0.1.6-update +``` + +### Task 5: 远程构建和替代发布包验收 + +**Files:** +- Create: `C:/Users/29238/Desktop/FrameCull-Pro-macOS-Test-0.1.6-run-$runId/FrameCull-Pro-macOS-arm64.zip` +- Create: `C:/Users/29238/Desktop/FrameCull-Pro-macOS-Test-0.1.6-run-$runId/FrameCull-Pro-macOS-x64.zip` + +- [ ] **Step 1: 监控三个 Actions jobs** + +定位 HEAD 对应 run,并运行: + +```powershell +gh run watch $runId --repo Ray25010/Frame-Cull-AI --exit-status --interval 15 +``` + +Expected: arm64、x64 和 Draft Release 全部 success;日志中统一脚本单测、Rust 聚焦测试、两次 staged `--verify-only` 和负向篡改测试通过。 + +- [ ] **Step 2: 下载新 Draft Release 两个 ZIP** + +下载到新的 run 目录。网络中断时使用 GitHub asset API、`curl --continue-at - --retry-all-errors` 续传,不接受部分文件。 + +- [ ] **Step 3: 验证外层与内部契约** + +逐架构要求: + +```text +FrameCull AI Pro_0.1.6_.dmg +RawTherapee_macOS_15.4_Universal_5.12.zip +FrameCull-Pro-Install.command +README-FrameCull-Pro-macOS-first-launch.txt +SHA256SUMS.txt +``` + +并验证: + +- 外层 ZIP SHA 与 GitHub asset digest 一致。 +- `SHA256SUMS.txt` 四个 payload 全部匹配。 +- RawTherapee SHA 固定为 `2f284d1c...fe688`。 +- 官方 ZIP 内实际包含 DMG、独立 CLI 和 `install-readme.txt`。 +- 统一脚本 Unix mode 可执行、LF 换行、无旧 helper、无 `sudo`、无 `eval`、无全局 Gatekeeper 命令。 +- README 包含右键打开和 `/bin/zsh ` 拖入终端回退。 +- 包和 runner 验证均为 FrameCull AI Pro,不含 Flash 身份。 + +- [ ] **Step 4: 报告动态验证边界** + +报告新 ZIP 绝对路径、SHA、Actions 与 Draft Release URL。明确说明:Actions 已验证构建、签名、状态机单测和 `--verify-only`;正常安装和真实“首次安装/后续更新”仍需 Mac 测试用户各执行一次,不能把 runner 自检表述成真实安装证明。 diff --git a/docs/superpowers/specs/2026-07-10-bundle-rawtherapee-with-macos-pro-design.md b/docs/superpowers/specs/2026-07-10-bundle-rawtherapee-with-macos-pro-design.md new file mode 100644 index 0000000..14c21aa --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-bundle-rawtherapee-with-macos-pro-design.md @@ -0,0 +1,88 @@ +# Bundle RawTherapee With macOS Pro Design + +## Goal + +Make both FrameCull AI Pro macOS tester ZIPs self-contained for downloading: +each outer ZIP includes the official RawTherapee 5.12 macOS Universal installer +ZIP alongside the FrameCull Pro DMG, first-launch helper, Chinese instructions, +and checksums. + +The tester still installs RawTherapee by extracting its included ZIP and +dragging `RawTherapee.app` into `/Applications`. No additional network download +is required after receiving the FrameCull Pro package. + +## Distribution Boundary + +- Preserve the official RawTherapee archive byte-for-byte instead of extracting + or re-signing `RawTherapee.app` during FrameCull packaging. +- Do not embed RawTherapee inside `FrameCull AI Pro.app` or its DMG. +- Do not automate copying RawTherapee into `/Applications`. +- Do not remove quarantine from RawTherapee in the FrameCull first-launch + helper; that helper remains scoped to `/Applications/FrameCull AI Pro.app`. +- Keep the existing ad-hoc FrameCull signing boundary unchanged. +- Expect each Pro tester ZIP to grow by approximately 181 MB before outer ZIP + overhead. + +## Pinned Upstream Artifact + +- Product: RawTherapee 5.12 macOS Universal +- Artifact: `RawTherapee_macOS_15.4_Universal_5.12.zip` +- Release: `https://github.com/RawTherapee/RawTherapee/releases/tag/5.12` +- Download: `https://github.com/RawTherapee/RawTherapee/releases/download/5.12/RawTherapee_macOS_15.4_Universal_5.12.zip` +- SHA-256: `2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688` + +Store this provenance in a small tracked JSON manifest. The 181 MB upstream +archive remains a CI download and must not be committed to Git. + +## Workflow + +1. Each Pro matrix job reads the tracked RawTherapee provenance manifest. +2. The job downloads the official archive with retry handling. +3. `shasum -a 256 --check` verifies the pinned upstream digest before staging. +4. The unchanged archive is copied into the architecture-specific Pro staging + directory. +5. `SHA256SUMS.txt` covers the FrameCull DMG, RawTherapee installer ZIP, + first-launch helper, and Chinese README. +6. The existing `ditto` step creates the final outer Pro ZIP. +7. The Draft Release continues to receive one arm64 and one x64 Pro ZIP. + +Any download, digest, file-presence, or checksum failure stops that matrix job +and prevents Draft Release creation. + +## Tester Experience + +The Chinese README presents this order: + +1. Extract the FrameCull Pro tester ZIP. +2. Extract the included RawTherapee Universal ZIP. +3. Drag `RawTherapee.app` into `/Applications` and open it once if macOS asks + for confirmation. +4. Open the FrameCull Pro DMG and drag `FrameCull AI Pro.app` into + `/Applications`. +5. Start FrameCull Pro; it automatically probes + `/Applications/RawTherapee.app/Contents/MacOS/rawtherapee-cli`. +6. Use the scoped FrameCull first-launch helper only when Gatekeeper blocks the + unnotarized FrameCull test app. + +## Validation + +- A pre-change contract check proves the current Pro ZIP lacks the RawTherapee + installer. +- Local static checks validate the provenance manifest, workflow syntax, and + updated README text. +- Both GitHub macOS matrix jobs verify the upstream SHA before packaging. +- The packaging job asserts the RawTherapee archive exists in each staged ZIP + and includes it in `SHA256SUMS.txt`. +- Download both new Draft Release assets to a new desktop run directory. +- Confirm GitHub asset digests match local ZIP SHA-256 values. +- Extract both outer ZIPs and verify every internal checksum, including the + bundled RawTherapee archive. +- Confirm the package still contains `FrameCull AI Pro`, never Flash, and that + the first-launch helper remains executable and FrameCull-only. + +## Success Criteria + +Both downloadable Pro ZIPs contain the pinned official RawTherapee Universal +installer and pass all existing Pro identity, model, architecture, runtime, +signature, and checksum gates. A tester can install both applications without +performing another download. diff --git a/docs/superpowers/specs/2026-07-10-macos-test-build-design.md b/docs/superpowers/specs/2026-07-10-macos-test-build-design.md new file mode 100644 index 0000000..1b4021e --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-macos-test-build-design.md @@ -0,0 +1,56 @@ +# macOS Test Build Design + +## Goal + +Produce downloadable FrameCull AI Flash test packages for Apple Silicon and +Intel Macs using GitHub-hosted macOS runners. + +The immediate tester build does not have Apple Developer Program credentials, +so it cannot be Developer ID signed or notarized. It will instead use ad-hoc +signing and include an explicit, user-invoked helper that removes quarantine +only from the installed FrameCull application. + +## Distribution Boundary + +- Build `FrameCull AI Flash` for `aarch64-apple-darwin` and + `x86_64-apple-darwin`. +- Upload GitHub Actions artifacts rather than publishing a public release. +- Package each architecture as a ZIP containing the generated DMG, the helper + command, Chinese instructions, and SHA-256 checksums. +- Do not disable Gatekeeper globally and do not use `sudo`. +- The helper may only remove `com.apple.quarantine` from + `/Applications/FrameCull AI Flash.app`, then open that application. +- A future public release still requires Developer ID signing, notarization, + and stapling through paid Apple Developer credentials. + +## Workflow + +1. Install Node.js, pnpm, Rust, and the matrix Rust target on a GitHub macOS + runner. +2. Install dependencies with the frozen lockfile. +3. Build the Flash Tauri application for the selected target with + `APPLE_SIGNING_IDENTITY=-` for ad-hoc signing. +4. Locate the generated DMG and stage it with the helper and instructions. +5. Verify the app bundle signature when available, generate checksums, and use + `ditto` to create a Finder-friendly ZIP. +6. Upload the ZIP as a GitHub Actions artifact. + +Before this workflow reaches the default branch, changing the workflow file on +a `codex/**` branch bootstraps one build through a narrowly scoped `push` +trigger. Normal subsequent runs use `workflow_dispatch` after merge. + +## Known Blockers + +- The committed lockfile lacks the current package override snapshot, causing + `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` in CI. +- `src/utils/peopleSplit.test.ts` repeats the `quality` property around object + spreads, causing `tsc --noEmit` to fail before the Tauri build starts. + +## Validation + +- `CI=true pnpm install --frozen-lockfile` succeeds locally. +- Focused Vitest and `tsc --noEmit` pass. +- `pnpm run build:release:flash` succeeds on Windows as a frontend check. +- Both GitHub macOS matrix jobs finish successfully and expose downloadable ZIP + artifacts. +- Downloaded ZIPs contain one DMG, the helper, instructions, and checksums. diff --git a/docs/superpowers/specs/2026-07-10-people-split-precision-design.md b/docs/superpowers/specs/2026-07-10-people-split-precision-design.md new file mode 100644 index 0000000..ab3cd6f --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-people-split-precision-design.md @@ -0,0 +1,118 @@ +# People Split Precision Design + +## Goal + +Improve People Split precision for the two user-reported failure modes: + +- B: different people are merged into one automatic cluster. +- D: objects or background patterns are shown as faces. + +The optimization is precision-first. Ambiguous samples may remain unassigned or +temporarily split into multiple clusters rather than contaminating a person +cluster. + +## Evaluation Boundary + +The source corpus is read-only: + +`C:\Users\29238\Desktop\26.6.7研学` + +- `新建文件夹 (10)`: development and threshold calibration set, 272 JPG files. +- `新建文件夹 (11)`: locked holdout set, 227 JPG files. +- Neither folder is used to train a model. +- Holdout results are inspected only after the implementation and development + thresholds are frozen. +- Generated manifests, embeddings, contact sheets, and reports live under + `output/people-split-precision/`; original photos are never modified. + +## Success Metrics + +The primary metric is automatic-cluster purity. + +- `foreign-face clusters`: automatic clusters containing more than one real + identity. Target: zero on the reviewed holdout clusters. +- `foreign-face rate`: faces assigned to a cluster whose majority identity is + different. Target: zero on reviewed holdout faces. +- `false-face accepted`: non-human detections admitted to an automatic person + cluster. Target: zero on the reviewed holdout. + +Secondary guardrails: + +- Report unassigned-face count and singleton/split growth explicitly. +- Do not recover recall by weakening the B or D precision gates. +- Record runtime and worker failures so an apparent precision gain cannot come + from silently skipping photos. + +## Root Causes To Address + +### SFace input mismatch + +The shipped SFace ONNX worker currently writes aligned image channels as BGR. +OpenCV `FaceRecognizerSF::feature` calls `blobFromImage(..., swapRB=true)`, so +the model receives RGB. The browser implementation must match that reference. +Incorrect channel ordering can degrade identity embeddings and narrow the gap +between same-person and different-person distances. + +### Permissive face admission + +The full-frame YuNet confidence threshold is `0.48`, while downstream gates +partly rely on handcrafted content scores. This admits low-confidence +face-shaped objects. Detection and automatic-cluster admission should be +separate: questionable candidates may be visible for review, but only strongly +confirmed faces can enter automatic identity clusters. + +### Single-link cluster contamination + +The post-merge path can merge two complete clusters when one sampled face pair +is close enough. One poor embedding can therefore contaminate an otherwise +clean cluster. Automatic merging must require agreement from cluster centroids +and multiple representative comparisons, with explicit rejection of ambiguous +matches. + +## Proposed Pipeline + +1. Detect candidates with YuNet and preserve five keypoints. +2. Apply existing content validation, then classify each candidate as: + `AUTO_ELIGIBLE`, `REVIEW_ONLY`, or rejected. +3. Align faces with the official SFace five-point template. +4. Feed SFace RGB tensors matching OpenCV's reference preprocessing. +5. Build conservative seed clusters from high-quality faces. +6. Assign a face only when its best cluster passes the strict threshold and is + separated from the second-best cluster by a minimum ambiguity margin. +7. Post-merge clusters only when centroid similarity and representative-pair + support agree; a single close pair is insufficient. +8. Keep all uncertain detections and identities in the unassigned review area. + +## Benchmark Artifacts + +The browser benchmark exports: + +- per-photo candidate boxes, confidence, keypoints, quality, and rejection + reason; +- normalized SFace embeddings for accepted real-face candidates; +- cluster membership and the distances that justified each assignment/merge; +- contact sheets for automatic clusters and rejected/uncertain detections; +- a machine-readable summary comparing baseline and candidate configurations. + +The benchmark reuses the same worker preprocessing and clustering utilities as +production so lab results cannot drift into a separate implementation. + +## Delivery Order + +1. Add regression tests for official RGB preprocessing and conservative merge + evidence. +2. Add a read-only browser benchmark/export path. +3. Capture the baseline on development set `(10)`. +4. Apply one change at a time: RGB preprocessing, automatic-face admission, + ambiguity-aware assignment, then supported cluster merge. +5. Freeze thresholds on `(10)` and run one final holdout pass on `(11)`. +6. Ship only changes that improve B/D precision without hidden processing + failures; otherwise report the remaining limitation and evaluate a second + identity model as a separate phase. + +## Non-Goals + +- Training or fine-tuning SFace or the semantic student. +- Optimizing same-person recall at the expense of cluster purity. +- Modifying the source photos. +- Starting macOS packaging work in this phase. diff --git a/docs/superpowers/specs/2026-07-11-unified-macos-pro-installer-design.md b/docs/superpowers/specs/2026-07-11-unified-macos-pro-installer-design.md new file mode 100644 index 0000000..c752f50 --- /dev/null +++ b/docs/superpowers/specs/2026-07-11-unified-macos-pro-installer-design.md @@ -0,0 +1,135 @@ +# FrameCull AI Pro macOS 统一安装器修正设计 + +## 背景与已验证事实 + +Actions run `29107235393` 已成功生成 arm64 和 x64 Pro 测试包,但实包检查发现旧设计对官方 RawTherapee 归档结构的假设错误,当前 Draft 不应发给测试用户。 + +官方 `RawTherapee_macOS_15.4_Universal_5.12.zip` 的固定 SHA-256 为: + +```text +2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688 +``` + +归档内实际包含: + +```text +RawTherapee_macOS_15.4_Universal_5.12_folder/ + RawTherapee_macOS_15.4_Universal_5.12.dmg + rawtherapee-cli + install-readme.txt +``` + +官方说明要求先把 DMG 中的 `RawTherapee.app` 安装到 `/Applications`;独立 `rawtherapee-cli` 会从该 app 动态加载运行库。CLI 不位于 `RawTherapee.app/Contents/MacOS/`,因此旧 README 和旧 macOS 自动探测路径均不成立。 + +## 目标 + +把测试用户的操作压缩为: + +1. 解压与 Mac 架构对应的 FrameCull Pro ZIP。 +2. 右键打开一个统一安装脚本。 +3. macOS 要求时确认一次标准管理员密码窗口。 +4. 等待脚本安装、验证并启动 FrameCull AI Pro。 + +用户不需要手动打开 DMG、拖动 app、复制 CLI、输入终端命令或配置路径。 +若 Finder 仍拦截 `.command`,README 提供唯一回退方式:在终端输入 `/bin/zsh `(保留末尾空格),把脚本拖入终端后按回车。脚本功能和授权流程保持不变。 + +统一脚本同时用于首次安装和后续 FrameCull Pro 更新。后续更新时,如果 RawTherapee app 和任一受支持位置的 CLI 已通过真实运行验证,脚本不得重复解压、挂载、替换或修改 RawTherapee,只更新 FrameCull AI Pro。 + +## 不采用直接嵌入 + +不把 RawTherapee CLI、app 或运行库嵌入 `FrameCull AI Pro.app`,原因如下: + +- 在 FrameCull 完成签名后修改 app 内容会使签名失效,可能导致 macOS 拒绝启动。 +- 只嵌入 CLI 不能消除 RawTherapee app 安装要求,因为 CLI 仍从 `/Applications/RawTherapee.app` 动态加载库。 +- 嵌入完整 RawTherapee 运行时会引入嵌套签名、路径、体积和更新边界,超出当前测试包目标。 + +官方 RawTherapee ZIP 在外层 Pro ZIP 中继续保持字节不变,由固定 SHA 和包内 `SHA256SUMS.txt` 双重覆盖。 + +## 统一安装脚本 + +用 `FrameCull-Pro-Install.command` 替换现有仅处理 FrameCull 隔离标记的首启 helper。脚本使用系统自带 `/bin/zsh`,执行以下步骤: + +1. 切换到脚本所在目录并定位唯一的 FrameCull Pro DMG、官方 RawTherapee ZIP、README 和 `SHA256SUMS.txt`。 +2. 使用 `/usr/bin/shasum -a 256 --check` 验证外层包内全部 payload,并再次强制核对 RawTherapee 固定 SHA。 +3. 检查 `/Applications/RawTherapee.app`,并依次尝试用户工具目录、`/usr/local/bin` 和 `/opt/homebrew/bin` 中的 `rawtherapee-cli`。只有 app 存在、固定位置 CLI 可执行且 `rawtherapee-cli -v` 成功返回 RawTherapee 版本时,才判定 RawTherapee 健康。任意 PATH 位置只作为 FrameCull 后端运行时兜底,不能用于安装器跳过修复。 +4. 选择以下唯一状态路径: + - `UPDATE_FRAMECULL_ONLY`:RawTherapee 健康,完全跳过官方 ZIP 解压、RawTherapee DMG 挂载、app/CLI 替换和 quarantine 修改。 + - `REPAIR_RAWTHERAPEE_AND_UPDATE_FRAMECULL`:RawTherapee app 或 CLI 缺失、不可执行、损坏,或 `-v` 验证失败,执行完整 RawTherapee 修复安装。 +5. 始终以只读、非 Finder 弹窗方式挂载 FrameCull DMG;仅在修复状态下,才在 `mktemp` 临时目录中解压官方 RawTherapee ZIP、验证其中三个固定文件并挂载 RawTherapee DMG。挂载点通过 `hdiutil -plist` 与系统 plist 工具读取。 +6. 通过一个 macOS 标准管理员授权窗口,始终精确替换: + - `/Applications/FrameCull AI Pro.app` + + 仅在修复状态下额外替换: + - `/Applications/RawTherapee.app` + + 管理员命令不得在更新状态下触碰 `/Applications/RawTherapee.app`。 +7. 仅在修复状态下,把官方独立 CLI 复制到当前用户目录: + - `~/Library/Application Support/com.framecull.ai.pro/tools/rawtherapee-cli` +8. 始终只对新安装的 FrameCull app 移除 quarantine;仅在修复状态下处理新安装的 RawTherapee app 和 CLI。不关闭全局 Gatekeeper,不执行 `spctl --master-disable`,不修改其他应用。 +9. 验证 FrameCull app 签名;更新状态复用先前已通过验证的 CLI,修复状态重新执行 CLI `-v`。全部成功后启动 FrameCull AI Pro。 +10. 无论成功或失败,都卸载本次实际挂载的映像、删除临时目录,并显示中英双语结果和所走状态路径。 + +脚本不得使用 `eval`,管理员命令只能操作固定目标路径。动态来源路径必须经过 AppleScript `quoted form` 或等价的安全参数传递,不能拼接未经转义的 shell 输入。 + +## 权限与首次打开 + +- 脚本不使用 `sudo`;需要写入 `/Applications` 时,通过 `osascript ... with administrator privileges` 显示 macOS 原生授权窗口。 +- 用户已接受可能出现一次管理员密码窗口。 +- 当前 FrameCull 仍是 ad-hoc 签名且未公证,因此没有 Developer ID 时,首次右键“打开”统一安装脚本仍是不可消除的最小 Gatekeeper 步骤。 +- 脚本复制已签名的 FrameCull app,不修改其 bundle 内容;复制和移除 quarantine 不应改变代码签名。 +- 后续更新即使需要管理员授权替换 FrameCull,也不得因此重新安装健康的 RawTherapee。 + +## FrameCull CLI 自动探测 + +后端增加 macOS 固定候选路径: + +```text +~/Library/Application Support/com.framecull.ai.pro/tools/rawtherapee-cli +/usr/local/bin/rawtherapee-cli +/opt/homebrew/bin/rawtherapee-cli +``` + +移除不符合 RawTherapee 5.12 官方包结构的 app 内 CLI 假设。统一安装器使用第一条用户目录路径,不需要额外配置 PATH 或管理员权限。 + +候选路径生成逻辑应拆成可在 Windows 开发机上运行的纯函数测试,确保 macOS 目标路径不会再次依赖未验证假设。 + +## CI 与打包 + +`.github/workflows/macos-pro-test-build.yml` 继续下载并验证官方 RawTherapee ZIP,同时增加: + +- `/bin/zsh -n` 检查统一安装脚本语法。 +- 检查官方 ZIP 内固定 DMG、CLI 和说明文件的实际结构。 +- 将统一脚本替换旧 helper 放入 arm64/x64 外层 ZIP,并保留可执行权限。 +- `SHA256SUMS.txt` 覆盖 FrameCull DMG、官方 RawTherapee ZIP、统一脚本和中文 README。 +- 在 staged package 中运行统一脚本的 `--verify-only` 模式,只验证校验和与 RawTherapee 归档结构,不挂载、不安装、不请求管理员权限。 +- Draft Release notes 明确“一次脚本安装”、ad-hoc 未公证状态和官方 RawTherapee ZIP 保持原样。 + +Flash 构建、Windows Pro 内置 RawTherapee 和现有 x64 ONNX Runtime 1.23.2 逻辑不变。 + +## 失败处理 + +- 任一哈希、归档结构、DMG、app、CLI、签名或挂载步骤失败时立即停止,不启动 FrameCull。 +- RawTherapee 健康判断必须依赖 app 存在、CLI 可执行和 `-v` 成功三个条件;单纯存在文件不足以跳过修复。 +- 更新状态不得解压官方 RawTherapee ZIP、挂载其 DMG 或修改 RawTherapee app/CLI。 +- 管理员授权被取消时,不继续复制或移除隔离标记。 +- 若目标 app 已存在,授权窗口中的固定安装命令允许精确替换同名 app;不删除其他路径。 +- trap 必须清理已挂载映像和临时目录,避免残留卷或临时文件。 +- README 保留手动安装作为故障回退,但主流程只展示统一脚本。 + +## 验证 + +本地和 CI 需要覆盖: + +- RawTherapee macOS 候选路径测试先失败、实现后通过。 +- 统一脚本语法和 `--verify-only` 包契约通过。 +- 安装器状态契约覆盖“健康时仅更新 FrameCull”和“缺失或验证失败时修复 RawTherapee”两条路径。 +- 29 个前端测试文件、220 项既有测试继续通过。 +- Pro release 检查和 `cargo check --features pro-bench --bins` 通过。 +- macOS arm64/x64 runner 均完成 app 构建、签名校验、统一脚本自检、ZIP staging 和 Draft Release。 +- 下载新 Draft Release 两个 ZIP,匹配 GitHub asset digest,并验证全部内部校验和、RawTherapee 固定 SHA、统一脚本可执行位和安全边界。 +- 新包必须是 FrameCull AI Pro,不能出现 FrameCull AI Flash 身份。 +- Actions runner 不执行统一脚本的正常安装模式。真实首次安装和后续仅更新 FrameCull 两条动态路径,仍需 Mac 测试用户各执行一次;最终报告必须把 runner 自检和真实安装证明分开。 + +## 成功标准 + +测试用户拿到对应架构 ZIP 后,只需解压、右键打开一个统一安装脚本,并在需要时确认一次系统授权。首次安装或 RawTherapee 损坏时,脚本完成两个 app 与 CLI 的安装;后续正常更新只替换 FrameCull AI Pro.app。FrameCull 自动识别 CLI,用户无需输入任何技术命令。 diff --git a/index.html b/index.html new file mode 100644 index 0000000..a9dba9a --- /dev/null +++ b/index.html @@ -0,0 +1,468 @@ + + + + + + FrameCull AI Flash + + + +

+ +
+ +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ab1fd6 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "framecull-ai", + "private": true, + "version": "0.1.6", + "type": "module", + "scripts": { + "dev": "vite", + "build": "pnpm run build:flash", + "build:flash": "cross-env FRAMECULL_EDITION=FLASH tsc && cross-env FRAMECULL_EDITION=FLASH vite build", + "build:pro": "cross-env FRAMECULL_EDITION=PRO tsc && cross-env FRAMECULL_EDITION=PRO vite build", + "build:release": "pnpm run build:release:flash", + "build:release:flash": "pnpm run build:flash && node tools/postprocess-release-artifacts.mjs && node tools/check-release-artifacts.mjs --edition flash", + "build:release:pro": "pnpm run build:pro && node tools/postprocess-release-artifacts.mjs && node tools/check-release-artifacts.mjs --edition pro", + "build:release:pro:macos": "pnpm run build:pro && node tools/postprocess-release-artifacts.mjs && node tools/check-release-artifacts.mjs --edition pro --target-platform macos", + "check:release": "node tools/check-release-artifacts.mjs", + "test": "vitest run", + "preview": "vite preview", + "tauri": "tauri", + "tauri:build": "pnpm run tauri:build:flash", + "tauri:build:flash": "tauri build --config src-tauri/tauri.flash.conf.json", + "tauri:build:pro": "tauri build --features pro --config src-tauri/tauri.pro.conf.json" + }, + "dependencies": { + "@mediapipe/tasks-vision": "^0.10.35", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.5.0", + "@tauri-apps/plugin-fs": "^2.4.5", + "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-os": "^2.3.2", + "libraw-wasm": "^1.1.2", + "lucide-react": "^1.17.0", + "onnxruntime-web": "^1.26.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@fortawesome/fontawesome-free": "^7.1.0", + "@tauri-apps/cli": "^2", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.23", + "cross-env": "^7.0.3", + "postcss": "^8.5.10", + "tailwindcss": "^3.4.19", + "typescript": "~5.6.2", + "vite": "^6.4.3", + "vitest": "^4.1.8" + }, + "pnpm": { + "overrides": { + "@babel/core": "7.29.6", + "rollup": "4.59.0", + "esbuild": "0.28.1", + "picomatch@2.3.1": "2.3.2", + "picomatch@4.0.3": "4.0.4" + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3c21ec0 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2350 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@babel/core': 7.29.6 + rollup: 4.59.0 + esbuild: 0.28.1 + picomatch@2.3.1: 2.3.2 + picomatch@4.0.3: 4.0.4 + +importers: + + .: + dependencies: + '@mediapipe/tasks-vision': + specifier: ^0.10.35 + version: 0.10.35 + '@tauri-apps/api': + specifier: ^2 + version: 2.9.1 + '@tauri-apps/plugin-dialog': + specifier: ^2.5.0 + version: 2.6.0 + '@tauri-apps/plugin-fs': + specifier: ^2.4.5 + version: 2.4.5 + '@tauri-apps/plugin-opener': + specifier: ^2 + version: 2.5.3 + '@tauri-apps/plugin-os': + specifier: ^2.3.2 + version: 2.3.2 + libraw-wasm: + specifier: ^1.1.2 + version: 1.1.2 + lucide-react: + specifier: ^1.17.0 + version: 1.17.0(react@18.3.1) + onnxruntime-web: + specifier: ^1.26.0 + version: 1.26.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@fortawesome/fontawesome-free': + specifier: ^7.1.0 + version: 7.1.0 + '@tauri-apps/cli': + specifier: ^2 + version: 2.9.6 + '@types/react': + specifier: ^18.3.1 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.3(@types/node@25.9.2)(jiti@1.21.7)) + autoprefixer: + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.10) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + postcss: + specifier: ^8.5.10 + version: 8.5.10 + tailwindcss: + specifier: ^3.4.19 + version: 3.4.19 + typescript: + specifier: ~5.6.2 + version: 5.6.3 + vite: + specifier: ^6.4.3 + version: 6.4.3(@types/node@25.9.2)(jiti@1.21.7) + vitest: + specifier: ^4.1.8 + version: 4.1.8(@types/node@25.9.2)(vite@6.4.3(@types/node@25.9.2)(jiti@1.21.7)) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.6': + resolution: {integrity: sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': 7.29.6 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': 7.29.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': 7.29.6 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fortawesome/fontawesome-free@7.1.0': + resolution: {integrity: sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==} + engines: {node: '>=6'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mediapipe/tasks-vision@0.10.35': + resolution: {integrity: sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tauri-apps/api@2.9.1': + resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} + + '@tauri-apps/cli-darwin-arm64@2.9.6': + resolution: {integrity: sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.9.6': + resolution: {integrity: sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': + resolution: {integrity: sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': + resolution: {integrity: sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.9.6': + resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': + resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.9.6': + resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.9.6': + resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': + resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': + resolution: {integrity: sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.9.6': + resolution: {integrity: sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.9.6': + resolution: {integrity: sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-dialog@2.6.0': + resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + + '@tauri-apps/plugin-fs@2.4.5': + resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==} + + '@tauri-apps/plugin-opener@2.5.3': + resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} + + '@tauri-apps/plugin-os@2.3.2': + resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@25.9.2': + resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.9.15: + resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001764: + resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: 4.0.4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + guid-typescript@1.0.9: + resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + libraw-wasm@1.1.2: + resolution: {integrity: sha512-lOVdqbzCkvsLtMoTz41Pc3alQptWHCyD71jGBTnQmoNK5CTVayXGqtTSBntLil7YaO3h/yESx7e9jMskMtnOEQ==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.17.0: + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + obug@2.1.2: + resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + engines: {node: '>=12.20.0'} + + onnxruntime-common@1.26.0: + resolution: {integrity: sha512-qVyMR4lcWgbkc4getFV+GQijsTnbg/siteoqcDwa3sI/LxbrMSNw4ePyvCq/ymdQaRomCA7YuWmhzsswxvymdw==} + + onnxruntime-web@1.26.0: + resolution: {integrity: sha512-LbRr/8zZt2xilI2smrVQGGKINo0U46i8qJp+UXyMBGfqN7KjnH1BiwCwLwyNIVV4i9CKFv7Sf4PwLKWnT8/bEA==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + protobufjs@7.6.4: + resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} + engines: {node: '>=12.0.0'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.29.6': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.6)': + dependencies: + '@babel/core': 7.29.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.6)': + dependencies: + '@babel/core': 7.29.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.6)': + dependencies: + '@babel/core': 7.29.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@fortawesome/fontawesome-free@7.1.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mediapipe/tasks-vision@0.10.35': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@tauri-apps/api@2.9.1': {} + + '@tauri-apps/cli-darwin-arm64@2.9.6': + optional: true + + '@tauri-apps/cli-darwin-x64@2.9.6': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.9.6': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.9.6': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.9.6': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.9.6': + optional: true + + '@tauri-apps/cli@2.9.6': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.9.6 + '@tauri-apps/cli-darwin-x64': 2.9.6 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.6 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.6 + '@tauri-apps/cli-linux-arm64-musl': 2.9.6 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-musl': 2.9.6 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.6 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.6 + '@tauri-apps/cli-win32-x64-msvc': 2.9.6 + + '@tauri-apps/plugin-dialog@2.6.0': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-fs@2.4.5': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-opener@2.5.3': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@tauri-apps/plugin-os@2.3.2': + dependencies: + '@tauri-apps/api': 2.9.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@25.9.2': + dependencies: + undici-types: 7.24.6 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@25.9.2)(jiti@1.21.7))': + dependencies: + '@babel/core': 7.29.6 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.6) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.6) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.3(@types/node@25.9.2)(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.1.8': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.8(vite@6.4.3(@types/node@25.9.2)(jiti@1.21.7))': + dependencies: + '@vitest/spy': 4.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.3(@types/node@25.9.2)(jiti@1.21.7) + + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.8': + dependencies: + '@vitest/utils': 4.1.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.8': {} + + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + assertion-error@2.0.1: {} + + autoprefixer@10.4.23(postcss@8.5.10): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001764 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.10 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.9.15: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.15 + caniuse-lite: 1.0.30001764 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001764: {} + + chai@6.2.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + commander@4.1.1: {} + + convert-source-map@2.0.0: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + electron-to-chromium@1.5.267: {} + + es-module-lexer@2.1.0: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flatbuffers@25.9.23: {} + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + guid-typescript@1.0.9: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + libraw-wasm@1.1.2: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + long@5.3.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.17.0(react@18.3.1): + dependencies: + react: 18.3.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + obug@2.1.2: {} + + onnxruntime-common@1.26.0: {} + + onnxruntime-web@1.26.0: + dependencies: + flatbuffers: 25.9.23 + guid-typescript: 1.0.9 + long: 5.3.2 + onnxruntime-common: 1.26.0 + platform: 1.3.6 + protobufjs: 7.6.4 + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + platform@1.3.6: {} + + postcss-import@15.1.0(postcss@8.5.10): + dependencies: + postcss: 8.5.10 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.10): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.10 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.10): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.10 + + postcss-nested@6.2.0(postcss@8.5.10): + dependencies: + postcss: 8.5.10 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + protobufjs@7.6.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 25.9.2 + long: 5.3.2 + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.10 + postcss-import: 15.1.0(postcss@8.5.10) + postcss-js: 4.1.0(postcss@8.5.10) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.10) + postcss-nested: 6.2.0(postcss@8.5.10) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + typescript@5.6.3: {} + + undici-types@7.24.6: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite@6.4.3(@types/node@25.9.2)(jiti@1.21.7): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.10 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.9.2 + fsevents: 2.3.3 + jiti: 1.21.7 + + vitest@4.1.8(@types/node@25.9.2)(vite@6.4.3(@types/node@25.9.2)(jiti@1.21.7)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@6.4.3(@types/node@25.9.2)(jiti@1.21.7)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.2 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.3(@types/node@25.9.2)(jiti@1.21.7) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.2 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + yallist@3.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..a506bd8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + esbuild: true + protobufjs: true diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/models/aesthetic/LICENSE.txt b/public/models/aesthetic/LICENSE.txt new file mode 100644 index 0000000..c48c4f3 --- /dev/null +++ b/public/models/aesthetic/LICENSE.txt @@ -0,0 +1,19 @@ +FrameCull AI aesthetic scoring model slot + +Expected model file: + public/models/aesthetic/nima_mobilenet.onnx + +The application can run without this file. If the ONNX model is absent or fails +to load, AI scoring falls back to local technical and visual heuristics and +records the aesthetic status as UNAVAILABLE or ERROR. + +Bundled local test model: + File: public/models/aesthetic/nima_mobilenet.onnx + Source: https://huggingface.co/cromsc/nima-mobilenet-aesthetic/resolve/main/nima_mobilenet_aesthetic.onnx + SHA256: C58B0C39B5B8F752B1B0EBF10E07E48406780CE3BF9D4647F8C43898748FE69C + +Distribution note: + The upstream Hugging Face repository did not expose a clear model card or + license at download time. Before public commercial distribution, replace this + file with a self-trained/converted model or another NIMA-compatible ONNX model + whose license, training data notes, and redistribution terms are explicit. diff --git a/public/models/aesthetic/nima_mobilenet.onnx b/public/models/aesthetic/nima_mobilenet.onnx new file mode 100644 index 0000000..704e8d3 Binary files /dev/null and b/public/models/aesthetic/nima_mobilenet.onnx differ diff --git a/public/models/mediapipe/face_detector/blaze_face_full_range.SOURCE.md b/public/models/mediapipe/face_detector/blaze_face_full_range.SOURCE.md new file mode 100644 index 0000000..f1bf76b --- /dev/null +++ b/public/models/mediapipe/face_detector/blaze_face_full_range.SOURCE.md @@ -0,0 +1,10 @@ +# BlazeFace Full Range + +- Model: `blaze_face_full_range.tflite` +- Official source: https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_full_range/float16/latest/blaze_face_full_range.tflite +- Official model card: https://storage.googleapis.com/mediapipe-assets/MediaPipe%20BlazeFace%20Model%20Card%20(Full%20Range).pdf +- Retrieved: 2026-07-10 +- SHA-256: `3698B18F063835BC609069EF052228FBE86D9C9A6DC8DCB7C7C2D69AED2B181B` +- License: Apache License 2.0, as stated in the official model card. + +This asset is used only for the read-only People Split detector-confirmation experiment until its precision and recall are validated. diff --git a/public/models/mediapipe/face_detector/blaze_face_full_range.tflite b/public/models/mediapipe/face_detector/blaze_face_full_range.tflite new file mode 100644 index 0000000..7128856 Binary files /dev/null and b/public/models/mediapipe/face_detector/blaze_face_full_range.tflite differ diff --git a/public/models/mediapipe/face_detector/blaze_face_short_range.tflite b/public/models/mediapipe/face_detector/blaze_face_short_range.tflite new file mode 100644 index 0000000..1b2b522 Binary files /dev/null and b/public/models/mediapipe/face_detector/blaze_face_short_range.tflite differ diff --git a/public/models/mediapipe/face_landmarker/face_landmarker.task b/public/models/mediapipe/face_landmarker/face_landmarker.task new file mode 100644 index 0000000..c50c845 Binary files /dev/null and b/public/models/mediapipe/face_landmarker/face_landmarker.task differ diff --git a/public/models/mediapipe/wasm/vision_wasm_internal.js b/public/models/mediapipe/wasm/vision_wasm_internal.js new file mode 100644 index 0000000..164a241 --- /dev/null +++ b/public/models/mediapipe/wasm/vision_wasm_internal.js @@ -0,0 +1,8827 @@ +// This code implements the `-sMODULARIZE` settings by taking the generated +// JS program code (INNER_JS_CODE) and wrapping it in a factory function. + +// Single threaded MINIMAL_RUNTIME programs do not need access to +// document.currentScript, so a simple export declaration is enough. +var ModuleFactory = (() => { + // When MODULARIZE this JS may be executed later, + // after document.currentScript is gone, so we save it. + // In EXPORT_ES6 mode we can just use 'import.meta.url'. + var _scriptName = globalThis.document?.currentScript?.src; + return async function(moduleArg = {}) { + var moduleRtn; + +// include: shell.js +// include: minimum_runtime_check.js +// end include: minimum_runtime_check.js +// The Module object: Our interface to the outside world. We import +// and export values on it. There are various ways Module can be used: +// 1. Not defined. We create it here +// 2. A function parameter, function(moduleArg) => Promise +// 3. pre-run appended it, var Module = {}; ..generated code.. +// 4. External script tag defines var Module. +// We need to check if Module already exists (e.g. case 3 above). +// Substitution will be replaced with actual code on later stage of the build, +// this way Closure Compiler will not mangle it (e.g. case 4. above). +// Note that if you want to run closure, and also to use Module +// after the generated code, you will need to define var Module = {}; +// before the code. Then that object will be used in the code, and you +// can continue to use Module afterwards as well. +var Module = moduleArg; + +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). +// Attempt to auto-detect the environment +var ENVIRONMENT_IS_WEB = !!globalThis.window; + +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; + +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != "renderer"; + +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) +var arguments_ = []; + +var thisProgram = "./this.program"; + +var quit_ = (status, toThrow) => { + throw toThrow; +}; + +if (typeof __filename != "undefined") { + // Node + _scriptName = __filename; +} else if (ENVIRONMENT_IS_WORKER) { + _scriptName = self.location.href; +} + +// `/` should be present at the end if `scriptDirectory` is not empty +var scriptDirectory = ""; + +function locateFile(path) { + if (Module["locateFile"]) { + return Module["locateFile"](path, scriptDirectory); + } + return scriptDirectory + path; +} + +// Hooks that are implemented differently in different runtime environments. +var readAsync, readBinary; + +if (ENVIRONMENT_IS_NODE) { + // These modules will usually be used on Node.js. Load them eagerly to avoid + // the complexity of lazy-loading. + var fs = require("node:fs"); + scriptDirectory = __dirname + "/"; + // include: node_shell_read.js + readBinary = filename => { + // We need to re-wrap `file://` strings to URLs. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename); + return ret; + }; + readAsync = async (filename, binary = true) => { + // See the comment in the `readBinary` function. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename, binary ? undefined : "utf8"); + return ret; + }; + // end include: node_shell_read.js + if (process.argv.length > 1) { + thisProgram = process.argv[1].replace(/\\/g, "/"); + } + arguments_ = process.argv.slice(2); + quit_ = (status, toThrow) => { + process.exitCode = status; + throw toThrow; + }; +} else // Note that this includes Node.js workers when relevant (pthreads is enabled). +// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and +// ENVIRONMENT_IS_NODE. +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + try { + scriptDirectory = new URL(".", _scriptName).href; + } catch {} + { + // include: web_or_worker_shell_read.js + if (ENVIRONMENT_IS_WORKER) { + readBinary = url => { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + xhr.responseType = "arraybuffer"; + xhr.send(null); + return new Uint8Array(/** @type{!ArrayBuffer} */ (xhr.response)); + }; + } + readAsync = async url => { + // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. + // See https://github.com/github/fetch/pull/92#issuecomment-140665932 + // Cordova or Electron apps are typically loaded from a file:// url. + // So use XHR on webview if URL is a file URL. + if (isFileURI(url)) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = () => { + if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { + // file URLs can return 0 + resolve(xhr.response); + return; + } + reject(xhr.status); + }; + xhr.onerror = reject; + xhr.send(null); + }); + } + var response = await fetch(url, { + credentials: "same-origin" + }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + " : " + response.url); + }; + } +} else {} + +var out = console.log.bind(console); + +var err = console.error.bind(console); + +// end include: shell.js +// include: preamble.js +// === Preamble library stuff === +// Documentation for the public APIs defined in this file must be updated in: +// site/source/docs/api_reference/preamble.js.rst +// A prebuilt local version of the documentation is available at: +// site/build/text/docs/api_reference/preamble.js.txt +// You can also build docs locally as HTML or other formats in site/ +// An online HTML version (which may be of a different version of Emscripten) +// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html +var wasmBinary; + +// Wasm globals +//======================================== +// Runtime essentials +//======================================== +// whether we are quitting the application. no code should run after this. +// set in exit() and abort() +var ABORT = false; + +// set by exit() and abort(). Passed to 'onExit' handler. +// NOTE: This is also used as the process return code in shell environments +// but only when noExitRuntime is false. +var EXITSTATUS; + +// In STRICT mode, we only define assert() when ASSERTIONS is set. i.e. we +// don't define it at all in release modes. This matches the behaviour of +// MINIMAL_RUNTIME. +// TODO(sbc): Make this the default even without STRICT enabled. +/** @type {function(*, string=)} */ function assert(condition, text) { + if (!condition) { + // This build was created without ASSERTIONS defined. `assert()` should not + // ever be called in this configuration but in case there are callers in + // the wild leave this simple abort() implementation here for now. + abort(text); + } +} + +/** + * Indicates whether filename is delivered via file protocol (as opposed to http/https) + * @noinline + */ var isFileURI = filename => filename.startsWith("file://"); + +// include: runtime_common.js +// include: runtime_stack_check.js +// end include: runtime_stack_check.js +// include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + +// end include: runtime_exceptions.js +// include: runtime_debug.js +// end include: runtime_debug.js +var readyPromiseResolve, readyPromiseReject; + +// Memory management +var runtimeInitialized = false; + +function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + Module["HEAPU8"] = HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + Module["HEAPU32"] = HEAPU32 = new Uint32Array(b); + Module["HEAPF32"] = HEAPF32 = new Float32Array(b); + Module["HEAPF64"] = HEAPF64 = new Float64Array(b); +} + +// include: memoryprofiler.js +// end include: memoryprofiler.js +// end include: runtime_common.js +function preRun() { + if (Module["preRun"]) { + if (typeof Module["preRun"] == "function") Module["preRun"] = [ Module["preRun"] ]; + while (Module["preRun"].length) { + addOnPreRun(Module["preRun"].shift()); + } + } + // Begin ATPRERUNS hooks + callRuntimeCallbacks(onPreRuns); +} + +function initRuntime() { + runtimeInitialized = true; + // Begin ATINITS hooks + if (!Module["noFSInit"] && !FS.initialized) FS.init(); + TTY.init(); + // End ATINITS hooks + wasmExports["jd"](); + // Begin ATPOSTCTORS hooks + FS.ignorePermissions = false; +} + +function postRun() { + // PThreads reuse the runtime from the main thread. + if (Module["postRun"]) { + if (typeof Module["postRun"] == "function") Module["postRun"] = [ Module["postRun"] ]; + while (Module["postRun"].length) { + addOnPostRun(Module["postRun"].shift()); + } + } + // Begin ATPOSTRUNS hooks + callRuntimeCallbacks(onPostRuns); +} + +/** + * @param {string|number=} what + */ function abort(what) { + Module["onAbort"]?.(what); + what = `Aborted(${what})`; + // TODO(sbc): Should we remove printing and leave it up to whoever + // catches the exception? + err(what); + ABORT = true; + what += ". Build with -sASSERTIONS for more info."; + // Use a wasm runtime error, because a JS error might be seen as a foreign + // exception, which means we'd run destructors on it. We need the error to + // simply make the program stop. + // FIXME This approach does not work in Wasm EH because it currently does not assume + // all RuntimeErrors are from traps; it decides whether a RuntimeError is from + // a trap or not based on a hidden field within the object. So at the moment + // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that + // allows this in the wasm spec. + // Suppress closure compiler warning here. Closure compiler's builtin extern + // definition for WebAssembly.RuntimeError claims it takes no arguments even + // though it can. + // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. + /** @suppress {checkTypes} */ var e = new WebAssembly.RuntimeError(what); + readyPromiseReject?.(e); + // Throw the error whether or not MODULARIZE is set because abort is used + // in code paths apart from instantiation where an exception is expected + // to be thrown when abort is called. + throw e; +} + +var wasmBinaryFile; + +function findWasmBinary() { + return locateFile("vision_wasm_internal.wasm"); +} + +function getBinarySync(file) { + if (file == wasmBinaryFile && wasmBinary) { + return new Uint8Array(wasmBinary); + } + if (readBinary) { + return readBinary(file); + } + // Throwing a plain string here, even though it not normally advisable since + // this gets turning into an `abort` in instantiateArrayBuffer. + throw "both async and sync fetching of the wasm failed"; +} + +async function getWasmBinary(binaryFile) { + // If we don't have the binary yet, load it asynchronously using readAsync. + if (!wasmBinary) { + // Fetch the binary using readAsync + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch {} + } + // Otherwise, getBinarySync should be able to get it synchronously + return getBinarySync(binaryFile); +} + +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); + return instance; + } catch (reason) { + err(`failed to asynchronously prepare wasm: ${reason}`); + abort(reason); + } +} + +async function instantiateAsync(binary, binaryFile, imports) { + if (!binary && !isFileURI(binaryFile) && !ENVIRONMENT_IS_NODE) { + try { + var response = fetch(binaryFile, { + credentials: "same-origin" + }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err(`wasm streaming compile failed: ${reason}`); + err("falling back to ArrayBuffer instantiation"); + } + } + return instantiateArrayBuffer(binaryFile, imports); +} + +function getWasmImports() { + // prepare imports + var imports = { + "a": wasmImports + }; + return imports; +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +async function createWasm() { + // Load the wasm module and create an instance of using native support in the JS engine. + // handle a generated wasm instance, receiving its exports and + // performing other necessary setup + /** @param {WebAssembly.Module=} module*/ function receiveInstance(instance, module) { + wasmExports = instance.exports; + assignWasmExports(wasmExports); + updateMemoryViews(); + return wasmExports; + } + // Prefer streaming instantiation if available. + function receiveInstantiationResult(result) { + // 'result' is a ResultObject object which has both the module and instance. + // receiveInstance() will swap in the exports (to Module.asm) so they can be called + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. + // When the regression is fixed, can restore the above PTHREADS-enabled path. + return receiveInstance(result["instance"]); + } + var info = getWasmImports(); + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback + // to manually instantiate the Wasm module themselves. This allows pages to + // run the instantiation parallel to any other async startup actions they are + // performing. + // Also pthreads and wasm workers initialize the wasm instance through this + // path. + if (Module["instantiateWasm"]) { + return new Promise((resolve, reject) => { + Module["instantiateWasm"](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + }); + } + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; +} + +// Globals used by JS i64 conversions (see makeSetValue) +var tempDouble; + +var tempI64; + +// end include: preamble.js +// Begin JS library code +var handleException = e => { + // Certain exception types we do not treat as errors since they are used for + // internal control flow. + // 1. ExitStatus, which is thrown by exit() + // 2. "unwind", which is thrown by emscripten_unwind_to_js_event_loop() and others + // that wish to return to JS event loop. + if (e instanceof ExitStatus || e == "unwind") { + return EXITSTATUS; + } + quit_(1, e); +}; + +class ExitStatus { + name="ExitStatus"; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; + } +} + +var runtimeKeepaliveCounter = 0; + +var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0; + +var _proc_exit = code => { + EXITSTATUS = code; + if (!keepRuntimeAlive()) { + Module["onExit"]?.(code); + ABORT = true; + } + quit_(code, new ExitStatus(code)); +}; + +/** @param {boolean|number=} implicit */ var exitJS = (status, implicit) => { + EXITSTATUS = status; + _proc_exit(status); +}; + +var _exit = exitJS; + +var maybeExit = () => { + if (!keepRuntimeAlive()) { + try { + _exit(EXITSTATUS); + } catch (e) { + handleException(e); + } + } +}; + +var callUserCallback = func => { + if (ABORT) { + return; + } + try { + return func(); + } catch (e) { + handleException(e); + } finally { + maybeExit(); + } +}; + +function getFullscreenElement() { + return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.webkitCurrentFullScreenElement || document.msFullscreenElement; +} + +/** @param {number=} timeout */ var safeSetTimeout = (func, timeout) => setTimeout(() => { + callUserCallback(func); +}, timeout); + +var warnOnce = text => { + warnOnce.shown ||= {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + if (ENVIRONMENT_IS_NODE) text = "warning: " + text; + err(text); + } +}; + +var preloadPlugins = []; + +var Browser = { + useWebGL: false, + isFullscreen: false, + pointerLock: false, + moduleContextCreatedCallbacks: [], + workers: [], + preloadedImages: {}, + preloadedAudios: {}, + getCanvas: () => Module["canvas"], + init() { + if (Browser.initted) return; + Browser.initted = true; + // Support for plugins that can process preloaded files. You can add more of these to + // your app by creating and appending to preloadPlugins. + // Each plugin is asked if it can handle a file based on the file's name. If it can, + // it is given the file's raw data. When it is done, it calls a callback with the file's + // (possibly modified) data. For example, a plugin might decompress a file, or it + // might create some side data structure for use later (like an Image element, etc.). + var imagePlugin = {}; + imagePlugin["canHandle"] = name => !Module["noImageDecoding"] && /\.(jpg|jpeg|png|bmp|webp)$/i.test(name); + imagePlugin["handle"] = async (byteArray, name) => { + var b = new Blob([ byteArray ], { + type: Browser.getMimetype(name) + }); + if (b.size !== byteArray.length) { + // Safari bug #118630 + // Safari's Blob can only take an ArrayBuffer + b = new Blob([ (new Uint8Array(byteArray)).buffer ], { + type: Browser.getMimetype(name) + }); + } + var url = URL.createObjectURL(b); + return new Promise((resolve, reject) => { + var img = new Image; + img.onload = () => { + var canvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas")); + canvas.width = img.width; + canvas.height = img.height; + var ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + Browser.preloadedImages[name] = canvas; + URL.revokeObjectURL(url); + resolve(byteArray); + }; + img.onerror = event => { + err(`Image ${url} could not be decoded`); + reject(); + }; + img.src = url; + }); + }; + preloadPlugins.push(imagePlugin); + var audioPlugin = {}; + audioPlugin["canHandle"] = name => !Module["noAudioDecoding"] && name.slice(-4) in { + ".ogg": 1, + ".wav": 1, + ".mp3": 1 + }; + audioPlugin["handle"] = async (byteArray, name) => new Promise((resolve, reject) => { + var done = false; + function finish(audio) { + if (done) return; + done = true; + Browser.preloadedAudios[name] = audio; + resolve(byteArray); + } + var b = new Blob([ byteArray ], { + type: Browser.getMimetype(name) + }); + var url = URL.createObjectURL(b); + // XXX we never revoke this! + var audio = new Audio; + audio.addEventListener("canplaythrough", () => finish(audio), false); + // use addEventListener due to chromium bug 124926 + audio.onerror = event => { + if (done) return; + err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`); + function encode64(data) { + var BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var PAD = "="; + var ret = ""; + var leftchar = 0; + var leftbits = 0; + for (var i = 0; i < data.length; i++) { + leftchar = (leftchar << 8) | data[i]; + leftbits += 8; + while (leftbits >= 6) { + var curr = (leftchar >> (leftbits - 6)) & 63; + leftbits -= 6; + ret += BASE[curr]; + } + } + if (leftbits == 2) { + ret += BASE[(leftchar & 3) << 4]; + ret += PAD + PAD; + } else if (leftbits == 4) { + ret += BASE[(leftchar & 15) << 2]; + ret += PAD; + } + return ret; + } + audio.src = "data:audio/x-" + name.slice(-3) + ";base64," + encode64(byteArray); + finish(audio); + }; + audio.src = url; + // workaround for chrome bug 124926 - we do not always get oncanplaythrough or onerror + safeSetTimeout(() => { + finish(audio); + }, 1e4); + }); + preloadPlugins.push(audioPlugin); + // Canvas event setup + function pointerLockChange() { + var canvas = Browser.getCanvas(); + Browser.pointerLock = document.pointerLockElement === canvas; + } + var canvas = Browser.getCanvas(); + if (canvas) { + // forced aspect ratio can be enabled by defining 'forcedAspectRatio' on Module + // Module['forcedAspectRatio'] = 4 / 3; + document.addEventListener("pointerlockchange", pointerLockChange, false); + if (Module["elementPointerLock"]) { + canvas.addEventListener("click", ev => { + if (!Browser.pointerLock && Browser.getCanvas().requestPointerLock) { + Browser.getCanvas().requestPointerLock(); + ev.preventDefault(); + } + }, false); + } + } + }, + createContext(/** @type {HTMLCanvasElement} */ canvas, useWebGL, setInModule, webGLContextAttributes) { + if (useWebGL && Module["ctx"] && canvas == Browser.getCanvas()) return Module["ctx"]; + // no need to recreate GL context if it's already been created for this canvas. + var ctx; + var contextHandle; + if (useWebGL) { + // For GLES2/desktop GL compatibility, adjust a few defaults to be different to WebGL defaults, so that they align better with the desktop defaults. + var contextAttributes = { + antialias: false, + alpha: false, + majorVersion: (typeof WebGL2RenderingContext != "undefined") ? 2 : 1 + }; + if (webGLContextAttributes) { + for (var attribute in webGLContextAttributes) { + contextAttributes[attribute] = webGLContextAttributes[attribute]; + } + } + // This check of existence of GL is here to satisfy Closure compiler, which yells if variable GL is referenced below but GL object is not + // actually compiled in because application is not doing any GL operations. TODO: Ideally if GL is not being used, this function + // Browser.createContext() should not even be emitted. + if (typeof GL != "undefined") { + contextHandle = GL.createContext(canvas, contextAttributes); + if (contextHandle) { + ctx = GL.getContext(contextHandle).GLctx; + } + } + } else { + ctx = canvas.getContext("2d"); + } + if (!ctx) return null; + if (setInModule) { + Module["ctx"] = ctx; + if (useWebGL) GL.makeContextCurrent(contextHandle); + Browser.useWebGL = useWebGL; + Browser.moduleContextCreatedCallbacks.forEach(callback => callback()); + Browser.init(); + } + return ctx; + }, + fullscreenHandlersInstalled: false, + lockPointer: undefined, + resizeCanvas: undefined, + requestFullscreen(lockPointer, resizeCanvas) { + Browser.lockPointer = lockPointer; + Browser.resizeCanvas = resizeCanvas; + if (typeof Browser.lockPointer == "undefined") Browser.lockPointer = true; + if (typeof Browser.resizeCanvas == "undefined") Browser.resizeCanvas = false; + var canvas = Browser.getCanvas(); + function fullscreenChange() { + Browser.isFullscreen = false; + var canvasContainer = canvas.parentNode; + if (getFullscreenElement() === canvasContainer) { + canvas.exitFullscreen = Browser.exitFullscreen; + if (Browser.lockPointer) canvas.requestPointerLock(); + Browser.isFullscreen = true; + if (Browser.resizeCanvas) { + Browser.setFullscreenCanvasSize(); + } else { + Browser.updateCanvasDimensions(canvas); + } + } else { + // remove the full screen specific parent of the canvas again to restore the HTML structure from before going full screen + canvasContainer.parentNode.insertBefore(canvas, canvasContainer); + canvasContainer.parentNode.removeChild(canvasContainer); + if (Browser.resizeCanvas) { + Browser.setWindowedCanvasSize(); + } else { + Browser.updateCanvasDimensions(canvas); + } + } + Module["onFullScreen"]?.(Browser.isFullscreen); + Module["onFullscreen"]?.(Browser.isFullscreen); + } + if (!Browser.fullscreenHandlersInstalled) { + Browser.fullscreenHandlersInstalled = true; + document.addEventListener("fullscreenchange", fullscreenChange, false); + document.addEventListener("mozfullscreenchange", fullscreenChange, false); + document.addEventListener("webkitfullscreenchange", fullscreenChange, false); + document.addEventListener("MSFullscreenChange", fullscreenChange, false); + } + // create a new parent to ensure the canvas has no siblings. this allows browsers to optimize full screen performance when its parent is the full screen root + var canvasContainer = document.createElement("div"); + canvas.parentNode.insertBefore(canvasContainer, canvas); + canvasContainer.appendChild(canvas); + // use parent of canvas as full screen root to allow aspect ratio correction (Firefox stretches the root to screen size) + canvasContainer.requestFullscreen = canvasContainer["requestFullscreen"] || canvasContainer["mozRequestFullScreen"] || canvasContainer["msRequestFullscreen"] || (canvasContainer["webkitRequestFullscreen"] ? () => canvasContainer["webkitRequestFullscreen"](Element["ALLOW_KEYBOARD_INPUT"]) : null) || (canvasContainer["webkitRequestFullScreen"] ? () => canvasContainer["webkitRequestFullScreen"](Element["ALLOW_KEYBOARD_INPUT"]) : null); + canvasContainer.requestFullscreen(); + }, + exitFullscreen() { + // This is workaround for chrome. Trying to exit from fullscreen + // not in fullscreen state will cause "TypeError: Document not active" + // in chrome. See https://github.com/emscripten-core/emscripten/pull/8236 + if (!Browser.isFullscreen) { + return false; + } + var CFS = document["exitFullscreen"] || document["cancelFullScreen"] || document["mozCancelFullScreen"] || document["msExitFullscreen"] || document["webkitCancelFullScreen"] || (() => {}); + CFS.apply(document, []); + return true; + }, + safeSetTimeout(func, timeout) { + // Legacy function, this is used by the SDL2 port so we need to keep it + // around at least until that is updated. + // See https://github.com/libsdl-org/SDL/pull/6304 + return safeSetTimeout(func, timeout); + }, + getMimetype(name) { + return { + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "png": "image/png", + "bmp": "image/bmp", + "ogg": "audio/ogg", + "wav": "audio/wav", + "mp3": "audio/mpeg" + }[name.slice(name.lastIndexOf(".") + 1)]; + }, + getUserMedia(func) { + window.getUserMedia ||= navigator["getUserMedia"] || navigator["mozGetUserMedia"]; + window.getUserMedia(func); + }, + getMovementX(event) { + return event["movementX"] || event["mozMovementX"] || event["webkitMovementX"] || 0; + }, + getMovementY(event) { + return event["movementY"] || event["mozMovementY"] || event["webkitMovementY"] || 0; + }, + getMouseWheelDelta(event) { + var delta = 0; + switch (event.type) { + case "DOMMouseScroll": + // 3 lines make up a step + delta = event.detail / 3; + break; + + case "mousewheel": + // 120 units make up a step + delta = event.wheelDelta / 120; + break; + + case "wheel": + delta = event.deltaY; + switch (event.deltaMode) { + case 0: + // DOM_DELTA_PIXEL: 100 pixels make up a step + delta /= 100; + break; + + case 1: + // DOM_DELTA_LINE: 3 lines make up a step + delta /= 3; + break; + + case 2: + // DOM_DELTA_PAGE: A page makes up 80 steps + delta *= 80; + break; + + default: + abort("unrecognized mouse wheel delta mode: " + event.deltaMode); + } + break; + + default: + abort("unrecognized mouse wheel event: " + event.type); + } + return delta; + }, + mouseX: 0, + mouseY: 0, + mouseMovementX: 0, + mouseMovementY: 0, + touches: {}, + lastTouches: {}, + calculateMouseCoords(pageX, pageY) { + // Calculate the movement based on the changes + // in the coordinates. + var canvas = Browser.getCanvas(); + var rect = canvas.getBoundingClientRect(); + // Neither .scrollX or .pageXOffset are defined in a spec, but + // we prefer .scrollX because it is currently in a spec draft. + // (see: http://www.w3.org/TR/2013/WD-cssom-view-20131217/) + var scrollX = ((typeof window.scrollX != "undefined") ? window.scrollX : window.pageXOffset); + var scrollY = ((typeof window.scrollY != "undefined") ? window.scrollY : window.pageYOffset); + var adjustedX = pageX - (scrollX + rect.left); + var adjustedY = pageY - (scrollY + rect.top); + // the canvas might be CSS-scaled compared to its backbuffer; + // SDL-using content will want mouse coordinates in terms + // of backbuffer units. + adjustedX = adjustedX * (canvas.width / rect.width); + adjustedY = adjustedY * (canvas.height / rect.height); + return { + x: adjustedX, + y: adjustedY + }; + }, + setMouseCoords(pageX, pageY) { + const {x, y} = Browser.calculateMouseCoords(pageX, pageY); + Browser.mouseMovementX = x - Browser.mouseX; + Browser.mouseMovementY = y - Browser.mouseY; + Browser.mouseX = x; + Browser.mouseY = y; + }, + calculateMouseEvent(event) { + // event should be mousemove, mousedown or mouseup + if (Browser.pointerLock) { + // When the pointer is locked, calculate the coordinates + // based on the movement of the mouse. + // Workaround for Firefox bug 764498 + if (event.type != "mousemove" && ("mozMovementX" in event)) { + Browser.mouseMovementX = Browser.mouseMovementY = 0; + } else { + Browser.mouseMovementX = Browser.getMovementX(event); + Browser.mouseMovementY = Browser.getMovementY(event); + } + // add the mouse delta to the current absolute mouse position + Browser.mouseX += Browser.mouseMovementX; + Browser.mouseY += Browser.mouseMovementY; + } else { + if (event.type === "touchstart" || event.type === "touchend" || event.type === "touchmove") { + var touch = event.touch; + if (touch === undefined) { + return; + } + var coords = Browser.calculateMouseCoords(touch.pageX, touch.pageY); + if (event.type === "touchstart") { + Browser.lastTouches[touch.identifier] = coords; + Browser.touches[touch.identifier] = coords; + } else if (event.type === "touchend" || event.type === "touchmove") { + var last = Browser.touches[touch.identifier]; + last ||= coords; + Browser.lastTouches[touch.identifier] = last; + Browser.touches[touch.identifier] = coords; + } + return; + } + Browser.setMouseCoords(event.pageX, event.pageY); + } + }, + resizeListeners: [], + updateResizeListeners() { + var canvas = Browser.getCanvas(); + Browser.resizeListeners.forEach(listener => listener(canvas.width, canvas.height)); + }, + setCanvasSize(width, height, noUpdates) { + var canvas = Browser.getCanvas(); + Browser.updateCanvasDimensions(canvas, width, height); + if (!noUpdates) Browser.updateResizeListeners(); + }, + windowedWidth: 0, + windowedHeight: 0, + setFullscreenCanvasSize() { + // check if SDL is available + if (typeof SDL != "undefined") { + var flags = HEAPU32[((SDL.screen) >> 2)]; + flags = flags | 8388608; + // set SDL_FULLSCREEN flag + HEAP32[((SDL.screen) >> 2)] = flags; + } + Browser.updateCanvasDimensions(Browser.getCanvas()); + Browser.updateResizeListeners(); + }, + setWindowedCanvasSize() { + // check if SDL is available + if (typeof SDL != "undefined") { + var flags = HEAPU32[((SDL.screen) >> 2)]; + flags = flags & ~8388608; + // clear SDL_FULLSCREEN flag + HEAP32[((SDL.screen) >> 2)] = flags; + } + Browser.updateCanvasDimensions(Browser.getCanvas()); + Browser.updateResizeListeners(); + }, + updateCanvasDimensions(canvas, wNative, hNative) { + if (wNative && hNative) { + canvas.widthNative = wNative; + canvas.heightNative = hNative; + } else { + wNative = canvas.widthNative; + hNative = canvas.heightNative; + } + var w = wNative; + var h = hNative; + if (Module["forcedAspectRatio"] > 0) { + if (w / h < Module["forcedAspectRatio"]) { + w = Math.round(h * Module["forcedAspectRatio"]); + } else { + h = Math.round(w / Module["forcedAspectRatio"]); + } + } + if ((getFullscreenElement() === canvas.parentNode) && (typeof screen != "undefined")) { + var factor = Math.min(screen.width / w, screen.height / h); + w = Math.round(w * factor); + h = Math.round(h * factor); + } + if (Browser.resizeCanvas) { + if (canvas.width != w) canvas.width = w; + if (canvas.height != h) canvas.height = h; + if (typeof canvas.style != "undefined") { + canvas.style.removeProperty("width"); + canvas.style.removeProperty("height"); + } + } else { + if (canvas.width != wNative) canvas.width = wNative; + if (canvas.height != hNative) canvas.height = hNative; + if (typeof canvas.style != "undefined") { + if (w != wNative || h != hNative) { + canvas.style.setProperty("width", w + "px", "important"); + canvas.style.setProperty("height", h + "px", "important"); + } else { + canvas.style.removeProperty("width"); + canvas.style.removeProperty("height"); + } + } + } + } +}; + +/** @type {!Int16Array} */ var HEAP16; + +/** @type {!Int32Array} */ var HEAP32; + +/** @type {!Int8Array} */ var HEAP8; + +/** @type {!Float32Array} */ var HEAPF32; + +/** @type {!Float64Array} */ var HEAPF64; + +/** @type {!Uint16Array} */ var HEAPU16; + +/** @type {!Uint32Array} */ var HEAPU32; + +/** @type {!Uint8Array} */ var HEAPU8; + +var callRuntimeCallbacks = callbacks => { + while (callbacks.length > 0) { + // Pass the module as the first argument. + callbacks.shift()(Module); + } +}; + +var onPostRuns = []; + +var addOnPostRun = cb => onPostRuns.push(cb); + +var onPreRuns = []; + +var addOnPreRun = cb => onPreRuns.push(cb); + +var noExitRuntime = true; + +var stackRestore = val => __emscripten_stack_restore(val); + +var stackSave = () => _emscripten_stack_get_current(); + +class ExceptionInfo { + // excPtr - Thrown object pointer to wrap. Metadata pointer is calculated from it. + constructor(excPtr) { + this.excPtr = excPtr; + this.ptr = excPtr - 24; + } + set_type(type) { + HEAPU32[(((this.ptr) + (4)) >> 2)] = type; + } + get_type() { + return HEAPU32[(((this.ptr) + (4)) >> 2)]; + } + set_destructor(destructor) { + HEAPU32[(((this.ptr) + (8)) >> 2)] = destructor; + } + get_destructor() { + return HEAPU32[(((this.ptr) + (8)) >> 2)]; + } + set_caught(caught) { + caught = caught ? 1 : 0; + HEAP8[(this.ptr) + (12)] = caught; + } + get_caught() { + return HEAP8[(this.ptr) + (12)] != 0; + } + set_rethrown(rethrown) { + rethrown = rethrown ? 1 : 0; + HEAP8[(this.ptr) + (13)] = rethrown; + } + get_rethrown() { + return HEAP8[(this.ptr) + (13)] != 0; + } + // Initialize native structure fields. Should be called once after allocated. + init(type, destructor) { + this.set_adjusted_ptr(0); + this.set_type(type); + this.set_destructor(destructor); + } + set_adjusted_ptr(adjustedPtr) { + HEAPU32[(((this.ptr) + (16)) >> 2)] = adjustedPtr; + } + get_adjusted_ptr() { + return HEAPU32[(((this.ptr) + (16)) >> 2)]; + } +} + +var uncaughtExceptionCount = 0; + +var ___cxa_throw = (ptr, type, destructor) => { + var info = new ExceptionInfo(ptr); + // Initialize ExceptionInfo content after it was allocated in __cxa_allocate_exception. + info.init(type, destructor); + uncaughtExceptionCount++; + abort(); +}; + +var PATH = { + isAbs: path => path.charAt(0) === "/", + splitPath: filename => { + var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + return splitPathRe.exec(filename).slice(1); + }, + normalizeArray: (parts, allowAboveRoot) => { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === ".") { + parts.splice(i, 1); + } else if (last === "..") { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (;up; up--) { + parts.unshift(".."); + } + } + return parts; + }, + normalize: path => { + var isAbsolute = PATH.isAbs(path), trailingSlash = path.slice(-1) === "/"; + // Normalize the path + path = PATH.normalizeArray(path.split("/").filter(p => !!p), !isAbsolute).join("/"); + if (!path && !isAbsolute) { + path = "."; + } + if (path && trailingSlash) { + path += "/"; + } + return (isAbsolute ? "/" : "") + path; + }, + dirname: path => { + var result = PATH.splitPath(path), root = result[0], dir = result[1]; + if (!root && !dir) { + // No dirname whatsoever + return "."; + } + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.slice(0, -1); + } + return root + dir; + }, + basename: path => path && path.match(/([^\/]+|\/)\/*$/)[1], + join: (...paths) => PATH.normalize(paths.join("/")), + join2: (l, r) => PATH.normalize(l + "/" + r) +}; + +var initRandomFill = () => { + // This block is not needed on v19+ since crypto.getRandomValues is builtin + if (ENVIRONMENT_IS_NODE) { + var nodeCrypto = require("node:crypto"); + return view => nodeCrypto.randomFillSync(view); + } + return view => (crypto.getRandomValues(view), 0); +}; + +var randomFill = view => (randomFill = initRandomFill())(view); + +var PATH_FS = { + resolve: (...args) => { + var resolvedPath = "", resolvedAbsolute = false; + for (var i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? args[i] : FS.cwd(); + // Skip empty and invalid entries + if (typeof path != "string") { + throw new TypeError("Arguments to path.resolve must be strings"); + } else if (!path) { + return ""; + } + resolvedPath = path + "/" + resolvedPath; + resolvedAbsolute = PATH.isAbs(path); + } + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + resolvedPath = PATH.normalizeArray(resolvedPath.split("/").filter(p => !!p), !resolvedAbsolute).join("/"); + return ((resolvedAbsolute ? "/" : "") + resolvedPath) || "."; + }, + relative: (from, to) => { + from = PATH_FS.resolve(from).slice(1); + to = PATH_FS.resolve(to).slice(1); + function trim(arr) { + var start = 0; + for (;start < arr.length; start++) { + if (arr[start] !== "") break; + } + var end = arr.length - 1; + for (;end >= 0; end--) { + if (arr[end] !== "") break; + } + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + var fromParts = trim(from.split("/")); + var toParts = trim(to.split("/")); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push(".."); + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join("/"); + } +}; + +var UTF8Decoder = new TextDecoder; + +var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on + // null terminator by itself. + // As a tiny code save trick, compare idx against maxIdx using a negation, + // so that maxBytesToRead=undefined/NaN means Infinity. + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; +}; + +/** + * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given + * array that contains uint8 values, returns a copy of that string as a + * Javascript String object. + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number=} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + return UTF8Decoder.decode(heapOrArray.buffer ? heapOrArray.subarray(idx, endPtr) : new Uint8Array(heapOrArray.slice(idx, endPtr))); +}; + +var FS_stdin_getChar_buffer = []; + +var lengthBytesUTF8 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code + // unit, not a Unicode code point of the character! So decode + // UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var c = str.charCodeAt(i); + // possibly a lead surrogate + if (c <= 127) { + len++; + } else if (c <= 2047) { + len += 2; + } else if (c >= 55296 && c <= 57343) { + len += 4; + ++i; + } else { + len += 3; + } + } + return len; +}; + +var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + // Parameter maxBytesToWrite is not optional. Negative values, 0, null, + // undefined and false each don't write out any bytes. + if (!(maxBytesToWrite > 0)) return 0; + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; + // -1 for string null terminator. + for (var i = 0; i < str.length; ++i) { + // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description + // and https://www.ietf.org/rfc/rfc2279.txt + // and https://tools.ietf.org/html/rfc3629 + var u = str.codePointAt(i); + if (u <= 127) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 2047) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 192 | (u >> 6); + heap[outIdx++] = 128 | (u & 63); + } else if (u <= 65535) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 224 | (u >> 12); + heap[outIdx++] = 128 | ((u >> 6) & 63); + heap[outIdx++] = 128 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + heap[outIdx++] = 240 | (u >> 18); + heap[outIdx++] = 128 | ((u >> 12) & 63); + heap[outIdx++] = 128 | ((u >> 6) & 63); + heap[outIdx++] = 128 | (u & 63); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + i++; + } + } + // Null-terminate the pointer to the buffer. + heap[outIdx] = 0; + return outIdx - startIdx; +}; + +/** @type {function(string, boolean=, number=)} */ var intArrayFromString = (stringy, dontAddNull, length) => { + var len = length > 0 ? length : lengthBytesUTF8(stringy) + 1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; +}; + +var FS_stdin_getChar = () => { + if (!FS_stdin_getChar_buffer.length) { + var result = null; + if (ENVIRONMENT_IS_NODE) { + // we will read data by chunks of BUFSIZE + var BUFSIZE = 256; + var buf = Buffer.alloc(BUFSIZE); + var bytesRead = 0; + // For some reason we must suppress a closure warning here, even though + // fd definitely exists on process.stdin, and is even the proper way to + // get the fd of stdin, + // https://github.com/nodejs/help/issues/2136#issuecomment-523649904 + // This started to happen after moving this logic out of library_tty.js, + // so it is related to the surrounding code in some unclear manner. + /** @suppress {missingProperties} */ var fd = process.stdin.fd; + try { + bytesRead = fs.readSync(fd, buf, 0, BUFSIZE); + } catch (e) { + // Cross-platform differences: on Windows, reading EOF throws an + // exception, but on other OSes, reading EOF returns 0. Uniformize + // behavior by treating the EOF exception to return 0. + if (e.toString().includes("EOF")) bytesRead = 0; else throw e; + } + if (bytesRead > 0) { + result = buf.slice(0, bytesRead).toString("utf-8"); + } + } else if (globalThis.window?.prompt) { + // Browser. + result = window.prompt("Input: "); + // returns null on cancel + if (result !== null) { + result += "\n"; + } + } else {} + if (!result) { + return null; + } + FS_stdin_getChar_buffer = intArrayFromString(result, true); + } + return FS_stdin_getChar_buffer.shift(); +}; + +var TTY = { + ttys: [], + init() {}, + shutdown() {}, + register(dev, ops) { + TTY.ttys[dev] = { + input: [], + output: [], + ops + }; + FS.registerDevice(dev, TTY.stream_ops); + }, + stream_ops: { + open(stream) { + var tty = TTY.ttys[stream.node.rdev]; + if (!tty) { + throw new FS.ErrnoError(43); + } + stream.tty = tty; + stream.seekable = false; + }, + close(stream) { + // flush any pending line data + stream.tty.ops.fsync(stream.tty); + }, + fsync(stream) { + stream.tty.ops.fsync(stream.tty); + }, + read(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.get_char) { + throw new FS.ErrnoError(60); + } + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = stream.tty.ops.get_char(stream.tty); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.put_char) { + throw new FS.ErrnoError(60); + } + try { + for (var i = 0; i < length; i++) { + stream.tty.ops.put_char(stream.tty, buffer[offset + i]); + } + } catch (e) { + throw new FS.ErrnoError(29); + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + } + }, + default_tty_ops: { + get_char(tty) { + return FS_stdin_getChar(); + }, + put_char(tty, val) { + if (val === null || val === 10) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } + }, + ioctl_tcgets(tty) { + // typical setting + return { + c_iflag: 25856, + c_oflag: 5, + c_cflag: 191, + c_lflag: 35387, + c_cc: [ 3, 28, 127, 21, 4, 0, 1, 0, 17, 19, 26, 0, 18, 15, 23, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] + }; + }, + ioctl_tcsets(tty, optional_actions, data) { + // currently just ignore + return 0; + }, + ioctl_tiocgwinsz(tty) { + return [ 24, 80 ]; + } + }, + default_tty1_ops: { + put_char(tty, val) { + if (val === null || val === 10) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } + } + } +}; + +var zeroMemory = (ptr, size) => HEAPU8.fill(0, ptr, ptr + size); + +var alignMemory = (size, alignment) => Math.ceil(size / alignment) * alignment; + +var mmapAlloc = size => { + size = alignMemory(size, 65536); + var ptr = _emscripten_builtin_memalign(65536, size); + if (ptr) zeroMemory(ptr, size); + return ptr; +}; + +var MEMFS = { + ops_table: null, + mount(mount) { + return MEMFS.createNode(null, "/", 16895, 0); + }, + createNode(parent, name, mode, dev) { + if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { + // not supported + throw new FS.ErrnoError(63); + } + MEMFS.ops_table ||= { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink + }, + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync + } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + }; + var node = FS.createNode(parent, name, mode, dev); + if (FS.isDir(node.mode)) { + node.node_ops = MEMFS.ops_table.dir.node; + node.stream_ops = MEMFS.ops_table.dir.stream; + node.contents = {}; + } else if (FS.isFile(node.mode)) { + node.node_ops = MEMFS.ops_table.file.node; + node.stream_ops = MEMFS.ops_table.file.stream; + // The actual number of bytes used in the typed array, as opposed to + // contents.length which gives the whole capacity. + node.usedBytes = 0; + // The byte data of the file is stored in a typed array. + // Note: typed arrays are not resizable like normal JS arrays are, so + // there is a small penalty involved for appending file writes that + // continuously grow a file similar to std::vector capacity vs used. + node.contents = MEMFS.emptyFileContents ??= new Uint8Array(0); + } else if (FS.isLink(node.mode)) { + node.node_ops = MEMFS.ops_table.link.node; + node.stream_ops = MEMFS.ops_table.link.stream; + } else if (FS.isChrdev(node.mode)) { + node.node_ops = MEMFS.ops_table.chrdev.node; + node.stream_ops = MEMFS.ops_table.chrdev.stream; + } + node.atime = node.mtime = node.ctime = Date.now(); + // add the new node to the parent + if (parent) { + parent.contents[name] = node; + parent.atime = parent.mtime = parent.ctime = node.atime; + } + return node; + }, + getFileDataAsTypedArray(node) { + return node.contents.subarray(0, node.usedBytes); + }, + expandFileStorage(node, newCapacity) { + var prevCapacity = node.contents.length; + if (prevCapacity >= newCapacity) return; + // No need to expand, the storage was already large enough. + // Don't expand strictly to the given requested limit if it's only a very + // small increase, but instead geometrically grow capacity. + // For small filesizes (<1MB), perform size*2 geometric increase, but for + // large sizes, do a much more conservative size*1.125 increase to avoid + // overshooting the allocation cap by a very large margin. + var CAPACITY_DOUBLING_MAX = 1024 * 1024; + newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2 : 1.125)) >>> 0); + if (prevCapacity) newCapacity = Math.max(newCapacity, 256); + // At minimum allocate 256b for each file when expanding. + var oldContents = MEMFS.getFileDataAsTypedArray(node); + node.contents = new Uint8Array(newCapacity); + // Allocate new storage. + node.contents.set(oldContents); + }, + resizeFileStorage(node, newSize) { + if (node.usedBytes == newSize) return; + var oldContents = node.contents; + node.contents = new Uint8Array(newSize); + // Allocate new storage. + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); + // Copy old data over to the new storage. + node.usedBytes = newSize; + }, + node_ops: { + getattr(node) { + var attr = {}; + // device numbers reuse inode numbers. + attr.dev = FS.isChrdev(node.mode) ? node.id : 1; + attr.ino = node.id; + attr.mode = node.mode; + attr.nlink = 1; + attr.uid = 0; + attr.gid = 0; + attr.rdev = node.rdev; + if (FS.isDir(node.mode)) { + attr.size = 4096; + } else if (FS.isFile(node.mode)) { + attr.size = node.usedBytes; + } else if (FS.isLink(node.mode)) { + attr.size = node.link.length; + } else { + attr.size = 0; + } + attr.atime = new Date(node.atime); + attr.mtime = new Date(node.mtime); + attr.ctime = new Date(node.ctime); + // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), + // but this is not required by the standard. + attr.blksize = 4096; + attr.blocks = Math.ceil(attr.size / attr.blksize); + return attr; + }, + setattr(node, attr) { + for (const key of [ "mode", "atime", "mtime", "ctime" ]) { + if (attr[key] != null) { + node[key] = attr[key]; + } + } + if (attr.size !== undefined) { + MEMFS.resizeFileStorage(node, attr.size); + } + }, + lookup(parent, name) { + // This error may happen quite a bit. To avoid overhead we reuse it (and + // suffer a lack of stack info). + if (!MEMFS.doesNotExistError) { + MEMFS.doesNotExistError = new FS.ErrnoError(44); + /** @suppress {checkTypes} */ MEMFS.doesNotExistError.stack = ""; + } + throw MEMFS.doesNotExistError; + }, + mknod(parent, name, mode, dev) { + return MEMFS.createNode(parent, name, mode, dev); + }, + rename(old_node, new_dir, new_name) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + if (new_node) { + if (FS.isDir(old_node.mode)) { + // if we're overwriting a directory at new_name, make sure it's empty. + for (var i in new_node.contents) { + throw new FS.ErrnoError(55); + } + } + FS.hashRemoveNode(new_node); + } + // do the internal rewiring + delete old_node.parent.contents[old_node.name]; + new_dir.contents[new_name] = old_node; + old_node.name = new_name; + new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now(); + }, + unlink(parent, name) { + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + rmdir(parent, name) { + var node = FS.lookupNode(parent, name); + for (var i in node.contents) { + throw new FS.ErrnoError(55); + } + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + readdir(node) { + return [ ".", "..", ...Object.keys(node.contents) ]; + }, + symlink(parent, newname, oldpath) { + var node = MEMFS.createNode(parent, newname, 511 | 40960, 0); + node.link = oldpath; + return node; + }, + readlink(node) { + if (!FS.isLink(node.mode)) { + throw new FS.ErrnoError(28); + } + return node.link; + } + }, + stream_ops: { + read(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= stream.node.usedBytes) return 0; + var size = Math.min(stream.node.usedBytes - position, length); + buffer.set(contents.subarray(position, position + size), offset); + return size; + }, + write(stream, buffer, offset, length, position, canOwn) { + // If the buffer is located in main memory (HEAP), and if + // memory can grow, we can't hold on to references of the + // memory buffer, as they may get invalidated. That means we + // need to copy its contents. + if (buffer.buffer === HEAP8.buffer) { + canOwn = false; + } + if (!length) return 0; + var node = stream.node; + node.mtime = node.ctime = Date.now(); + if (canOwn) { + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + } else if (node.usedBytes === 0 && position === 0) { + // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. + node.contents = buffer.slice(offset, offset + length); + node.usedBytes = length; + } else { + MEMFS.expandFileStorage(node, position + length); + // Use typed array write which is available. + node.contents.set(buffer.subarray(offset, offset + length), position); + node.usedBytes = Math.max(node.usedBytes, position + length); + } + return length; + }, + llseek(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position; + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + position += stream.node.usedBytes; + } + } + if (position < 0) { + throw new FS.ErrnoError(28); + } + return position; + }, + mmap(stream, length, position, prot, flags) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + var ptr; + var allocated; + var contents = stream.node.contents; + // Only make a new copy when MAP_PRIVATE is specified. + if (!(flags & 2) && contents.buffer === HEAP8.buffer) { + // We can't emulate MAP_SHARED when the file is not backed by the + // buffer we're mapping to (e.g. the HEAP buffer). + allocated = false; + ptr = contents.byteOffset; + } else { + allocated = true; + ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + if (contents) { + // Try to avoid unnecessary slices. + if (position > 0 || position + length < contents.length) { + if (contents.subarray) { + contents = contents.subarray(position, position + length); + } else { + contents = Array.prototype.slice.call(contents, position, position + length); + } + } + HEAP8.set(contents, ptr); + } + } + return { + ptr, + allocated + }; + }, + msync(stream, buffer, offset, length, mmapFlags) { + MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); + // should we check if bytesWritten and length are the same? + return 0; + } + } +}; + +var FS_modeStringToFlags = str => { + if (typeof str != "string") return str; + var flagModes = { + "r": 0, + "r+": 2, + "w": 512 | 64 | 1, + "w+": 512 | 64 | 2, + "a": 1024 | 64 | 1, + "a+": 1024 | 64 | 2 + }; + var flags = flagModes[str]; + if (typeof flags == "undefined") { + throw new Error(`Unknown file open mode: ${str}`); + } + return flags; +}; + +var FS_fileDataToTypedArray = data => { + if (typeof data == "string") { + data = intArrayFromString(data, true); + } + if (!data.subarray) { + data = new Uint8Array(data); + } + return data; +}; + +var FS_getMode = (canRead, canWrite) => { + var mode = 0; + if (canRead) mode |= 292 | 73; + if (canWrite) mode |= 146; + return mode; +}; + +var asyncLoad = async url => { + var arrayBuffer = await readAsync(url); + return new Uint8Array(arrayBuffer); +}; + +var FS_createDataFile = (...args) => FS.createDataFile(...args); + +var getUniqueRunDependency = id => id; + +var runDependencies = 0; + +var dependenciesFulfilled = null; + +var removeRunDependency = id => { + runDependencies--; + Module["monitorRunDependencies"]?.(runDependencies); + if (runDependencies == 0) { + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); + } + } +}; + +var addRunDependency = id => { + runDependencies++; + Module["monitorRunDependencies"]?.(runDependencies); +}; + +var FS_handledByPreloadPlugin = async (byteArray, fullname) => { + // Ensure plugins are ready. + if (typeof Browser != "undefined") Browser.init(); + for (var plugin of preloadPlugins) { + if (plugin["canHandle"](fullname)) { + return plugin["handle"](byteArray, fullname); + } + } + // If no plugin handled this file then return the original/unmodified + // byteArray. + return byteArray; +}; + +var FS_preloadFile = async (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => { + // TODO we should allow people to just pass in a complete filename instead + // of parent and name being that we just join them anyways + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency(`cp ${fullname}`); + // might have several active requests for the same fullname + addRunDependency(dep); + try { + var byteArray = url; + if (typeof url == "string") { + byteArray = await asyncLoad(url); + } + byteArray = await FS_handledByPreloadPlugin(byteArray, fullname); + preFinish?.(); + if (!dontCreateFile) { + FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); + } + } finally { + removeRunDependency(dep); + } +}; + +var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { + FS_preloadFile(parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish).then(onload).catch(onerror); +}; + +var FS = { + root: null, + mounts: [], + devices: {}, + streams: [], + nextInode: 1, + nameTable: null, + currentPath: "/", + initialized: false, + ignorePermissions: true, + filesystems: null, + syncFSRequests: 0, + ErrnoError: class { + name="ErrnoError"; + // We set the `name` property to be able to identify `FS.ErrnoError` + // - the `name` is a standard ECMA-262 property of error objects. Kind of good to have it anyway. + // - when using PROXYFS, an error can come from an underlying FS + // as different FS objects have their own FS.ErrnoError each, + // the test `err instanceof FS.ErrnoError` won't detect an error coming from another filesystem, causing bugs. + // we'll use the reliable test `err.name == "ErrnoError"` instead + constructor(errno) { + this.errno = errno; + } + }, + FSStream: class { + shared={}; + get object() { + return this.node; + } + set object(val) { + this.node = val; + } + get isRead() { + return (this.flags & 2097155) !== 1; + } + get isWrite() { + return (this.flags & 2097155) !== 0; + } + get isAppend() { + return (this.flags & 1024); + } + get flags() { + return this.shared.flags; + } + set flags(val) { + this.shared.flags = val; + } + get position() { + return this.shared.position; + } + set position(val) { + this.shared.position = val; + } + }, + FSNode: class { + node_ops={}; + stream_ops={}; + readMode=292 | 73; + writeMode=146; + mounted=null; + constructor(parent, name, mode, rdev) { + if (!parent) { + parent = this; + } + this.parent = parent; + this.mount = parent.mount; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.rdev = rdev; + this.atime = this.mtime = this.ctime = Date.now(); + } + get read() { + return (this.mode & this.readMode) === this.readMode; + } + set read(val) { + val ? this.mode |= this.readMode : this.mode &= ~this.readMode; + } + get write() { + return (this.mode & this.writeMode) === this.writeMode; + } + set write(val) { + val ? this.mode |= this.writeMode : this.mode &= ~this.writeMode; + } + get isFolder() { + return FS.isDir(this.mode); + } + get isDevice() { + return FS.isChrdev(this.mode); + } + }, + lookupPath(path, opts = {}) { + if (!path) { + throw new FS.ErrnoError(44); + } + opts.follow_mount ??= true; + if (!PATH.isAbs(path)) { + path = FS.cwd() + "/" + path; + } + // limit max consecutive symlinks to 40 (SYMLOOP_MAX). + linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { + // split the absolute path + var parts = path.split("/").filter(p => !!p); + // start at the root + var current = FS.root; + var current_path = "/"; + for (var i = 0; i < parts.length; i++) { + var islast = (i === parts.length - 1); + if (islast && opts.parent) { + // stop resolving + break; + } + if (parts[i] === ".") { + continue; + } + if (parts[i] === "..") { + current_path = PATH.dirname(current_path); + if (FS.isRoot(current)) { + path = current_path + "/" + parts.slice(i + 1).join("/"); + // We're making progress here, don't let many consecutive ..'s + // lead to ELOOP + nlinks--; + continue linkloop; + } else { + current = current.parent; + } + continue; + } + current_path = PATH.join2(current_path, parts[i]); + try { + current = FS.lookupNode(current, parts[i]); + } catch (e) { + // if noent_okay is true, suppress a ENOENT in the last component + // and return an object with an undefined node. This is needed for + // resolving symlinks in the path when creating a file. + if ((e?.errno === 44) && islast && opts.noent_okay) { + return { + path: current_path + }; + } + throw e; + } + // jump to the mount's root node if this is a mountpoint + if (FS.isMountpoint(current) && (!islast || opts.follow_mount)) { + current = current.mounted.root; + } + // by default, lookupPath will not follow a symlink if it is the final path component. + // setting opts.follow = true will override this behavior. + if (FS.isLink(current.mode) && (!islast || opts.follow)) { + if (!current.node_ops.readlink) { + throw new FS.ErrnoError(52); + } + var link = current.node_ops.readlink(current); + if (!PATH.isAbs(link)) { + link = PATH.dirname(current_path) + "/" + link; + } + path = link + "/" + parts.slice(i + 1).join("/"); + continue linkloop; + } + } + return { + path: current_path, + node: current + }; + } + throw new FS.ErrnoError(32); + }, + getPath(node) { + var path; + while (true) { + if (FS.isRoot(node)) { + var mount = node.mount.mountpoint; + if (!path) return mount; + return mount[mount.length - 1] !== "/" ? `${mount}/${path}` : mount + path; + } + path = path ? `${node.name}/${path}` : node.name; + node = node.parent; + } + }, + hashName(parentid, name) { + var hash = 0; + for (var i = 0; i < name.length; i++) { + hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; + } + return ((parentid + hash) >>> 0) % FS.nameTable.length; + }, + hashAddNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + node.name_next = FS.nameTable[hash]; + FS.nameTable[hash] = node; + }, + hashRemoveNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + if (FS.nameTable[hash] === node) { + FS.nameTable[hash] = node.name_next; + } else { + var current = FS.nameTable[hash]; + while (current) { + if (current.name_next === node) { + current.name_next = node.name_next; + break; + } + current = current.name_next; + } + } + }, + lookupNode(parent, name) { + var errCode = FS.mayLookup(parent); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + var hash = FS.hashName(parent.id, name); + for (var node = FS.nameTable[hash]; node; node = node.name_next) { + var nodeName = node.name; + if (node.parent.id === parent.id && nodeName === name) { + return node; + } + } + // if we failed to find it in the cache, call into the VFS + return FS.lookup(parent, name); + }, + createNode(parent, name, mode, rdev) { + var node = new FS.FSNode(parent, name, mode, rdev); + FS.hashAddNode(node); + return node; + }, + destroyNode(node) { + FS.hashRemoveNode(node); + }, + isRoot(node) { + return node === node.parent; + }, + isMountpoint(node) { + return !!node.mounted; + }, + isFile(mode) { + return (mode & 61440) === 32768; + }, + isDir(mode) { + return (mode & 61440) === 16384; + }, + isLink(mode) { + return (mode & 61440) === 40960; + }, + isChrdev(mode) { + return (mode & 61440) === 8192; + }, + isBlkdev(mode) { + return (mode & 61440) === 24576; + }, + isFIFO(mode) { + return (mode & 61440) === 4096; + }, + isSocket(mode) { + return (mode & 49152) === 49152; + }, + flagsToPermissionString(flag) { + var perms = [ "r", "w", "rw" ][flag & 3]; + if ((flag & 512)) { + perms += "w"; + } + return perms; + }, + nodePermissions(node, perms) { + if (FS.ignorePermissions) { + return 0; + } + // return 0 if any user, group or owner bits are set. + if (perms.includes("r") && !(node.mode & 292)) { + return 2; + } + if (perms.includes("w") && !(node.mode & 146)) { + return 2; + } + if (perms.includes("x") && !(node.mode & 73)) { + return 2; + } + return 0; + }, + mayLookup(dir) { + if (!FS.isDir(dir.mode)) return 54; + var errCode = FS.nodePermissions(dir, "x"); + if (errCode) return errCode; + if (!dir.node_ops.lookup) return 2; + return 0; + }, + mayCreate(dir, name) { + if (!FS.isDir(dir.mode)) { + return 54; + } + try { + var node = FS.lookupNode(dir, name); + return 20; + } catch (e) {} + return FS.nodePermissions(dir, "wx"); + }, + mayDelete(dir, name, isdir) { + var node; + try { + node = FS.lookupNode(dir, name); + } catch (e) { + return e.errno; + } + var errCode = FS.nodePermissions(dir, "wx"); + if (errCode) { + return errCode; + } + if (isdir) { + if (!FS.isDir(node.mode)) { + return 54; + } + if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { + return 10; + } + } else if (FS.isDir(node.mode)) { + return 31; + } + return 0; + }, + mayOpen(node, flags) { + if (!node) { + return 44; + } + if (FS.isLink(node.mode)) { + return 32; + } + var mode = FS.flagsToPermissionString(flags); + if (FS.isDir(node.mode)) { + // opening for write + // TODO: check for O_SEARCH? (== search for dir only) + if (mode !== "r" || (flags & (512 | 64))) { + return 31; + } + } + return FS.nodePermissions(node, mode); + }, + checkOpExists(op, err) { + if (!op) { + throw new FS.ErrnoError(err); + } + return op; + }, + MAX_OPEN_FDS: 4096, + nextfd() { + for (var fd = 0; fd <= FS.MAX_OPEN_FDS; fd++) { + if (!FS.streams[fd]) { + return fd; + } + } + throw new FS.ErrnoError(33); + }, + getStreamChecked(fd) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); + } + return stream; + }, + getStream: fd => FS.streams[fd], + createStream(stream, fd = -1) { + // clone it, so we can return an instance of FSStream + stream = Object.assign(new FS.FSStream, stream); + if (fd == -1) { + fd = FS.nextfd(); + } + stream.fd = fd; + FS.streams[fd] = stream; + return stream; + }, + closeStream(fd) { + FS.streams[fd] = null; + }, + dupStream(origStream, fd = -1) { + var stream = FS.createStream(origStream, fd); + stream.stream_ops?.dup?.(stream); + return stream; + }, + doSetAttr(stream, node, attr) { + var setattr = stream?.stream_ops.setattr; + var arg = setattr ? stream : node; + setattr ??= node.node_ops.setattr; + FS.checkOpExists(setattr, 63); + setattr(arg, attr); + }, + chrdev_stream_ops: { + open(stream) { + var device = FS.getDevice(stream.node.rdev); + // override node's stream ops with the device's + stream.stream_ops = device.stream_ops; + // forward the open call + stream.stream_ops.open?.(stream); + }, + llseek() { + throw new FS.ErrnoError(70); + } + }, + major: dev => ((dev) >> 8), + minor: dev => ((dev) & 255), + makedev: (ma, mi) => ((ma) << 8 | (mi)), + registerDevice(dev, ops) { + FS.devices[dev] = { + stream_ops: ops + }; + }, + getDevice: dev => FS.devices[dev], + getMounts(mount) { + var mounts = []; + var check = [ mount ]; + while (check.length) { + var m = check.pop(); + mounts.push(m); + check.push(...m.mounts); + } + return mounts; + }, + syncfs(populate, callback) { + if (typeof populate == "function") { + callback = populate; + populate = false; + } + FS.syncFSRequests++; + if (FS.syncFSRequests > 1) { + err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`); + } + var mounts = FS.getMounts(FS.root.mount); + var completed = 0; + function doCallback(errCode) { + FS.syncFSRequests--; + return callback(errCode); + } + function done(errCode) { + if (errCode) { + if (!done.errored) { + done.errored = true; + return doCallback(errCode); + } + return; + } + if (++completed >= mounts.length) { + doCallback(null); + } + } + // sync all mounts + for (var mount of mounts) { + if (mount.type.syncfs) { + mount.type.syncfs(mount, populate, done); + } else { + done(null); + } + } + }, + mount(type, opts, mountpoint) { + var root = mountpoint === "/"; + var pseudo = !mountpoint; + var node; + if (root && FS.root) { + throw new FS.ErrnoError(10); + } else if (!root && !pseudo) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + mountpoint = lookup.path; + // use the absolute path + node = lookup.node; + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + if (!FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + } + var mount = { + type, + opts, + mountpoint, + mounts: [] + }; + // create a root node for the fs + var mountRoot = type.mount(mount); + mountRoot.mount = mount; + mount.root = mountRoot; + if (root) { + FS.root = mountRoot; + } else if (node) { + // set as a mountpoint + node.mounted = mount; + // add the new mount to the current mount's children + if (node.mount) { + node.mount.mounts.push(mount); + } + } + return mountRoot; + }, + unmount(mountpoint) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + if (!FS.isMountpoint(lookup.node)) { + throw new FS.ErrnoError(28); + } + // destroy the nodes for this mount, and all its child mounts + var node = lookup.node; + var mount = node.mounted; + var mounts = FS.getMounts(mount); + for (var [hash, current] of Object.entries(FS.nameTable)) { + while (current) { + var next = current.name_next; + if (mounts.includes(current.mount)) { + FS.destroyNode(current); + } + current = next; + } + } + // no longer a mountpoint + node.mounted = null; + // remove this mount from the child mounts + var idx = node.mount.mounts.indexOf(mount); + node.mount.mounts.splice(idx, 1); + }, + lookup(parent, name) { + return parent.node_ops.lookup(parent, name); + }, + mknod(path, mode, dev) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + if (!name) { + throw new FS.ErrnoError(28); + } + if (name === "." || name === "..") { + throw new FS.ErrnoError(20); + } + var errCode = FS.mayCreate(parent, name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.mknod) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.mknod(parent, name, mode, dev); + }, + statfs(path) { + return FS.statfsNode(FS.lookupPath(path, { + follow: true + }).node); + }, + statfsStream(stream) { + // We keep a separate statfsStream function because noderawfs overrides + // it. In noderawfs, stream.node is sometimes null. Instead, we need to + // look at stream.path. + return FS.statfsNode(stream.node); + }, + statfsNode(node) { + // NOTE: None of the defaults here are true. We're just returning safe and + // sane values. Currently nodefs and rawfs replace these defaults, + // other file systems leave them alone. + var rtn = { + bsize: 4096, + frsize: 4096, + blocks: 1e6, + bfree: 5e5, + bavail: 5e5, + files: FS.nextInode, + ffree: FS.nextInode - 1, + fsid: 42, + flags: 2, + namelen: 255 + }; + if (node.node_ops.statfs) { + Object.assign(rtn, node.node_ops.statfs(node.mount.opts.root)); + } + return rtn; + }, + create(path, mode = 438) { + mode &= 4095; + mode |= 32768; + return FS.mknod(path, mode, 0); + }, + mkdir(path, mode = 511) { + mode &= 511 | 512; + mode |= 16384; + return FS.mknod(path, mode, 0); + }, + mkdirTree(path, mode) { + var dirs = path.split("/"); + var d = ""; + for (var dir of dirs) { + if (!dir) continue; + if (d || PATH.isAbs(path)) d += "/"; + d += dir; + try { + FS.mkdir(d, mode); + } catch (e) { + if (e.errno != 20) throw e; + } + } + }, + mkdev(path, mode, dev) { + if (typeof dev == "undefined") { + dev = mode; + mode = 438; + } + mode |= 8192; + return FS.mknod(path, mode, dev); + }, + symlink(oldpath, newpath) { + if (!PATH_FS.resolve(oldpath)) { + throw new FS.ErrnoError(44); + } + var lookup = FS.lookupPath(newpath, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.symlink) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.symlink(parent, newname, oldpath); + }, + rename(old_path, new_path) { + var old_dirname = PATH.dirname(old_path); + var new_dirname = PATH.dirname(new_path); + var old_name = PATH.basename(old_path); + var new_name = PATH.basename(new_path); + // parents must exist + var lookup, old_dir, new_dir; + // let the errors from non existent directories percolate up + lookup = FS.lookupPath(old_path, { + parent: true + }); + old_dir = lookup.node; + lookup = FS.lookupPath(new_path, { + parent: true + }); + new_dir = lookup.node; + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); + // need to be part of the same mount + if (old_dir.mount !== new_dir.mount) { + throw new FS.ErrnoError(75); + } + // source must exist + var old_node = FS.lookupNode(old_dir, old_name); + // old path should not be an ancestor of the new path + var relative = PATH_FS.relative(old_path, new_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(28); + } + // new path should not be an ancestor of the old path + relative = PATH_FS.relative(new_path, old_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(55); + } + // see if the new path already exists + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + // early out if nothing needs to change + if (old_node === new_node) { + return; + } + // we'll need to delete the old entry + var isdir = FS.isDir(old_node.mode); + var errCode = FS.mayDelete(old_dir, old_name, isdir); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + // need delete permissions if we'll be overwriting. + // need create permissions if new doesn't already exist. + errCode = new_node ? FS.mayDelete(new_dir, new_name, isdir) : FS.mayCreate(new_dir, new_name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!old_dir.node_ops.rename) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(old_node) || (new_node && FS.isMountpoint(new_node))) { + throw new FS.ErrnoError(10); + } + // if we are going to change the parent, check write permissions + if (new_dir !== old_dir) { + errCode = FS.nodePermissions(old_dir, "w"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + // remove the node from the lookup hash + FS.hashRemoveNode(old_node); + // do the underlying fs rename + try { + old_dir.node_ops.rename(old_node, new_dir, new_name); + // update old node (we do this here to avoid each backend + // needing to) + old_node.parent = new_dir; + } catch (e) { + throw e; + } finally { + // add the node back to the hash (in case node_ops.rename + // changed its name) + FS.hashAddNode(old_node); + } + }, + rmdir(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, true); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.rmdir) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.rmdir(parent, name); + FS.destroyNode(node); + }, + readdir(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + var readdir = FS.checkOpExists(node.node_ops.readdir, 54); + return readdir(node); + }, + unlink(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, false); + if (errCode) { + // According to POSIX, we should map EISDIR to EPERM, but + // we instead do what Linux does (and we must, as we use + // the musl linux libc). + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.unlink) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.unlink(parent, name); + FS.destroyNode(node); + }, + readlink(path) { + var lookup = FS.lookupPath(path); + var link = lookup.node; + if (!link) { + throw new FS.ErrnoError(44); + } + if (!link.node_ops.readlink) { + throw new FS.ErrnoError(28); + } + return link.node_ops.readlink(link); + }, + stat(path, dontFollow) { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + var node = lookup.node; + var getattr = FS.checkOpExists(node.node_ops.getattr, 63); + return getattr(node); + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + var node = stream.node; + var getattr = stream.stream_ops.getattr; + var arg = getattr ? stream : node; + getattr ??= node.node_ops.getattr; + FS.checkOpExists(getattr, 63); + return getattr(arg); + }, + lstat(path) { + return FS.stat(path, true); + }, + doChmod(stream, node, mode, dontFollow) { + FS.doSetAttr(stream, node, { + mode: (mode & 4095) | (node.mode & ~4095), + ctime: Date.now(), + dontFollow + }); + }, + chmod(path, mode, dontFollow) { + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node; + } else { + node = path; + } + FS.doChmod(null, node, mode, dontFollow); + }, + lchmod(path, mode) { + FS.chmod(path, mode, true); + }, + fchmod(fd, mode) { + var stream = FS.getStreamChecked(fd); + FS.doChmod(stream, stream.node, mode, false); + }, + doChown(stream, node, dontFollow) { + FS.doSetAttr(stream, node, { + timestamp: Date.now(), + dontFollow + }); + }, + chown(path, uid, gid, dontFollow) { + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node; + } else { + node = path; + } + FS.doChown(null, node, dontFollow); + }, + lchown(path, uid, gid) { + FS.chown(path, uid, gid, true); + }, + fchown(fd, uid, gid) { + var stream = FS.getStreamChecked(fd); + FS.doChown(stream, stream.node, false); + }, + doTruncate(stream, node, len) { + if (FS.isDir(node.mode)) { + throw new FS.ErrnoError(31); + } + if (!FS.isFile(node.mode)) { + throw new FS.ErrnoError(28); + } + var errCode = FS.nodePermissions(node, "w"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.doSetAttr(stream, node, { + size: len, + timestamp: Date.now() + }); + }, + truncate(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28); + } + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: true + }); + node = lookup.node; + } else { + node = path; + } + FS.doTruncate(null, node, len); + }, + ftruncate(fd, len) { + var stream = FS.getStreamChecked(fd); + if (len < 0 || (stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(28); + } + FS.doTruncate(stream, stream.node, len); + }, + utime(path, atime, mtime) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + var setattr = FS.checkOpExists(node.node_ops.setattr, 63); + setattr(node, { + atime, + mtime + }); + }, + open(path, flags, mode = 438) { + if (path === "") { + throw new FS.ErrnoError(44); + } + flags = FS_modeStringToFlags(flags); + if ((flags & 64)) { + mode = (mode & 4095) | 32768; + } else { + mode = 0; + } + var node; + var isDirPath; + if (typeof path == "object") { + node = path; + } else { + isDirPath = path.endsWith("/"); + // noent_okay makes it so that if the final component of the path + // doesn't exist, lookupPath returns `node: undefined`. `path` will be + // updated to point to the target of all symlinks. + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072), + noent_okay: true + }); + node = lookup.node; + path = lookup.path; + } + // perhaps we need to create the node + var created = false; + if ((flags & 64)) { + if (node) { + // if O_CREAT and O_EXCL are set, error out if the node already exists + if ((flags & 128)) { + throw new FS.ErrnoError(20); + } + } else if (isDirPath) { + throw new FS.ErrnoError(31); + } else { + // node doesn't exist, try to create it + // Ignore the permission bits here to ensure we can `open` this new + // file below. We use chmod below to apply the permissions once the + // file is open. + node = FS.mknod(path, mode | 511, 0); + created = true; + } + } + if (!node) { + throw new FS.ErrnoError(44); + } + // can't truncate a device + if (FS.isChrdev(node.mode)) { + flags &= ~512; + } + // if asked only for a directory, then this must be one + if ((flags & 65536) && !FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + // check permissions, if this is not a file we just created now (it is ok to + // create and write to a file with read-only permissions; it is read-only + // for later use) + if (!created) { + var errCode = FS.mayOpen(node, flags); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + // do truncation if necessary + if ((flags & 512) && !created) { + FS.truncate(node, 0); + } + // we've already handled these, don't pass down to the underlying vfs + flags &= ~(128 | 512 | 131072); + // register the stream with the filesystem + var stream = FS.createStream({ + node, + path: FS.getPath(node), + // we want the absolute path to the node + flags, + seekable: true, + position: 0, + stream_ops: node.stream_ops, + // used by the file family libc calls (fopen, fwrite, ferror, etc.) + ungotten: [], + error: false + }); + // call the new stream's open function + if (stream.stream_ops.open) { + stream.stream_ops.open(stream); + } + if (created) { + FS.chmod(node, mode & 511); + } + return stream; + }, + close(stream) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (stream.getdents) stream.getdents = null; + // free readdir state + try { + if (stream.stream_ops.close) { + stream.stream_ops.close(stream); + } + } catch (e) { + throw e; + } finally { + FS.closeStream(stream.fd); + } + stream.fd = null; + }, + isClosed(stream) { + return stream.fd === null; + }, + llseek(stream, offset, whence) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (!stream.seekable || !stream.stream_ops.llseek) { + throw new FS.ErrnoError(70); + } + if (whence != 0 && whence != 1 && whence != 2) { + throw new FS.ErrnoError(28); + } + stream.position = stream.stream_ops.llseek(stream, offset, whence); + stream.ungotten = []; + return stream.position; + }, + read(stream, buffer, offset, length, position) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.read) { + throw new FS.ErrnoError(28); + } + var seeking = typeof position != "undefined"; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); + if (!seeking) stream.position += bytesRead; + return bytesRead; + }, + write(stream, buffer, offset, length, position, canOwn) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.write) { + throw new FS.ErrnoError(28); + } + if (stream.seekable && stream.flags & 1024) { + // seek to the end before writing in append mode + FS.llseek(stream, 0, 2); + } + var seeking = typeof position != "undefined"; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); + if (!seeking) stream.position += bytesWritten; + return bytesWritten; + }, + mmap(stream, length, position, prot, flags) { + // User requests writing to file (prot & PROT_WRITE != 0). + // Checking if we have permissions to write to the file unless + // MAP_PRIVATE flag is set. According to POSIX spec it is possible + // to write to file opened in read-only mode with MAP_PRIVATE flag, + // as all modifications will be visible only in the memory of + // the current process. + if ((prot & 2) !== 0 && (flags & 2) === 0 && (stream.flags & 2097155) !== 2) { + throw new FS.ErrnoError(2); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(2); + } + if (!stream.stream_ops.mmap) { + throw new FS.ErrnoError(43); + } + if (!length) { + throw new FS.ErrnoError(28); + } + return stream.stream_ops.mmap(stream, length, position, prot, flags); + }, + msync(stream, buffer, offset, length, mmapFlags) { + if (!stream.stream_ops.msync) { + return 0; + } + return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); + }, + ioctl(stream, cmd, arg) { + if (!stream.stream_ops.ioctl) { + throw new FS.ErrnoError(59); + } + return stream.stream_ops.ioctl(stream, cmd, arg); + }, + readFile(path, opts = {}) { + opts.flags = opts.flags || 0; + opts.encoding = opts.encoding || "binary"; + if (opts.encoding !== "utf8" && opts.encoding !== "binary") { + abort(`Invalid encoding type "${opts.encoding}"`); + } + var stream = FS.open(path, opts.flags); + var stat = FS.stat(path); + var length = stat.size; + var buf = new Uint8Array(length); + FS.read(stream, buf, 0, length, 0); + if (opts.encoding === "utf8") { + buf = UTF8ArrayToString(buf); + } + FS.close(stream); + return buf; + }, + writeFile(path, data, opts = {}) { + opts.flags = opts.flags || 577; + var stream = FS.open(path, opts.flags, opts.mode); + data = FS_fileDataToTypedArray(data); + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); + FS.close(stream); + }, + cwd: () => FS.currentPath, + chdir(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + if (lookup.node === null) { + throw new FS.ErrnoError(44); + } + if (!FS.isDir(lookup.node.mode)) { + throw new FS.ErrnoError(54); + } + var errCode = FS.nodePermissions(lookup.node, "x"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.currentPath = lookup.path; + }, + createDefaultDirectories() { + FS.mkdir("/tmp"); + FS.mkdir("/home"); + FS.mkdir("/home/web_user"); + }, + createDefaultDevices() { + // create /dev + FS.mkdir("/dev"); + // setup /dev/null + FS.registerDevice(FS.makedev(1, 3), { + read: () => 0, + write: (stream, buffer, offset, length, pos) => length, + llseek: () => 0 + }); + FS.mkdev("/dev/null", FS.makedev(1, 3)); + // setup /dev/tty and /dev/tty1 + // stderr needs to print output using err() rather than out() + // so we register a second tty just for it. + TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); + TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); + FS.mkdev("/dev/tty", FS.makedev(5, 0)); + FS.mkdev("/dev/tty1", FS.makedev(6, 0)); + // setup /dev/[u]random + // use a buffer to avoid overhead of individual crypto calls per byte + var randomBuffer = new Uint8Array(1024), randomLeft = 0; + var randomByte = () => { + if (randomLeft === 0) { + randomFill(randomBuffer); + randomLeft = randomBuffer.byteLength; + } + return randomBuffer[--randomLeft]; + }; + FS.createDevice("/dev", "random", randomByte); + FS.createDevice("/dev", "urandom", randomByte); + // we're not going to emulate the actual shm device, + // just create the tmp dirs that reside in it commonly + FS.mkdir("/dev/shm"); + FS.mkdir("/dev/shm/tmp"); + }, + createSpecialDirectories() { + // create /proc/self/fd which allows /proc/self/fd/6 => readlink gives the + // name of the stream for fd 6 (see test_unistd_ttyname) + FS.mkdir("/proc"); + var proc_self = FS.mkdir("/proc/self"); + FS.mkdir("/proc/self/fd"); + FS.mount({ + mount() { + var node = FS.createNode(proc_self, "fd", 16895, 73); + node.stream_ops = { + llseek: MEMFS.stream_ops.llseek + }; + node.node_ops = { + lookup(parent, name) { + var fd = +name; + var stream = FS.getStreamChecked(fd); + var ret = { + parent: null, + mount: { + mountpoint: "fake" + }, + node_ops: { + readlink: () => stream.path + }, + id: fd + 1 + }; + ret.parent = ret; + // make it look like a simple root node + return ret; + }, + readdir() { + return Array.from(FS.streams.entries()).filter(([k, v]) => v).map(([k, v]) => k.toString()); + } + }; + return node; + } + }, {}, "/proc/self/fd"); + }, + createStandardStreams(input, output, error) { + // TODO deprecate the old functionality of a single + // input / output callback and that utilizes FS.createDevice + // and instead require a unique set of stream ops + // by default, we symlink the standard streams to the + // default tty devices. however, if the standard streams + // have been overwritten we create a unique device for + // them instead. + if (input) { + FS.createDevice("/dev", "stdin", input); + } else { + FS.symlink("/dev/tty", "/dev/stdin"); + } + if (output) { + FS.createDevice("/dev", "stdout", null, output); + } else { + FS.symlink("/dev/tty", "/dev/stdout"); + } + if (error) { + FS.createDevice("/dev", "stderr", null, error); + } else { + FS.symlink("/dev/tty1", "/dev/stderr"); + } + // open default streams for the stdin, stdout and stderr devices + var stdin = FS.open("/dev/stdin", 0); + var stdout = FS.open("/dev/stdout", 1); + var stderr = FS.open("/dev/stderr", 1); + }, + staticInit() { + FS.nameTable = new Array(4096); + FS.mount(MEMFS, {}, "/"); + FS.createDefaultDirectories(); + FS.createDefaultDevices(); + FS.createSpecialDirectories(); + FS.filesystems = { + "MEMFS": MEMFS + }; + }, + init(input, output, error) { + FS.initialized = true; + // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here + input ??= Module["stdin"]; + output ??= Module["stdout"]; + error ??= Module["stderr"]; + FS.createStandardStreams(input, output, error); + }, + quit() { + FS.initialized = false; + // force-flush all streams, so we get musl std streams printed out + // close all of our streams + for (var stream of FS.streams) { + if (stream) { + FS.close(stream); + } + } + }, + findObject(path, dontResolveLastLink) { + var ret = FS.analyzePath(path, dontResolveLastLink); + if (!ret.exists) { + return null; + } + return ret.object; + }, + analyzePath(path, dontResolveLastLink) { + // operate from within the context of the symlink's target + try { + var lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + path = lookup.path; + } catch (e) {} + var ret = { + isRoot: false, + exists: false, + error: 0, + name: null, + path: null, + object: null, + parentExists: false, + parentPath: null, + parentObject: null + }; + try { + var lookup = FS.lookupPath(path, { + parent: true + }); + ret.parentExists = true; + ret.parentPath = lookup.path; + ret.parentObject = lookup.node; + ret.name = PATH.basename(path); + lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + ret.exists = true; + ret.path = lookup.path; + ret.object = lookup.node; + ret.name = lookup.node.name; + ret.isRoot = lookup.path === "/"; + } catch (e) { + ret.error = e.errno; + } + return ret; + }, + createPath(parent, path, canRead, canWrite) { + parent = typeof parent == "string" ? parent : FS.getPath(parent); + var parts = path.split("/").reverse(); + while (parts.length) { + var part = parts.pop(); + if (!part) continue; + var current = PATH.join2(parent, part); + try { + FS.mkdir(current); + } catch (e) { + if (e.errno != 20) throw e; + } + parent = current; + } + return current; + }, + createFile(parent, name, properties, canRead, canWrite) { + var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name); + var mode = FS_getMode(canRead, canWrite); + return FS.create(path, mode); + }, + createDataFile(parent, name, data, canRead, canWrite, canOwn) { + var path = name; + if (parent) { + parent = typeof parent == "string" ? parent : FS.getPath(parent); + path = name ? PATH.join2(parent, name) : parent; + } + var mode = FS_getMode(canRead, canWrite); + var node = FS.create(path, mode); + if (data) { + data = FS_fileDataToTypedArray(data); + // make sure we can write to the file + FS.chmod(node, mode | 146); + var stream = FS.open(node, 577); + FS.write(stream, data, 0, data.length, 0, canOwn); + FS.close(stream); + FS.chmod(node, mode); + } + }, + createDevice(parent, name, input, output) { + var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name); + var mode = FS_getMode(!!input, !!output); + FS.createDevice.major ??= 64; + var dev = FS.makedev(FS.createDevice.major++, 0); + // Create a fake device that a set of stream ops to emulate + // the old behavior. + FS.registerDevice(dev, { + open(stream) { + stream.seekable = false; + }, + close(stream) { + // flush any pending line data + if (output?.buffer?.length) { + output(10); + } + }, + read(stream, buffer, offset, length, pos) { + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = input(); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + for (var i = 0; i < length; i++) { + try { + output(buffer[offset + i]); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + } + }); + return FS.mkdev(path, mode, dev); + }, + forceLoadFile(obj) { + if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; + if (globalThis.XMLHttpRequest) { + abort("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + } else { + // Command-line. + try { + obj.contents = readBinary(obj.url); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + }, + createLazyFile(parent, name, url, canRead, canWrite) { + // Lazy chunked Uint8Array (implements get and length from Uint8Array). + // Actual getting is abstracted away for eventual reuse. + class LazyUint8Array { + lengthKnown=false; + chunks=[]; + // Loaded chunks. Index is the chunk number + get(idx) { + if (idx > this.length - 1 || idx < 0) { + return undefined; + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = (idx / this.chunkSize) | 0; + return this.getter(chunkNum)[chunkOffset]; + } + setDataGetter(getter) { + this.getter = getter; + } + cacheLength() { + // Find length + var xhr = new XMLHttpRequest; + xhr.open("HEAD", url, false); + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + var chunkSize = 1024 * 1024; + // Chunk size in bytes + if (!hasByteServing) chunkSize = datalength; + // Function to get a range from the remote URL. + var doXHR = (from, to) => { + if (from > to) abort("invalid range (" + from + ", " + to + ") or no bytes requested!"); + if (to > datalength - 1) abort("only " + datalength + " bytes available! programmer error!"); + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + // Some hints to the browser that we want binary data. + xhr.responseType = "arraybuffer"; + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/plain; charset=x-user-defined"); + } + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array(/** @type{Array} */ (xhr.response || [])); + } + return intArrayFromString(xhr.responseText || "", true); + }; + var lazyArray = this; + lazyArray.setDataGetter(chunkNum => { + var start = chunkNum * chunkSize; + var end = (chunkNum + 1) * chunkSize - 1; + // including this byte + end = Math.min(end, datalength - 1); + // if datalength-1 is selected, this is the last block + if (typeof lazyArray.chunks[chunkNum] == "undefined") { + lazyArray.chunks[chunkNum] = doXHR(start, end); + } + if (typeof lazyArray.chunks[chunkNum] == "undefined") abort("doXHR failed!"); + return lazyArray.chunks[chunkNum]; + }); + if (usesGzip || !datalength) { + // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length + chunkSize = datalength = 1; + // this will force getter(0)/doXHR do download the whole file + datalength = this.getter(0).length; + chunkSize = datalength; + out("LazyFiles on gzip forces download of the whole file when length is accessed"); + } + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true; + } + get length() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._length; + } + get chunkSize() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._chunkSize; + } + } + if (globalThis.XMLHttpRequest) { + if (!ENVIRONMENT_IS_WORKER) abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc"); + var lazyArray = new LazyUint8Array; + var properties = { + isDevice: false, + contents: lazyArray + }; + } else { + var properties = { + isDevice: false, + url + }; + } + var node = FS.createFile(parent, name, properties, canRead, canWrite); + // This is a total hack, but I want to get this lazy file code out of the + // core of MEMFS. If we want to keep this lazy file concept I feel it should + // be its own thin LAZYFS proxying calls to MEMFS. + if (properties.contents) { + node.contents = properties.contents; + } else if (properties.url) { + node.contents = null; + node.url = properties.url; + } + // Add a function that defers querying the file size until it is asked the first time. + Object.defineProperties(node, { + usedBytes: { + get: function() { + return this.contents.length; + } + } + }); + // override each stream op with one that tries to force load the lazy file first + var stream_ops = {}; + for (const [key, fn] of Object.entries(node.stream_ops)) { + stream_ops[key] = (...args) => { + FS.forceLoadFile(node); + return fn(...args); + }; + } + function writeChunks(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= contents.length) return 0; + var size = Math.min(contents.length - position, length); + if (contents.slice) { + // normal array + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents[position + i]; + } + } else { + for (var i = 0; i < size; i++) { + // LazyUint8Array from sync binary XHR + buffer[offset + i] = contents.get(position + i); + } + } + return size; + } + // use a custom read function + stream_ops.read = (stream, buffer, offset, length, position) => { + FS.forceLoadFile(node); + return writeChunks(stream, buffer, offset, length, position); + }; + // use a custom mmap function + stream_ops.mmap = (stream, length, position, prot, flags) => { + FS.forceLoadFile(node); + var ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + writeChunks(stream, HEAP8, ptr, length, position); + return { + ptr, + allocated: true + }; + }; + node.stream_ops = stream_ops; + return node; + } +}; + +/** + * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the + * emscripten HEAP, returns a copy of that string as a Javascript String object. + * + * @param {number} ptr + * @param {number=} maxBytesToRead - An optional length that specifies the + * maximum number of bytes to read. You can omit this parameter to scan the + * string until the first 0 byte. If maxBytesToRead is passed, and the string + * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the + * string will cut short at that byte index. + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + if (!ptr) return ""; + var end = findStringEnd(HEAPU8, ptr, maxBytesToRead, ignoreNul); + return UTF8Decoder.decode(HEAPU8.subarray(ptr, end)); +}; + +var SYSCALLS = { + calculateAt(dirfd, path, allowEmpty) { + if (PATH.isAbs(path)) { + return path; + } + // relative path + var dir; + if (dirfd === -100) { + dir = FS.cwd(); + } else { + var dirstream = SYSCALLS.getStreamFromFD(dirfd); + dir = dirstream.path; + } + if (path.length == 0) { + if (!allowEmpty) { + throw new FS.ErrnoError(44); + } + return dir; + } + return dir + "/" + path; + }, + writeStat(buf, stat) { + HEAPU32[((buf) >> 2)] = stat.dev; + HEAPU32[(((buf) + (4)) >> 2)] = stat.mode; + HEAPU32[(((buf) + (8)) >> 2)] = stat.nlink; + HEAPU32[(((buf) + (12)) >> 2)] = stat.uid; + HEAPU32[(((buf) + (16)) >> 2)] = stat.gid; + HEAPU32[(((buf) + (20)) >> 2)] = stat.rdev; + (tempI64 = [ stat.size >>> 0, (tempDouble = stat.size, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (24)) >> 2)] = tempI64[0], HEAP32[(((buf) + (28)) >> 2)] = tempI64[1]); + HEAP32[(((buf) + (32)) >> 2)] = 4096; + HEAP32[(((buf) + (36)) >> 2)] = stat.blocks; + var atime = stat.atime.getTime(); + var mtime = stat.mtime.getTime(); + var ctime = stat.ctime.getTime(); + (tempI64 = [ Math.floor(atime / 1e3) >>> 0, (tempDouble = Math.floor(atime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (40)) >> 2)] = tempI64[0], HEAP32[(((buf) + (44)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (48)) >> 2)] = (atime % 1e3) * 1e3 * 1e3; + (tempI64 = [ Math.floor(mtime / 1e3) >>> 0, (tempDouble = Math.floor(mtime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (56)) >> 2)] = tempI64[0], HEAP32[(((buf) + (60)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (64)) >> 2)] = (mtime % 1e3) * 1e3 * 1e3; + (tempI64 = [ Math.floor(ctime / 1e3) >>> 0, (tempDouble = Math.floor(ctime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (72)) >> 2)] = tempI64[0], HEAP32[(((buf) + (76)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (80)) >> 2)] = (ctime % 1e3) * 1e3 * 1e3; + (tempI64 = [ stat.ino >>> 0, (tempDouble = stat.ino, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (88)) >> 2)] = tempI64[0], HEAP32[(((buf) + (92)) >> 2)] = tempI64[1]); + return 0; + }, + writeStatFs(buf, stats) { + HEAPU32[(((buf) + (4)) >> 2)] = stats.bsize; + HEAPU32[(((buf) + (60)) >> 2)] = stats.bsize; + (tempI64 = [ stats.blocks >>> 0, (tempDouble = stats.blocks, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (8)) >> 2)] = tempI64[0], HEAP32[(((buf) + (12)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.bfree >>> 0, (tempDouble = stats.bfree, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (16)) >> 2)] = tempI64[0], HEAP32[(((buf) + (20)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.bavail >>> 0, (tempDouble = stats.bavail, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (24)) >> 2)] = tempI64[0], HEAP32[(((buf) + (28)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.files >>> 0, (tempDouble = stats.files, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (32)) >> 2)] = tempI64[0], HEAP32[(((buf) + (36)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.ffree >>> 0, (tempDouble = stats.ffree, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (40)) >> 2)] = tempI64[0], HEAP32[(((buf) + (44)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (48)) >> 2)] = stats.fsid; + HEAPU32[(((buf) + (64)) >> 2)] = stats.flags; + // ST_NOSUID + HEAPU32[(((buf) + (56)) >> 2)] = stats.namelen; + }, + doMsync(addr, stream, len, flags, offset) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + if (flags & 2) { + // MAP_PRIVATE calls need not to be synced back to underlying fs + return 0; + } + var buffer = HEAPU8.slice(addr, addr + len); + FS.msync(stream, buffer, offset, len, flags); + }, + getStreamFromFD(fd) { + var stream = FS.getStreamChecked(fd); + return stream; + }, + varargs: undefined, + getStr(ptr) { + var ret = UTF8ToString(ptr); + return ret; + } +}; + +function ___syscall_dup(fd) { + try { + var old = SYSCALLS.getStreamFromFD(fd); + return FS.dupStream(old).fd; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_faccessat(dirfd, path, amode, flags) { + try { + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + if (amode & ~7) { + // need a valid mode + return -28; + } + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + if (!node) { + return -44; + } + var perms = ""; + if (amode & 4) perms += "r"; + if (amode & 2) perms += "w"; + if (amode & 1) perms += "x"; + if (perms && FS.nodePermissions(node, perms)) { + return -2; + } + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var syscallGetVarargI = () => { + // the `+` prepended here is necessary to convince the JSCompiler that varargs is indeed a number. + var ret = HEAP32[((+SYSCALLS.varargs) >> 2)]; + SYSCALLS.varargs += 4; + return ret; +}; + +var syscallGetVarargP = syscallGetVarargI; + +function ___syscall_fcntl64(fd, cmd, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(fd); + switch (cmd) { + case 0: + { + var arg = syscallGetVarargI(); + if (arg < 0) { + return -28; + } + while (FS.streams[arg]) { + arg++; + } + var newStream; + newStream = FS.dupStream(stream, arg); + return newStream.fd; + } + + case 1: + case 2: + return 0; + + // FD_CLOEXEC makes no sense for a single process. + case 3: + return stream.flags; + + case 4: + { + var arg = syscallGetVarargI(); + stream.flags |= arg; + return 0; + } + + case 12: + { + var arg = syscallGetVarargP(); + var offset = 0; + // We're always unlocked. + HEAP16[(((arg) + (offset)) >> 1)] = 2; + return 0; + } + + case 13: + case 14: + // Pretend that the locking is successful. These are process-level locks, + // and Emscripten programs are a single process. If we supported linking a + // filesystem between programs, we'd need to do more here. + // See https://github.com/emscripten-core/emscripten/issues/23697 + return 0; + } + return -28; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_fstat64(fd, buf) { + try { + return SYSCALLS.writeStat(buf, FS.fstat(fd)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var convertI32PairToI53Checked = (lo, hi) => ((hi + 2097152) >>> 0 < 4194305 - !!lo) ? (lo >>> 0) + hi * 4294967296 : NaN; + +function ___syscall_ftruncate64(fd, length_low, length_high) { + var length = convertI32PairToI53Checked(length_low, length_high); + try { + if (isNaN(length)) return -61; + FS.ftruncate(fd, length); + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_ioctl(fd, op, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(fd); + switch (op) { + case 21509: + { + if (!stream.tty) return -59; + return 0; + } + + case 21505: + { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcgets) { + var termios = stream.tty.ops.ioctl_tcgets(stream); + var argp = syscallGetVarargP(); + HEAP32[((argp) >> 2)] = termios.c_iflag || 0; + HEAP32[(((argp) + (4)) >> 2)] = termios.c_oflag || 0; + HEAP32[(((argp) + (8)) >> 2)] = termios.c_cflag || 0; + HEAP32[(((argp) + (12)) >> 2)] = termios.c_lflag || 0; + for (var i = 0; i < 32; i++) { + HEAP8[(argp + i) + (17)] = termios.c_cc[i] || 0; + } + return 0; + } + return 0; + } + + case 21510: + case 21511: + case 21512: + { + if (!stream.tty) return -59; + return 0; + } + + case 21506: + case 21507: + case 21508: + { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcsets) { + var argp = syscallGetVarargP(); + var c_iflag = HEAP32[((argp) >> 2)]; + var c_oflag = HEAP32[(((argp) + (4)) >> 2)]; + var c_cflag = HEAP32[(((argp) + (8)) >> 2)]; + var c_lflag = HEAP32[(((argp) + (12)) >> 2)]; + var c_cc = []; + for (var i = 0; i < 32; i++) { + c_cc.push(HEAP8[(argp + i) + (17)]); + } + return stream.tty.ops.ioctl_tcsets(stream.tty, op, { + c_iflag, + c_oflag, + c_cflag, + c_lflag, + c_cc + }); + } + return 0; + } + + case 21519: + { + if (!stream.tty) return -59; + var argp = syscallGetVarargP(); + HEAP32[((argp) >> 2)] = 0; + return 0; + } + + case 21520: + { + if (!stream.tty) return -59; + return -28; + } + + case 21537: + case 21531: + { + var argp = syscallGetVarargP(); + return FS.ioctl(stream, op, argp); + } + + case 21523: + { + // TODO: in theory we should write to the winsize struct that gets + // passed in, but for now musl doesn't read anything on it + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tiocgwinsz) { + var winsize = stream.tty.ops.ioctl_tiocgwinsz(stream.tty); + var argp = syscallGetVarargP(); + HEAP16[((argp) >> 1)] = winsize[0]; + HEAP16[(((argp) + (2)) >> 1)] = winsize[1]; + } + return 0; + } + + case 21524: + { + // TODO: technically, this ioctl call should change the window size. + // but, since emscripten doesn't have any concept of a terminal window + // yet, we'll just silently throw it away as we do TIOCGWINSZ + if (!stream.tty) return -59; + return 0; + } + + case 21515: + { + if (!stream.tty) return -59; + return 0; + } + + default: + return -28; + } + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_lstat64(path, buf) { + try { + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.lstat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_newfstatat(dirfd, path, buf, flags) { + try { + path = SYSCALLS.getStr(path); + var nofollow = flags & 256; + var allowEmpty = flags & 4096; + flags = flags & (~6400); + path = SYSCALLS.calculateAt(dirfd, path, allowEmpty); + return SYSCALLS.writeStat(buf, nofollow ? FS.lstat(path) : FS.stat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_openat(dirfd, path, flags, varargs) { + SYSCALLS.varargs = varargs; + try { + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + var mode = varargs ? syscallGetVarargI() : 0; + return FS.open(path, flags, mode).fd; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_stat64(path, buf) { + try { + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.stat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var __abort_js = () => abort(""); + +var __embind_register_bigint = (primitiveType, name, size, minRange, maxRange) => {}; + +var AsciiToString = ptr => { + var str = ""; + while (1) { + var ch = HEAPU8[ptr++]; + if (!ch) return str; + str += String.fromCharCode(ch); + } +}; + +var awaitingDependencies = {}; + +var registeredTypes = {}; + +var typeDependencies = {}; + +var BindingError = class BindingError extends Error { + constructor(message) { + super(message); + this.name = "BindingError"; + } +}; + +var throwBindingError = message => { + throw new BindingError(message); +}; + +/** @param {Object=} options */ function sharedRegisterType(rawType, registeredInstance, options = {}) { + var name = registeredInstance.name; + if (!rawType) { + throwBindingError(`type "${name}" must have a positive integer typeid pointer`); + } + if (registeredTypes.hasOwnProperty(rawType)) { + if (options.ignoreDuplicateRegistrations) { + return; + } else { + throwBindingError(`Cannot register type '${name}' twice`); + } + } + registeredTypes[rawType] = registeredInstance; + delete typeDependencies[rawType]; + if (awaitingDependencies.hasOwnProperty(rawType)) { + var callbacks = awaitingDependencies[rawType]; + delete awaitingDependencies[rawType]; + callbacks.forEach(cb => cb()); + } +} + +/** @param {Object=} options */ function registerType(rawType, registeredInstance, options = {}) { + return sharedRegisterType(rawType, registeredInstance, options); +} + +/** @suppress {globalThis} */ var __embind_register_bool = (rawType, name, trueValue, falseValue) => { + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: function(wt) { + // ambiguous emscripten ABI: sometimes return values are + // true or false, and sometimes integers (0 or 1) + return !!wt; + }, + toWireType: function(destructors, o) { + return o ? trueValue : falseValue; + }, + readValueFromPointer: function(pointer) { + return this.fromWireType(HEAPU8[pointer]); + }, + destructorFunction: null + }); +}; + +var emval_freelist = []; + +var emval_handles = [ 0, 1, , 1, null, 1, true, 1, false, 1 ]; + +var __emval_decref = handle => { + if (handle > 9 && 0 === --emval_handles[handle + 1]) { + var value = emval_handles[handle]; + emval_handles[handle] = undefined; + emval_freelist.push(handle); + } +}; + +var Emval = { + toValue: handle => { + if (!handle) { + throwBindingError(`Cannot use deleted val. handle = ${handle}`); + } + return emval_handles[handle]; + }, + toHandle: value => { + switch (value) { + case undefined: + return 2; + + case null: + return 4; + + case true: + return 6; + + case false: + return 8; + + default: + { + const handle = emval_freelist.pop() || emval_handles.length; + emval_handles[handle] = value; + emval_handles[handle + 1] = 1; + return handle; + } + } + } +}; + +/** @suppress {globalThis} */ function readPointer(pointer) { + return this.fromWireType(HEAPU32[((pointer) >> 2)]); +} + +var EmValType = { + name: "emscripten::val", + fromWireType: handle => { + var rv = Emval.toValue(handle); + __emval_decref(handle); + return rv; + }, + toWireType: (destructors, value) => Emval.toHandle(value), + readValueFromPointer: readPointer, + destructorFunction: null +}; + +var __embind_register_emval = rawType => registerType(rawType, EmValType); + +var floatReadValueFromPointer = (name, width) => { + switch (width) { + case 4: + return function(pointer) { + return this.fromWireType(HEAPF32[((pointer) >> 2)]); + }; + + case 8: + return function(pointer) { + return this.fromWireType(HEAPF64[((pointer) >> 3)]); + }; + + default: + throw new TypeError(`invalid float width (${width}): ${name}`); + } +}; + +var __embind_register_float = (rawType, name, size) => { + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: value => value, + toWireType: (destructors, value) => value, + readValueFromPointer: floatReadValueFromPointer(name, size), + destructorFunction: null + }); +}; + +var integerReadValueFromPointer = (name, width, signed) => { + // integers are quite common, so generate very specialized functions + switch (width) { + case 1: + return signed ? pointer => HEAP8[pointer] : pointer => HEAPU8[pointer]; + + case 2: + return signed ? pointer => HEAP16[((pointer) >> 1)] : pointer => HEAPU16[((pointer) >> 1)]; + + case 4: + return signed ? pointer => HEAP32[((pointer) >> 2)] : pointer => HEAPU32[((pointer) >> 2)]; + + default: + throw new TypeError(`invalid integer width (${width}): ${name}`); + } +}; + +/** @suppress {globalThis} */ var __embind_register_integer = (primitiveType, name, size, minRange, maxRange) => { + name = AsciiToString(name); + const isUnsignedType = minRange === 0; + let fromWireType = value => value; + if (isUnsignedType) { + var bitshift = 32 - 8 * size; + fromWireType = value => (value << bitshift) >>> bitshift; + maxRange = fromWireType(maxRange); + } + registerType(primitiveType, { + name, + fromWireType, + toWireType: (destructors, value) => value, + readValueFromPointer: integerReadValueFromPointer(name, size, minRange !== 0), + destructorFunction: null + }); +}; + +var __embind_register_memory_view = (rawType, dataTypeIndex, name) => { + var typeMapping = [ Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array ]; + var TA = typeMapping[dataTypeIndex]; + function decodeMemoryView(handle) { + var size = HEAPU32[((handle) >> 2)]; + var data = HEAPU32[(((handle) + (4)) >> 2)]; + return new TA(HEAP8.buffer, data, size); + } + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: decodeMemoryView, + readValueFromPointer: decodeMemoryView + }, { + ignoreDuplicateRegistrations: true + }); +}; + +var stringToUTF8 = (str, outPtr, maxBytesToWrite) => stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + +var __embind_register_std_string = (rawType, name) => { + name = AsciiToString(name); + var stdStringIsUTF8 = true; + registerType(rawType, { + name, + // For some method names we use string keys here since they are part of + // the public/external API and/or used by the runtime-generated code. + fromWireType(value) { + var length = HEAPU32[((value) >> 2)]; + var payload = value + 4; + var str; + if (stdStringIsUTF8) { + str = UTF8ToString(payload, length, true); + } else { + str = ""; + for (var i = 0; i < length; ++i) { + str += String.fromCharCode(HEAPU8[payload + i]); + } + } + _free(value); + return str; + }, + toWireType(destructors, value) { + if (value instanceof ArrayBuffer) { + value = new Uint8Array(value); + } + var length; + var valueIsOfTypeString = (typeof value == "string"); + // We accept `string` or array views with single byte elements + if (!(valueIsOfTypeString || (ArrayBuffer.isView(value) && value.BYTES_PER_ELEMENT == 1))) { + throwBindingError("Cannot pass non-string to std::string"); + } + if (stdStringIsUTF8 && valueIsOfTypeString) { + length = lengthBytesUTF8(value); + } else { + length = value.length; + } + // assumes POINTER_SIZE alignment + var base = _malloc(4 + length + 1); + var ptr = base + 4; + HEAPU32[((base) >> 2)] = length; + if (valueIsOfTypeString) { + if (stdStringIsUTF8) { + stringToUTF8(value, ptr, length + 1); + } else { + for (var i = 0; i < length; ++i) { + var charCode = value.charCodeAt(i); + if (charCode > 255) { + _free(base); + throwBindingError("String has UTF-16 code units that do not fit in 8 bits"); + } + HEAPU8[ptr + i] = charCode; + } + } + } else { + HEAPU8.set(value, ptr); + } + if (destructors !== null) { + destructors.push(_free, base); + } + return base; + }, + readValueFromPointer: readPointer, + destructorFunction(ptr) { + _free(ptr); + } + }); +}; + +var UTF16Decoder = new TextDecoder("utf-16le"); + +var UTF16ToString = (ptr, maxBytesToRead, ignoreNul) => { + var idx = ((ptr) >> 1); + var endIdx = findStringEnd(HEAPU16, idx, maxBytesToRead / 2, ignoreNul); + return UTF16Decoder.decode(HEAPU16.subarray(idx, endIdx)); +}; + +var stringToUTF16 = (str, outPtr, maxBytesToWrite) => { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + maxBytesToWrite ??= 2147483647; + if (maxBytesToWrite < 2) return 0; + maxBytesToWrite -= 2; + // Null terminator. + var startPtr = outPtr; + var numCharsToWrite = (maxBytesToWrite < str.length * 2) ? (maxBytesToWrite / 2) : str.length; + for (var i = 0; i < numCharsToWrite; ++i) { + // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. + var codeUnit = str.charCodeAt(i); + // possibly a lead surrogate + HEAP16[((outPtr) >> 1)] = codeUnit; + outPtr += 2; + } + // Null-terminate the pointer to the HEAP. + HEAP16[((outPtr) >> 1)] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF16 = str => str.length * 2; + +var UTF32ToString = (ptr, maxBytesToRead, ignoreNul) => { + var str = ""; + var startIdx = ((ptr) >> 2); + // If maxBytesToRead is not passed explicitly, it will be undefined, and this + // will always evaluate to true. This saves on code size. + for (var i = 0; !(i >= maxBytesToRead / 4); i++) { + var utf32 = HEAPU32[startIdx + i]; + if (!utf32 && !ignoreNul) break; + str += String.fromCodePoint(utf32); + } + return str; +}; + +var stringToUTF32 = (str, outPtr, maxBytesToWrite) => { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + maxBytesToWrite ??= 2147483647; + if (maxBytesToWrite < 4) return 0; + var startPtr = outPtr; + var endPtr = startPtr + maxBytesToWrite - 4; + for (var i = 0; i < str.length; ++i) { + var codePoint = str.codePointAt(i); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + if (codePoint > 65535) { + i++; + } + HEAP32[((outPtr) >> 2)] = codePoint; + outPtr += 4; + if (outPtr + 4 > endPtr) break; + } + // Null-terminate the pointer to the HEAP. + HEAP32[((outPtr) >> 2)] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF32 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var codePoint = str.codePointAt(i); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + if (codePoint > 65535) { + i++; + } + len += 4; + } + return len; +}; + +var __embind_register_std_wstring = (rawType, charSize, name) => { + name = AsciiToString(name); + var decodeString, encodeString, lengthBytesUTF; + if (charSize === 2) { + decodeString = UTF16ToString; + encodeString = stringToUTF16; + lengthBytesUTF = lengthBytesUTF16; + } else { + decodeString = UTF32ToString; + encodeString = stringToUTF32; + lengthBytesUTF = lengthBytesUTF32; + } + registerType(rawType, { + name, + fromWireType: value => { + // Code mostly taken from _embind_register_std_string fromWireType + var length = HEAPU32[((value) >> 2)]; + var str = decodeString(value + 4, length * charSize, true); + _free(value); + return str; + }, + toWireType: (destructors, value) => { + if (!(typeof value == "string")) { + throwBindingError(`Cannot pass non-string to C++ string type ${name}`); + } + // assumes POINTER_SIZE alignment + var length = lengthBytesUTF(value); + var ptr = _malloc(4 + length + charSize); + HEAPU32[((ptr) >> 2)] = length / charSize; + encodeString(value, ptr + 4, length + charSize); + if (destructors !== null) { + destructors.push(_free, ptr); + } + return ptr; + }, + readValueFromPointer: readPointer, + destructorFunction(ptr) { + _free(ptr); + } + }); +}; + +var __embind_register_void = (rawType, name) => { + name = AsciiToString(name); + registerType(rawType, { + isVoid: true, + // void return values can be optimized out sometimes + name, + fromWireType: () => undefined, + // TODO: assert if anything else is given? + toWireType: (destructors, o) => undefined + }); +}; + +var emval_methodCallers = []; + +var emval_addMethodCaller = caller => { + var id = emval_methodCallers.length; + emval_methodCallers.push(caller); + return id; +}; + +var getTypeName = type => { + var ptr = ___getTypeName(type); + var rv = AsciiToString(ptr); + _free(ptr); + return rv; +}; + +var requireRegisteredType = (rawType, humanName) => { + var impl = registeredTypes[rawType]; + if (undefined === impl) { + throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`); + } + return impl; +}; + +var emval_lookupTypes = (argCount, argTypes) => { + var a = new Array(argCount); + for (var i = 0; i < argCount; ++i) { + a[i] = requireRegisteredType(HEAPU32[(((argTypes) + (i * 4)) >> 2)], `parameter ${i}`); + } + return a; +}; + +var createNamedFunction = (name, func) => Object.defineProperty(func, "name", { + value: name +}); + +var emval_returnValue = (toReturnWire, destructorsRef, handle) => { + var destructors = []; + var result = toReturnWire(destructors, handle); + if (destructors.length) { + // void, primitives and any other types w/o destructors don't need to allocate a handle + HEAPU32[((destructorsRef) >> 2)] = Emval.toHandle(destructors); + } + return result; +}; + +var emval_symbols = {}; + +var getStringOrSymbol = address => { + var symbol = emval_symbols[address]; + if (symbol === undefined) { + return AsciiToString(address); + } + return symbol; +}; + +var __emval_create_invoker = (argCount, argTypesPtr, kind) => { + var GenericWireTypeSize = 8; + var [retType, ...argTypes] = emval_lookupTypes(argCount, argTypesPtr); + var toReturnWire = retType.toWireType.bind(retType); + var argFromPtr = argTypes.map(type => type.readValueFromPointer.bind(type)); + argCount--; + // remove the extracted return type + var argN = new Array(argCount); + var invokerFunction = (handle, methodName, destructorsRef, args) => { + var offset = 0; + for (var i = 0; i < argCount; ++i) { + argN[i] = argFromPtr[i](args + offset); + offset += GenericWireTypeSize; + } + var rv; + switch (kind) { + case 0: + rv = Emval.toValue(handle).apply(null, argN); + break; + + case 2: + rv = Reflect.construct(Emval.toValue(handle), argN); + break; + + case 3: + // no-op, just return the argument + rv = argN[0]; + break; + + case 1: + rv = Emval.toValue(handle)[getStringOrSymbol(methodName)](...argN); + break; + } + return emval_returnValue(toReturnWire, destructorsRef, rv); + }; + var functionName = `methodCaller<(${argTypes.map(t => t.name)}) => ${retType.name}>`; + return emval_addMethodCaller(createNamedFunction(functionName, invokerFunction)); +}; + +var __emval_get_global = name => { + if (!name) { + return Emval.toHandle(globalThis); + } + name = getStringOrSymbol(name); + return Emval.toHandle(globalThis[name]); +}; + +var __emval_get_property = (handle, key) => { + handle = Emval.toValue(handle); + key = Emval.toValue(key); + return Emval.toHandle(handle[key]); +}; + +var __emval_incref = handle => { + if (handle > 9) { + emval_handles[handle + 1] += 1; + } +}; + +var __emval_instanceof = (object, constructor) => { + object = Emval.toValue(object); + constructor = Emval.toValue(constructor); + return object instanceof constructor; +}; + +var __emval_invoke = (caller, handle, methodName, destructorsRef, args) => emval_methodCallers[caller](handle, methodName, destructorsRef, args); + +var __emval_new_cstring = v => Emval.toHandle(getStringOrSymbol(v)); + +var runDestructors = destructors => { + while (destructors.length) { + var ptr = destructors.pop(); + var del = destructors.pop(); + del(ptr); + } +}; + +var __emval_run_destructors = handle => { + var destructors = Emval.toValue(handle); + runDestructors(destructors); + __emval_decref(handle); +}; + +var __emval_set_property = (handle, key, value) => { + handle = Emval.toValue(handle); + key = Emval.toValue(key); + value = Emval.toValue(value); + handle[key] = value; +}; + +var __emval_typeof = handle => { + handle = Emval.toValue(handle); + return Emval.toHandle(typeof handle); +}; + +function __gmtime_js(time_low, time_high, tmPtr) { + var time = convertI32PairToI53Checked(time_low, time_high); + var date = new Date(time * 1e3); + HEAP32[((tmPtr) >> 2)] = date.getUTCSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getUTCMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getUTCHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getUTCDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getUTCMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getUTCFullYear() - 1900; + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getUTCDay(); + var start = Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0); + var yday = ((date.getTime() - start) / (1e3 * 60 * 60 * 24)) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; +} + +var isLeapYear = year => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + +var MONTH_DAYS_LEAP_CUMULATIVE = [ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 ]; + +var MONTH_DAYS_REGULAR_CUMULATIVE = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ]; + +var ydayFromDate = date => { + var leap = isLeapYear(date.getFullYear()); + var monthDaysCumulative = (leap ? MONTH_DAYS_LEAP_CUMULATIVE : MONTH_DAYS_REGULAR_CUMULATIVE); + var yday = monthDaysCumulative[date.getMonth()] + date.getDate() - 1; + // -1 since it's days since Jan 1 + return yday; +}; + +function __localtime_js(time_low, time_high, tmPtr) { + var time = convertI32PairToI53Checked(time_low, time_high); + var date = new Date(time * 1e3); + HEAP32[((tmPtr) >> 2)] = date.getSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getFullYear() - 1900; + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; + HEAP32[(((tmPtr) + (36)) >> 2)] = -(date.getTimezoneOffset() * 60); + // Attention: DST is in December in South, and some regions don't have DST at all. + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0; + HEAP32[(((tmPtr) + (32)) >> 2)] = dst; +} + +var setTempRet0 = val => __emscripten_tempret_set(val); + +var __mktime_js = function(tmPtr) { + var ret = (() => { + var date = new Date(HEAP32[(((tmPtr) + (20)) >> 2)] + 1900, HEAP32[(((tmPtr) + (16)) >> 2)], HEAP32[(((tmPtr) + (12)) >> 2)], HEAP32[(((tmPtr) + (8)) >> 2)], HEAP32[(((tmPtr) + (4)) >> 2)], HEAP32[((tmPtr) >> 2)], 0); + // There's an ambiguous hour when the time goes back; the tm_isdst field is + // used to disambiguate it. Date() basically guesses, so we fix it up if it + // guessed wrong, or fill in tm_isdst with the guess if it's -1. + var dst = HEAP32[(((tmPtr) + (32)) >> 2)]; + var guessedOffset = date.getTimezoneOffset(); + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dstOffset = Math.min(winterOffset, summerOffset); + // DST is in December in South + if (dst < 0) { + // Attention: some regions don't have DST at all. + HEAP32[(((tmPtr) + (32)) >> 2)] = Number(summerOffset != winterOffset && dstOffset == guessedOffset); + } else if ((dst > 0) != (dstOffset == guessedOffset)) { + var nonDstOffset = Math.max(winterOffset, summerOffset); + var trueOffset = dst > 0 ? dstOffset : nonDstOffset; + // Don't try setMinutes(date.getMinutes() + ...) -- it's messed up. + date.setTime(date.getTime() + (trueOffset - guessedOffset) * 6e4); + } + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; + // To match expected behavior, update fields from date + HEAP32[((tmPtr) >> 2)] = date.getSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getYear(); + var timeMs = date.getTime(); + if (isNaN(timeMs)) { + return -1; + } + // Return time in microseconds + return timeMs / 1e3; + })(); + return (setTempRet0((tempDouble = ret, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0)), + ret >>> 0); +}; + +function __mmap_js(len, prot, flags, fd, offset_low, offset_high, allocated, addr) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var res = FS.mmap(stream, len, offset, prot, flags); + var ptr = res.ptr; + HEAP32[((allocated) >> 2)] = res.allocated; + HEAPU32[((addr) >> 2)] = ptr; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function __munmap_js(addr, len, prot, flags, fd, offset_low, offset_high) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + var stream = SYSCALLS.getStreamFromFD(fd); + if (prot & 2) { + SYSCALLS.doMsync(addr, stream, len, flags, offset); + } + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var __tzset_js = (timezone, daylight, std_name, dst_name) => { + // TODO: Use (malleable) environment variables instead of system settings. + var currentYear = (new Date).getFullYear(); + var winter = new Date(currentYear, 0, 1); + var summer = new Date(currentYear, 6, 1); + var winterOffset = winter.getTimezoneOffset(); + var summerOffset = summer.getTimezoneOffset(); + // Local standard timezone offset. Local standard time is not adjusted for + // daylight savings. This code uses the fact that getTimezoneOffset returns + // a greater value during Standard Time versus Daylight Saving Time (DST). + // Thus it determines the expected output during Standard Time, and it + // compares whether the output of the given date the same (Standard) or less + // (DST). + var stdTimezoneOffset = Math.max(winterOffset, summerOffset); + // timezone is specified as seconds west of UTC ("The external variable + // `timezone` shall be set to the difference, in seconds, between + // Coordinated Universal Time (UTC) and local standard time."), the same + // as returned by stdTimezoneOffset. + // See http://pubs.opengroup.org/onlinepubs/009695399/functions/tzset.html + HEAPU32[((timezone) >> 2)] = stdTimezoneOffset * 60; + HEAP32[((daylight) >> 2)] = Number(winterOffset != summerOffset); + var extractZone = timezoneOffset => { + // Why inverse sign? + // Read here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset + var sign = timezoneOffset >= 0 ? "-" : "+"; + var absOffset = Math.abs(timezoneOffset); + var hours = String(Math.floor(absOffset / 60)).padStart(2, "0"); + var minutes = String(absOffset % 60).padStart(2, "0"); + return `UTC${sign}${hours}${minutes}`; + }; + var winterName = extractZone(winterOffset); + var summerName = extractZone(summerOffset); + if (summerOffset < winterOffset) { + // Northern hemisphere + stringToUTF8(winterName, std_name, 17); + stringToUTF8(summerName, dst_name, 17); + } else { + stringToUTF8(winterName, dst_name, 17); + stringToUTF8(summerName, std_name, 17); + } +}; + +var _emscripten_get_now = () => performance.now(); + +var _emscripten_date_now = () => Date.now(); + +var nowIsMonotonic = 1; + +var checkWasiClock = clock_id => clock_id >= 0 && clock_id <= 3; + +function _clock_time_get(clk_id, ignored_precision_low, ignored_precision_high, ptime) { + var ignored_precision = convertI32PairToI53Checked(ignored_precision_low, ignored_precision_high); + if (!checkWasiClock(clk_id)) { + return 28; + } + var now; + // all wasi clocks but realtime are monotonic + if (clk_id === 0) { + now = _emscripten_date_now(); + } else if (nowIsMonotonic) { + now = _emscripten_get_now(); + } else { + return 52; + } + // "now" is in ms, and wasi times are in ns. + var nsec = Math.round(now * 1e3 * 1e3); + (tempI64 = [ nsec >>> 0, (tempDouble = nsec, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[((ptime) >> 2)] = tempI64[0], HEAP32[(((ptime) + (4)) >> 2)] = tempI64[1]); + return 0; +} + +var readEmAsmArgsArray = []; + +var readEmAsmArgs = (sigPtr, buf) => { + readEmAsmArgsArray.length = 0; + var ch; + // Most arguments are i32s, so shift the buffer pointer so it is a plain + // index into HEAP32. + while (ch = HEAPU8[sigPtr++]) { + // Floats are always passed as doubles, so all types except for 'i' + // are 8 bytes and require alignment. + var wide = (ch != 105); + wide &= (ch != 112); + buf += wide && (buf % 8) ? 4 : 0; + readEmAsmArgsArray.push(// Special case for pointers under wasm64 or CAN_ADDRESS_2GB mode. + ch == 112 ? HEAPU32[((buf) >> 2)] : ch == 105 ? HEAP32[((buf) >> 2)] : HEAPF64[((buf) >> 3)]); + buf += wide ? 8 : 4; + } + return readEmAsmArgsArray; +}; + +var runEmAsmFunction = (code, sigPtr, argbuf) => { + var args = readEmAsmArgs(sigPtr, argbuf); + return ASM_CONSTS[code](...args); +}; + +var _emscripten_asm_const_int = (code, sigPtr, argbuf) => runEmAsmFunction(code, sigPtr, argbuf); + +var _emscripten_asm_const_ptr = (code, sigPtr, argbuf) => runEmAsmFunction(code, sigPtr, argbuf); + +var _emscripten_errn = (str, len) => err(UTF8ToString(str, len)); + +var getHeapMax = () => // Stay one Wasm page short of 4GB: while e.g. Chrome is able to allocate +// full 4GB Wasm memories, the size will wrap back to 0 bytes in Wasm side +// for any code that deals with heap sizes, which would require special +// casing all heap size related code to treat 0 specially. +2147483648; + +var _emscripten_get_heap_max = () => getHeapMax(); + +var _emscripten_has_asyncify = () => 0; + +var _emscripten_outn = (str, len) => out(UTF8ToString(str, len)); + +var UNWIND_CACHE = {}; + +var stringToNewUTF8 = str => { + var size = lengthBytesUTF8(str) + 1; + var ret = _malloc(size); + if (ret) stringToUTF8(str, ret, size); + return ret; +}; + +/** @returns {number} */ var convertFrameToPC = frame => { + var match; + if (match = /\bwasm-function\[\d+\]:(0x[0-9a-f]+)/.exec(frame)) { + // Wasm engines give the binary offset directly, so we use that as return address + return +match[1]; + } else if (match = /:(\d+):\d+(?:\)|$)/.exec(frame)) { + // If we are in js, we can use the js line number as the "return address". + // This should work for wasm2js. We tag the high bit to distinguish this + // from wasm addresses. + return 2147483648 | +match[1]; + } + // return 0 if we can't find any + return 0; +}; + +var saveInUnwindCache = callstack => { + for (var line of callstack) { + var pc = convertFrameToPC(line); + if (pc) { + UNWIND_CACHE[pc] = line; + } + } +}; + +var jsStackTrace = () => (new Error).stack.toString(); + +var _emscripten_stack_snapshot = () => { + var callstack = jsStackTrace().split("\n"); + if (callstack[0] == "Error") { + callstack.shift(); + } + saveInUnwindCache(callstack); + // Caches the stack snapshot so that emscripten_stack_unwind_buffer() can + // unwind from this spot. + UNWIND_CACHE.last_addr = convertFrameToPC(callstack[3]); + UNWIND_CACHE.last_stack = callstack; + return UNWIND_CACHE.last_addr; +}; + +var _emscripten_pc_get_function = pc => { + var frame = UNWIND_CACHE[pc]; + if (!frame) return 0; + var name; + var match; + // First try to match foo.wasm.sym files explcitly. e.g. + // at test_return_address.wasm.main (wasm://wasm/test_return_address.wasm-0012cc2a:wasm-function[26]:0x9f3 + // Then match JS symbols which don't include that module name: + // at invokeEntryPoint (.../test_return_address.js:1500:42) + // Finally match firefox format: + // Object._main@http://server.com:4324:12' + if (match = /^\s+at .*\.wasm\.(.*) \(.*\)$/.exec(frame)) { + name = match[1]; + } else if (match = /^\s+at (.*) \(.*\)$/.exec(frame)) { + name = match[1]; + } else if (match = /^(.+?)@/.exec(frame)) { + name = match[1]; + } else { + return 0; + } + _free(_emscripten_pc_get_function.ret ?? 0); + _emscripten_pc_get_function.ret = stringToNewUTF8(name); + return _emscripten_pc_get_function.ret; +}; + +var growMemory = size => { + var oldHeapSize = wasmMemory.buffer.byteLength; + var pages = ((size - oldHeapSize + 65535) / 65536) | 0; + try { + // round size grow request up to wasm page size (fixed 64KB per spec) + wasmMemory.grow(pages); + // .grow() takes a delta compared to the previous size + updateMemoryViews(); + return 1; + } catch (e) {} +}; + +var _emscripten_resize_heap = requestedSize => { + var oldSize = HEAPU8.length; + // With CAN_ADDRESS_2GB or MEMORY64, pointers are already unsigned. + requestedSize >>>= 0; + // With multithreaded builds, races can happen (another thread might increase the size + // in between), so return a failure, and let the caller retry. + // Memory resize rules: + // 1. Always increase heap size to at least the requested size, rounded up + // to next page multiple. + // 2a. If MEMORY_GROWTH_LINEAR_STEP == -1, excessively resize the heap + // geometrically: increase the heap size according to + // MEMORY_GROWTH_GEOMETRIC_STEP factor (default +20%), At most + // overreserve by MEMORY_GROWTH_GEOMETRIC_CAP bytes (default 96MB). + // 2b. If MEMORY_GROWTH_LINEAR_STEP != -1, excessively resize the heap + // linearly: increase the heap size by at least + // MEMORY_GROWTH_LINEAR_STEP bytes. + // 3. Max size for the heap is capped at 2048MB-WASM_PAGE_SIZE, or by + // MAXIMUM_MEMORY, or by ASAN limit, depending on which is smallest + // 4. If we were unable to allocate as much memory, it may be due to + // over-eager decision to excessively reserve due to (3) above. + // Hence if an allocation fails, cut down on the amount of excess + // growth, in an attempt to succeed to perform a smaller allocation. + // A limit is set for how much we can grow. We should not exceed that + // (the wasm binary specifies it, so if we tried, we'd fail anyhow). + var maxHeapSize = getHeapMax(); + if (requestedSize > maxHeapSize) { + return false; + } + // Loop through potential heap size increases. If we attempt a too eager + // reservation that fails, cut down on the attempted size and reserve a + // smaller bump instead. (max 3 times, chosen somewhat arbitrarily) + for (var cutDown = 1; cutDown <= 4; cutDown *= 2) { + var overGrownHeapSize = oldSize * (1 + .2 / cutDown); + // ensure geometric growth + // but limit overreserving (default to capping at +96MB overgrowth at most) + overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296); + var newSize = Math.min(maxHeapSize, alignMemory(Math.max(requestedSize, overGrownHeapSize), 65536)); + var replacement = growMemory(newSize); + if (replacement) { + return true; + } + } + return false; +}; + +var _emscripten_stack_unwind_buffer = (addr, buffer, count) => { + var stack; + if (UNWIND_CACHE.last_addr == addr) { + stack = UNWIND_CACHE.last_stack; + } else { + stack = jsStackTrace().split("\n"); + if (stack[0] == "Error") { + stack.shift(); + } + saveInUnwindCache(stack); + } + var offset = 3; + while (stack[offset] && convertFrameToPC(stack[offset]) != addr) { + ++offset; + } + for (var i = 0; i < count && stack[i + offset]; ++i) { + HEAP32[(((buffer) + (i * 4)) >> 2)] = convertFrameToPC(stack[i + offset]); + } + return i; +}; + +var GLctx; + +var webgl_enable_ANGLE_instanced_arrays = ctx => { + // Extension available in WebGL 1 from Firefox 26 and Google Chrome 30 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("ANGLE_instanced_arrays"); + // Because this extension is a core function in WebGL 2, assign the extension entry points in place of + // where the core functions will reside in WebGL 2. This way the calling code can call these without + // having to dynamically branch depending if running against WebGL 1 or WebGL 2. + if (ext) { + ctx["vertexAttribDivisor"] = (index, divisor) => ext["vertexAttribDivisorANGLE"](index, divisor); + ctx["drawArraysInstanced"] = (mode, first, count, primcount) => ext["drawArraysInstancedANGLE"](mode, first, count, primcount); + ctx["drawElementsInstanced"] = (mode, count, type, indices, primcount) => ext["drawElementsInstancedANGLE"](mode, count, type, indices, primcount); + return 1; + } +}; + +var webgl_enable_OES_vertex_array_object = ctx => { + // Extension available in WebGL 1 from Firefox 25 and WebKit 536.28/desktop Safari 6.0.3 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("OES_vertex_array_object"); + if (ext) { + ctx["createVertexArray"] = () => ext["createVertexArrayOES"](); + ctx["deleteVertexArray"] = vao => ext["deleteVertexArrayOES"](vao); + ctx["bindVertexArray"] = vao => ext["bindVertexArrayOES"](vao); + ctx["isVertexArray"] = vao => ext["isVertexArrayOES"](vao); + return 1; + } +}; + +var webgl_enable_WEBGL_draw_buffers = ctx => { + // Extension available in WebGL 1 from Firefox 28 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("WEBGL_draw_buffers"); + if (ext) { + ctx["drawBuffers"] = (n, bufs) => ext["drawBuffersWEBGL"](n, bufs); + return 1; + } +}; + +var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance = ctx => // Closure is expected to be allowed to minify the '.dibvbi' property, so not accessing it quoted. +!!(ctx.dibvbi = ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance")); + +var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance = ctx => !!(ctx.mdibvbi = ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance")); + +var webgl_enable_EXT_polygon_offset_clamp = ctx => !!(ctx.extPolygonOffsetClamp = ctx.getExtension("EXT_polygon_offset_clamp")); + +var webgl_enable_EXT_clip_control = ctx => !!(ctx.extClipControl = ctx.getExtension("EXT_clip_control")); + +var webgl_enable_WEBGL_polygon_mode = ctx => !!(ctx.webglPolygonMode = ctx.getExtension("WEBGL_polygon_mode")); + +var webgl_enable_WEBGL_multi_draw = ctx => // Closure is expected to be allowed to minify the '.multiDrawWebgl' property, so not accessing it quoted. +!!(ctx.multiDrawWebgl = ctx.getExtension("WEBGL_multi_draw")); + +var getEmscriptenSupportedExtensions = ctx => { + // Restrict the list of advertised extensions to those that we actually + // support. + var supportedExtensions = [ // WebGL 1 extensions + "ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_disjoint_timer_query", "EXT_frag_depth", "EXT_shader_texture_lod", "EXT_sRGB", "OES_element_index_uint", "OES_fbo_render_mipmap", "OES_standard_derivatives", "OES_texture_float", "OES_texture_half_float", "OES_texture_half_float_linear", "OES_vertex_array_object", "WEBGL_color_buffer_float", "WEBGL_depth_texture", "WEBGL_draw_buffers", // WebGL 2 extensions + "EXT_color_buffer_float", "EXT_conservative_depth", "EXT_disjoint_timer_query_webgl2", "EXT_texture_norm16", "NV_shader_noperspective_interpolation", "WEBGL_clip_cull_distance", // WebGL 1 and WebGL 2 extensions + "EXT_clip_control", "EXT_color_buffer_half_float", "EXT_depth_clamp", "EXT_float_blend", "EXT_polygon_offset_clamp", "EXT_texture_compression_bptc", "EXT_texture_compression_rgtc", "EXT_texture_filter_anisotropic", "KHR_parallel_shader_compile", "OES_texture_float_linear", "WEBGL_blend_func_extended", "WEBGL_compressed_texture_astc", "WEBGL_compressed_texture_etc", "WEBGL_compressed_texture_etc1", "WEBGL_compressed_texture_s3tc", "WEBGL_compressed_texture_s3tc_srgb", "WEBGL_debug_renderer_info", "WEBGL_debug_shaders", "WEBGL_lose_context", "WEBGL_multi_draw", "WEBGL_polygon_mode" ]; + // .getSupportedExtensions() can return null if context is lost, so coerce to empty array. + return (ctx.getSupportedExtensions() || []).filter(ext => supportedExtensions.includes(ext)); +}; + +var registerPreMainLoop = f => { + // Does nothing unless $MainLoop is included/used. + typeof MainLoop != "undefined" && MainLoop.preMainLoop.push(f); +}; + +var GL = { + counter: 1, + buffers: [], + mappedBuffers: {}, + programs: [], + framebuffers: [], + renderbuffers: [], + textures: [], + shaders: [], + vaos: [], + contexts: [], + offscreenCanvases: {}, + queries: [], + samplers: [], + transformFeedbacks: [], + syncs: [], + byteSizeByTypeRoot: 5120, + byteSizeByType: [ 1, 1, 2, 2, 4, 4, 4, 2, 3, 4, 8 ], + stringCache: {}, + stringiCache: {}, + unpackAlignment: 4, + unpackRowLength: 0, + recordError: errorCode => { + if (!GL.lastError) { + GL.lastError = errorCode; + } + }, + getNewId: table => { + var ret = GL.counter++; + for (var i = table.length; i < ret; i++) { + table[i] = null; + } + // Skip over any non-null elements that might have been created by + // glBindBuffer. + while (table[ret]) { + ret = GL.counter++; + } + return ret; + }, + genObject: (n, buffers, createFunction, objectTable) => { + for (var i = 0; i < n; i++) { + var buffer = GLctx[createFunction](); + var id = buffer && GL.getNewId(objectTable); + if (buffer) { + buffer.name = id; + objectTable[id] = buffer; + } else { + GL.recordError(1282); + } + HEAP32[(((buffers) + (i * 4)) >> 2)] = id; + } + }, + MAX_TEMP_BUFFER_SIZE: 2097152, + numTempVertexBuffersPerSize: 64, + log2ceilLookup: i => 32 - Math.clz32(i === 0 ? 0 : i - 1), + generateTempBuffers: (quads, context) => { + var largestIndex = GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE); + context.tempVertexBufferCounters1 = []; + context.tempVertexBufferCounters2 = []; + context.tempVertexBufferCounters1.length = context.tempVertexBufferCounters2.length = largestIndex + 1; + context.tempVertexBuffers1 = []; + context.tempVertexBuffers2 = []; + context.tempVertexBuffers1.length = context.tempVertexBuffers2.length = largestIndex + 1; + context.tempIndexBuffers = []; + context.tempIndexBuffers.length = largestIndex + 1; + for (var i = 0; i <= largestIndex; ++i) { + context.tempIndexBuffers[i] = null; + // Created on-demand + context.tempVertexBufferCounters1[i] = context.tempVertexBufferCounters2[i] = 0; + var ringbufferLength = GL.numTempVertexBuffersPerSize; + context.tempVertexBuffers1[i] = []; + context.tempVertexBuffers2[i] = []; + var ringbuffer1 = context.tempVertexBuffers1[i]; + var ringbuffer2 = context.tempVertexBuffers2[i]; + ringbuffer1.length = ringbuffer2.length = ringbufferLength; + for (var j = 0; j < ringbufferLength; ++j) { + ringbuffer1[j] = ringbuffer2[j] = null; + } + } + if (quads) { + // GL_QUAD indexes can be precalculated + context.tempQuadIndexBuffer = GLctx.createBuffer(); + context.GLctx.bindBuffer(34963, context.tempQuadIndexBuffer); + var numIndexes = GL.MAX_TEMP_BUFFER_SIZE >> 1; + var quadIndexes = new Uint16Array(numIndexes); + var i = 0, v = 0; + while (1) { + quadIndexes[i++] = v; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 1; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 2; + if (i >= numIndexes) break; + quadIndexes[i++] = v; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 2; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 3; + if (i >= numIndexes) break; + v += 4; + } + context.GLctx.bufferData(34963, quadIndexes, 35044); + context.GLctx.bindBuffer(34963, null); + } + }, + getTempVertexBuffer: sizeBytes => { + var idx = GL.log2ceilLookup(sizeBytes); + var ringbuffer = GL.currentContext.tempVertexBuffers1[idx]; + var nextFreeBufferIndex = GL.currentContext.tempVertexBufferCounters1[idx]; + GL.currentContext.tempVertexBufferCounters1[idx] = (GL.currentContext.tempVertexBufferCounters1[idx] + 1) & (GL.numTempVertexBuffersPerSize - 1); + var vbo = ringbuffer[nextFreeBufferIndex]; + if (vbo) { + return vbo; + } + var prevVBO = GLctx.getParameter(34964); + ringbuffer[nextFreeBufferIndex] = GLctx.createBuffer(); + GLctx.bindBuffer(34962, ringbuffer[nextFreeBufferIndex]); + GLctx.bufferData(34962, 1 << idx, 35048); + GLctx.bindBuffer(34962, prevVBO); + return ringbuffer[nextFreeBufferIndex]; + }, + getTempIndexBuffer: sizeBytes => { + var idx = GL.log2ceilLookup(sizeBytes); + var ibo = GL.currentContext.tempIndexBuffers[idx]; + if (ibo) { + return ibo; + } + var prevIBO = GLctx.getParameter(34965); + GL.currentContext.tempIndexBuffers[idx] = GLctx.createBuffer(); + GLctx.bindBuffer(34963, GL.currentContext.tempIndexBuffers[idx]); + GLctx.bufferData(34963, 1 << idx, 35048); + GLctx.bindBuffer(34963, prevIBO); + return GL.currentContext.tempIndexBuffers[idx]; + }, + newRenderingFrameStarted: () => { + if (!GL.currentContext) { + return; + } + var vb = GL.currentContext.tempVertexBuffers1; + GL.currentContext.tempVertexBuffers1 = GL.currentContext.tempVertexBuffers2; + GL.currentContext.tempVertexBuffers2 = vb; + vb = GL.currentContext.tempVertexBufferCounters1; + GL.currentContext.tempVertexBufferCounters1 = GL.currentContext.tempVertexBufferCounters2; + GL.currentContext.tempVertexBufferCounters2 = vb; + var largestIndex = GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE); + for (var i = 0; i <= largestIndex; ++i) { + GL.currentContext.tempVertexBufferCounters1[i] = 0; + } + }, + getSource: (shader, count, string, length) => { + var source = ""; + for (var i = 0; i < count; ++i) { + var len = length ? HEAPU32[(((length) + (i * 4)) >> 2)] : undefined; + source += UTF8ToString(HEAPU32[(((string) + (i * 4)) >> 2)], len); + } + return source; + }, + calcBufLength: (size, type, stride, count) => { + if (stride > 0) { + return count * stride; + } + var typeSize = GL.byteSizeByType[type - GL.byteSizeByTypeRoot]; + return size * typeSize * count; + }, + usedTempBuffers: [], + preDrawHandleClientVertexAttribBindings: count => { + GL.resetBufferBinding = false; + // TODO: initial pass to detect ranges we need to upload, might not need + // an upload per attrib + for (var i = 0; i < GL.currentContext.maxVertexAttribs; ++i) { + var cb = GL.currentContext.clientBuffers[i]; + if (!cb.clientside || !cb.enabled) continue; + GL.resetBufferBinding = true; + var size = GL.calcBufLength(cb.size, cb.type, cb.stride, count); + var buf = GL.getTempVertexBuffer(size); + GLctx.bindBuffer(34962, buf); + GLctx.bufferSubData(34962, 0, HEAPU8.subarray(cb.ptr, cb.ptr + size)); + cb.vertexAttribPointerAdaptor.call(GLctx, i, cb.size, cb.type, cb.normalized, cb.stride, 0); + } + }, + postDrawHandleClientVertexAttribBindings: () => { + if (GL.resetBufferBinding) { + GLctx.bindBuffer(34962, GL.buffers[GLctx.currentArrayBufferBinding]); + } + }, + createContext: (/** @type {HTMLCanvasElement} */ canvas, webGLContextAttributes) => { + // BUG: Workaround Safari WebGL issue: After successfully acquiring WebGL + // context on a canvas, calling .getContext() will always return that + // context independent of which 'webgl' or 'webgl2' + // context version was passed. See: + // https://webkit.org/b/222758 + // and: + // https://github.com/emscripten-core/emscripten/issues/13295. + // TODO: Once the bug is fixed and shipped in Safari, adjust the Safari + // version field in above check. + if (!canvas.getContextSafariWebGL2Fixed) { + canvas.getContextSafariWebGL2Fixed = canvas.getContext; + /** @type {function(this:HTMLCanvasElement, string, (Object|null)=): (Object|null)} */ function fixedGetContext(ver, attrs) { + var gl = canvas.getContextSafariWebGL2Fixed(ver, attrs); + return ((ver == "webgl") == (gl instanceof WebGLRenderingContext)) ? gl : null; + } + canvas.getContext = fixedGetContext; + } + var ctx = (webGLContextAttributes.majorVersion > 1) ? canvas.getContext("webgl2", webGLContextAttributes) : canvas.getContext("webgl", webGLContextAttributes); + if (!ctx) return 0; + var handle = GL.registerContext(ctx, webGLContextAttributes); + return handle; + }, + registerContext: (ctx, webGLContextAttributes) => { + // without pthreads a context is just an integer ID + var handle = GL.getNewId(GL.contexts); + var context = { + handle, + attributes: webGLContextAttributes, + version: webGLContextAttributes.majorVersion, + GLctx: ctx + }; + // Store the created context object so that we can access the context + // given a canvas without having to pass the parameters again. + if (ctx.canvas) ctx.canvas.GLctxObject = context; + GL.contexts[handle] = context; + if (typeof webGLContextAttributes.enableExtensionsByDefault == "undefined" || webGLContextAttributes.enableExtensionsByDefault) { + GL.initExtensions(context); + } + context.maxVertexAttribs = context.GLctx.getParameter(34921); + context.clientBuffers = []; + for (var i = 0; i < context.maxVertexAttribs; i++) { + context.clientBuffers[i] = { + enabled: false, + clientside: false, + size: 0, + type: 0, + normalized: 0, + stride: 0, + ptr: 0, + vertexAttribPointerAdaptor: null + }; + } + GL.generateTempBuffers(false, context); + return handle; + }, + makeContextCurrent: contextHandle => { + // Active Emscripten GL layer context object. + GL.currentContext = GL.contexts[contextHandle]; + // Active WebGL context object. + Module["ctx"] = GLctx = GL.currentContext?.GLctx; + return !(contextHandle && !GLctx); + }, + getContext: contextHandle => GL.contexts[contextHandle], + deleteContext: contextHandle => { + if (GL.currentContext === GL.contexts[contextHandle]) { + GL.currentContext = null; + } + if (typeof JSEvents == "object") { + // Release all JS event handlers on the DOM element that the GL context is + // associated with since the context is now deleted. + JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas); + } + // Make sure the canvas object no longer refers to the context object so + // there are no GC surprises. + if (GL.contexts[contextHandle]?.GLctx.canvas) { + GL.contexts[contextHandle].GLctx.canvas.GLctxObject = undefined; + } + GL.contexts[contextHandle] = null; + }, + initExtensions: context => { + // If this function is called without a specific context object, init the + // extensions of the currently active context. + context ||= GL.currentContext; + if (context.initExtensionsDone) return; + context.initExtensionsDone = true; + var GLctx = context.GLctx; + // Detect the presence of a few extensions manually, since the GL interop + // layer itself will need to know if they exist. + // Extensions that are available in both WebGL 1 and WebGL 2 + webgl_enable_WEBGL_multi_draw(GLctx); + webgl_enable_EXT_polygon_offset_clamp(GLctx); + webgl_enable_EXT_clip_control(GLctx); + webgl_enable_WEBGL_polygon_mode(GLctx); + // Extensions that are only available in WebGL 1 (the calls will be no-ops + // if called on a WebGL 2 context active) + webgl_enable_ANGLE_instanced_arrays(GLctx); + webgl_enable_OES_vertex_array_object(GLctx); + webgl_enable_WEBGL_draw_buffers(GLctx); + // Extensions that are available from WebGL >= 2 (no-op if called on a WebGL 1 context active) + webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx); + webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx); + // On WebGL 2, EXT_disjoint_timer_query is replaced with an alternative + // that's based on core APIs, and exposes only the queryCounterEXT() + // entrypoint. + if (context.version >= 2) { + GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query_webgl2"); + } + // However, Firefox exposes the WebGL 1 version on WebGL 2 as well and + // thus we look for the WebGL 1 version again if the WebGL 2 version + // isn't present. https://bugzil.la/1328882 + if (context.version < 2 || !GLctx.disjointTimerQueryExt) { + GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query"); + } + for (var ext of getEmscriptenSupportedExtensions(GLctx)) { + // WEBGL_lose_context, WEBGL_debug_renderer_info and WEBGL_debug_shaders + // are not enabled by default. + if (!ext.includes("lose_context") && !ext.includes("debug")) { + // Call .getExtension() to enable that extension permanently. + GLctx.getExtension(ext); + } + } + } +}; + +var webglPowerPreferences = [ "default", "low-power", "high-performance" ]; + +/** @type {Object} */ var specialHTMLTargets = [ 0, globalThis.document ?? 0, globalThis.window ?? 0 ]; + +var findEventTarget = target => { + // The sensible "default" target varies between events, but use window as the default + // since DOM events mostly can default to that. Specific callback registrations + // override their own defaults. + if (!target) return window; + if (typeof target == "number") target = specialHTMLTargets[target] || UTF8ToString(target); + if (target === "#window") return window; else if (target === "#document") return document; else if (target === "#screen") return screen; else if (target === "#canvas") return Module["canvas"]; else if (typeof target == "string") return globalThis.document?.getElementById(target); + return target; +}; + +var findCanvasEventTarget = target => { + if (typeof target == "number") target = UTF8ToString(target); + if (!target || target === "#canvas") { + if (typeof GL != "undefined" && GL.offscreenCanvases["canvas"]) return GL.offscreenCanvases["canvas"]; + // TODO: Remove this line, target '#canvas' should refer only to Module['canvas'], not to GL.offscreenCanvases['canvas'] - but need stricter tests to be able to remove this line. + return Module["canvas"]; + } + if (typeof GL != "undefined" && GL.offscreenCanvases[target]) return GL.offscreenCanvases[target]; + return findEventTarget(target); +}; + +var _emscripten_webgl_do_create_context = (target, attributes) => { + var attr32 = ((attributes) >> 2); + var powerPreference = HEAP32[attr32 + (8 >> 2)]; + var contextAttributes = { + "alpha": !!HEAP8[attributes + 0], + "depth": !!HEAP8[attributes + 1], + "stencil": !!HEAP8[attributes + 2], + "antialias": !!HEAP8[attributes + 3], + "premultipliedAlpha": !!HEAP8[attributes + 4], + "preserveDrawingBuffer": !!HEAP8[attributes + 5], + "powerPreference": webglPowerPreferences[powerPreference], + "failIfMajorPerformanceCaveat": !!HEAP8[attributes + 12], + // The following are not predefined WebGL context attributes in the WebGL specification, so the property names can be minified by Closure. + majorVersion: HEAP32[attr32 + (16 >> 2)], + minorVersion: HEAP32[attr32 + (20 >> 2)], + enableExtensionsByDefault: HEAP8[attributes + 24], + explicitSwapControl: HEAP8[attributes + 25], + proxyContextToMainThread: HEAP32[attr32 + (28 >> 2)], + renderViaOffscreenBackBuffer: HEAP8[attributes + 32] + }; + var canvas = findCanvasEventTarget(target); + if (!canvas) { + return 0; + } + if (contextAttributes.explicitSwapControl) { + return 0; + } + var contextHandle = GL.createContext(canvas, contextAttributes); + return contextHandle; +}; + +var _emscripten_webgl_create_context = _emscripten_webgl_do_create_context; + +var _emscripten_webgl_destroy_context = contextHandle => { + if (GL.currentContext == contextHandle) GL.currentContext = 0; + GL.deleteContext(contextHandle); +}; + +var _emscripten_webgl_get_context_attributes = (c, a) => { + if (!a) return -5; + c = GL.contexts[c]; + if (!c) return -3; + var t = c.GLctx?.getContextAttributes(); + if (!t) return -3; + HEAP8[a] = t.alpha; + HEAP8[(a) + (1)] = t.depth; + HEAP8[(a) + (2)] = t.stencil; + HEAP8[(a) + (3)] = t.antialias; + HEAP8[(a) + (4)] = t.premultipliedAlpha; + HEAP8[(a) + (5)] = t.preserveDrawingBuffer; + var power = t["powerPreference"] && webglPowerPreferences.indexOf(t["powerPreference"]); + HEAP32[(((a) + (8)) >> 2)] = power; + HEAP8[(a) + (12)] = t.failIfMajorPerformanceCaveat; + HEAP32[(((a) + (16)) >> 2)] = c.version; + HEAP32[(((a) + (20)) >> 2)] = 0; + HEAP8[(a) + (24)] = c.attributes.enableExtensionsByDefault; + return 0; +}; + +var _emscripten_webgl_do_get_current_context = () => GL.currentContext ? GL.currentContext.handle : 0; + +var _emscripten_webgl_get_current_context = _emscripten_webgl_do_get_current_context; + +var _emscripten_webgl_make_context_current = contextHandle => { + var success = GL.makeContextCurrent(contextHandle); + return success ? 0 : -5; +}; + +var stackAlloc = sz => __emscripten_stack_alloc(sz); + +var stringToUTF8OnStack = str => { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8(str, ret, size); + return ret; +}; + +var writeI53ToI64 = (ptr, num) => { + HEAPU32[((ptr) >> 2)] = num; + var lower = HEAPU32[((ptr) >> 2)]; + HEAPU32[(((ptr) + (4)) >> 2)] = (num - lower) / 4294967296; +}; + +var readI53FromI64 = ptr => HEAPU32[((ptr) >> 2)] + HEAP32[(((ptr) + (4)) >> 2)] * 4294967296; + +var wasmTableMirror = []; + +var getWasmTableEntry = funcPtr => { + var func = wasmTableMirror[funcPtr]; + if (!func) { + /** @suppress {checkTypes} */ wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr); + } + return func; +}; + +var WebGPU = { + Internals: { + jsObjects: [], + jsObjectInsert: (ptr, jsObject) => { + ptr >>>= 0; + WebGPU.Internals.jsObjects[ptr] = jsObject; + }, + bufferOnUnmaps: [], + futures: [], + futureInsert: (futureId, promise) => {} + }, + getJsObject: ptr => { + if (!ptr) return undefined; + ptr >>>= 0; + return WebGPU.Internals.jsObjects[ptr]; + }, + importJsAdapter: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateAdapter(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBindGroup: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateBindGroup(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBindGroupLayout: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateBindGroupLayout(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBuffer: (buffer, parentPtr = 0) => { + // At the moment, we do not allow importing pending buffers. + assert(buffer.mapState === "unmapped"); + var bufferPtr = _emwgpuImportBuffer(parentPtr); + WebGPU.Internals.jsObjectInsert(bufferPtr, buffer); + return bufferPtr; + }, + importJsCommandBuffer: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateCommandBuffer(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsCommandEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateCommandEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsComputePassEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateComputePassEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsComputePipeline: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateComputePipeline(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsDevice: (device, parentPtr = 0) => { + var queuePtr = _emwgpuCreateQueue(parentPtr); + var devicePtr = _emwgpuCreateDevice(parentPtr, queuePtr); + WebGPU.Internals.jsObjectInsert(queuePtr, device.queue); + WebGPU.Internals.jsObjectInsert(devicePtr, device); + return devicePtr; + }, + importJsExternalTexture: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateExternalTexture(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsPipelineLayout: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreatePipelineLayout(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsQuerySet: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateQuerySet(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsQueue: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateQueue(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderBundle: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderBundle(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderBundleEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderBundleEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderPassEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderPassEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderPipeline: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderPipeline(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsSampler: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateSampler(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsShaderModule: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateShaderModule(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsSurface: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateSurface(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsTexture: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateTexture(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsTextureView: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateTextureView(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + errorCallback: (callback, type, message, userdata) => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(message); + getWasmTableEntry(callback)(type, messagePtr, userdata); + stackRestore(sp); + }, + iterateExtensions: (root, handlers) => { + for (var ptr = HEAPU32[((root) >> 2)]; ptr; ptr = HEAPU32[((ptr) >> 2)]) { + var sType = HEAP32[(((ptr) + (4)) >> 2)]; + // This will crash if there's no handler indicating either a bogus + // sType, or one we haven't implemented yet. + var handler = handlers[sType](ptr); + } + }, + setStringView: (ptr, data, length) => { + HEAPU32[((ptr) >> 2)] = data; + HEAPU32[(((ptr) + (4)) >> 2)] = length; + }, + makeStringFromStringView: stringViewPtr => { + var ptr = HEAPU32[((stringViewPtr) >> 2)]; + var length = HEAPU32[(((stringViewPtr) + (4)) >> 2)]; + // UTF8ToString stops at the first null terminator character in the + // string regardless of the length. + return UTF8ToString(ptr, length); + }, + makeStringFromOptionalStringView: stringViewPtr => { + var ptr = HEAPU32[((stringViewPtr) >> 2)]; + var length = HEAPU32[(((stringViewPtr) + (4)) >> 2)]; + // If we don't have a valid string pointer, just return undefined when + // optional. + if (!ptr) { + if (length === 0) { + return ""; + } + return undefined; + } + // UTF8ToString stops at the first null terminator character in the + // string regardless of the length. + return UTF8ToString(ptr, length); + }, + makeColor: ptr => ({ + "r": HEAPF64[((ptr) >> 3)], + "g": HEAPF64[(((ptr) + (8)) >> 3)], + "b": HEAPF64[(((ptr) + (16)) >> 3)], + "a": HEAPF64[(((ptr) + (24)) >> 3)] + }), + makeExtent3D: ptr => ({ + "width": HEAPU32[((ptr) >> 2)], + "height": HEAPU32[(((ptr) + (4)) >> 2)], + "depthOrArrayLayers": HEAPU32[(((ptr) + (8)) >> 2)] + }), + makeOrigin3D: ptr => ({ + "x": HEAPU32[((ptr) >> 2)], + "y": HEAPU32[(((ptr) + (4)) >> 2)], + "z": HEAPU32[(((ptr) + (8)) >> 2)] + }), + makeTexelCopyTextureInfo: ptr => ({ + "texture": WebGPU.getJsObject(HEAPU32[((ptr) >> 2)]), + "mipLevel": HEAPU32[(((ptr) + (4)) >> 2)], + "origin": WebGPU.makeOrigin3D(ptr + 8), + "aspect": WebGPU.TextureAspect[HEAP32[(((ptr) + (20)) >> 2)]] + }), + makeTexelCopyBufferLayout: ptr => { + var bytesPerRow = HEAPU32[(((ptr) + (8)) >> 2)]; + var rowsPerImage = HEAPU32[(((ptr) + (12)) >> 2)]; + return { + "offset": readI53FromI64(ptr), + "bytesPerRow": bytesPerRow === 4294967295 ? undefined : bytesPerRow, + "rowsPerImage": rowsPerImage === 4294967295 ? undefined : rowsPerImage + }; + }, + makeTexelCopyBufferInfo: ptr => { + var layoutPtr = ptr + 0; + var bufferCopyView = WebGPU.makeTexelCopyBufferLayout(layoutPtr); + bufferCopyView["buffer"] = WebGPU.getJsObject(HEAPU32[(((ptr) + (16)) >> 2)]); + return bufferCopyView; + }, + makePassTimestampWrites: ptr => { + if (ptr === 0) return undefined; + return { + "querySet": WebGPU.getJsObject(HEAPU32[(((ptr) + (4)) >> 2)]), + "beginningOfPassWriteIndex": HEAPU32[(((ptr) + (8)) >> 2)], + "endOfPassWriteIndex": HEAPU32[(((ptr) + (12)) >> 2)] + }; + }, + makePipelineConstants: (constantCount, constantsPtr) => { + if (!constantCount) return; + var constants = {}; + for (var i = 0; i < constantCount; ++i) { + var entryPtr = constantsPtr + 24 * i; + var key = WebGPU.makeStringFromStringView(entryPtr + 4); + constants[key] = HEAPF64[(((entryPtr) + (16)) >> 3)]; + } + return constants; + }, + makePipelineLayout: layoutPtr => { + if (!layoutPtr) return "auto"; + return WebGPU.getJsObject(layoutPtr); + }, + makeComputeState: ptr => { + if (!ptr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((ptr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((ptr) + (16)) >> 2)], HEAPU32[(((ptr) + (20)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(ptr + 8) + }; + return desc; + }, + makeComputePipelineDesc: descriptor => { + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.makePipelineLayout(HEAPU32[(((descriptor) + (12)) >> 2)]), + "compute": WebGPU.makeComputeState(descriptor + 16) + }; + return desc; + }, + makeRenderPipelineDesc: descriptor => { + function makePrimitiveState(psPtr) { + if (!psPtr) return undefined; + return { + "topology": WebGPU.PrimitiveTopology[HEAP32[(((psPtr) + (4)) >> 2)]], + "stripIndexFormat": WebGPU.IndexFormat[HEAP32[(((psPtr) + (8)) >> 2)]], + "frontFace": WebGPU.FrontFace[HEAP32[(((psPtr) + (12)) >> 2)]], + "cullMode": WebGPU.CullMode[HEAP32[(((psPtr) + (16)) >> 2)]], + "unclippedDepth": !!(HEAPU32[(((psPtr) + (20)) >> 2)]) + }; + } + function makeBlendComponent(bdPtr) { + if (!bdPtr) return undefined; + return { + "operation": WebGPU.BlendOperation[HEAP32[((bdPtr) >> 2)]], + "srcFactor": WebGPU.BlendFactor[HEAP32[(((bdPtr) + (4)) >> 2)]], + "dstFactor": WebGPU.BlendFactor[HEAP32[(((bdPtr) + (8)) >> 2)]] + }; + } + function makeBlendState(bsPtr) { + if (!bsPtr) return undefined; + return { + "alpha": makeBlendComponent(bsPtr + 12), + "color": makeBlendComponent(bsPtr + 0) + }; + } + function makeColorState(csPtr) { + var format = WebGPU.TextureFormat[HEAP32[(((csPtr) + (4)) >> 2)]]; + return format ? { + "format": format, + "blend": makeBlendState(HEAPU32[(((csPtr) + (8)) >> 2)]), + "writeMask": HEAPU32[(((csPtr) + (16)) >> 2)] + } : undefined; + } + function makeColorStates(count, csArrayPtr) { + var states = []; + for (var i = 0; i < count; ++i) { + states.push(makeColorState(csArrayPtr + 24 * i)); + } + return states; + } + function makeStencilStateFace(ssfPtr) { + return { + "compare": WebGPU.CompareFunction[HEAP32[((ssfPtr) >> 2)]], + "failOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (4)) >> 2)]], + "depthFailOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (8)) >> 2)]], + "passOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (12)) >> 2)]] + }; + } + function makeDepthStencilState(dssPtr) { + if (!dssPtr) return undefined; + return { + "format": WebGPU.TextureFormat[HEAP32[(((dssPtr) + (4)) >> 2)]], + "depthWriteEnabled": !!(HEAPU32[(((dssPtr) + (8)) >> 2)]), + "depthCompare": WebGPU.CompareFunction[HEAP32[(((dssPtr) + (12)) >> 2)]], + "stencilFront": makeStencilStateFace(dssPtr + 16), + "stencilBack": makeStencilStateFace(dssPtr + 32), + "stencilReadMask": HEAPU32[(((dssPtr) + (48)) >> 2)], + "stencilWriteMask": HEAPU32[(((dssPtr) + (52)) >> 2)], + "depthBias": HEAP32[(((dssPtr) + (56)) >> 2)], + "depthBiasSlopeScale": HEAPF32[(((dssPtr) + (60)) >> 2)], + "depthBiasClamp": HEAPF32[(((dssPtr) + (64)) >> 2)] + }; + } + function makeVertexAttribute(vaPtr) { + return { + "format": WebGPU.VertexFormat[HEAP32[(((vaPtr) + (4)) >> 2)]], + "offset": readI53FromI64((vaPtr) + (8)), + "shaderLocation": HEAPU32[(((vaPtr) + (16)) >> 2)] + }; + } + function makeVertexAttributes(count, vaArrayPtr) { + var vas = []; + for (var i = 0; i < count; ++i) { + vas.push(makeVertexAttribute(vaArrayPtr + i * 24)); + } + return vas; + } + function makeVertexBuffer(vbPtr) { + if (!vbPtr) return undefined; + var stepMode = WebGPU.VertexStepMode[HEAP32[(((vbPtr) + (4)) >> 2)]]; + var attributeCount = HEAPU32[(((vbPtr) + (16)) >> 2)]; + if (!stepMode && !attributeCount) { + return null; + } + return { + "arrayStride": readI53FromI64((vbPtr) + (8)), + "stepMode": stepMode, + "attributes": makeVertexAttributes(attributeCount, HEAPU32[(((vbPtr) + (20)) >> 2)]) + }; + } + function makeVertexBuffers(count, vbArrayPtr) { + if (!count) return undefined; + var vbs = []; + for (var i = 0; i < count; ++i) { + vbs.push(makeVertexBuffer(vbArrayPtr + i * 24)); + } + return vbs; + } + function makeVertexState(viPtr) { + if (!viPtr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((viPtr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((viPtr) + (16)) >> 2)], HEAPU32[(((viPtr) + (20)) >> 2)]), + "buffers": makeVertexBuffers(HEAPU32[(((viPtr) + (24)) >> 2)], HEAPU32[(((viPtr) + (28)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(viPtr + 8) + }; + return desc; + } + function makeMultisampleState(msPtr) { + if (!msPtr) return undefined; + return { + "count": HEAPU32[(((msPtr) + (4)) >> 2)], + "mask": HEAPU32[(((msPtr) + (8)) >> 2)], + "alphaToCoverageEnabled": !!(HEAPU32[(((msPtr) + (12)) >> 2)]) + }; + } + function makeFragmentState(fsPtr) { + if (!fsPtr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((fsPtr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((fsPtr) + (16)) >> 2)], HEAPU32[(((fsPtr) + (20)) >> 2)]), + "targets": makeColorStates(HEAPU32[(((fsPtr) + (24)) >> 2)], HEAPU32[(((fsPtr) + (28)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(fsPtr + 8) + }; + return desc; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.makePipelineLayout(HEAPU32[(((descriptor) + (12)) >> 2)]), + "vertex": makeVertexState(descriptor + 16), + "primitive": makePrimitiveState(descriptor + 48), + "depthStencil": makeDepthStencilState(HEAPU32[(((descriptor) + (72)) >> 2)]), + "multisample": makeMultisampleState(descriptor + 76), + "fragment": makeFragmentState(HEAPU32[(((descriptor) + (92)) >> 2)]) + }; + return desc; + }, + fillLimitStruct: (limits, limitsOutPtr) => { + var nextInChainPtr = HEAPU32[((limitsOutPtr) >> 2)]; + function setLimitValueU32(name, basePtr, limitOffset, fallbackValue = 0) { + var limitValue = limits[name] ?? fallbackValue; + HEAPU32[(((basePtr) + (limitOffset)) >> 2)] = limitValue; + } + function setLimitValueU64(name, basePtr, limitOffset, fallbackValue = 0) { + var limitValue = limits[name] ?? fallbackValue; + // Limits are integer-valued JS `Number`s, so they fit in 'i53'. + writeI53ToI64((basePtr) + (limitOffset), limitValue); + } + setLimitValueU32("maxTextureDimension1D", limitsOutPtr, 4); + setLimitValueU32("maxTextureDimension2D", limitsOutPtr, 8); + setLimitValueU32("maxTextureDimension3D", limitsOutPtr, 12); + setLimitValueU32("maxTextureArrayLayers", limitsOutPtr, 16); + setLimitValueU32("maxBindGroups", limitsOutPtr, 20); + setLimitValueU32("maxBindGroupsPlusVertexBuffers", limitsOutPtr, 24); + setLimitValueU32("maxBindingsPerBindGroup", limitsOutPtr, 28); + setLimitValueU32("maxDynamicUniformBuffersPerPipelineLayout", limitsOutPtr, 32); + setLimitValueU32("maxDynamicStorageBuffersPerPipelineLayout", limitsOutPtr, 36); + setLimitValueU32("maxSampledTexturesPerShaderStage", limitsOutPtr, 40); + setLimitValueU32("maxSamplersPerShaderStage", limitsOutPtr, 44); + setLimitValueU32("maxStorageBuffersPerShaderStage", limitsOutPtr, 48); + setLimitValueU32("maxStorageTexturesPerShaderStage", limitsOutPtr, 52); + setLimitValueU32("maxUniformBuffersPerShaderStage", limitsOutPtr, 56); + setLimitValueU32("minUniformBufferOffsetAlignment", limitsOutPtr, 80); + setLimitValueU32("minStorageBufferOffsetAlignment", limitsOutPtr, 84); + setLimitValueU64("maxUniformBufferBindingSize", limitsOutPtr, 64); + setLimitValueU64("maxStorageBufferBindingSize", limitsOutPtr, 72); + setLimitValueU32("maxVertexBuffers", limitsOutPtr, 88); + setLimitValueU64("maxBufferSize", limitsOutPtr, 96); + setLimitValueU32("maxVertexAttributes", limitsOutPtr, 104); + setLimitValueU32("maxVertexBufferArrayStride", limitsOutPtr, 108); + setLimitValueU32("maxInterStageShaderVariables", limitsOutPtr, 112); + setLimitValueU32("maxColorAttachments", limitsOutPtr, 116); + setLimitValueU32("maxColorAttachmentBytesPerSample", limitsOutPtr, 120); + setLimitValueU32("maxComputeWorkgroupStorageSize", limitsOutPtr, 124); + setLimitValueU32("maxComputeInvocationsPerWorkgroup", limitsOutPtr, 128); + setLimitValueU32("maxComputeWorkgroupSizeX", limitsOutPtr, 132); + setLimitValueU32("maxComputeWorkgroupSizeY", limitsOutPtr, 136); + setLimitValueU32("maxComputeWorkgroupSizeZ", limitsOutPtr, 140); + setLimitValueU32("maxComputeWorkgroupsPerDimension", limitsOutPtr, 144); + // Note this limit is new and won't be present in all browsers for a while. Fall back to 0. + setLimitValueU32("maxImmediateSize", limitsOutPtr, 148); + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var compatibilityModeLimitsPtr = nextInChainPtr; + // Note these limits are new and won't be present in all browsers for a while. Fall back to exposing the PerShaderStage limit. + setLimitValueU32("maxStorageBuffersInVertexStage", compatibilityModeLimitsPtr, 8, limits.maxStorageBuffersPerShaderStage); + setLimitValueU32("maxStorageBuffersInFragmentStage", compatibilityModeLimitsPtr, 16, limits.maxStorageBuffersPerShaderStage); + setLimitValueU32("maxStorageTexturesInVertexStage", compatibilityModeLimitsPtr, 12, limits.maxStorageTexturesPerShaderStage); + setLimitValueU32("maxStorageTexturesInFragmentStage", compatibilityModeLimitsPtr, 20, limits.maxStorageTexturesPerShaderStage); + } + }, + fillAdapterInfoStruct: (info, infoStruct) => { + // Populate subgroup limits. + HEAPU32[(((infoStruct) + (52)) >> 2)] = info.subgroupMinSize; + HEAPU32[(((infoStruct) + (56)) >> 2)] = info.subgroupMaxSize; + // Append all the strings together to condense into a single malloc. + var strs = info.vendor + info.architecture + info.device + info.description; + var strPtr = stringToNewUTF8(strs); + var vendorLen = lengthBytesUTF8(info.vendor); + WebGPU.setStringView(infoStruct + 4, strPtr, vendorLen); + strPtr += vendorLen; + var architectureLen = lengthBytesUTF8(info.architecture); + WebGPU.setStringView(infoStruct + 12, strPtr, architectureLen); + strPtr += architectureLen; + var deviceLen = lengthBytesUTF8(info.device); + WebGPU.setStringView(infoStruct + 20, strPtr, deviceLen); + strPtr += deviceLen; + var descriptionLen = lengthBytesUTF8(info.description); + WebGPU.setStringView(infoStruct + 28, strPtr, descriptionLen); + strPtr += descriptionLen; + HEAP32[(((infoStruct) + (36)) >> 2)] = 2; + var adapterType = info.isFallbackAdapter ? 3 : 4; + HEAP32[(((infoStruct) + (40)) >> 2)] = adapterType; + HEAPU32[(((infoStruct) + (44)) >> 2)] = 0; + HEAPU32[(((infoStruct) + (48)) >> 2)] = 0; + }, + AddressMode: [ , "clamp-to-edge", "repeat", "mirror-repeat" ], + BlendFactor: [ , "zero", "one", "src", "one-minus-src", "src-alpha", "one-minus-src-alpha", "dst", "one-minus-dst", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated", "constant", "one-minus-constant", "src1", "one-minus-src1", "src1-alpha", "one-minus-src1-alpha" ], + BlendOperation: [ , "add", "subtract", "reverse-subtract", "min", "max" ], + BufferBindingType: [ , , "uniform", "storage", "read-only-storage" ], + BufferMapState: [ , "unmapped", "pending", "mapped" ], + CompareFunction: [ , "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always" ], + CompilationInfoRequestStatus: [ , "success", "callback-cancelled" ], + ComponentSwizzle: [ , "0", "1", "r", "g", "b", "a" ], + CompositeAlphaMode: [ , "opaque", "premultiplied", "unpremultiplied", "inherit" ], + CullMode: [ , "none", "front", "back" ], + ErrorFilter: [ , "validation", "out-of-memory", "internal" ], + FeatureLevel: [ , "compatibility", "core" ], + FeatureName: { + 1: "core-features-and-limits", + 2: "depth-clip-control", + 3: "depth32float-stencil8", + 4: "texture-compression-bc", + 5: "texture-compression-bc-sliced-3d", + 6: "texture-compression-etc2", + 7: "texture-compression-astc", + 8: "texture-compression-astc-sliced-3d", + 9: "timestamp-query", + 10: "indirect-first-instance", + 11: "shader-f16", + 12: "rg11b10ufloat-renderable", + 13: "bgra8unorm-storage", + 14: "float32-filterable", + 15: "float32-blendable", + 16: "clip-distances", + 17: "dual-source-blending", + 18: "subgroups", + 19: "texture-formats-tier1", + 20: "texture-formats-tier2", + 21: "primitive-index", + 22: "texture-component-swizzle", + 327692: "chromium-experimental-unorm16-texture-formats", + 327729: "chromium-experimental-multi-draw-indirect" + }, + FilterMode: [ , "nearest", "linear" ], + FrontFace: [ , "ccw", "cw" ], + IndexFormat: [ , "uint16", "uint32" ], + InstanceFeatureName: [ , "timed-wait-any", "shader-source-spirv", "multiple-devices-per-adapter" ], + LoadOp: [ , "load", "clear" ], + MipmapFilterMode: [ , "nearest", "linear" ], + OptionalBool: [ "false", "true" ], + PowerPreference: [ , "low-power", "high-performance" ], + PredefinedColorSpace: [ , "srgb", "display-p3" ], + PrimitiveTopology: [ , "point-list", "line-list", "line-strip", "triangle-list", "triangle-strip" ], + QueryType: [ , "occlusion", "timestamp" ], + SamplerBindingType: [ , , "filtering", "non-filtering", "comparison" ], + Status: [ , "success", "error" ], + StencilOperation: [ , "keep", "zero", "replace", "invert", "increment-clamp", "decrement-clamp", "increment-wrap", "decrement-wrap" ], + StorageTextureAccess: [ , , "write-only", "read-only", "read-write" ], + StoreOp: [ , "store", "discard" ], + SurfaceGetCurrentTextureStatus: [ , "success-optimal", "success-suboptimal", "timeout", "outdated", "lost", "error" ], + TextureAspect: [ , "all", "stencil-only", "depth-only" ], + TextureDimension: [ , "1d", "2d", "3d" ], + TextureFormat: [ , "r8unorm", "r8snorm", "r8uint", "r8sint", "r16unorm", "r16snorm", "r16uint", "r16sint", "r16float", "rg8unorm", "rg8snorm", "rg8uint", "rg8sint", "r32float", "r32uint", "r32sint", "rg16unorm", "rg16snorm", "rg16uint", "rg16sint", "rg16float", "rgba8unorm", "rgba8unorm-srgb", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rgb9e5ufloat", "rg32float", "rg32uint", "rg32sint", "rgba16unorm", "rgba16snorm", "rgba16uint", "rgba16sint", "rgba16float", "rgba32float", "rgba32uint", "rgba32sint", "stencil8", "depth16unorm", "depth24plus", "depth24plus-stencil8", "depth32float", "depth32float-stencil8", "bc1-rgba-unorm", "bc1-rgba-unorm-srgb", "bc2-rgba-unorm", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm", "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb", "etc2-rgb8unorm", "etc2-rgb8unorm-srgb", "etc2-rgb8a1unorm", "etc2-rgb8a1unorm-srgb", "etc2-rgba8unorm", "etc2-rgba8unorm-srgb", "eac-r11unorm", "eac-r11snorm", "eac-rg11unorm", "eac-rg11snorm", "astc-4x4-unorm", "astc-4x4-unorm-srgb", "astc-5x4-unorm", "astc-5x4-unorm-srgb", "astc-5x5-unorm", "astc-5x5-unorm-srgb", "astc-6x5-unorm", "astc-6x5-unorm-srgb", "astc-6x6-unorm", "astc-6x6-unorm-srgb", "astc-8x5-unorm", "astc-8x5-unorm-srgb", "astc-8x6-unorm", "astc-8x6-unorm-srgb", "astc-8x8-unorm", "astc-8x8-unorm-srgb", "astc-10x5-unorm", "astc-10x5-unorm-srgb", "astc-10x6-unorm", "astc-10x6-unorm-srgb", "astc-10x8-unorm", "astc-10x8-unorm-srgb", "astc-10x10-unorm", "astc-10x10-unorm-srgb", "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", "astc-12x12-unorm-srgb" ], + TextureSampleType: [ , , "float", "unfilterable-float", "depth", "sint", "uint" ], + TextureViewDimension: [ , "1d", "2d", "2d-array", "cube", "cube-array", "3d" ], + ToneMappingMode: [ , "standard", "extended" ], + VertexFormat: [ , "uint8", "uint8x2", "uint8x4", "sint8", "sint8x2", "sint8x4", "unorm8", "unorm8x2", "unorm8x4", "snorm8", "snorm8x2", "snorm8x4", "uint16", "uint16x2", "uint16x4", "sint16", "sint16x2", "sint16x4", "unorm16", "unorm16x2", "unorm16x4", "snorm16", "snorm16x2", "snorm16x4", "float16", "float16x2", "float16x4", "float32", "float32x2", "float32x3", "float32x4", "uint32", "uint32x2", "uint32x3", "uint32x4", "sint32", "sint32x2", "sint32x3", "sint32x4", "unorm10-10-10-2", "unorm8x4-bgra" ], + VertexStepMode: [ , "vertex", "instance" ], + WGSLLanguageFeatureName: [ , "readonly_and_readwrite_storage_textures", "packed_4x8_integer_dot_product", "unrestricted_pointer_parameters", "pointer_composite_access", "uniform_buffer_standard_layout", "subgroup_id", "texture_and_sampler_let", "subgroup_uniformity", "texture_formats_tier1", "linear_indexing" ] +}; + +var _emscripten_webgpu_get_device = () => { + if (WebGPU.preinitializedDeviceId === undefined) { + WebGPU.preinitializedDeviceId = WebGPU.importJsDevice(Module["preinitializedWebGPUDevice"]); + // Some users depend on this keeping the device alive, so we add an + // additional reference when we first initialize it. + _wgpuDeviceAddRef(WebGPU.preinitializedDeviceId); + } + _wgpuDeviceAddRef(WebGPU.preinitializedDeviceId); + return WebGPU.preinitializedDeviceId; +}; + +var _emwgpuBufferDestroy = bufferPtr => { + var buffer = WebGPU.getJsObject(bufferPtr); + var onUnmap = WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + if (onUnmap) { + for (var i = 0; i < onUnmap.length; ++i) { + onUnmap[i](); + } + delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + } + buffer.destroy(); +}; + +var _emwgpuBufferGetMappedRange = (bufferPtr, offset, size) => { + var buffer = WebGPU.getJsObject(bufferPtr); + if (size == -1) size = undefined; + var mapped; + try { + mapped = buffer.getMappedRange(offset, size); + } catch (ex) { + return 0; + } + var data = _memalign(16, mapped.byteLength); + HEAPU8.fill(0, data, mapped.byteLength); + WebGPU.Internals.bufferOnUnmaps[bufferPtr].push(() => { + new Uint8Array(mapped).set(HEAPU8.subarray(data, data + mapped.byteLength)); + _free(data); + }); + return data; +}; + +var _emwgpuBufferUnmap = bufferPtr => { + var buffer = WebGPU.getJsObject(bufferPtr); + var onUnmap = WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + if (!onUnmap) { + // Already unmapped + return; + } + for (var i = 0; i < onUnmap.length; ++i) { + onUnmap[i](); + } + delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + buffer.unmap(); +}; + +var _emwgpuDelete = ptr => { + delete WebGPU.Internals.jsObjects[ptr]; +}; + +var _emwgpuDeviceCreateBuffer = (devicePtr, descriptor, bufferPtr) => { + var mappedAtCreation = !!(HEAPU32[(((descriptor) + (32)) >> 2)]); + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "usage": HEAPU32[(((descriptor) + (16)) >> 2)], + "size": readI53FromI64((descriptor) + (24)), + "mappedAtCreation": mappedAtCreation + }; + var device = WebGPU.getJsObject(devicePtr); + var buffer; + try { + buffer = device.createBuffer(desc); + } catch (ex) { + // The only exception should be RangeError if mapping at creation ran out of memory. + return false; + } + WebGPU.Internals.jsObjectInsert(bufferPtr, buffer); + if (mappedAtCreation) { + WebGPU.Internals.bufferOnUnmaps[bufferPtr] = []; + } + return true; +}; + +var _emwgpuDeviceCreateComputePipelineAsync = function(devicePtr, futureId_low, futureId_high, descriptor, pipelinePtr) { + var futureId = convertI32PairToI53Checked(futureId_low, futureId_high); + var desc = WebGPU.makeComputePipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + // createComputePipelineAsync + WebGPU.Internals.futureInsert(futureId, device.createComputePipelineAsync(desc).then(pipeline => { + // createComputePipelineAsync fulfilled + callUserCallback(() => { + WebGPU.Internals.jsObjectInsert(pipelinePtr, pipeline); + _emwgpuOnCreateComputePipelineCompleted(futureId, 1, pipelinePtr, 0); + }); + }, pipelineError => { + // createComputePipelineAsync rejected + callUserCallback(() => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(pipelineError.message); + var status = pipelineError.reason === "validation" ? 3 : pipelineError.reason === "internal" ? 4 : 0; + _emwgpuOnCreateComputePipelineCompleted(futureId, status, pipelinePtr, messagePtr); + stackRestore(sp); + }); + })); +}; + +var _emwgpuDeviceCreateRenderPipelineAsync = function(devicePtr, futureId_low, futureId_high, descriptor, pipelinePtr) { + var futureId = convertI32PairToI53Checked(futureId_low, futureId_high); + var desc = WebGPU.makeRenderPipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + // createRenderPipelineAsync + WebGPU.Internals.futureInsert(futureId, device.createRenderPipelineAsync(desc).then(pipeline => { + // createRenderPipelineAsync fulfilled + callUserCallback(() => { + WebGPU.Internals.jsObjectInsert(pipelinePtr, pipeline); + _emwgpuOnCreateRenderPipelineCompleted(futureId, 1, pipelinePtr, 0); + }); + }, pipelineError => { + // createRenderPipelineAsync rejected + callUserCallback(() => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(pipelineError.message); + var status = pipelineError.reason === "validation" ? 3 : pipelineError.reason === "internal" ? 4 : 0; + _emwgpuOnCreateRenderPipelineCompleted(futureId, status, pipelinePtr, messagePtr); + stackRestore(sp); + }); + })); +}; + +var _emwgpuDeviceCreateShaderModule = (devicePtr, descriptor, shaderModulePtr) => { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "code": "" + }; + switch (sType) { + case 2: + { + desc["code"] = WebGPU.makeStringFromStringView(nextInChainPtr + 8); + break; + } + } + var device = WebGPU.getJsObject(devicePtr); + WebGPU.Internals.jsObjectInsert(shaderModulePtr, device.createShaderModule(desc)); +}; + +var _emwgpuDeviceDestroy = devicePtr => { + const device = WebGPU.getJsObject(devicePtr); + // Remove the onuncapturederror handler which holds a pointer to the WGPUDevice. + device.onuncapturederror = null; + device.destroy(); +}; + +var _emwgpuWaitAny = (futurePtr, futureCount, timeoutMSPtr) => { + abort("TODO: Implement asyncify-free WaitAny for timeout=0"); +}; + +var ENV = {}; + +var getExecutableName = () => thisProgram || "./this.program"; + +var getEnvStrings = () => { + if (!getEnvStrings.strings) { + // Default values. + // Browser language detection #8751 + var lang = (globalThis.navigator?.language ?? "C").replace("-", "_") + ".UTF-8"; + var env = { + "USER": "web_user", + "LOGNAME": "web_user", + "PATH": "/", + "PWD": "/", + "HOME": "/home/web_user", + "LANG": lang, + "_": getExecutableName() + }; + // Apply the user-provided values, if any. + for (var x in ENV) { + // x is a key in ENV; if ENV[x] is undefined, that means it was + // explicitly set to be so. We allow user code to do that to + // force variables with default values to remain unset. + if (ENV[x] === undefined) delete env[x]; else env[x] = ENV[x]; + } + var strings = []; + for (var x in env) { + strings.push(`${x}=${env[x]}`); + } + getEnvStrings.strings = strings; + } + return getEnvStrings.strings; +}; + +var _environ_get = (__environ, environ_buf) => { + var bufSize = 0; + var envp = 0; + for (var string of getEnvStrings()) { + var ptr = environ_buf + bufSize; + HEAPU32[(((__environ) + (envp)) >> 2)] = ptr; + bufSize += stringToUTF8(string, ptr, Infinity) + 1; + envp += 4; + } + return 0; +}; + +var _environ_sizes_get = (penviron_count, penviron_buf_size) => { + var strings = getEnvStrings(); + HEAPU32[((penviron_count) >> 2)] = strings.length; + var bufSize = 0; + for (var string of strings) { + bufSize += lengthBytesUTF8(string) + 1; + } + HEAPU32[((penviron_buf_size) >> 2)] = bufSize; + return 0; +}; + +function _fd_close(fd) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + FS.close(stream); + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +/** @param {number=} offset */ var doReadv = (stream, iov, iovcnt, offset) => { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[((iov) >> 2)]; + var len = HEAPU32[(((iov) + (4)) >> 2)]; + iov += 8; + var curr = FS.read(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) break; + // nothing more to read + if (typeof offset != "undefined") { + offset += curr; + } + } + return ret; +}; + +function _fd_read(fd, iov, iovcnt, pnum) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doReadv(stream, iov, iovcnt); + HEAPU32[((pnum) >> 2)] = num; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +function _fd_seek(fd, offset_low, offset_high, whence, newOffset) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + if (isNaN(offset)) return 61; + var stream = SYSCALLS.getStreamFromFD(fd); + FS.llseek(stream, offset, whence); + (tempI64 = [ stream.position >>> 0, (tempDouble = stream.position, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[((newOffset) >> 2)] = tempI64[0], HEAP32[(((newOffset) + (4)) >> 2)] = tempI64[1]); + if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; + // reset readdir state + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +/** @param {number=} offset */ var doWritev = (stream, iov, iovcnt, offset) => { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[((iov) >> 2)]; + var len = HEAPU32[(((iov) + (4)) >> 2)]; + iov += 8; + var curr = FS.write(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) { + // No more space to write. + break; + } + if (typeof offset != "undefined") { + offset += curr; + } + } + return ret; +}; + +function _fd_write(fd, iov, iovcnt, pnum) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doWritev(stream, iov, iovcnt); + HEAPU32[((pnum) >> 2)] = num; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +var _emscripten_glActiveTexture = x0 => GLctx.activeTexture(x0); + +var _glActiveTexture = _emscripten_glActiveTexture; + +var _emscripten_glAttachShader = (program, shader) => { + GLctx.attachShader(GL.programs[program], GL.shaders[shader]); +}; + +var _glAttachShader = _emscripten_glAttachShader; + +var _emscripten_glBindAttribLocation = (program, index, name) => { + GLctx.bindAttribLocation(GL.programs[program], index, UTF8ToString(name)); +}; + +var _glBindAttribLocation = _emscripten_glBindAttribLocation; + +var _emscripten_glBindBuffer = (target, buffer) => { + // Calling glBindBuffer with an unknown buffer will implicitly create a + // new one. Here we bypass `GL.counter` and directly using the ID passed + // in. + if (buffer && !GL.buffers[buffer]) { + var b = GLctx.createBuffer(); + b.name = buffer; + GL.buffers[buffer] = b; + } + if (target == 34962) { + GLctx.currentArrayBufferBinding = buffer; + } else if (target == 34963) { + GLctx.currentElementArrayBufferBinding = buffer; + } + if (target == 35051) { + // In WebGL 2 glReadPixels entry point, we need to use a different WebGL 2 + // API function call when a buffer is bound to + // GL_PIXEL_PACK_BUFFER_BINDING point, so must keep track whether that + // binding point is non-null to know what is the proper API function to + // call. + GLctx.currentPixelPackBufferBinding = buffer; + } else if (target == 35052) { + // In WebGL 2 gl(Compressed)Tex(Sub)Image[23]D entry points, we need to + // use a different WebGL 2 API function call when a buffer is bound to + // GL_PIXEL_UNPACK_BUFFER_BINDING point, so must keep track whether that + // binding point is non-null to know what is the proper API function to + // call. + GLctx.currentPixelUnpackBufferBinding = buffer; + } + GLctx.bindBuffer(target, GL.buffers[buffer]); +}; + +var _glBindBuffer = _emscripten_glBindBuffer; + +var _emscripten_glBindBufferBase = (target, index, buffer) => { + GLctx.bindBufferBase(target, index, GL.buffers[buffer]); +}; + +var _glBindBufferBase = _emscripten_glBindBufferBase; + +var _emscripten_glBindFramebuffer = (target, framebuffer) => { + GLctx.bindFramebuffer(target, GL.framebuffers[framebuffer]); +}; + +var _glBindFramebuffer = _emscripten_glBindFramebuffer; + +var _emscripten_glBindTexture = (target, texture) => { + GLctx.bindTexture(target, GL.textures[texture]); +}; + +var _glBindTexture = _emscripten_glBindTexture; + +var _emscripten_glBindVertexArray = vao => { + GLctx.bindVertexArray(GL.vaos[vao]); + var ibo = GLctx.getParameter(34965); + GLctx.currentElementArrayBufferBinding = ibo ? (ibo.name | 0) : 0; +}; + +var _glBindVertexArray = _emscripten_glBindVertexArray; + +var _emscripten_glBlendEquation = x0 => GLctx.blendEquation(x0); + +var _glBlendEquation = _emscripten_glBlendEquation; + +var _emscripten_glBlendFunc = (x0, x1) => GLctx.blendFunc(x0, x1); + +var _glBlendFunc = _emscripten_glBlendFunc; + +var _emscripten_glBufferData = (target, size, data, usage) => { + if (GL.currentContext.version >= 2) { + // If size is zero, WebGL would interpret uploading the whole input + // arraybuffer (starting from given offset), which would not make sense in + // WebAssembly, so avoid uploading if size is zero. However we must still + // call bufferData to establish a backing storage of zero bytes. + if (data && size) { + GLctx.bufferData(target, HEAPU8, usage, data, size); + } else { + GLctx.bufferData(target, size, usage); + } + return; + } + // N.b. here first form specifies a heap subarray, second form an integer + // size, so the ?: code here is polymorphic. It is advised to avoid + // randomly mixing both uses in calling code, to avoid any potential JS + // engine JIT issues. + GLctx.bufferData(target, data ? HEAPU8.subarray(data, data + size) : size, usage); +}; + +var _glBufferData = _emscripten_glBufferData; + +var _emscripten_glClear = x0 => GLctx.clear(x0); + +var _glClear = _emscripten_glClear; + +var _emscripten_glClearColor = (x0, x1, x2, x3) => GLctx.clearColor(x0, x1, x2, x3); + +var _glClearColor = _emscripten_glClearColor; + +var convertI32PairToI53 = (lo, hi) => (lo >>> 0) + hi * 4294967296; + +var _emscripten_glClientWaitSync = (sync, flags, timeout_low, timeout_high) => { + // WebGL2 vs GLES3 differences: in GLES3, the timeout parameter is a uint64, where 0xFFFFFFFFFFFFFFFFULL means GL_TIMEOUT_IGNORED. + // In JS, there's no 64-bit value types, so instead timeout is taken to be signed, and GL_TIMEOUT_IGNORED is given value -1. + // Inherently the value accepted in the timeout is lossy, and can't take in arbitrary u64 bit pattern (but most likely doesn't matter) + // See https://www.khronos.org/registry/webgl/specs/latest/2.0/#5.15 + var timeout = convertI32PairToI53(timeout_low, timeout_high); + return GLctx.clientWaitSync(GL.syncs[sync], flags, timeout); +}; + +var _glClientWaitSync = _emscripten_glClientWaitSync; + +var _emscripten_glColorMask = (red, green, blue, alpha) => { + GLctx.colorMask(!!red, !!green, !!blue, !!alpha); +}; + +var _glColorMask = _emscripten_glColorMask; + +var _emscripten_glCompileShader = shader => { + GLctx.compileShader(GL.shaders[shader]); +}; + +var _glCompileShader = _emscripten_glCompileShader; + +var _emscripten_glCreateProgram = () => { + var id = GL.getNewId(GL.programs); + var program = GLctx.createProgram(); + // Store additional information needed for each shader program: + program.name = id; + // Lazy cache results of + // glGetProgramiv(GL_ACTIVE_UNIFORM_MAX_LENGTH/GL_ACTIVE_ATTRIBUTE_MAX_LENGTH/GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH) + program.maxUniformLength = program.maxAttributeLength = program.maxUniformBlockNameLength = 0; + program.uniformIdCounter = 1; + GL.programs[id] = program; + return id; +}; + +var _glCreateProgram = _emscripten_glCreateProgram; + +var _emscripten_glCreateShader = shaderType => { + var id = GL.getNewId(GL.shaders); + GL.shaders[id] = GLctx.createShader(shaderType); + return id; +}; + +var _glCreateShader = _emscripten_glCreateShader; + +var _emscripten_glDeleteBuffers = (n, buffers) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((buffers) + (i * 4)) >> 2)]; + var buffer = GL.buffers[id]; + // From spec: "glDeleteBuffers silently ignores 0's and names that do not + // correspond to existing buffer objects." + if (!buffer) continue; + GLctx.deleteBuffer(buffer); + buffer.name = 0; + GL.buffers[id] = null; + if (id == GLctx.currentArrayBufferBinding) GLctx.currentArrayBufferBinding = 0; + if (id == GLctx.currentElementArrayBufferBinding) GLctx.currentElementArrayBufferBinding = 0; + if (id == GLctx.currentPixelPackBufferBinding) GLctx.currentPixelPackBufferBinding = 0; + if (id == GLctx.currentPixelUnpackBufferBinding) GLctx.currentPixelUnpackBufferBinding = 0; + } +}; + +var _glDeleteBuffers = _emscripten_glDeleteBuffers; + +var _emscripten_glDeleteFramebuffers = (n, framebuffers) => { + for (var i = 0; i < n; ++i) { + var id = HEAP32[(((framebuffers) + (i * 4)) >> 2)]; + var framebuffer = GL.framebuffers[id]; + if (!framebuffer) continue; + // GL spec: "glDeleteFramebuffers silently ignores 0s and names that do not correspond to existing framebuffer objects". + GLctx.deleteFramebuffer(framebuffer); + framebuffer.name = 0; + GL.framebuffers[id] = null; + } +}; + +var _glDeleteFramebuffers = _emscripten_glDeleteFramebuffers; + +var _emscripten_glDeleteProgram = id => { + if (!id) return; + var program = GL.programs[id]; + if (!program) { + // glDeleteProgram actually signals an error when deleting a nonexisting + // object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteProgram(program); + program.name = 0; + GL.programs[id] = null; +}; + +var _glDeleteProgram = _emscripten_glDeleteProgram; + +var _emscripten_glDeleteShader = id => { + if (!id) return; + var shader = GL.shaders[id]; + if (!shader) { + // glDeleteShader actually signals an error when deleting a nonexisting + // object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteShader(shader); + GL.shaders[id] = null; +}; + +var _glDeleteShader = _emscripten_glDeleteShader; + +var _emscripten_glDeleteSync = id => { + if (!id) return; + var sync = GL.syncs[id]; + if (!sync) { + // glDeleteSync signals an error when deleting a nonexisting object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteSync(sync); + sync.name = 0; + GL.syncs[id] = null; +}; + +var _glDeleteSync = _emscripten_glDeleteSync; + +var _emscripten_glDeleteTextures = (n, textures) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((textures) + (i * 4)) >> 2)]; + var texture = GL.textures[id]; + // GL spec: "glDeleteTextures silently ignores 0s and names that do not + // correspond to existing textures". + if (!texture) continue; + GLctx.deleteTexture(texture); + texture.name = 0; + GL.textures[id] = null; + } +}; + +var _glDeleteTextures = _emscripten_glDeleteTextures; + +var _emscripten_glDeleteVertexArrays = (n, vaos) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((vaos) + (i * 4)) >> 2)]; + GLctx.deleteVertexArray(GL.vaos[id]); + GL.vaos[id] = null; + } +}; + +var _glDeleteVertexArrays = _emscripten_glDeleteVertexArrays; + +var _emscripten_glDetachShader = (program, shader) => { + GLctx.detachShader(GL.programs[program], GL.shaders[shader]); +}; + +var _glDetachShader = _emscripten_glDetachShader; + +var _emscripten_glDisable = x0 => GLctx.disable(x0); + +var _glDisable = _emscripten_glDisable; + +var _emscripten_glDisableVertexAttribArray = index => { + var cb = GL.currentContext.clientBuffers[index]; + cb.enabled = false; + GLctx.disableVertexAttribArray(index); +}; + +var _glDisableVertexAttribArray = _emscripten_glDisableVertexAttribArray; + +var _emscripten_glDrawArrays = (mode, first, count) => { + // bind any client-side buffers + GL.preDrawHandleClientVertexAttribBindings(first + count); + GLctx.drawArrays(mode, first, count); + GL.postDrawHandleClientVertexAttribBindings(); +}; + +var _glDrawArrays = _emscripten_glDrawArrays; + +var tempFixedLengthArray = []; + +var _emscripten_glDrawBuffers = (n, bufs) => { + var bufArray = tempFixedLengthArray[n]; + for (var i = 0; i < n; i++) { + bufArray[i] = HEAP32[(((bufs) + (i * 4)) >> 2)]; + } + GLctx.drawBuffers(bufArray); +}; + +var _glDrawBuffers = _emscripten_glDrawBuffers; + +var _emscripten_glEnable = x0 => GLctx.enable(x0); + +var _glEnable = _emscripten_glEnable; + +var _emscripten_glEnableVertexAttribArray = index => { + var cb = GL.currentContext.clientBuffers[index]; + cb.enabled = true; + GLctx.enableVertexAttribArray(index); +}; + +var _glEnableVertexAttribArray = _emscripten_glEnableVertexAttribArray; + +var _emscripten_glFenceSync = (condition, flags) => { + var sync = GLctx.fenceSync(condition, flags); + if (sync) { + var id = GL.getNewId(GL.syncs); + sync.name = id; + GL.syncs[id] = sync; + return id; + } + return 0; +}; + +var _glFenceSync = _emscripten_glFenceSync; + +var _emscripten_glFinish = () => GLctx.finish(); + +var _glFinish = _emscripten_glFinish; + +var _emscripten_glFlush = () => GLctx.flush(); + +var _glFlush = _emscripten_glFlush; + +var _emscripten_glFramebufferTexture2D = (target, attachment, textarget, texture, level) => { + GLctx.framebufferTexture2D(target, attachment, textarget, GL.textures[texture], level); +}; + +var _glFramebufferTexture2D = _emscripten_glFramebufferTexture2D; + +var _emscripten_glFramebufferTextureLayer = (target, attachment, texture, level, layer) => { + GLctx.framebufferTextureLayer(target, attachment, GL.textures[texture], level, layer); +}; + +var _glFramebufferTextureLayer = _emscripten_glFramebufferTextureLayer; + +var _emscripten_glGenBuffers = (n, buffers) => { + GL.genObject(n, buffers, "createBuffer", GL.buffers); +}; + +var _glGenBuffers = _emscripten_glGenBuffers; + +var _emscripten_glGenFramebuffers = (n, ids) => { + GL.genObject(n, ids, "createFramebuffer", GL.framebuffers); +}; + +var _glGenFramebuffers = _emscripten_glGenFramebuffers; + +var _emscripten_glGenTextures = (n, textures) => { + GL.genObject(n, textures, "createTexture", GL.textures); +}; + +var _glGenTextures = _emscripten_glGenTextures; + +var _emscripten_glGenVertexArrays = (n, arrays) => { + GL.genObject(n, arrays, "createVertexArray", GL.vaos); +}; + +var _glGenVertexArrays = _emscripten_glGenVertexArrays; + +var _emscripten_glGetAttribLocation = (program, name) => GLctx.getAttribLocation(GL.programs[program], UTF8ToString(name)); + +var _glGetAttribLocation = _emscripten_glGetAttribLocation; + +var _emscripten_glGetError = () => { + var error = GLctx.getError() || GL.lastError; + GL.lastError = 0; + return error; +}; + +var _glGetError = _emscripten_glGetError; + +var webglGetExtensions = () => { + var exts = getEmscriptenSupportedExtensions(GLctx); + exts = exts.concat(exts.map(e => "GL_" + e)); + return exts; +}; + +var emscriptenWebGLGet = (name_, p, type) => { + // Guard against user passing a null pointer. + // Note that GLES2 spec does not say anything about how passing a null + // pointer should be treated. Testing on desktop core GL 3, the application + // crashes on glGetIntegerv to a null pointer, but better to report an error + // instead of doing anything random. + if (!p) { + GL.recordError(1281); + return; + } + var ret = undefined; + switch (name_) { + // Handle a few trivial GLES values + case 36346: + // GL_SHADER_COMPILER + ret = 1; + break; + + case 36344: + // GL_SHADER_BINARY_FORMATS + if (type != 0 && type != 1) { + GL.recordError(1280); + } + // Do not write anything to the out pointer, since no binary formats are + // supported. + return; + + case 34814: + // GL_NUM_PROGRAM_BINARY_FORMATS + case 36345: + // GL_NUM_SHADER_BINARY_FORMATS + ret = 0; + break; + + case 34466: + // GL_NUM_COMPRESSED_TEXTURE_FORMATS + // WebGL doesn't have GL_NUM_COMPRESSED_TEXTURE_FORMATS (it's obsolete + // since GL_COMPRESSED_TEXTURE_FORMATS returns a JS array that can be + // queried for length), so implement it ourselves to allow C++ GLES2 + // code to get the length. + var formats = GLctx.getParameter(34467); + ret = formats ? formats.length : 0; + break; + + case 33309: + // GL_NUM_EXTENSIONS + if (GL.currentContext.version < 2) { + // Calling GLES3/WebGL2 function with a GLES2/WebGL1 context + GL.recordError(1282); + return; + } + ret = webglGetExtensions().length; + break; + + case 33307: + // GL_MAJOR_VERSION + case 33308: + // GL_MINOR_VERSION + if (GL.currentContext.version < 2) { + GL.recordError(1280); + // GL_INVALID_ENUM + return; + } + ret = name_ == 33307 ? 3 : 0; + // return version 3.0 + break; + } + if (ret === undefined) { + var result = GLctx.getParameter(name_); + switch (typeof result) { + case "number": + ret = result; + break; + + case "boolean": + ret = result ? 1 : 0; + break; + + case "string": + GL.recordError(1280); + // GL_INVALID_ENUM + return; + + case "object": + if (result === null) { + // null is a valid result for some (e.g., which buffer is bound - + // perhaps nothing is bound), but otherwise can mean an invalid + // name_, which we need to report as an error + switch (name_) { + case 34964: + // ARRAY_BUFFER_BINDING + case 35725: + // CURRENT_PROGRAM + case 34965: + // ELEMENT_ARRAY_BUFFER_BINDING + case 36006: + // FRAMEBUFFER_BINDING or DRAW_FRAMEBUFFER_BINDING + case 36007: + // RENDERBUFFER_BINDING + case 32873: + // TEXTURE_BINDING_2D + case 34229: + // WebGL 2 GL_VERTEX_ARRAY_BINDING, or WebGL 1 extension OES_vertex_array_object GL_VERTEX_ARRAY_BINDING_OES + case 36662: + // COPY_READ_BUFFER_BINDING or COPY_READ_BUFFER + case 36663: + // COPY_WRITE_BUFFER_BINDING or COPY_WRITE_BUFFER + case 35053: + // PIXEL_PACK_BUFFER_BINDING + case 35055: + // PIXEL_UNPACK_BUFFER_BINDING + case 36010: + // READ_FRAMEBUFFER_BINDING + case 35097: + // SAMPLER_BINDING + case 35869: + // TEXTURE_BINDING_2D_ARRAY + case 32874: + // TEXTURE_BINDING_3D + case 36389: + // TRANSFORM_FEEDBACK_BINDING + case 35983: + // TRANSFORM_FEEDBACK_BUFFER_BINDING + case 35368: + // UNIFORM_BUFFER_BINDING + case 34068: + { + // TEXTURE_BINDING_CUBE_MAP + ret = 0; + break; + } + + default: + { + GL.recordError(1280); + // GL_INVALID_ENUM + return; + } + } + } else if (result instanceof Float32Array || result instanceof Uint32Array || result instanceof Int32Array || result instanceof Array) { + for (var i = 0; i < result.length; ++i) { + switch (type) { + case 0: + HEAP32[(((p) + (i * 4)) >> 2)] = result[i]; + break; + + case 2: + HEAPF32[(((p) + (i * 4)) >> 2)] = result[i]; + break; + + case 4: + HEAP8[(p) + (i)] = result[i] ? 1 : 0; + break; + } + } + return; + } else { + try { + ret = result.name | 0; + } catch (e) { + GL.recordError(1280); + // GL_INVALID_ENUM + err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`); + return; + } + } + break; + + default: + GL.recordError(1280); + // GL_INVALID_ENUM + err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof (result)}!`); + return; + } + } + switch (type) { + case 1: + writeI53ToI64(p, ret); + break; + + case 0: + HEAP32[((p) >> 2)] = ret; + break; + + case 2: + HEAPF32[((p) >> 2)] = ret; + break; + + case 4: + HEAP8[p] = ret ? 1 : 0; + break; + } +}; + +var _emscripten_glGetFloatv = (name_, p) => emscriptenWebGLGet(name_, p, 2); + +var _glGetFloatv = _emscripten_glGetFloatv; + +var _emscripten_glGetIntegerv = (name_, p) => emscriptenWebGLGet(name_, p, 0); + +var _glGetIntegerv = _emscripten_glGetIntegerv; + +var _emscripten_glGetProgramiv = (program, pname, p) => { + if (!p) { + // GLES2 specification does not specify how to behave if p is a null + // pointer. Since calling this function does not make sense if p == null, + // issue a GL error to notify user about it. + GL.recordError(1281); + return; + } + if (program >= GL.counter) { + GL.recordError(1281); + return; + } + program = GL.programs[program]; + if (pname == 35716) { + // GL_INFO_LOG_LENGTH + var log = GLctx.getProgramInfoLog(program); + if (log === null) log = "(unknown error)"; + HEAP32[((p) >> 2)] = log.length + 1; + } else if (pname == 35719) { + if (!program.maxUniformLength) { + var numActiveUniforms = GLctx.getProgramParameter(program, 35718); + for (var i = 0; i < numActiveUniforms; ++i) { + program.maxUniformLength = Math.max(program.maxUniformLength, GLctx.getActiveUniform(program, i).name.length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxUniformLength; + } else if (pname == 35722) { + if (!program.maxAttributeLength) { + var numActiveAttributes = GLctx.getProgramParameter(program, 35721); + for (var i = 0; i < numActiveAttributes; ++i) { + program.maxAttributeLength = Math.max(program.maxAttributeLength, GLctx.getActiveAttrib(program, i).name.length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxAttributeLength; + } else if (pname == 35381) { + if (!program.maxUniformBlockNameLength) { + var numActiveUniformBlocks = GLctx.getProgramParameter(program, 35382); + for (var i = 0; i < numActiveUniformBlocks; ++i) { + program.maxUniformBlockNameLength = Math.max(program.maxUniformBlockNameLength, GLctx.getActiveUniformBlockName(program, i).length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxUniformBlockNameLength; + } else { + HEAP32[((p) >> 2)] = GLctx.getProgramParameter(program, pname); + } +}; + +var _glGetProgramiv = _emscripten_glGetProgramiv; + +var _emscripten_glGetShaderInfoLog = (shader, maxLength, length, infoLog) => { + var log = GLctx.getShaderInfoLog(GL.shaders[shader]); + if (log === null) log = "(unknown error)"; + var numBytesWrittenExclNull = (maxLength > 0 && infoLog) ? stringToUTF8(log, infoLog, maxLength) : 0; + if (length) HEAP32[((length) >> 2)] = numBytesWrittenExclNull; +}; + +var _glGetShaderInfoLog = _emscripten_glGetShaderInfoLog; + +var _emscripten_glGetShaderiv = (shader, pname, p) => { + if (!p) { + // GLES2 specification does not specify how to behave if p is a null + // pointer. Since calling this function does not make sense if p == null, + // issue a GL error to notify user about it. + GL.recordError(1281); + return; + } + if (pname == 35716) { + // GL_INFO_LOG_LENGTH + var log = GLctx.getShaderInfoLog(GL.shaders[shader]); + if (log === null) log = "(unknown error)"; + // The GLES2 specification says that if the shader has an empty info log, + // a value of 0 is returned. Otherwise the log has a null char appended. + // (An empty string is falsey, so we can just check that instead of + // looking at log.length.) + var logLength = log ? log.length + 1 : 0; + HEAP32[((p) >> 2)] = logLength; + } else if (pname == 35720) { + // GL_SHADER_SOURCE_LENGTH + var source = GLctx.getShaderSource(GL.shaders[shader]); + // source may be a null, or the empty string, both of which are falsey + // values that we report a 0 length for. + var sourceLength = source ? source.length + 1 : 0; + HEAP32[((p) >> 2)] = sourceLength; + } else { + HEAP32[((p) >> 2)] = GLctx.getShaderParameter(GL.shaders[shader], pname); + } +}; + +var _glGetShaderiv = _emscripten_glGetShaderiv; + +var _emscripten_glGetString = name_ => { + var ret = GL.stringCache[name_]; + if (!ret) { + switch (name_) { + case 7939: + ret = stringToNewUTF8(webglGetExtensions().join(" ")); + break; + + case 7936: + case 7937: + case 37445: + case 37446: + var s = GLctx.getParameter(name_); + if (!s) { + GL.recordError(1280); + } + ret = s ? stringToNewUTF8(s) : 0; + break; + + case 7938: + var webGLVersion = GLctx.getParameter(7938); + // return GLES version string corresponding to the version of the WebGL context + var glVersion = `OpenGL ES 2.0 (${webGLVersion})`; + if (GL.currentContext.version >= 2) glVersion = `OpenGL ES 3.0 (${webGLVersion})`; + ret = stringToNewUTF8(glVersion); + break; + + case 35724: + var glslVersion = GLctx.getParameter(35724); + // extract the version number 'N.M' from the string 'WebGL GLSL ES N.M ...' + var ver_re = /^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/; + var ver_num = glslVersion.match(ver_re); + if (ver_num !== null) { + if (ver_num[1].length == 3) ver_num[1] = ver_num[1] + "0"; + // ensure minor version has 2 digits + glslVersion = `OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`; + } + ret = stringToNewUTF8(glslVersion); + break; + + default: + GL.recordError(1280); + } + GL.stringCache[name_] = ret; + } + return ret; +}; + +var _glGetString = _emscripten_glGetString; + +var _emscripten_glGetUniformBlockIndex = (program, uniformBlockName) => GLctx.getUniformBlockIndex(GL.programs[program], UTF8ToString(uniformBlockName)); + +var _glGetUniformBlockIndex = _emscripten_glGetUniformBlockIndex; + +/** @suppress {checkTypes} */ var jstoi_q = str => parseInt(str); + +/** @noinline */ var webglGetLeftBracePos = name => name.slice(-1) == "]" && name.lastIndexOf("["); + +var webglPrepareUniformLocationsBeforeFirstUse = program => { + var uniformLocsById = program.uniformLocsById, // Maps GLuint -> WebGLUniformLocation + uniformSizeAndIdsByName = program.uniformSizeAndIdsByName, // Maps name -> [uniform array length, GLuint] + i, j; + // On the first time invocation of glGetUniformLocation on this shader program: + // initialize cache data structures and discover which uniforms are arrays. + if (!uniformLocsById) { + // maps GLint integer locations to WebGLUniformLocations + program.uniformLocsById = uniformLocsById = {}; + // maps integer locations back to uniform name strings, so that we can lazily fetch uniform array locations + program.uniformArrayNamesById = {}; + var numActiveUniforms = GLctx.getProgramParameter(program, 35718); + for (i = 0; i < numActiveUniforms; ++i) { + var u = GLctx.getActiveUniform(program, i); + var nm = u.name; + var sz = u.size; + var lb = webglGetLeftBracePos(nm); + var arrayName = lb > 0 ? nm.slice(0, lb) : nm; + // Assign a new location. + var id = program.uniformIdCounter; + program.uniformIdCounter += sz; + // Eagerly get the location of the uniformArray[0] base element. + // The remaining indices >0 will be left for lazy evaluation to + // improve performance. Those may never be needed to fetch, if the + // application fills arrays always in full starting from the first + // element of the array. + uniformSizeAndIdsByName[arrayName] = [ sz, id ]; + // Store placeholder integers in place that highlight that these + // >0 index locations are array indices pending population. + for (j = 0; j < sz; ++j) { + uniformLocsById[id] = j; + program.uniformArrayNamesById[id++] = arrayName; + } + } + } +}; + +var _emscripten_glGetUniformLocation = (program, name) => { + name = UTF8ToString(name); + if (program = GL.programs[program]) { + webglPrepareUniformLocationsBeforeFirstUse(program); + var uniformLocsById = program.uniformLocsById; + // Maps GLuint -> WebGLUniformLocation + var arrayIndex = 0; + var uniformBaseName = name; + // Invariant: when populating integer IDs for uniform locations, we must + // maintain the precondition that arrays reside in contiguous addresses, + // i.e. for a 'vec4 colors[10];', colors[4] must be at location + // colors[0]+4. However, user might call glGetUniformLocation(program, + // "colors") for an array, so we cannot discover based on the user input + // arguments whether the uniform we are dealing with is an array. The only + // way to discover which uniforms are arrays is to enumerate over all the + // active uniforms in the program. + var leftBrace = webglGetLeftBracePos(name); + // If user passed an array accessor "[index]", parse the array index off the accessor. + if (leftBrace > 0) { + arrayIndex = jstoi_q(name.slice(leftBrace + 1)) >>> 0; + // "index]", coerce parseInt(']') with >>>0 to treat "foo[]" as "foo[0]" and foo[-1] as unsigned out-of-bounds. + uniformBaseName = name.slice(0, leftBrace); + } + // Have we cached the location of this uniform before? + // A pair [array length, GLint of the uniform location] + var sizeAndId = program.uniformSizeAndIdsByName[uniformBaseName]; + // If a uniform with this name exists, and if its index is within the + // array limits (if it's even an array), query the WebGLlocation, or + // return an existing cached location. + if (sizeAndId && arrayIndex < sizeAndId[0]) { + arrayIndex += sizeAndId[1]; + // Add the base location of the uniform to the array index offset. + if ((uniformLocsById[arrayIndex] = uniformLocsById[arrayIndex] || GLctx.getUniformLocation(program, name))) { + return arrayIndex; + } + } + } else { + // N.b. we are currently unable to distinguish between GL program IDs that + // never existed vs GL program IDs that have been deleted, so report + // GL_INVALID_VALUE in both cases. + GL.recordError(1281); + } + return -1; +}; + +var _glGetUniformLocation = _emscripten_glGetUniformLocation; + +var _emscripten_glLineWidth = x0 => GLctx.lineWidth(x0); + +var _glLineWidth = _emscripten_glLineWidth; + +var _emscripten_glLinkProgram = program => { + program = GL.programs[program]; + GLctx.linkProgram(program); + // Invalidate earlier computed uniform->ID mappings, those have now become stale + program.uniformLocsById = 0; + // Mark as null-like so that glGetUniformLocation() knows to populate this again. + program.uniformSizeAndIdsByName = {}; +}; + +var _glLinkProgram = _emscripten_glLinkProgram; + +var _emscripten_glPixelStorei = (pname, param) => { + if (pname == 3317) { + GL.unpackAlignment = param; + } else if (pname == 3314) { + GL.unpackRowLength = param; + } + GLctx.pixelStorei(pname, param); +}; + +var _glPixelStorei = _emscripten_glPixelStorei; + +var computeUnpackAlignedImageSize = (width, height, sizePerPixel) => { + function roundedToNextMultipleOf(x, y) { + return (x + y - 1) & -y; + } + var plainRowSize = (GL.unpackRowLength || width) * sizePerPixel; + var alignedRowSize = roundedToNextMultipleOf(plainRowSize, GL.unpackAlignment); + return height * alignedRowSize; +}; + +var colorChannelsInGlTextureFormat = format => { + // Micro-optimizations for size: map format to size by subtracting smallest + // enum value (0x1902) from all values first. Also omit the most common + // size value (1) from the list, which is assumed by formats not on the + // list. + var colorChannels = { + // 0x1902 /* GL_DEPTH_COMPONENT */ - 0x1902: 1, + // 0x1906 /* GL_ALPHA */ - 0x1902: 1, + 5: 3, + 6: 4, + // 0x1909 /* GL_LUMINANCE */ - 0x1902: 1, + 8: 2, + 29502: 3, + 29504: 4, + // 0x1903 /* GL_RED */ - 0x1902: 1, + 26917: 2, + 26918: 2, + // 0x8D94 /* GL_RED_INTEGER */ - 0x1902: 1, + 29846: 3, + 29847: 4 + }; + return colorChannels[format - 6402] || 1; +}; + +var heapObjectForWebGLType = type => { + // Micro-optimization for size: Subtract lowest GL enum number (0x1400/* GL_BYTE */) from type to compare + // smaller values for the heap, for shorter generated code size. + // Also the type HEAPU16 is not tested for explicitly, but any unrecognized type will return out HEAPU16. + // (since most types are HEAPU16) + type -= 5120; + if (type == 0) return HEAP8; + if (type == 1) return HEAPU8; + if (type == 2) return HEAP16; + if (type == 4) return HEAP32; + if (type == 6) return HEAPF32; + if (type == 5 || type == 28922 || type == 28520 || type == 30779 || type == 30782) return HEAPU32; + return HEAPU16; +}; + +var toTypedArrayIndex = (pointer, heap) => pointer >>> (31 - Math.clz32(heap.BYTES_PER_ELEMENT)); + +var emscriptenWebGLGetTexPixelData = (type, format, width, height, pixels, internalFormat) => { + var heap = heapObjectForWebGLType(type); + var sizePerPixel = colorChannelsInGlTextureFormat(format) * heap.BYTES_PER_ELEMENT; + var bytes = computeUnpackAlignedImageSize(width, height, sizePerPixel); + return heap.subarray(toTypedArrayIndex(pixels, heap), toTypedArrayIndex(pixels + bytes, heap)); +}; + +var _emscripten_glReadPixels = (x, y, width, height, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelPackBufferBinding) { + GLctx.readPixels(x, y, width, height, format, type, pixels); + return; + } + var heap = heapObjectForWebGLType(type); + var target = toTypedArrayIndex(pixels, heap); + GLctx.readPixels(x, y, width, height, format, type, heap, target); + return; + } + var pixelData = emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, format); + if (!pixelData) { + GL.recordError(1280); + return; + } + GLctx.readPixels(x, y, width, height, format, type, pixelData); +}; + +var _glReadPixels = _emscripten_glReadPixels; + +var _emscripten_glShaderSource = (shader, count, string, length) => { + var source = GL.getSource(shader, count, string, length); + GLctx.shaderSource(GL.shaders[shader], source); +}; + +var _glShaderSource = _emscripten_glShaderSource; + +var _emscripten_glTexImage2D = (target, level, internalFormat, width, height, border, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixels); + return; + } + if (pixels) { + var heap = heapObjectForWebGLType(type); + var index = toTypedArrayIndex(pixels, heap); + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, heap, index); + return; + } + } + var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, internalFormat) : null; + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixelData); +}; + +var _glTexImage2D = _emscripten_glTexImage2D; + +var _emscripten_glTexParameterf = (x0, x1, x2) => GLctx.texParameterf(x0, x1, x2); + +var _glTexParameterf = _emscripten_glTexParameterf; + +var _emscripten_glTexParameterfv = (target, pname, params) => { + var param = HEAPF32[((params) >> 2)]; + GLctx.texParameterf(target, pname, param); +}; + +var _glTexParameterfv = _emscripten_glTexParameterfv; + +var _emscripten_glTexParameteri = (x0, x1, x2) => GLctx.texParameteri(x0, x1, x2); + +var _glTexParameteri = _emscripten_glTexParameteri; + +var _emscripten_glTexStorage2D = (x0, x1, x2, x3, x4) => GLctx.texStorage2D(x0, x1, x2, x3, x4); + +var _glTexStorage2D = _emscripten_glTexStorage2D; + +var _emscripten_glTexStorage3D = (x0, x1, x2, x3, x4, x5) => GLctx.texStorage3D(x0, x1, x2, x3, x4, x5); + +var _glTexStorage3D = _emscripten_glTexStorage3D; + +var _emscripten_glTexSubImage2D = (target, level, xoffset, yoffset, width, height, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + return; + } + if (pixels) { + var heap = heapObjectForWebGLType(type); + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, heap, toTypedArrayIndex(pixels, heap)); + return; + } + } + var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, 0) : null; + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixelData); +}; + +var _glTexSubImage2D = _emscripten_glTexSubImage2D; + +var _emscripten_glTexSubImage3D = (target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels) => { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels); + } else if (pixels) { + var heap = heapObjectForWebGLType(type); + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, heap, toTypedArrayIndex(pixels, heap)); + } else { + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, null); + } +}; + +var _glTexSubImage3D = _emscripten_glTexSubImage3D; + +var webglGetUniformLocation = location => { + var p = GLctx.currentProgram; + if (p) { + var webglLoc = p.uniformLocsById[location]; + // p.uniformLocsById[location] stores either an integer, or a + // WebGLUniformLocation. + // If an integer, we have not yet bound the location, so do it now. The + // integer value specifies the array index we should bind to. + if (typeof webglLoc == "number") { + p.uniformLocsById[location] = webglLoc = GLctx.getUniformLocation(p, p.uniformArrayNamesById[location] + (webglLoc > 0 ? `[${webglLoc}]` : "")); + } + // Else an already cached WebGLUniformLocation, return it. + return webglLoc; + } else { + GL.recordError(1282); + } +}; + +var _emscripten_glUniform1f = (location, v0) => { + GLctx.uniform1f(webglGetUniformLocation(location), v0); +}; + +var _glUniform1f = _emscripten_glUniform1f; + +var miniTempWebGLFloatBuffers = []; + +var _emscripten_glUniform1fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform1fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count); + return; + } + if (count <= 288) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; ++i) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 4) >> 2)); + } + GLctx.uniform1fv(webglGetUniformLocation(location), view); +}; + +var _glUniform1fv = _emscripten_glUniform1fv; + +var _emscripten_glUniform1i = (location, v0) => { + GLctx.uniform1i(webglGetUniformLocation(location), v0); +}; + +var _glUniform1i = _emscripten_glUniform1i; + +var _emscripten_glUniform2f = (location, v0, v1) => { + GLctx.uniform2f(webglGetUniformLocation(location), v0, v1); +}; + +var _glUniform2f = _emscripten_glUniform2f; + +var _emscripten_glUniform2fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform2fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count * 2); + return; + } + if (count <= 144) { + // avoid allocation when uploading few enough uniforms + count *= 2; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 2) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 8) >> 2)); + } + GLctx.uniform2fv(webglGetUniformLocation(location), view); +}; + +var _glUniform2fv = _emscripten_glUniform2fv; + +var _emscripten_glUniform3f = (location, v0, v1, v2) => { + GLctx.uniform3f(webglGetUniformLocation(location), v0, v1, v2); +}; + +var _glUniform3f = _emscripten_glUniform3f; + +var _emscripten_glUniform4f = (location, v0, v1, v2, v3) => { + GLctx.uniform4f(webglGetUniformLocation(location), v0, v1, v2, v3); +}; + +var _glUniform4f = _emscripten_glUniform4f; + +var _emscripten_glUniform4fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform4fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[4 * count]; + // hoist the heap out of the loop for size and for pthreads+growth. + var heap = HEAPF32; + value = ((value) >> 2); + count *= 4; + for (var i = 0; i < count; i += 4) { + var dst = value + i; + view[i] = heap[dst]; + view[i + 1] = heap[dst + 1]; + view[i + 2] = heap[dst + 2]; + view[i + 3] = heap[dst + 3]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniform4fv(webglGetUniformLocation(location), view); +}; + +var _glUniform4fv = _emscripten_glUniform4fv; + +var miniTempWebGLIntBuffers = []; + +var _emscripten_glUniform4iv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform4iv(webglGetUniformLocation(location), HEAP32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + count *= 4; + var view = miniTempWebGLIntBuffers[count]; + for (var i = 0; i < count; i += 4) { + view[i] = HEAP32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAP32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAP32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAP32[(((value) + (4 * i + 12)) >> 2)]; + } + } else { + var view = HEAP32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniform4iv(webglGetUniformLocation(location), view); +}; + +var _glUniform4iv = _emscripten_glUniform4iv; + +var _emscripten_glUniformBlockBinding = (program, uniformBlockIndex, uniformBlockBinding) => { + program = GL.programs[program]; + GLctx.uniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); +}; + +var _glUniformBlockBinding = _emscripten_glUniformBlockBinding; + +var _emscripten_glUniformMatrix2fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + count *= 4; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 4) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAPF32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAPF32[(((value) + (4 * i + 12)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix2fv = _emscripten_glUniformMatrix2fv; + +var _emscripten_glUniformMatrix3fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 9); + return; + } + if (count <= 32) { + // avoid allocation when uploading few enough uniforms + count *= 9; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 9) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAPF32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAPF32[(((value) + (4 * i + 12)) >> 2)]; + view[i + 4] = HEAPF32[(((value) + (4 * i + 16)) >> 2)]; + view[i + 5] = HEAPF32[(((value) + (4 * i + 20)) >> 2)]; + view[i + 6] = HEAPF32[(((value) + (4 * i + 24)) >> 2)]; + view[i + 7] = HEAPF32[(((value) + (4 * i + 28)) >> 2)]; + view[i + 8] = HEAPF32[(((value) + (4 * i + 32)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 36) >> 2)); + } + GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix3fv = _emscripten_glUniformMatrix3fv; + +var _emscripten_glUniformMatrix4fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 16); + return; + } + if (count <= 18) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[16 * count]; + // hoist the heap out of the loop for size and for pthreads+growth. + var heap = HEAPF32; + value = ((value) >> 2); + count *= 16; + for (var i = 0; i < count; i += 16) { + var dst = value + i; + view[i] = heap[dst]; + view[i + 1] = heap[dst + 1]; + view[i + 2] = heap[dst + 2]; + view[i + 3] = heap[dst + 3]; + view[i + 4] = heap[dst + 4]; + view[i + 5] = heap[dst + 5]; + view[i + 6] = heap[dst + 6]; + view[i + 7] = heap[dst + 7]; + view[i + 8] = heap[dst + 8]; + view[i + 9] = heap[dst + 9]; + view[i + 10] = heap[dst + 10]; + view[i + 11] = heap[dst + 11]; + view[i + 12] = heap[dst + 12]; + view[i + 13] = heap[dst + 13]; + view[i + 14] = heap[dst + 14]; + view[i + 15] = heap[dst + 15]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 64) >> 2)); + } + GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix4fv = _emscripten_glUniformMatrix4fv; + +var _emscripten_glUseProgram = program => { + program = GL.programs[program]; + GLctx.useProgram(program); + // Record the currently active program so that we can access the uniform + // mapping table of that program. + GLctx.currentProgram = program; +}; + +var _glUseProgram = _emscripten_glUseProgram; + +var _emscripten_glVertexAttribPointer = (index, size, type, normalized, stride, ptr) => { + var cb = GL.currentContext.clientBuffers[index]; + if (!GLctx.currentArrayBufferBinding) { + cb.size = size; + cb.type = type; + cb.normalized = normalized; + cb.stride = stride; + cb.ptr = ptr; + cb.clientside = true; + cb.vertexAttribPointerAdaptor = function(index, size, type, normalized, stride, ptr) { + this.vertexAttribPointer(index, size, type, normalized, stride, ptr); + }; + return; + } + cb.clientside = false; + GLctx.vertexAttribPointer(index, size, type, !!normalized, stride, ptr); +}; + +var _glVertexAttribPointer = _emscripten_glVertexAttribPointer; + +var _emscripten_glViewport = (x0, x1, x2, x3) => GLctx.viewport(x0, x1, x2, x3); + +var _glViewport = _emscripten_glViewport; + +function _mediapipe_find_canvas_event_target(canvasSelector) { + let target = findCanvasEventTarget(canvasSelector); + // WebGPU-on-worker uses this function to try to grab the canvas, but + // doesn't have a DOM element to find. So as a quick patch, if the default + // behavior is unsuccessful here then we try a webgpu canvas property + // which is set by the user directly on the Module, much like how our old + // pipeline used the Module.canvas property. See b/265271517 for details. + if (Module && !target) { + target = Module.canvasWebGpu; + } + return Emval.toHandle(target); +} + +function _mediapipe_webgl_tex_image_drawable(drawableHandle) { + const drawable = Emval.toValue(drawableHandle); + GLctx.texImage2D(GLctx.TEXTURE_2D, 0, GLctx.RGBA, GLctx.RGBA, GLctx.UNSIGNED_BYTE, drawable); +} + +var _random_get = (buffer, size) => randomFill(HEAPU8.subarray(buffer, buffer + size)); + +var _wgpuCommandEncoderBeginComputePass = (encoderPtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "timestampWrites": WebGPU.makePassTimestampWrites(HEAPU32[(((descriptor) + (12)) >> 2)]) + }; + } + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateComputePassEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.beginComputePass(desc)); + return ptr; +}; + +var _wgpuCommandEncoderBeginRenderPass = (encoderPtr, descriptor) => { + function makeColorAttachment(caPtr) { + var viewPtr = HEAPU32[(((caPtr) + (4)) >> 2)]; + if (viewPtr === 0) { + // Null `view` means no attachment in this slot. + return undefined; + } + var depthSlice = HEAPU32[(((caPtr) + (8)) >> 2)]; + if (depthSlice == 4294967295) depthSlice = undefined; + return { + "view": WebGPU.getJsObject(viewPtr), + "depthSlice": depthSlice, + "resolveTarget": WebGPU.getJsObject(HEAPU32[(((caPtr) + (12)) >> 2)]), + "clearValue": WebGPU.makeColor(caPtr + 24), + "loadOp": WebGPU.LoadOp[HEAP32[(((caPtr) + (16)) >> 2)]], + "storeOp": WebGPU.StoreOp[HEAP32[(((caPtr) + (20)) >> 2)]] + }; + } + function makeColorAttachments(count, caPtr) { + var attachments = []; + for (var i = 0; i < count; ++i) { + attachments.push(makeColorAttachment(caPtr + 56 * i)); + } + return attachments; + } + function makeDepthStencilAttachment(dsaPtr) { + if (dsaPtr === 0) return undefined; + return { + "view": WebGPU.getJsObject(HEAPU32[(((dsaPtr) + (4)) >> 2)]), + "depthClearValue": HEAPF32[(((dsaPtr) + (16)) >> 2)], + "depthLoadOp": WebGPU.LoadOp[HEAP32[(((dsaPtr) + (8)) >> 2)]], + "depthStoreOp": WebGPU.StoreOp[HEAP32[(((dsaPtr) + (12)) >> 2)]], + "depthReadOnly": !!(HEAPU32[(((dsaPtr) + (20)) >> 2)]), + "stencilClearValue": HEAPU32[(((dsaPtr) + (32)) >> 2)], + "stencilLoadOp": WebGPU.LoadOp[HEAP32[(((dsaPtr) + (24)) >> 2)]], + "stencilStoreOp": WebGPU.StoreOp[HEAP32[(((dsaPtr) + (28)) >> 2)]], + "stencilReadOnly": !!(HEAPU32[(((dsaPtr) + (36)) >> 2)]) + }; + } + function makeRenderPassDescriptor(descriptor) { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var maxDrawCount = undefined; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var renderPassMaxDrawCount = nextInChainPtr; + // Note: The user could have passed a really huge value here, which is technically valid in + // C but will not be allowed by WebGPU in JS because of [EnforceRange]. We intentionally + // ignore that case because it's not useful - apps can just pick a smaller maxDrawCount. + maxDrawCount = readI53FromI64((renderPassMaxDrawCount) + (8)); + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "colorAttachments": makeColorAttachments(HEAPU32[(((descriptor) + (12)) >> 2)], HEAPU32[(((descriptor) + (16)) >> 2)]), + "depthStencilAttachment": makeDepthStencilAttachment(HEAPU32[(((descriptor) + (20)) >> 2)]), + "occlusionQuerySet": WebGPU.getJsObject(HEAPU32[(((descriptor) + (24)) >> 2)]), + "timestampWrites": WebGPU.makePassTimestampWrites(HEAPU32[(((descriptor) + (28)) >> 2)]), + "maxDrawCount": maxDrawCount + }; + return desc; + } + var desc = makeRenderPassDescriptor(descriptor); + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateRenderPassEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.beginRenderPass(desc)); + return ptr; +}; + +var _wgpuCommandEncoderCopyBufferToTexture = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyBufferToTexture(WebGPU.makeTexelCopyBufferInfo(srcPtr), WebGPU.makeTexelCopyTextureInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderCopyTextureToBuffer = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyTextureToBuffer(WebGPU.makeTexelCopyTextureInfo(srcPtr), WebGPU.makeTexelCopyBufferInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderCopyTextureToTexture = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyTextureToTexture(WebGPU.makeTexelCopyTextureInfo(srcPtr), WebGPU.makeTexelCopyTextureInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderFinish = (encoderPtr, descriptor) => { + // TODO: Use the descriptor. + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateCommandBuffer(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.finish()); + return ptr; +}; + +var _wgpuComputePassEncoderDispatchWorkgroups = (passPtr, x, y, z) => { + var pass = WebGPU.getJsObject(passPtr); + pass.dispatchWorkgroups(x, y, z); +}; + +var _wgpuComputePassEncoderEnd = passPtr => { + var pass = WebGPU.getJsObject(passPtr); + pass.end(); +}; + +var _wgpuComputePassEncoderSetBindGroup = (passPtr, groupIndex, groupPtr, dynamicOffsetCount, dynamicOffsetsPtr) => { + var pass = WebGPU.getJsObject(passPtr); + var group = WebGPU.getJsObject(groupPtr); + if (dynamicOffsetCount == 0) { + pass.setBindGroup(groupIndex, group); + } else { + pass.setBindGroup(groupIndex, group, HEAPU32, ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount); + } +}; + +var _wgpuComputePassEncoderSetPipeline = (passPtr, pipelinePtr) => { + var pass = WebGPU.getJsObject(passPtr); + var pipeline = WebGPU.getJsObject(pipelinePtr); + pass.setPipeline(pipeline); +}; + +var _wgpuComputePipelineGetBindGroupLayout = (pipelinePtr, groupIndex) => { + var pipeline = WebGPU.getJsObject(pipelinePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, pipeline.getBindGroupLayout(groupIndex)); + return ptr; +}; + +var _wgpuDeviceCreateBindGroup = (devicePtr, descriptor) => { + function makeEntry(entryPtr) { + var bufferPtr = HEAPU32[(((entryPtr) + (8)) >> 2)]; + var samplerPtr = HEAPU32[(((entryPtr) + (32)) >> 2)]; + var textureViewPtr = HEAPU32[(((entryPtr) + (36)) >> 2)]; + var externalTexturePtr = 0; + WebGPU.iterateExtensions(entryPtr, { + 14: ptr => { + externalTexturePtr = HEAPU32[(((ptr) + (8)) >> 2)]; + } + }); + var resource; + if (bufferPtr) { + // Note the sentinel UINT64_MAX will be read as -1. + var size = readI53FromI64((entryPtr) + (24)); + if (size == -1) size = undefined; + resource = { + "buffer": WebGPU.getJsObject(bufferPtr), + "offset": readI53FromI64((entryPtr) + (16)), + "size": size + }; + } else { + resource = WebGPU.getJsObject(samplerPtr || textureViewPtr || externalTexturePtr); + } + return { + "binding": HEAPU32[(((entryPtr) + (4)) >> 2)], + "resource": resource + }; + } + function makeEntries(count, entriesPtrs) { + var entries = []; + for (var i = 0; i < count; ++i) { + entries.push(makeEntry(entriesPtrs + 40 * i)); + } + return entries; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.getJsObject(HEAPU32[(((descriptor) + (12)) >> 2)]), + "entries": makeEntries(HEAPU32[(((descriptor) + (16)) >> 2)], HEAPU32[(((descriptor) + (20)) >> 2)]) + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateBindGroup(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createBindGroup(desc)); + return ptr; +}; + +var _wgpuDeviceCreateBindGroupLayout = (devicePtr, descriptor) => { + function makeBufferEntry(substructPtr) { + var typeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!typeInt) return undefined; + return { + "type": WebGPU.BufferBindingType[typeInt], + "hasDynamicOffset": !!(HEAPU32[(((substructPtr) + (8)) >> 2)]), + "minBindingSize": readI53FromI64((substructPtr) + (16)) + }; + } + function makeSamplerEntry(substructPtr) { + var typeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!typeInt) return undefined; + return { + "type": WebGPU.SamplerBindingType[typeInt] + }; + } + function makeTextureEntry(substructPtr) { + var sampleTypeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!sampleTypeInt) return undefined; + return { + "sampleType": WebGPU.TextureSampleType[sampleTypeInt], + "viewDimension": WebGPU.TextureViewDimension[HEAP32[(((substructPtr) + (8)) >> 2)]], + "multisampled": !!(HEAPU32[(((substructPtr) + (12)) >> 2)]) + }; + } + function makeStorageTextureEntry(substructPtr) { + var accessInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!accessInt) return undefined; + return { + "access": WebGPU.StorageTextureAccess[accessInt], + "format": WebGPU.TextureFormat[HEAP32[(((substructPtr) + (8)) >> 2)]], + "viewDimension": WebGPU.TextureViewDimension[HEAP32[(((substructPtr) + (12)) >> 2)]] + }; + } + function makeEntry(entryPtr) { + var entry = { + "binding": HEAPU32[(((entryPtr) + (4)) >> 2)], + "visibility": HEAPU32[(((entryPtr) + (8)) >> 2)], + "buffer": makeBufferEntry(entryPtr + 24), + "sampler": makeSamplerEntry(entryPtr + 48), + "texture": makeTextureEntry(entryPtr + 56), + "storageTexture": makeStorageTextureEntry(entryPtr + 72) + }; + WebGPU.iterateExtensions(entryPtr, { + 13: ptr => { + entry["externalTexture"] = {}; + } + }); + return entry; + } + function makeEntries(count, entriesPtrs) { + var entries = []; + for (var i = 0; i < count; ++i) { + entries.push(makeEntry(entriesPtrs + 88 * i)); + } + return entries; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "entries": makeEntries(HEAPU32[(((descriptor) + (12)) >> 2)], HEAPU32[(((descriptor) + (16)) >> 2)]) + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createBindGroupLayout(desc)); + return ptr; +}; + +var _wgpuDeviceCreateCommandEncoder = (devicePtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4) + }; + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateCommandEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createCommandEncoder(desc)); + return ptr; +}; + +var _wgpuDeviceCreateComputePipeline = (devicePtr, descriptor) => { + var desc = WebGPU.makeComputePipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateComputePipeline(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createComputePipeline(desc)); + return ptr; +}; + +var _wgpuDeviceCreatePipelineLayout = (devicePtr, descriptor) => { + var bglCount = HEAPU32[(((descriptor) + (12)) >> 2)]; + var bglPtr = HEAPU32[(((descriptor) + (16)) >> 2)]; + var bgls = []; + for (var i = 0; i < bglCount; ++i) { + bgls.push(WebGPU.getJsObject(HEAPU32[(((bglPtr) + (4 * i)) >> 2)])); + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "bindGroupLayouts": bgls + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreatePipelineLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createPipelineLayout(desc)); + return ptr; +}; + +var _wgpuDeviceCreateRenderPipeline = (devicePtr, descriptor) => { + var desc = WebGPU.makeRenderPipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateRenderPipeline(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createRenderPipeline(desc)); + return ptr; +}; + +var _wgpuDeviceCreateSampler = (devicePtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "addressModeU": WebGPU.AddressMode[HEAP32[(((descriptor) + (12)) >> 2)]], + "addressModeV": WebGPU.AddressMode[HEAP32[(((descriptor) + (16)) >> 2)]], + "addressModeW": WebGPU.AddressMode[HEAP32[(((descriptor) + (20)) >> 2)]], + "magFilter": WebGPU.FilterMode[HEAP32[(((descriptor) + (24)) >> 2)]], + "minFilter": WebGPU.FilterMode[HEAP32[(((descriptor) + (28)) >> 2)]], + "mipmapFilter": WebGPU.MipmapFilterMode[HEAP32[(((descriptor) + (32)) >> 2)]], + "lodMinClamp": HEAPF32[(((descriptor) + (36)) >> 2)], + "lodMaxClamp": HEAPF32[(((descriptor) + (40)) >> 2)], + "compare": WebGPU.CompareFunction[HEAP32[(((descriptor) + (44)) >> 2)]], + "maxAnisotropy": HEAPU16[(((descriptor) + (48)) >> 1)] + }; + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateSampler(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createSampler(desc)); + return ptr; +}; + +var _wgpuDeviceCreateTexture = (devicePtr, descriptor) => { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var textureBindingViewDimension; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var textureBindingViewDimensionDescriptor = nextInChainPtr; + textureBindingViewDimension = WebGPU.TextureViewDimension[HEAP32[(((textureBindingViewDimensionDescriptor) + (8)) >> 2)]]; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "size": WebGPU.makeExtent3D(descriptor + 28), + "mipLevelCount": HEAPU32[(((descriptor) + (44)) >> 2)], + "sampleCount": HEAPU32[(((descriptor) + (48)) >> 2)], + "dimension": WebGPU.TextureDimension[HEAP32[(((descriptor) + (24)) >> 2)]], + "format": WebGPU.TextureFormat[HEAP32[(((descriptor) + (40)) >> 2)]], + "usage": HEAPU32[(((descriptor) + (16)) >> 2)], + "textureBindingViewDimension": textureBindingViewDimension + }; + var viewFormatCount = HEAPU32[(((descriptor) + (52)) >> 2)]; + if (viewFormatCount) { + var viewFormatsPtr = HEAPU32[(((descriptor) + (56)) >> 2)]; + // viewFormatsPtr pointer to an array of TextureFormat which is an enum of size uint32_t + desc["viewFormats"] = Array.from(HEAP32.subarray((((viewFormatsPtr) >> 2)), ((viewFormatsPtr + viewFormatCount * 4) >> 2)), format => WebGPU.TextureFormat[format]); + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateTexture(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createTexture(desc)); + return ptr; +}; + +var _wgpuQueueSubmit = (queuePtr, commandCount, commands) => { + var queue = WebGPU.getJsObject(queuePtr); + var cmds = Array.from(HEAP32.subarray((((commands) >> 2)), ((commands + commandCount * 4) >> 2)), id => WebGPU.getJsObject(id)); + queue.submit(cmds); +}; + +function _wgpuQueueWriteBuffer(queuePtr, bufferPtr, bufferOffset_low, bufferOffset_high, data, size) { + var bufferOffset = convertI32PairToI53Checked(bufferOffset_low, bufferOffset_high); + var queue = WebGPU.getJsObject(queuePtr); + var buffer = WebGPU.getJsObject(bufferPtr); + // There is a size limitation for ArrayBufferView. Work around by passing in a subarray + // instead of the whole heap. crbug.com/1201109 + var subarray = HEAPU8.subarray(data, data + size); + queue.writeBuffer(buffer, bufferOffset, subarray, 0, size); +} + +var _wgpuQueueWriteTexture = (queuePtr, destinationPtr, data, dataSize, dataLayoutPtr, writeSizePtr) => { + var queue = WebGPU.getJsObject(queuePtr); + var destination = WebGPU.makeTexelCopyTextureInfo(destinationPtr); + var dataLayout = WebGPU.makeTexelCopyBufferLayout(dataLayoutPtr); + var writeSize = WebGPU.makeExtent3D(writeSizePtr); + // This subarray isn't strictly necessary, but helps work around an issue + // where Chromium makes a copy of the entire heap. crbug.com/1134457 + var subarray = HEAPU8.subarray(data, data + dataSize); + queue.writeTexture(destination, subarray, dataLayout, writeSize); +}; + +var _wgpuRenderPassEncoderDraw = (passPtr, vertexCount, instanceCount, firstVertex, firstInstance) => { + firstVertex >>>= 0; + firstInstance >>>= 0; + var pass = WebGPU.getJsObject(passPtr); + pass.draw(vertexCount, instanceCount, firstVertex, firstInstance); +}; + +var _wgpuRenderPassEncoderEnd = encoderPtr => { + var encoder = WebGPU.getJsObject(encoderPtr); + encoder.end(); +}; + +var _wgpuRenderPassEncoderSetBindGroup = (passPtr, groupIndex, groupPtr, dynamicOffsetCount, dynamicOffsetsPtr) => { + var pass = WebGPU.getJsObject(passPtr); + var group = WebGPU.getJsObject(groupPtr); + if (dynamicOffsetCount == 0) { + pass.setBindGroup(groupIndex, group); + } else { + pass.setBindGroup(groupIndex, group, HEAPU32, ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount); + } +}; + +var _wgpuRenderPassEncoderSetPipeline = (passPtr, pipelinePtr) => { + var pass = WebGPU.getJsObject(passPtr); + var pipeline = WebGPU.getJsObject(pipelinePtr); + pass.setPipeline(pipeline); +}; + +var _wgpuRenderPipelineGetBindGroupLayout = (pipelinePtr, groupIndex) => { + var pipeline = WebGPU.getJsObject(pipelinePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, pipeline.getBindGroupLayout(groupIndex)); + return ptr; +}; + +var _wgpuTextureCreateView = (texturePtr, descriptor) => { + var desc; + if (descriptor) { + var swizzle; + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var swizzleDescriptor = nextInChainPtr; + var swizzlePtr = swizzleDescriptor + 8; + var r = WebGPU.ComponentSwizzle[HEAP32[((swizzlePtr) >> 2)]] || "r"; + var g = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (4)) >> 2)]] || "g"; + var b = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (8)) >> 2)]] || "b"; + var a = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (12)) >> 2)]] || "a"; + swizzle = `${r}${g}${b}${a}`; + } + var mipLevelCount = HEAPU32[(((descriptor) + (24)) >> 2)]; + var arrayLayerCount = HEAPU32[(((descriptor) + (32)) >> 2)]; + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "format": WebGPU.TextureFormat[HEAP32[(((descriptor) + (12)) >> 2)]], + "dimension": WebGPU.TextureViewDimension[HEAP32[(((descriptor) + (16)) >> 2)]], + "baseMipLevel": HEAPU32[(((descriptor) + (20)) >> 2)], + "mipLevelCount": mipLevelCount === 4294967295 ? undefined : mipLevelCount, + "baseArrayLayer": HEAPU32[(((descriptor) + (28)) >> 2)], + "arrayLayerCount": arrayLayerCount === 4294967295 ? undefined : arrayLayerCount, + "aspect": WebGPU.TextureAspect[HEAP32[(((descriptor) + (36)) >> 2)]], + "usage": HEAPU32[(((descriptor) + (40)) >> 2)], + "swizzle": swizzle + }; + } + var texture = WebGPU.getJsObject(texturePtr); + var ptr = _emwgpuCreateTextureView(0); + WebGPU.Internals.jsObjectInsert(ptr, texture.createView(desc)); + return ptr; +}; + +var _wgpuTextureDestroy = texturePtr => { + WebGPU.getJsObject(texturePtr).destroy(); +}; + +var _wgpuTextureGetFormat = texturePtr => { + var texture = WebGPU.getJsObject(texturePtr); + // Should return the enum integer instead of string. + return WebGPU.TextureFormat.indexOf(texture.format); +}; + +var getCFunc = ident => { + var func = Module["_" + ident]; + // closure exported function + return func; +}; + +var writeArrayToMemory = (array, buffer) => { + HEAP8.set(array, buffer); +}; + +/** + * @param {string|null=} returnType + * @param {Array=} argTypes + * @param {Array=} args + * @param {Object=} opts + */ var ccall = (ident, returnType, argTypes, args, opts) => { + // For fast lookup of conversion functions + var toC = { + "string": str => { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { + // null string + ret = stringToUTF8OnStack(str); + } + return ret; + }, + "array": arr => { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret; + } + }; + function convertReturnValue(ret) { + if (returnType === "string") { + return UTF8ToString(ret); + } + if (returnType === "boolean") return Boolean(ret); + return ret; + } + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func(...cArgs); + function onDone(ret) { + if (stack !== 0) stackRestore(stack); + return convertReturnValue(ret); + } + ret = onDone(ret); + return ret; +}; + +var FS_createPath = (...args) => FS.createPath(...args); + +var FS_unlink = (...args) => FS.unlink(...args); + +var FS_createLazyFile = (...args) => FS.createLazyFile(...args); + +var FS_createDevice = (...args) => FS.createDevice(...args); + +FS.createPreloadedFile = FS_createPreloadedFile; + +FS.preloadFile = FS_preloadFile; + +FS.staticInit(); + +// Signal GL rendering layer that processing of a new frame is about to +// start. This helps it optimize VBO double-buffering and reduce GPU stalls. +registerPreMainLoop(() => GL.newRenderingFrameStarted()); + +for (let i = 0; i < 32; ++i) tempFixedLengthArray.push(new Array(i)); + +var miniTempWebGLFloatBuffersStorage = new Float32Array(288); + +// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive +for (/**@suppress{duplicate}*/ var i = 0; i <= 288; ++i) { + miniTempWebGLFloatBuffers[i] = miniTempWebGLFloatBuffersStorage.subarray(0, i); +} + +var miniTempWebGLIntBuffersStorage = new Int32Array(288); + +// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive +for (/**@suppress{duplicate}*/ var i = 0; i <= 288; ++i) { + miniTempWebGLIntBuffers[i] = miniTempWebGLIntBuffersStorage.subarray(0, i); +} + +// End JS library code +// include: postlibrary.js +// This file is included after the automatically-generated JS library code +// but before the wasm module is created. +{ + // Begin ATMODULES hooks + if (Module["preloadPlugins"]) preloadPlugins = Module["preloadPlugins"]; + if (Module["noExitRuntime"]) noExitRuntime = Module["noExitRuntime"]; + if (Module["print"]) out = Module["print"]; + if (Module["printErr"]) err = Module["printErr"]; + if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; + // End ATMODULES hooks + if (Module["arguments"]) arguments_ = Module["arguments"]; + if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; + if (Module["preInit"]) { + if (typeof Module["preInit"] == "function") Module["preInit"] = [ Module["preInit"] ]; + while (Module["preInit"].length > 0) { + Module["preInit"].shift()(); + } + } +} + +// Begin runtime exports +Module["addRunDependency"] = addRunDependency; + +Module["removeRunDependency"] = removeRunDependency; + +Module["ccall"] = ccall; + +Module["stringToNewUTF8"] = stringToNewUTF8; + +Module["FS_preloadFile"] = FS_preloadFile; + +Module["FS_unlink"] = FS_unlink; + +Module["FS_createPath"] = FS_createPath; + +Module["FS_createDevice"] = FS_createDevice; + +Module["FS_createDataFile"] = FS_createDataFile; + +Module["FS_createLazyFile"] = FS_createLazyFile; + +// End runtime exports +// Begin JS library exports +// End JS library exports +// end include: postlibrary.js +var ASM_CONSTS = { + 1462283: $0 => { + const canvas = Emval.toValue($0); + const context = canvas.getContext("webgpu"); + return WebGPU.importJsTexture(context.getCurrentTexture()); + }, + 1462426: ($0, $1, $2, $3, $4) => { + const drawable = Emval.toValue($0); + const device = WebGPU.getJsObject($1); + const texture = WebGPU.getJsObject($2); + const width = $3; + const height = $4; + device.queue.copyExternalImageToTexture({ + source: drawable + }, { + texture + }, [ width, height ]); + }, + 1462685: ($0, $1, $2, $3) => { + const sourceExtTex = Emval.toValue($0); + const device = WebGPU.getJsObject($1); + const sampler = WebGPU.getJsObject($2); + const bgLayout = WebGPU.getJsObject($3); + const bindGroup = device.createBindGroup({ + layout: bgLayout, + entries: [ { + binding: 0, + resource: sampler + }, { + binding: 1, + resource: sourceExtTex + } ] + }); + return WebGPU.importJsBindGroup(bindGroup); + }, + 1463055: ($0, $1) => { + const input = Emval.toValue($0); + const output = Emval.toValue($1); + const ctx = output.getContext("2d"); + ctx.drawImage(input, 0, 0, output.width, output.height); + }, + 1463220: ($0, $1) => { + const inputArray = Emval.toValue($0); + const output = Emval.toValue($1); + const ctx = output.getContext("2d"); + const image_data = new ImageData(inputArray, output.width, output.height); + ctx.putImageData(image_data, 0, 0); + }, + 1463444: ($0, $1) => { + const input = Emval.toValue($0); + const outputArray = Emval.toValue($1); + const ctx = input.getContext("2d"); + const data = ctx.getImageData(0, 0, input.width, input.height); + outputArray.set(data.data); + }, + 1463648: () => (typeof HTMLCanvasElement !== "undefined"), + 1463703: () => !!Module["preinitializedWebGPUDevice"], + 1463754: () => { + specialHTMLTargets["#canvas"] = Module.canvas; + } +}; + +function BeginGlQueryTiming(calc_name, num_repetitions) { + const gl = Module.canvas.getContext("webgl2"); + const query = gl.createQuery(); + Module.WEBGL_SHADER_CALC_METRICS = Module.WEBGL_SHADER_CALC_METRICS || {}; + Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)] = { + query, + repetitions: num_repetitions + }; + Module.WEBGL_QUERY_TIMER_EXT = Module.WEBGL_QUERY_TIMER_EXT || gl.getExtension("EXT_disjoint_timer_query_webgl2"); + gl.beginQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT, query); +} + +function EndGlQueryTiming(calc_name) { + const gl = Module.canvas.getContext("webgl2"); + gl.endQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT, Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)].query); +} + +function JsWrapImageConverter() { + if (!Module._imageConverter) { + Module._imageConverter = (binaryPtr, binarySize, width, height, numChannels, makeDeepCopy, outputType) => { + const imageData = new outputType(makeDeepCopy ? Module.HEAPU8.slice(binaryPtr, binaryPtr + binarySize).buffer : Module.HEAPU8.buffer, binaryPtr, width * height * numChannels); + return { + data: imageData, + width, + height + }; + }; + } +} + +function JsOnUint8ArrayImageListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Uint8Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, timestamp_ms); +} + +function JsOnFloat32ArrayImageListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Float32Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, timestamp_ms); +} + +function JsOnWebGLTextureListener(output_stream_name, name, width, height, timestamp_ms) { + Module._wrapSimpleListenerOutput(output_stream_name, { + data: GL.textures[name], + width, + height + }, timestamp_ms); +} + +function JsOnUint8ArrayImageVectorListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Uint8Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, false, timestamp_ms); +} + +function JsOnFloat32ArrayImageVectorListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Float32Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, false, timestamp_ms); +} + +function JsOnWebGLTextureVectorListener(output_stream_name, name, width, height, timestamp_ms) { + Module._wrapSimpleListenerOutput(output_stream_name, { + data: GL.textures[name], + width, + height + }, false, timestamp_ms); +} + +function JsOnEmptyPacketListener(output_stream_name, timestamp) { + Module._wrapEmptyPacketListenerOutput(output_stream_name, timestamp); +} + +function JsOnVectorFinishedListener(output_stream_name, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, undefined, true, timestamp); +} + +function JsOnSimpleListenerBool(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerBool(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerInt(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerInt(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerUint(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerUint(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerDouble(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerDouble(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerFloat(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerFloat(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerString(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, UTF8ToString(out_data), timestamp); +} + +function JsOnVectorListenerString(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, UTF8ToString(out_data), false, timestamp); +} + +function JsOnVectorListenerProto(output_stream_name, proto_ptr, proto_size, make_deep_copy, timestamp) { + const newProtoArray = make_deep_copy ? Module.HEAPU8.slice(proto_ptr, proto_ptr + proto_size) : new Uint8Array(Module.HEAPU8.buffer, proto_ptr, proto_size); + Module._wrapSimpleListenerOutput(output_stream_name, newProtoArray, false, timestamp); +} + +function JsWrapSimpleListeners() { + if (!Module._wrapSimpleListenerOutput) { + Module._wrapSimpleListenerOutput = (outputStreamName, ...args) => { + if (Module.simpleListeners) { + const streamName = UTF8ToString(outputStreamName); + if (Module.simpleListeners[streamName]) { + Module.simpleListeners[streamName](...args); + } + } + }; + } + if (!Module._wrapEmptyPacketListenerOutput) { + Module._wrapEmptyPacketListenerOutput = (outputStreamName, timestamp) => { + if (Module.emptyPacketListeners) { + const streamName = UTF8ToString(outputStreamName); + if (Module.emptyPacketListeners[streamName]) { + Module.emptyPacketListeners[streamName](timestamp); + } + } + }; + } +} + +function JsOnSimpleListenerBinaryArray(output_stream_name, binary_ptr, binary_size, make_deep_copy, timestamp) { + const newProtoArray = make_deep_copy ? Module.HEAPU8.slice(binary_ptr, binary_ptr + binary_size) : new Uint8Array(Module.HEAPU8.buffer, binary_ptr, binary_size); + Module._wrapSimpleListenerOutput(output_stream_name, newProtoArray, timestamp); +} + +function mediapipe_import_external_texture(device_handle, source_handle) { + const device = WebGPU.getJsObject(device_handle); + const source = Emval.toValue(source_handle); + const externalTexture = device.importExternalTexture({ + source + }); + return Emval.toHandle(externalTexture); +} + +function mediapipe_create_utility_canvas2d() { + let canvas; + if (typeof HTMLCanvasElement !== "undefined") { + canvas = document.createElement("canvas"); + canvas.style.display = "none"; + } else { + canvas = new OffscreenCanvas(0, 0); + } + return Emval.toHandle(canvas); +} + +function GetAdapterArchitecture() { + const device = Module["preinitializedWebGPUDevice"]; + const architecture = device.adapterInfo ? device.adapterInfo.architecture : "Unknown"; + return stringToNewUTF8(architecture); +} + +function GetAdapterDescription() { + const device = Module["preinitializedWebGPUDevice"]; + const description = device.adapterInfo ? device.adapterInfo.description : "Unknown"; + return stringToNewUTF8(description); +} + +function GetAdapterDeviceName() { + const device = Module["preinitializedWebGPUDevice"]; + const deviceName = device.adapterInfo ? device.adapterInfo.device : "Unknown"; + return stringToNewUTF8(deviceName); +} + +function GetAdapterVendor() { + const device = Module["preinitializedWebGPUDevice"]; + const vendor = device.adapterInfo ? device.adapterInfo.vendor : "Unknown"; + return stringToNewUTF8(vendor); +} + +function __asyncjs__mediapipe_map_buffer_jspi(buffer_handle, data) { + return Asyncify.handleAsync(async () => { + const buffer = WebGPU.getJsObject(buffer_handle); + if ("mapSync" in buffer) { + buffer.mapSync(GPUMapMode.READ); + } else { + await buffer.mapAsync(GPUMapMode.READ); + } + const mapped = buffer.getMappedRange(); + HEAPU8.set(new Uint8Array(mapped), data); + buffer.unmap(); + }); +} + +function hardware_concurrency() { + var concurrency = 1; + try { + concurrency = self.navigator.hardwareConcurrency; + } catch (e) {} + return concurrency; +} + +function JsWrapErrorListener(code, message) { + if (Module.errorListener) { + const stringMessage = UTF8ToString(message); + Module.errorListener(code, stringMessage); + } +} + +function UseBottomLeftGpuOrigin() { + return (Module && Module.gpuOriginForWebTexturesIsBottomLeft); +} + +function custom_emscripten_dbgn(str, len) { + if (typeof (dbg) !== "undefined") { + dbg(UTF8ToString(str, len)); + } else { + if (typeof (custom_dbg) === "undefined") { + function custom_dbg(text) { + console.warn.apply(console, arguments); + } + } + custom_dbg(UTF8ToString(str, len)); + } +} + +// Imports from the Wasm binary. +var _free, _malloc, _wgpuDeviceAddRef, _addBoundTextureAsImageToStream, _attachImageListener, _attachImageVectorListener, _registerModelResourcesGraphService, _bindTextureToStream, _addBoundTextureToStream, _addDoubleToInputStream, _addFloatToInputStream, _addBoolToInputStream, _addIntToInputStream, _addUintToInputStream, _addStringToInputStream, _addRawDataSpanToInputStream, _allocateBoolVector, _allocateFloatVector, _allocateDoubleVector, _allocateIntVector, _allocateUintVector, _allocateStringVector, _addBoolVectorEntry, _addFloatVectorEntry, _addDoubleVectorEntry, _addIntVectorEntry, _addUintVectorEntry, _addStringVectorEntry, _addBoolVectorToInputStream, _addFloatVectorToInputStream, _addDoubleVectorToInputStream, _addIntVectorToInputStream, _addUintVectorToInputStream, _addStringVectorToInputStream, _addFlatHashMapToInputStream, _addProtoToInputStream, _addEmptyPacketToInputStream, _addBoolToInputSidePacket, _addDoubleToInputSidePacket, _addFloatToInputSidePacket, _addIntToInputSidePacket, _addUintToInputSidePacket, _addStringToInputSidePacket, _addRawDataSpanToInputSidePacket, _addProtoToInputSidePacket, _addBoolVectorToInputSidePacket, _addDoubleVectorToInputSidePacket, _addFloatVectorToInputSidePacket, _addIntVectorToInputSidePacket, _addUintVectorToInputSidePacket, _addStringVectorToInputSidePacket, _attachBoolListener, _attachBoolVectorListener, _attachDoubleListener, _attachDoubleVectorListener, _attachFloatListener, _attachFloatVectorListener, _attachIntListener, _attachIntVectorListener, _attachUintListener, _attachUintVectorListener, _attachStringListener, _attachStringVectorListener, _attachProtoListener, _attachProtoVectorListener, _getGraphConfig, ___getTypeName, _emwgpuCreateBindGroup, _emwgpuCreateBindGroupLayout, _emwgpuCreateCommandBuffer, _emwgpuCreateCommandEncoder, _emwgpuCreateComputePassEncoder, _emwgpuCreateComputePipeline, _emwgpuCreateExternalTexture, _emwgpuCreatePipelineLayout, _emwgpuCreateQuerySet, _emwgpuCreateRenderBundle, _emwgpuCreateRenderBundleEncoder, _emwgpuCreateRenderPassEncoder, _emwgpuCreateRenderPipeline, _emwgpuCreateSampler, _emwgpuCreateSurface, _emwgpuCreateTexture, _emwgpuCreateTextureView, _emwgpuCreateAdapter, _emwgpuImportBuffer, _emwgpuCreateDevice, _emwgpuCreateQueue, _emwgpuCreateShaderModule, _emwgpuOnCreateComputePipelineCompleted, _emwgpuOnCreateRenderPipelineCompleted, _clearSubgraphs, _pushBinarySubgraph, _pushTextSubgraph, _changeBinaryGraph, _changeTextGraph, _processGl, _process, _bindTextureToCanvas, _requestShaderRefreshOnGraphChange, _waitUntilIdle, _closeGraph, _setAutoRenderToScreen, _emscripten_builtin_memalign, _memalign, __emscripten_tempret_set, __emscripten_stack_restore, __emscripten_stack_alloc, _emscripten_stack_get_current, dynCall_ji, dynCall_jii, dynCall_iiiijij, dynCall_viiji, dynCall_viji, dynCall_iiiji, dynCall_jjj, dynCall_iiiijj, dynCall_viijj, dynCall_viiijjj, dynCall_vij, dynCall_viiiji, dynCall_viijii, dynCall_vijjj, dynCall_vj, dynCall_viij, dynCall_jiji, dynCall_iiiiij, dynCall_iiiiijj, dynCall_iiiiiijj, memory, _kVersionStampBuildChangelistStr, _kVersionStampCitcSnapshotStr, _kVersionStampCitcWorkspaceIdStr, _kVersionStampSourceUriStr, _kVersionStampBuildClientStr, _kVersionStampBuildClientMintStatusStr, _kVersionStampBuildCompilerStr, _kVersionStampBuildDateTimePstStr, _kVersionStampBuildDepotPathStr, _kVersionStampBuildIdStr, _kVersionStampBuildInfoStr, _kVersionStampBuildLabelStr, _kVersionStampBuildTargetStr, _kVersionStampBuildTimestampStr, _kVersionStampBuildToolStr, _kVersionStampG3BuildTargetStr, _kVersionStampVerifiableStr, _kVersionStampBuildFdoTypeStr, _kVersionStampBuildBaselineChangelistStr, _kVersionStampBuildLtoTypeStr, _kVersionStampBuildPropellerTypeStr, _kVersionStampBuildPghoTypeStr, _kVersionStampBuildUsernameStr, _kVersionStampBuildHostnameStr, _kVersionStampBuildDirectoryStr, _kVersionStampBuildChangelistInt, _kVersionStampCitcSnapshotInt, _kVersionStampBuildClientMintStatusInt, _kVersionStampBuildTimestampInt, _kVersionStampVerifiableInt, _kVersionStampBuildCoverageEnabledInt, _kVersionStampBuildBaselineChangelistInt, _kVersionStampPrecookedTimestampStr, _kVersionStampPrecookedClientInfoStr, __indirect_function_table, wasmMemory, wasmTable; + +function assignWasmExports(wasmExports) { + _free = Module["_free"] = wasmExports["Td"]; + _malloc = Module["_malloc"] = wasmExports["Ud"]; + _wgpuDeviceAddRef = wasmExports["Vd"]; + _addBoundTextureAsImageToStream = Module["_addBoundTextureAsImageToStream"] = wasmExports["Wd"]; + _attachImageListener = Module["_attachImageListener"] = wasmExports["Xd"]; + _attachImageVectorListener = Module["_attachImageVectorListener"] = wasmExports["Yd"]; + _registerModelResourcesGraphService = Module["_registerModelResourcesGraphService"] = wasmExports["Zd"]; + _bindTextureToStream = Module["_bindTextureToStream"] = wasmExports["_d"]; + _addBoundTextureToStream = Module["_addBoundTextureToStream"] = wasmExports["$d"]; + _addDoubleToInputStream = Module["_addDoubleToInputStream"] = wasmExports["ae"]; + _addFloatToInputStream = Module["_addFloatToInputStream"] = wasmExports["be"]; + _addBoolToInputStream = Module["_addBoolToInputStream"] = wasmExports["ce"]; + _addIntToInputStream = Module["_addIntToInputStream"] = wasmExports["de"]; + _addUintToInputStream = Module["_addUintToInputStream"] = wasmExports["ee"]; + _addStringToInputStream = Module["_addStringToInputStream"] = wasmExports["fe"]; + _addRawDataSpanToInputStream = Module["_addRawDataSpanToInputStream"] = wasmExports["ge"]; + _allocateBoolVector = Module["_allocateBoolVector"] = wasmExports["he"]; + _allocateFloatVector = Module["_allocateFloatVector"] = wasmExports["ie"]; + _allocateDoubleVector = Module["_allocateDoubleVector"] = wasmExports["je"]; + _allocateIntVector = Module["_allocateIntVector"] = wasmExports["ke"]; + _allocateUintVector = Module["_allocateUintVector"] = wasmExports["le"]; + _allocateStringVector = Module["_allocateStringVector"] = wasmExports["me"]; + _addBoolVectorEntry = Module["_addBoolVectorEntry"] = wasmExports["ne"]; + _addFloatVectorEntry = Module["_addFloatVectorEntry"] = wasmExports["oe"]; + _addDoubleVectorEntry = Module["_addDoubleVectorEntry"] = wasmExports["pe"]; + _addIntVectorEntry = Module["_addIntVectorEntry"] = wasmExports["qe"]; + _addUintVectorEntry = Module["_addUintVectorEntry"] = wasmExports["re"]; + _addStringVectorEntry = Module["_addStringVectorEntry"] = wasmExports["se"]; + _addBoolVectorToInputStream = Module["_addBoolVectorToInputStream"] = wasmExports["te"]; + _addFloatVectorToInputStream = Module["_addFloatVectorToInputStream"] = wasmExports["ue"]; + _addDoubleVectorToInputStream = Module["_addDoubleVectorToInputStream"] = wasmExports["ve"]; + _addIntVectorToInputStream = Module["_addIntVectorToInputStream"] = wasmExports["we"]; + _addUintVectorToInputStream = Module["_addUintVectorToInputStream"] = wasmExports["xe"]; + _addStringVectorToInputStream = Module["_addStringVectorToInputStream"] = wasmExports["ye"]; + _addFlatHashMapToInputStream = Module["_addFlatHashMapToInputStream"] = wasmExports["ze"]; + _addProtoToInputStream = Module["_addProtoToInputStream"] = wasmExports["Ae"]; + _addEmptyPacketToInputStream = Module["_addEmptyPacketToInputStream"] = wasmExports["Be"]; + _addBoolToInputSidePacket = Module["_addBoolToInputSidePacket"] = wasmExports["Ce"]; + _addDoubleToInputSidePacket = Module["_addDoubleToInputSidePacket"] = wasmExports["De"]; + _addFloatToInputSidePacket = Module["_addFloatToInputSidePacket"] = wasmExports["Ee"]; + _addIntToInputSidePacket = Module["_addIntToInputSidePacket"] = wasmExports["Fe"]; + _addUintToInputSidePacket = Module["_addUintToInputSidePacket"] = wasmExports["Ge"]; + _addStringToInputSidePacket = Module["_addStringToInputSidePacket"] = wasmExports["He"]; + _addRawDataSpanToInputSidePacket = Module["_addRawDataSpanToInputSidePacket"] = wasmExports["Ie"]; + _addProtoToInputSidePacket = Module["_addProtoToInputSidePacket"] = wasmExports["Je"]; + _addBoolVectorToInputSidePacket = Module["_addBoolVectorToInputSidePacket"] = wasmExports["Ke"]; + _addDoubleVectorToInputSidePacket = Module["_addDoubleVectorToInputSidePacket"] = wasmExports["Le"]; + _addFloatVectorToInputSidePacket = Module["_addFloatVectorToInputSidePacket"] = wasmExports["Me"]; + _addIntVectorToInputSidePacket = Module["_addIntVectorToInputSidePacket"] = wasmExports["Ne"]; + _addUintVectorToInputSidePacket = Module["_addUintVectorToInputSidePacket"] = wasmExports["Oe"]; + _addStringVectorToInputSidePacket = Module["_addStringVectorToInputSidePacket"] = wasmExports["Pe"]; + _attachBoolListener = Module["_attachBoolListener"] = wasmExports["Qe"]; + _attachBoolVectorListener = Module["_attachBoolVectorListener"] = wasmExports["Re"]; + _attachDoubleListener = Module["_attachDoubleListener"] = wasmExports["Se"]; + _attachDoubleVectorListener = Module["_attachDoubleVectorListener"] = wasmExports["Te"]; + _attachFloatListener = Module["_attachFloatListener"] = wasmExports["Ue"]; + _attachFloatVectorListener = Module["_attachFloatVectorListener"] = wasmExports["Ve"]; + _attachIntListener = Module["_attachIntListener"] = wasmExports["We"]; + _attachIntVectorListener = Module["_attachIntVectorListener"] = wasmExports["Xe"]; + _attachUintListener = Module["_attachUintListener"] = wasmExports["Ye"]; + _attachUintVectorListener = Module["_attachUintVectorListener"] = wasmExports["Ze"]; + _attachStringListener = Module["_attachStringListener"] = wasmExports["_e"]; + _attachStringVectorListener = Module["_attachStringVectorListener"] = wasmExports["$e"]; + _attachProtoListener = Module["_attachProtoListener"] = wasmExports["af"]; + _attachProtoVectorListener = Module["_attachProtoVectorListener"] = wasmExports["bf"]; + _getGraphConfig = Module["_getGraphConfig"] = wasmExports["cf"]; + ___getTypeName = wasmExports["df"]; + _emwgpuCreateBindGroup = wasmExports["ef"]; + _emwgpuCreateBindGroupLayout = wasmExports["ff"]; + _emwgpuCreateCommandBuffer = wasmExports["gf"]; + _emwgpuCreateCommandEncoder = wasmExports["hf"]; + _emwgpuCreateComputePassEncoder = wasmExports["jf"]; + _emwgpuCreateComputePipeline = wasmExports["kf"]; + _emwgpuCreateExternalTexture = wasmExports["lf"]; + _emwgpuCreatePipelineLayout = wasmExports["mf"]; + _emwgpuCreateQuerySet = wasmExports["nf"]; + _emwgpuCreateRenderBundle = wasmExports["of"]; + _emwgpuCreateRenderBundleEncoder = wasmExports["pf"]; + _emwgpuCreateRenderPassEncoder = wasmExports["qf"]; + _emwgpuCreateRenderPipeline = wasmExports["rf"]; + _emwgpuCreateSampler = wasmExports["sf"]; + _emwgpuCreateSurface = wasmExports["tf"]; + _emwgpuCreateTexture = wasmExports["uf"]; + _emwgpuCreateTextureView = wasmExports["vf"]; + _emwgpuCreateAdapter = wasmExports["wf"]; + _emwgpuImportBuffer = wasmExports["xf"]; + _emwgpuCreateDevice = wasmExports["yf"]; + _emwgpuCreateQueue = wasmExports["zf"]; + _emwgpuCreateShaderModule = wasmExports["Af"]; + _emwgpuOnCreateComputePipelineCompleted = wasmExports["Bf"]; + _emwgpuOnCreateRenderPipelineCompleted = wasmExports["Cf"]; + _clearSubgraphs = Module["_clearSubgraphs"] = wasmExports["Df"]; + _pushBinarySubgraph = Module["_pushBinarySubgraph"] = wasmExports["Ef"]; + _pushTextSubgraph = Module["_pushTextSubgraph"] = wasmExports["Ff"]; + _changeBinaryGraph = Module["_changeBinaryGraph"] = wasmExports["Gf"]; + _changeTextGraph = Module["_changeTextGraph"] = wasmExports["Hf"]; + _processGl = Module["_processGl"] = wasmExports["If"]; + _process = Module["_process"] = wasmExports["Jf"]; + _bindTextureToCanvas = Module["_bindTextureToCanvas"] = wasmExports["Kf"]; + _requestShaderRefreshOnGraphChange = Module["_requestShaderRefreshOnGraphChange"] = wasmExports["Lf"]; + _waitUntilIdle = Module["_waitUntilIdle"] = wasmExports["Mf"]; + _closeGraph = Module["_closeGraph"] = wasmExports["Nf"]; + _setAutoRenderToScreen = Module["_setAutoRenderToScreen"] = wasmExports["Of"]; + _emscripten_builtin_memalign = wasmExports["Pf"]; + _memalign = wasmExports["Qf"]; + __emscripten_tempret_set = wasmExports["Rf"]; + __emscripten_stack_restore = wasmExports["Sf"]; + __emscripten_stack_alloc = wasmExports["Tf"]; + _emscripten_stack_get_current = wasmExports["Uf"]; + dynCall_ji = wasmExports["dynCall_ji"]; + dynCall_jii = wasmExports["dynCall_jii"]; + dynCall_iiiijij = wasmExports["dynCall_iiiijij"]; + dynCall_viiji = wasmExports["dynCall_viiji"]; + dynCall_viji = wasmExports["dynCall_viji"]; + dynCall_iiiji = wasmExports["dynCall_iiiji"]; + dynCall_jjj = wasmExports["dynCall_jjj"]; + dynCall_iiiijj = wasmExports["dynCall_iiiijj"]; + dynCall_viijj = wasmExports["dynCall_viijj"]; + dynCall_viiijjj = wasmExports["dynCall_viiijjj"]; + dynCall_vij = wasmExports["dynCall_vij"]; + dynCall_viiiji = wasmExports["dynCall_viiiji"]; + dynCall_viijii = wasmExports["dynCall_viijii"]; + dynCall_vijjj = wasmExports["dynCall_vijjj"]; + dynCall_vj = wasmExports["dynCall_vj"]; + dynCall_viij = wasmExports["dynCall_viij"]; + dynCall_jiji = wasmExports["dynCall_jiji"]; + dynCall_iiiiij = wasmExports["dynCall_iiiiij"]; + dynCall_iiiiijj = wasmExports["dynCall_iiiiijj"]; + dynCall_iiiiiijj = wasmExports["dynCall_iiiiiijj"]; + memory = wasmMemory = wasmExports["id"]; + _kVersionStampBuildChangelistStr = Module["_kVersionStampBuildChangelistStr"] = wasmExports["kd"].value; + _kVersionStampCitcSnapshotStr = Module["_kVersionStampCitcSnapshotStr"] = wasmExports["ld"].value; + _kVersionStampCitcWorkspaceIdStr = Module["_kVersionStampCitcWorkspaceIdStr"] = wasmExports["md"].value; + _kVersionStampSourceUriStr = Module["_kVersionStampSourceUriStr"] = wasmExports["nd"].value; + _kVersionStampBuildClientStr = Module["_kVersionStampBuildClientStr"] = wasmExports["od"].value; + _kVersionStampBuildClientMintStatusStr = Module["_kVersionStampBuildClientMintStatusStr"] = wasmExports["pd"].value; + _kVersionStampBuildCompilerStr = Module["_kVersionStampBuildCompilerStr"] = wasmExports["qd"].value; + _kVersionStampBuildDateTimePstStr = Module["_kVersionStampBuildDateTimePstStr"] = wasmExports["rd"].value; + _kVersionStampBuildDepotPathStr = Module["_kVersionStampBuildDepotPathStr"] = wasmExports["sd"].value; + _kVersionStampBuildIdStr = Module["_kVersionStampBuildIdStr"] = wasmExports["td"].value; + _kVersionStampBuildInfoStr = Module["_kVersionStampBuildInfoStr"] = wasmExports["ud"].value; + _kVersionStampBuildLabelStr = Module["_kVersionStampBuildLabelStr"] = wasmExports["vd"].value; + _kVersionStampBuildTargetStr = Module["_kVersionStampBuildTargetStr"] = wasmExports["wd"].value; + _kVersionStampBuildTimestampStr = Module["_kVersionStampBuildTimestampStr"] = wasmExports["xd"].value; + _kVersionStampBuildToolStr = Module["_kVersionStampBuildToolStr"] = wasmExports["yd"].value; + _kVersionStampG3BuildTargetStr = Module["_kVersionStampG3BuildTargetStr"] = wasmExports["zd"].value; + _kVersionStampVerifiableStr = Module["_kVersionStampVerifiableStr"] = wasmExports["Ad"].value; + _kVersionStampBuildFdoTypeStr = Module["_kVersionStampBuildFdoTypeStr"] = wasmExports["Bd"].value; + _kVersionStampBuildBaselineChangelistStr = Module["_kVersionStampBuildBaselineChangelistStr"] = wasmExports["Cd"].value; + _kVersionStampBuildLtoTypeStr = Module["_kVersionStampBuildLtoTypeStr"] = wasmExports["Dd"].value; + _kVersionStampBuildPropellerTypeStr = Module["_kVersionStampBuildPropellerTypeStr"] = wasmExports["Ed"].value; + _kVersionStampBuildPghoTypeStr = Module["_kVersionStampBuildPghoTypeStr"] = wasmExports["Fd"].value; + _kVersionStampBuildUsernameStr = Module["_kVersionStampBuildUsernameStr"] = wasmExports["Gd"].value; + _kVersionStampBuildHostnameStr = Module["_kVersionStampBuildHostnameStr"] = wasmExports["Hd"].value; + _kVersionStampBuildDirectoryStr = Module["_kVersionStampBuildDirectoryStr"] = wasmExports["Id"].value; + _kVersionStampBuildChangelistInt = Module["_kVersionStampBuildChangelistInt"] = wasmExports["Jd"].value; + _kVersionStampCitcSnapshotInt = Module["_kVersionStampCitcSnapshotInt"] = wasmExports["Kd"].value; + _kVersionStampBuildClientMintStatusInt = Module["_kVersionStampBuildClientMintStatusInt"] = wasmExports["Ld"].value; + _kVersionStampBuildTimestampInt = Module["_kVersionStampBuildTimestampInt"] = wasmExports["Md"].value; + _kVersionStampVerifiableInt = Module["_kVersionStampVerifiableInt"] = wasmExports["Nd"].value; + _kVersionStampBuildCoverageEnabledInt = Module["_kVersionStampBuildCoverageEnabledInt"] = wasmExports["Od"].value; + _kVersionStampBuildBaselineChangelistInt = Module["_kVersionStampBuildBaselineChangelistInt"] = wasmExports["Pd"].value; + _kVersionStampPrecookedTimestampStr = Module["_kVersionStampPrecookedTimestampStr"] = wasmExports["Qd"].value; + _kVersionStampPrecookedClientInfoStr = Module["_kVersionStampPrecookedClientInfoStr"] = wasmExports["Rd"].value; + __indirect_function_table = wasmTable = wasmExports["Sd"]; +} + +var wasmImports = { + /** @export */ hd: BeginGlQueryTiming, + /** @export */ gd: EndGlQueryTiming, + /** @export */ fd: GetAdapterArchitecture, + /** @export */ ed: GetAdapterDescription, + /** @export */ dd: GetAdapterDeviceName, + /** @export */ cd: GetAdapterVendor, + /** @export */ bd: JsOnEmptyPacketListener, + /** @export */ ad: JsOnFloat32ArrayImageListener, + /** @export */ $c: JsOnFloat32ArrayImageVectorListener, + /** @export */ pb: JsOnSimpleListenerBinaryArray, + /** @export */ _c: JsOnSimpleListenerBool, + /** @export */ Zc: JsOnSimpleListenerDouble, + /** @export */ Yc: JsOnSimpleListenerFloat, + /** @export */ Xc: JsOnSimpleListenerInt, + /** @export */ Wc: JsOnSimpleListenerString, + /** @export */ Vc: JsOnSimpleListenerUint, + /** @export */ Uc: JsOnUint8ArrayImageListener, + /** @export */ Tc: JsOnUint8ArrayImageVectorListener, + /** @export */ P: JsOnVectorFinishedListener, + /** @export */ Sc: JsOnVectorListenerBool, + /** @export */ Rc: JsOnVectorListenerDouble, + /** @export */ Qc: JsOnVectorListenerFloat, + /** @export */ Pc: JsOnVectorListenerInt, + /** @export */ Oc: JsOnVectorListenerProto, + /** @export */ Nc: JsOnVectorListenerString, + /** @export */ Mc: JsOnVectorListenerUint, + /** @export */ Lc: JsOnWebGLTextureListener, + /** @export */ Kc: JsOnWebGLTextureVectorListener, + /** @export */ Pa: JsWrapErrorListener, + /** @export */ ob: JsWrapImageConverter, + /** @export */ u: JsWrapSimpleListeners, + /** @export */ nb: UseBottomLeftGpuOrigin, + /** @export */ yb: __asyncjs__mediapipe_map_buffer_jspi, + /** @export */ r: ___cxa_throw, + /** @export */ Jc: ___syscall_dup, + /** @export */ Ic: ___syscall_faccessat, + /** @export */ mb: ___syscall_fcntl64, + /** @export */ Hc: ___syscall_fstat64, + /** @export */ Mb: ___syscall_ftruncate64, + /** @export */ Gc: ___syscall_ioctl, + /** @export */ Fc: ___syscall_lstat64, + /** @export */ Ec: ___syscall_newfstatat, + /** @export */ lb: ___syscall_openat, + /** @export */ Dc: ___syscall_stat64, + /** @export */ yc: __abort_js, + /** @export */ Jb: __embind_register_bigint, + /** @export */ xc: __embind_register_bool, + /** @export */ wc: __embind_register_emval, + /** @export */ jb: __embind_register_float, + /** @export */ J: __embind_register_integer, + /** @export */ q: __embind_register_memory_view, + /** @export */ vc: __embind_register_std_string, + /** @export */ Ma: __embind_register_std_wstring, + /** @export */ uc: __embind_register_void, + /** @export */ $: __emval_create_invoker, + /** @export */ p: __emval_decref, + /** @export */ La: __emval_get_global, + /** @export */ ib: __emval_get_property, + /** @export */ ja: __emval_incref, + /** @export */ Ka: __emval_instanceof, + /** @export */ _: __emval_invoke, + /** @export */ ua: __emval_new_cstring, + /** @export */ Z: __emval_run_destructors, + /** @export */ hb: __emval_set_property, + /** @export */ tc: __emval_typeof, + /** @export */ Ib: __gmtime_js, + /** @export */ Hb: __localtime_js, + /** @export */ Gb: __mktime_js, + /** @export */ Fb: __mmap_js, + /** @export */ Eb: __munmap_js, + /** @export */ sc: __tzset_js, + /** @export */ Lb: _clock_time_get, + /** @export */ rc: custom_emscripten_dbgn, + /** @export */ Y: _emscripten_asm_const_int, + /** @export */ gb: _emscripten_asm_const_ptr, + /** @export */ Ja: _emscripten_errn, + /** @export */ qc: _emscripten_get_heap_max, + /** @export */ y: _emscripten_get_now, + /** @export */ ia: _emscripten_has_asyncify, + /** @export */ pc: _emscripten_outn, + /** @export */ oc: _emscripten_pc_get_function, + /** @export */ nc: _emscripten_resize_heap, + /** @export */ fb: _emscripten_stack_snapshot, + /** @export */ mc: _emscripten_stack_unwind_buffer, + /** @export */ lc: _emscripten_webgl_create_context, + /** @export */ kc: _emscripten_webgl_destroy_context, + /** @export */ jc: _emscripten_webgl_get_context_attributes, + /** @export */ ha: _emscripten_webgl_get_current_context, + /** @export */ ic: _emscripten_webgl_make_context_current, + /** @export */ R: _emscripten_webgpu_get_device, + /** @export */ hc: _emwgpuBufferDestroy, + /** @export */ gc: _emwgpuBufferGetMappedRange, + /** @export */ fc: _emwgpuBufferUnmap, + /** @export */ x: _emwgpuDelete, + /** @export */ ec: _emwgpuDeviceCreateBuffer, + /** @export */ Db: _emwgpuDeviceCreateComputePipelineAsync, + /** @export */ Cb: _emwgpuDeviceCreateRenderPipelineAsync, + /** @export */ dc: _emwgpuDeviceCreateShaderModule, + /** @export */ cc: _emwgpuDeviceDestroy, + /** @export */ bc: _emwgpuWaitAny, + /** @export */ Cc: _environ_get, + /** @export */ Bc: _environ_sizes_get, + /** @export */ eb: _exit, + /** @export */ Oa: _fd_close, + /** @export */ kb: _fd_read, + /** @export */ Kb: _fd_seek, + /** @export */ Na: _fd_write, + /** @export */ b: _glActiveTexture, + /** @export */ ta: _glAttachShader, + /** @export */ ac: _glBindAttribLocation, + /** @export */ c: _glBindBuffer, + /** @export */ db: _glBindBufferBase, + /** @export */ t: _glBindFramebuffer, + /** @export */ a: _glBindTexture, + /** @export */ m: _glBindVertexArray, + /** @export */ cb: _glBlendEquation, + /** @export */ $b: _glBlendFunc, + /** @export */ j: _glBufferData, + /** @export */ I: _glClear, + /** @export */ H: _glClearColor, + /** @export */ da: _glClientWaitSync, + /** @export */ ga: _glColorMask, + /** @export */ bb: _glCompileShader, + /** @export */ ab: _glCreateProgram, + /** @export */ $a: _glCreateShader, + /** @export */ o: _glDeleteBuffers, + /** @export */ Q: _glDeleteFramebuffers, + /** @export */ h: _glDeleteProgram, + /** @export */ sa: _glDeleteShader, + /** @export */ ra: _glDeleteSync, + /** @export */ D: _glDeleteTextures, + /** @export */ A: _glDeleteVertexArrays, + /** @export */ _a: _glDetachShader, + /** @export */ G: _glDisable, + /** @export */ n: _glDisableVertexAttribArray, + /** @export */ i: _glDrawArrays, + /** @export */ fa: _glDrawBuffers, + /** @export */ _b: _glEnable, + /** @export */ l: _glEnableVertexAttribArray, + /** @export */ Za: _glFenceSync, + /** @export */ qa: _glFinish, + /** @export */ v: _glFlush, + /** @export */ C: _glFramebufferTexture2D, + /** @export */ Ya: _glFramebufferTextureLayer, + /** @export */ s: _glGenBuffers, + /** @export */ X: _glGenFramebuffers, + /** @export */ F: _glGenTextures, + /** @export */ B: _glGenVertexArrays, + /** @export */ Xa: _glGetAttribLocation, + /** @export */ ea: _glGetError, + /** @export */ Zb: _glGetFloatv, + /** @export */ w: _glGetIntegerv, + /** @export */ Yb: _glGetProgramiv, + /** @export */ Xb: _glGetShaderInfoLog, + /** @export */ Wb: _glGetShaderiv, + /** @export */ O: _glGetString, + /** @export */ Vb: _glGetUniformBlockIndex, + /** @export */ d: _glGetUniformLocation, + /** @export */ Ub: _glLineWidth, + /** @export */ Wa: _glLinkProgram, + /** @export */ pa: _glPixelStorei, + /** @export */ oa: _glReadPixels, + /** @export */ Va: _glShaderSource, + /** @export */ E: _glTexImage2D, + /** @export */ na: _glTexParameterf, + /** @export */ Ua: _glTexParameterfv, + /** @export */ f: _glTexParameteri, + /** @export */ ma: _glTexStorage2D, + /** @export */ Tb: _glTexStorage3D, + /** @export */ W: _glTexSubImage2D, + /** @export */ Sb: _glTexSubImage3D, + /** @export */ N: _glUniform1f, + /** @export */ la: _glUniform1fv, + /** @export */ e: _glUniform1i, + /** @export */ V: _glUniform2f, + /** @export */ Rb: _glUniform2fv, + /** @export */ Ia: _glUniform3f, + /** @export */ Ta: _glUniform4f, + /** @export */ U: _glUniform4fv, + /** @export */ Qb: _glUniform4iv, + /** @export */ Pb: _glUniformBlockBinding, + /** @export */ Ob: _glUniformMatrix2fv, + /** @export */ Nb: _glUniformMatrix3fv, + /** @export */ Ha: _glUniformMatrix4fv, + /** @export */ g: _glUseProgram, + /** @export */ k: _glVertexAttribPointer, + /** @export */ T: _glViewport, + /** @export */ Ga: hardware_concurrency, + /** @export */ Bb: mediapipe_create_utility_canvas2d, + /** @export */ Ab: _mediapipe_find_canvas_event_target, + /** @export */ zb: mediapipe_import_external_texture, + /** @export */ xb: _mediapipe_webgl_tex_image_drawable, + /** @export */ Ac: _proc_exit, + /** @export */ zc: _random_get, + /** @export */ Fa: _wgpuCommandEncoderBeginComputePass, + /** @export */ Ea: _wgpuCommandEncoderBeginRenderPass, + /** @export */ wb: _wgpuCommandEncoderCopyBufferToTexture, + /** @export */ vb: _wgpuCommandEncoderCopyTextureToBuffer, + /** @export */ ub: _wgpuCommandEncoderCopyTextureToTexture, + /** @export */ M: _wgpuCommandEncoderFinish, + /** @export */ Da: _wgpuComputePassEncoderDispatchWorkgroups, + /** @export */ Ca: _wgpuComputePassEncoderEnd, + /** @export */ Ba: _wgpuComputePassEncoderSetBindGroup, + /** @export */ Aa: _wgpuComputePassEncoderSetPipeline, + /** @export */ za: _wgpuComputePipelineGetBindGroupLayout, + /** @export */ ca: _wgpuDeviceCreateBindGroup, + /** @export */ tb: _wgpuDeviceCreateBindGroupLayout, + /** @export */ L: _wgpuDeviceCreateCommandEncoder, + /** @export */ sb: _wgpuDeviceCreateComputePipeline, + /** @export */ rb: _wgpuDeviceCreatePipelineLayout, + /** @export */ Sa: _wgpuDeviceCreateRenderPipeline, + /** @export */ S: _wgpuDeviceCreateSampler, + /** @export */ ba: _wgpuDeviceCreateTexture, + /** @export */ K: _wgpuQueueSubmit, + /** @export */ ka: _wgpuQueueWriteBuffer, + /** @export */ qb: _wgpuQueueWriteTexture, + /** @export */ ya: _wgpuRenderPassEncoderDraw, + /** @export */ xa: _wgpuRenderPassEncoderEnd, + /** @export */ wa: _wgpuRenderPassEncoderSetBindGroup, + /** @export */ va: _wgpuRenderPassEncoderSetPipeline, + /** @export */ Ra: _wgpuRenderPipelineGetBindGroupLayout, + /** @export */ z: _wgpuTextureCreateView, + /** @export */ Qa: _wgpuTextureDestroy, + /** @export */ aa: _wgpuTextureGetFormat +}; + +// include: postamble.js +// === Auto-generated postamble setup entry stuff === +function run() { + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + preRun(); + // a preRun added a dependency, run will be called later + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + function doRun() { + // run may have just been called through dependencies being fulfilled just in this very frame, + // or while the async setStatus time below was happening + Module["calledRun"] = true; + if (ABORT) return; + initRuntime(); + readyPromiseResolve?.(Module); + Module["onRuntimeInitialized"]?.(); + postRun(); + } + if (Module["setStatus"]) { + Module["setStatus"]("Running..."); + setTimeout(() => { + setTimeout(() => Module["setStatus"](""), 1); + doRun(); + }, 1); + } else { + doRun(); + } +} + +var wasmExports; + +// In modularize mode the generated code is within a factory function so we +// can use await here (since it's not top-level-await). +wasmExports = await (createWasm()); + +run(); + +// end include: postamble.js +// include: postamble_modularize.js +// In MODULARIZE mode we wrap the generated code in a factory function +// and return either the Module itself, or a promise of the module. +// We assign to the `moduleRtn` global here and configure closure to see +// this as an extern so it won't get minified. +if (runtimeInitialized) { + moduleRtn = Module; +} else { + // Set up the promise that indicates the Module is initialized + moduleRtn = new Promise((resolve, reject) => { + readyPromiseResolve = resolve; + readyPromiseReject = reject; + }); +} + + + return moduleRtn; + }; +})(); + +// Export using a UMD style export, or ES6 exports if selected +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = ModuleFactory; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = ModuleFactory; +} else if (typeof define === 'function' && define['amd']) + define([], () => ModuleFactory); + diff --git a/public/models/mediapipe/wasm/vision_wasm_internal.wasm b/public/models/mediapipe/wasm/vision_wasm_internal.wasm new file mode 100644 index 0000000..7ca3e05 Binary files /dev/null and b/public/models/mediapipe/wasm/vision_wasm_internal.wasm differ diff --git a/public/models/mediapipe/wasm/vision_wasm_module_internal.js b/public/models/mediapipe/wasm/vision_wasm_module_internal.js new file mode 100644 index 0000000..8cd5730 --- /dev/null +++ b/public/models/mediapipe/wasm/vision_wasm_module_internal.js @@ -0,0 +1,8823 @@ +// This code implements the `-sMODULARIZE` settings by taking the generated +// JS program code (INNER_JS_CODE) and wrapping it in a factory function. + +// When targeting node and ES6 we use `await import ..` in the generated code +// so the outer function needs to be marked as async. +async function ModuleFactory(moduleArg = {}) { + var moduleRtn; + +// include: shell.js +// include: minimum_runtime_check.js +// end include: minimum_runtime_check.js +// The Module object: Our interface to the outside world. We import +// and export values on it. There are various ways Module can be used: +// 1. Not defined. We create it here +// 2. A function parameter, function(moduleArg) => Promise +// 3. pre-run appended it, var Module = {}; ..generated code.. +// 4. External script tag defines var Module. +// We need to check if Module already exists (e.g. case 3 above). +// Substitution will be replaced with actual code on later stage of the build, +// this way Closure Compiler will not mangle it (e.g. case 4. above). +// Note that if you want to run closure, and also to use Module +// after the generated code, you will need to define var Module = {}; +// before the code. Then that object will be used in the code, and you +// can continue to use Module afterwards as well. +var Module = moduleArg; + +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). +// Attempt to auto-detect the environment +var ENVIRONMENT_IS_WEB = !!globalThis.window; + +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; + +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != "renderer"; + +if (ENVIRONMENT_IS_NODE) { + // When building an ES module `require` is not normally available. + // We need to use `createRequire()` to construct the require()` function. + const {createRequire} = await import("node:module"); + /** @suppress{duplicate} */ var require = createRequire(import.meta.url); +} + +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) +var arguments_ = []; + +var thisProgram = "./this.program"; + +var quit_ = (status, toThrow) => { + throw toThrow; +}; + +var _scriptName = import.meta.url; + +// `/` should be present at the end if `scriptDirectory` is not empty +var scriptDirectory = ""; + +function locateFile(path) { + if (Module["locateFile"]) { + return Module["locateFile"](path, scriptDirectory); + } + return scriptDirectory + path; +} + +// Hooks that are implemented differently in different runtime environments. +var readAsync, readBinary; + +if (ENVIRONMENT_IS_NODE) { + // These modules will usually be used on Node.js. Load them eagerly to avoid + // the complexity of lazy-loading. + var fs = require("node:fs"); + if (_scriptName.startsWith("file:")) { + scriptDirectory = require("node:path").dirname(require("node:url").fileURLToPath(_scriptName)) + "/"; + } + // include: node_shell_read.js + readBinary = filename => { + // We need to re-wrap `file://` strings to URLs. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename); + return ret; + }; + readAsync = async (filename, binary = true) => { + // See the comment in the `readBinary` function. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename, binary ? undefined : "utf8"); + return ret; + }; + // end include: node_shell_read.js + if (process.argv.length > 1) { + thisProgram = process.argv[1].replace(/\\/g, "/"); + } + arguments_ = process.argv.slice(2); + quit_ = (status, toThrow) => { + process.exitCode = status; + throw toThrow; + }; +} else // Note that this includes Node.js workers when relevant (pthreads is enabled). +// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and +// ENVIRONMENT_IS_NODE. +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + try { + scriptDirectory = new URL(".", _scriptName).href; + } catch {} + { + // include: web_or_worker_shell_read.js + if (ENVIRONMENT_IS_WORKER) { + readBinary = url => { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + xhr.responseType = "arraybuffer"; + xhr.send(null); + return new Uint8Array(/** @type{!ArrayBuffer} */ (xhr.response)); + }; + } + readAsync = async url => { + // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. + // See https://github.com/github/fetch/pull/92#issuecomment-140665932 + // Cordova or Electron apps are typically loaded from a file:// url. + // So use XHR on webview if URL is a file URL. + if (isFileURI(url)) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = () => { + if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { + // file URLs can return 0 + resolve(xhr.response); + return; + } + reject(xhr.status); + }; + xhr.onerror = reject; + xhr.send(null); + }); + } + var response = await fetch(url, { + credentials: "same-origin" + }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + " : " + response.url); + }; + } +} else {} + +var out = console.log.bind(console); + +var err = console.error.bind(console); + +// end include: shell.js +// include: preamble.js +// === Preamble library stuff === +// Documentation for the public APIs defined in this file must be updated in: +// site/source/docs/api_reference/preamble.js.rst +// A prebuilt local version of the documentation is available at: +// site/build/text/docs/api_reference/preamble.js.txt +// You can also build docs locally as HTML or other formats in site/ +// An online HTML version (which may be of a different version of Emscripten) +// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html +var wasmBinary; + +// Wasm globals +//======================================== +// Runtime essentials +//======================================== +// whether we are quitting the application. no code should run after this. +// set in exit() and abort() +var ABORT = false; + +// set by exit() and abort(). Passed to 'onExit' handler. +// NOTE: This is also used as the process return code in shell environments +// but only when noExitRuntime is false. +var EXITSTATUS; + +// In STRICT mode, we only define assert() when ASSERTIONS is set. i.e. we +// don't define it at all in release modes. This matches the behaviour of +// MINIMAL_RUNTIME. +// TODO(sbc): Make this the default even without STRICT enabled. +/** @type {function(*, string=)} */ function assert(condition, text) { + if (!condition) { + // This build was created without ASSERTIONS defined. `assert()` should not + // ever be called in this configuration but in case there are callers in + // the wild leave this simple abort() implementation here for now. + abort(text); + } +} + +/** + * Indicates whether filename is delivered via file protocol (as opposed to http/https) + * @noinline + */ var isFileURI = filename => filename.startsWith("file://"); + +// include: runtime_common.js +// include: runtime_stack_check.js +// end include: runtime_stack_check.js +// include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + +// end include: runtime_exceptions.js +// include: runtime_debug.js +// end include: runtime_debug.js +var readyPromiseResolve, readyPromiseReject; + +// Memory management +var runtimeInitialized = false; + +function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + Module["HEAPU8"] = HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + Module["HEAPU32"] = HEAPU32 = new Uint32Array(b); + Module["HEAPF32"] = HEAPF32 = new Float32Array(b); + Module["HEAPF64"] = HEAPF64 = new Float64Array(b); +} + +// include: memoryprofiler.js +// end include: memoryprofiler.js +// end include: runtime_common.js +function preRun() { + if (Module["preRun"]) { + if (typeof Module["preRun"] == "function") Module["preRun"] = [ Module["preRun"] ]; + while (Module["preRun"].length) { + addOnPreRun(Module["preRun"].shift()); + } + } + // Begin ATPRERUNS hooks + callRuntimeCallbacks(onPreRuns); +} + +function initRuntime() { + runtimeInitialized = true; + // Begin ATINITS hooks + if (!Module["noFSInit"] && !FS.initialized) FS.init(); + TTY.init(); + // End ATINITS hooks + wasmExports["jd"](); + // Begin ATPOSTCTORS hooks + FS.ignorePermissions = false; +} + +function postRun() { + // PThreads reuse the runtime from the main thread. + if (Module["postRun"]) { + if (typeof Module["postRun"] == "function") Module["postRun"] = [ Module["postRun"] ]; + while (Module["postRun"].length) { + addOnPostRun(Module["postRun"].shift()); + } + } + // Begin ATPOSTRUNS hooks + callRuntimeCallbacks(onPostRuns); +} + +/** + * @param {string|number=} what + */ function abort(what) { + Module["onAbort"]?.(what); + what = `Aborted(${what})`; + // TODO(sbc): Should we remove printing and leave it up to whoever + // catches the exception? + err(what); + ABORT = true; + what += ". Build with -sASSERTIONS for more info."; + // Use a wasm runtime error, because a JS error might be seen as a foreign + // exception, which means we'd run destructors on it. We need the error to + // simply make the program stop. + // FIXME This approach does not work in Wasm EH because it currently does not assume + // all RuntimeErrors are from traps; it decides whether a RuntimeError is from + // a trap or not based on a hidden field within the object. So at the moment + // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that + // allows this in the wasm spec. + // Suppress closure compiler warning here. Closure compiler's builtin extern + // definition for WebAssembly.RuntimeError claims it takes no arguments even + // though it can. + // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. + /** @suppress {checkTypes} */ var e = new WebAssembly.RuntimeError(what); + readyPromiseReject?.(e); + // Throw the error whether or not MODULARIZE is set because abort is used + // in code paths apart from instantiation where an exception is expected + // to be thrown when abort is called. + throw e; +} + +var wasmBinaryFile; + +function findWasmBinary() { + if (Module["locateFile"]) { + return locateFile("vision_wasm_module_raw_internal.wasm"); + } + // Use bundler-friendly `new URL(..., import.meta.url)` pattern; works in browsers too. + return new URL("vision_wasm_module_raw_internal.wasm", import.meta.url).href; +} + +function getBinarySync(file) { + if (file == wasmBinaryFile && wasmBinary) { + return new Uint8Array(wasmBinary); + } + if (readBinary) { + return readBinary(file); + } + // Throwing a plain string here, even though it not normally advisable since + // this gets turning into an `abort` in instantiateArrayBuffer. + throw "both async and sync fetching of the wasm failed"; +} + +async function getWasmBinary(binaryFile) { + // If we don't have the binary yet, load it asynchronously using readAsync. + if (!wasmBinary) { + // Fetch the binary using readAsync + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch {} + } + // Otherwise, getBinarySync should be able to get it synchronously + return getBinarySync(binaryFile); +} + +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); + return instance; + } catch (reason) { + err(`failed to asynchronously prepare wasm: ${reason}`); + abort(reason); + } +} + +async function instantiateAsync(binary, binaryFile, imports) { + if (!binary && !isFileURI(binaryFile) && !ENVIRONMENT_IS_NODE) { + try { + var response = fetch(binaryFile, { + credentials: "same-origin" + }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err(`wasm streaming compile failed: ${reason}`); + err("falling back to ArrayBuffer instantiation"); + } + } + return instantiateArrayBuffer(binaryFile, imports); +} + +function getWasmImports() { + // prepare imports + var imports = { + "a": wasmImports + }; + return imports; +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +async function createWasm() { + // Load the wasm module and create an instance of using native support in the JS engine. + // handle a generated wasm instance, receiving its exports and + // performing other necessary setup + /** @param {WebAssembly.Module=} module*/ function receiveInstance(instance, module) { + wasmExports = instance.exports; + assignWasmExports(wasmExports); + updateMemoryViews(); + return wasmExports; + } + // Prefer streaming instantiation if available. + function receiveInstantiationResult(result) { + // 'result' is a ResultObject object which has both the module and instance. + // receiveInstance() will swap in the exports (to Module.asm) so they can be called + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. + // When the regression is fixed, can restore the above PTHREADS-enabled path. + return receiveInstance(result["instance"]); + } + var info = getWasmImports(); + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback + // to manually instantiate the Wasm module themselves. This allows pages to + // run the instantiation parallel to any other async startup actions they are + // performing. + // Also pthreads and wasm workers initialize the wasm instance through this + // path. + if (Module["instantiateWasm"]) { + return new Promise((resolve, reject) => { + Module["instantiateWasm"](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + }); + } + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; +} + +// Globals used by JS i64 conversions (see makeSetValue) +var tempDouble; + +var tempI64; + +// end include: preamble.js +// Begin JS library code +var handleException = e => { + // Certain exception types we do not treat as errors since they are used for + // internal control flow. + // 1. ExitStatus, which is thrown by exit() + // 2. "unwind", which is thrown by emscripten_unwind_to_js_event_loop() and others + // that wish to return to JS event loop. + if (e instanceof ExitStatus || e == "unwind") { + return EXITSTATUS; + } + quit_(1, e); +}; + +class ExitStatus { + name="ExitStatus"; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; + } +} + +var runtimeKeepaliveCounter = 0; + +var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0; + +var _proc_exit = code => { + EXITSTATUS = code; + if (!keepRuntimeAlive()) { + Module["onExit"]?.(code); + ABORT = true; + } + quit_(code, new ExitStatus(code)); +}; + +/** @param {boolean|number=} implicit */ var exitJS = (status, implicit) => { + EXITSTATUS = status; + _proc_exit(status); +}; + +var _exit = exitJS; + +var maybeExit = () => { + if (!keepRuntimeAlive()) { + try { + _exit(EXITSTATUS); + } catch (e) { + handleException(e); + } + } +}; + +var callUserCallback = func => { + if (ABORT) { + return; + } + try { + return func(); + } catch (e) { + handleException(e); + } finally { + maybeExit(); + } +}; + +function getFullscreenElement() { + return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.webkitCurrentFullScreenElement || document.msFullscreenElement; +} + +/** @param {number=} timeout */ var safeSetTimeout = (func, timeout) => setTimeout(() => { + callUserCallback(func); +}, timeout); + +var warnOnce = text => { + warnOnce.shown ||= {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + if (ENVIRONMENT_IS_NODE) text = "warning: " + text; + err(text); + } +}; + +var preloadPlugins = []; + +var Browser = { + useWebGL: false, + isFullscreen: false, + pointerLock: false, + moduleContextCreatedCallbacks: [], + workers: [], + preloadedImages: {}, + preloadedAudios: {}, + getCanvas: () => Module["canvas"], + init() { + if (Browser.initted) return; + Browser.initted = true; + // Support for plugins that can process preloaded files. You can add more of these to + // your app by creating and appending to preloadPlugins. + // Each plugin is asked if it can handle a file based on the file's name. If it can, + // it is given the file's raw data. When it is done, it calls a callback with the file's + // (possibly modified) data. For example, a plugin might decompress a file, or it + // might create some side data structure for use later (like an Image element, etc.). + var imagePlugin = {}; + imagePlugin["canHandle"] = name => !Module["noImageDecoding"] && /\.(jpg|jpeg|png|bmp|webp)$/i.test(name); + imagePlugin["handle"] = async (byteArray, name) => { + var b = new Blob([ byteArray ], { + type: Browser.getMimetype(name) + }); + if (b.size !== byteArray.length) { + // Safari bug #118630 + // Safari's Blob can only take an ArrayBuffer + b = new Blob([ (new Uint8Array(byteArray)).buffer ], { + type: Browser.getMimetype(name) + }); + } + var url = URL.createObjectURL(b); + return new Promise((resolve, reject) => { + var img = new Image; + img.onload = () => { + var canvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas")); + canvas.width = img.width; + canvas.height = img.height; + var ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + Browser.preloadedImages[name] = canvas; + URL.revokeObjectURL(url); + resolve(byteArray); + }; + img.onerror = event => { + err(`Image ${url} could not be decoded`); + reject(); + }; + img.src = url; + }); + }; + preloadPlugins.push(imagePlugin); + var audioPlugin = {}; + audioPlugin["canHandle"] = name => !Module["noAudioDecoding"] && name.slice(-4) in { + ".ogg": 1, + ".wav": 1, + ".mp3": 1 + }; + audioPlugin["handle"] = async (byteArray, name) => new Promise((resolve, reject) => { + var done = false; + function finish(audio) { + if (done) return; + done = true; + Browser.preloadedAudios[name] = audio; + resolve(byteArray); + } + var b = new Blob([ byteArray ], { + type: Browser.getMimetype(name) + }); + var url = URL.createObjectURL(b); + // XXX we never revoke this! + var audio = new Audio; + audio.addEventListener("canplaythrough", () => finish(audio), false); + // use addEventListener due to chromium bug 124926 + audio.onerror = event => { + if (done) return; + err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`); + function encode64(data) { + var BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var PAD = "="; + var ret = ""; + var leftchar = 0; + var leftbits = 0; + for (var i = 0; i < data.length; i++) { + leftchar = (leftchar << 8) | data[i]; + leftbits += 8; + while (leftbits >= 6) { + var curr = (leftchar >> (leftbits - 6)) & 63; + leftbits -= 6; + ret += BASE[curr]; + } + } + if (leftbits == 2) { + ret += BASE[(leftchar & 3) << 4]; + ret += PAD + PAD; + } else if (leftbits == 4) { + ret += BASE[(leftchar & 15) << 2]; + ret += PAD; + } + return ret; + } + audio.src = "data:audio/x-" + name.slice(-3) + ";base64," + encode64(byteArray); + finish(audio); + }; + audio.src = url; + // workaround for chrome bug 124926 - we do not always get oncanplaythrough or onerror + safeSetTimeout(() => { + finish(audio); + }, 1e4); + }); + preloadPlugins.push(audioPlugin); + // Canvas event setup + function pointerLockChange() { + var canvas = Browser.getCanvas(); + Browser.pointerLock = document.pointerLockElement === canvas; + } + var canvas = Browser.getCanvas(); + if (canvas) { + // forced aspect ratio can be enabled by defining 'forcedAspectRatio' on Module + // Module['forcedAspectRatio'] = 4 / 3; + document.addEventListener("pointerlockchange", pointerLockChange, false); + if (Module["elementPointerLock"]) { + canvas.addEventListener("click", ev => { + if (!Browser.pointerLock && Browser.getCanvas().requestPointerLock) { + Browser.getCanvas().requestPointerLock(); + ev.preventDefault(); + } + }, false); + } + } + }, + createContext(/** @type {HTMLCanvasElement} */ canvas, useWebGL, setInModule, webGLContextAttributes) { + if (useWebGL && Module["ctx"] && canvas == Browser.getCanvas()) return Module["ctx"]; + // no need to recreate GL context if it's already been created for this canvas. + var ctx; + var contextHandle; + if (useWebGL) { + // For GLES2/desktop GL compatibility, adjust a few defaults to be different to WebGL defaults, so that they align better with the desktop defaults. + var contextAttributes = { + antialias: false, + alpha: false, + majorVersion: (typeof WebGL2RenderingContext != "undefined") ? 2 : 1 + }; + if (webGLContextAttributes) { + for (var attribute in webGLContextAttributes) { + contextAttributes[attribute] = webGLContextAttributes[attribute]; + } + } + // This check of existence of GL is here to satisfy Closure compiler, which yells if variable GL is referenced below but GL object is not + // actually compiled in because application is not doing any GL operations. TODO: Ideally if GL is not being used, this function + // Browser.createContext() should not even be emitted. + if (typeof GL != "undefined") { + contextHandle = GL.createContext(canvas, contextAttributes); + if (contextHandle) { + ctx = GL.getContext(contextHandle).GLctx; + } + } + } else { + ctx = canvas.getContext("2d"); + } + if (!ctx) return null; + if (setInModule) { + Module["ctx"] = ctx; + if (useWebGL) GL.makeContextCurrent(contextHandle); + Browser.useWebGL = useWebGL; + Browser.moduleContextCreatedCallbacks.forEach(callback => callback()); + Browser.init(); + } + return ctx; + }, + fullscreenHandlersInstalled: false, + lockPointer: undefined, + resizeCanvas: undefined, + requestFullscreen(lockPointer, resizeCanvas) { + Browser.lockPointer = lockPointer; + Browser.resizeCanvas = resizeCanvas; + if (typeof Browser.lockPointer == "undefined") Browser.lockPointer = true; + if (typeof Browser.resizeCanvas == "undefined") Browser.resizeCanvas = false; + var canvas = Browser.getCanvas(); + function fullscreenChange() { + Browser.isFullscreen = false; + var canvasContainer = canvas.parentNode; + if (getFullscreenElement() === canvasContainer) { + canvas.exitFullscreen = Browser.exitFullscreen; + if (Browser.lockPointer) canvas.requestPointerLock(); + Browser.isFullscreen = true; + if (Browser.resizeCanvas) { + Browser.setFullscreenCanvasSize(); + } else { + Browser.updateCanvasDimensions(canvas); + } + } else { + // remove the full screen specific parent of the canvas again to restore the HTML structure from before going full screen + canvasContainer.parentNode.insertBefore(canvas, canvasContainer); + canvasContainer.parentNode.removeChild(canvasContainer); + if (Browser.resizeCanvas) { + Browser.setWindowedCanvasSize(); + } else { + Browser.updateCanvasDimensions(canvas); + } + } + Module["onFullScreen"]?.(Browser.isFullscreen); + Module["onFullscreen"]?.(Browser.isFullscreen); + } + if (!Browser.fullscreenHandlersInstalled) { + Browser.fullscreenHandlersInstalled = true; + document.addEventListener("fullscreenchange", fullscreenChange, false); + document.addEventListener("mozfullscreenchange", fullscreenChange, false); + document.addEventListener("webkitfullscreenchange", fullscreenChange, false); + document.addEventListener("MSFullscreenChange", fullscreenChange, false); + } + // create a new parent to ensure the canvas has no siblings. this allows browsers to optimize full screen performance when its parent is the full screen root + var canvasContainer = document.createElement("div"); + canvas.parentNode.insertBefore(canvasContainer, canvas); + canvasContainer.appendChild(canvas); + // use parent of canvas as full screen root to allow aspect ratio correction (Firefox stretches the root to screen size) + canvasContainer.requestFullscreen = canvasContainer["requestFullscreen"] || canvasContainer["mozRequestFullScreen"] || canvasContainer["msRequestFullscreen"] || (canvasContainer["webkitRequestFullscreen"] ? () => canvasContainer["webkitRequestFullscreen"](Element["ALLOW_KEYBOARD_INPUT"]) : null) || (canvasContainer["webkitRequestFullScreen"] ? () => canvasContainer["webkitRequestFullScreen"](Element["ALLOW_KEYBOARD_INPUT"]) : null); + canvasContainer.requestFullscreen(); + }, + exitFullscreen() { + // This is workaround for chrome. Trying to exit from fullscreen + // not in fullscreen state will cause "TypeError: Document not active" + // in chrome. See https://github.com/emscripten-core/emscripten/pull/8236 + if (!Browser.isFullscreen) { + return false; + } + var CFS = document["exitFullscreen"] || document["cancelFullScreen"] || document["mozCancelFullScreen"] || document["msExitFullscreen"] || document["webkitCancelFullScreen"] || (() => {}); + CFS.apply(document, []); + return true; + }, + safeSetTimeout(func, timeout) { + // Legacy function, this is used by the SDL2 port so we need to keep it + // around at least until that is updated. + // See https://github.com/libsdl-org/SDL/pull/6304 + return safeSetTimeout(func, timeout); + }, + getMimetype(name) { + return { + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "png": "image/png", + "bmp": "image/bmp", + "ogg": "audio/ogg", + "wav": "audio/wav", + "mp3": "audio/mpeg" + }[name.slice(name.lastIndexOf(".") + 1)]; + }, + getUserMedia(func) { + window.getUserMedia ||= navigator["getUserMedia"] || navigator["mozGetUserMedia"]; + window.getUserMedia(func); + }, + getMovementX(event) { + return event["movementX"] || event["mozMovementX"] || event["webkitMovementX"] || 0; + }, + getMovementY(event) { + return event["movementY"] || event["mozMovementY"] || event["webkitMovementY"] || 0; + }, + getMouseWheelDelta(event) { + var delta = 0; + switch (event.type) { + case "DOMMouseScroll": + // 3 lines make up a step + delta = event.detail / 3; + break; + + case "mousewheel": + // 120 units make up a step + delta = event.wheelDelta / 120; + break; + + case "wheel": + delta = event.deltaY; + switch (event.deltaMode) { + case 0: + // DOM_DELTA_PIXEL: 100 pixels make up a step + delta /= 100; + break; + + case 1: + // DOM_DELTA_LINE: 3 lines make up a step + delta /= 3; + break; + + case 2: + // DOM_DELTA_PAGE: A page makes up 80 steps + delta *= 80; + break; + + default: + abort("unrecognized mouse wheel delta mode: " + event.deltaMode); + } + break; + + default: + abort("unrecognized mouse wheel event: " + event.type); + } + return delta; + }, + mouseX: 0, + mouseY: 0, + mouseMovementX: 0, + mouseMovementY: 0, + touches: {}, + lastTouches: {}, + calculateMouseCoords(pageX, pageY) { + // Calculate the movement based on the changes + // in the coordinates. + var canvas = Browser.getCanvas(); + var rect = canvas.getBoundingClientRect(); + // Neither .scrollX or .pageXOffset are defined in a spec, but + // we prefer .scrollX because it is currently in a spec draft. + // (see: http://www.w3.org/TR/2013/WD-cssom-view-20131217/) + var scrollX = ((typeof window.scrollX != "undefined") ? window.scrollX : window.pageXOffset); + var scrollY = ((typeof window.scrollY != "undefined") ? window.scrollY : window.pageYOffset); + var adjustedX = pageX - (scrollX + rect.left); + var adjustedY = pageY - (scrollY + rect.top); + // the canvas might be CSS-scaled compared to its backbuffer; + // SDL-using content will want mouse coordinates in terms + // of backbuffer units. + adjustedX = adjustedX * (canvas.width / rect.width); + adjustedY = adjustedY * (canvas.height / rect.height); + return { + x: adjustedX, + y: adjustedY + }; + }, + setMouseCoords(pageX, pageY) { + const {x, y} = Browser.calculateMouseCoords(pageX, pageY); + Browser.mouseMovementX = x - Browser.mouseX; + Browser.mouseMovementY = y - Browser.mouseY; + Browser.mouseX = x; + Browser.mouseY = y; + }, + calculateMouseEvent(event) { + // event should be mousemove, mousedown or mouseup + if (Browser.pointerLock) { + // When the pointer is locked, calculate the coordinates + // based on the movement of the mouse. + // Workaround for Firefox bug 764498 + if (event.type != "mousemove" && ("mozMovementX" in event)) { + Browser.mouseMovementX = Browser.mouseMovementY = 0; + } else { + Browser.mouseMovementX = Browser.getMovementX(event); + Browser.mouseMovementY = Browser.getMovementY(event); + } + // add the mouse delta to the current absolute mouse position + Browser.mouseX += Browser.mouseMovementX; + Browser.mouseY += Browser.mouseMovementY; + } else { + if (event.type === "touchstart" || event.type === "touchend" || event.type === "touchmove") { + var touch = event.touch; + if (touch === undefined) { + return; + } + var coords = Browser.calculateMouseCoords(touch.pageX, touch.pageY); + if (event.type === "touchstart") { + Browser.lastTouches[touch.identifier] = coords; + Browser.touches[touch.identifier] = coords; + } else if (event.type === "touchend" || event.type === "touchmove") { + var last = Browser.touches[touch.identifier]; + last ||= coords; + Browser.lastTouches[touch.identifier] = last; + Browser.touches[touch.identifier] = coords; + } + return; + } + Browser.setMouseCoords(event.pageX, event.pageY); + } + }, + resizeListeners: [], + updateResizeListeners() { + var canvas = Browser.getCanvas(); + Browser.resizeListeners.forEach(listener => listener(canvas.width, canvas.height)); + }, + setCanvasSize(width, height, noUpdates) { + var canvas = Browser.getCanvas(); + Browser.updateCanvasDimensions(canvas, width, height); + if (!noUpdates) Browser.updateResizeListeners(); + }, + windowedWidth: 0, + windowedHeight: 0, + setFullscreenCanvasSize() { + // check if SDL is available + if (typeof SDL != "undefined") { + var flags = HEAPU32[((SDL.screen) >> 2)]; + flags = flags | 8388608; + // set SDL_FULLSCREEN flag + HEAP32[((SDL.screen) >> 2)] = flags; + } + Browser.updateCanvasDimensions(Browser.getCanvas()); + Browser.updateResizeListeners(); + }, + setWindowedCanvasSize() { + // check if SDL is available + if (typeof SDL != "undefined") { + var flags = HEAPU32[((SDL.screen) >> 2)]; + flags = flags & ~8388608; + // clear SDL_FULLSCREEN flag + HEAP32[((SDL.screen) >> 2)] = flags; + } + Browser.updateCanvasDimensions(Browser.getCanvas()); + Browser.updateResizeListeners(); + }, + updateCanvasDimensions(canvas, wNative, hNative) { + if (wNative && hNative) { + canvas.widthNative = wNative; + canvas.heightNative = hNative; + } else { + wNative = canvas.widthNative; + hNative = canvas.heightNative; + } + var w = wNative; + var h = hNative; + if (Module["forcedAspectRatio"] > 0) { + if (w / h < Module["forcedAspectRatio"]) { + w = Math.round(h * Module["forcedAspectRatio"]); + } else { + h = Math.round(w / Module["forcedAspectRatio"]); + } + } + if ((getFullscreenElement() === canvas.parentNode) && (typeof screen != "undefined")) { + var factor = Math.min(screen.width / w, screen.height / h); + w = Math.round(w * factor); + h = Math.round(h * factor); + } + if (Browser.resizeCanvas) { + if (canvas.width != w) canvas.width = w; + if (canvas.height != h) canvas.height = h; + if (typeof canvas.style != "undefined") { + canvas.style.removeProperty("width"); + canvas.style.removeProperty("height"); + } + } else { + if (canvas.width != wNative) canvas.width = wNative; + if (canvas.height != hNative) canvas.height = hNative; + if (typeof canvas.style != "undefined") { + if (w != wNative || h != hNative) { + canvas.style.setProperty("width", w + "px", "important"); + canvas.style.setProperty("height", h + "px", "important"); + } else { + canvas.style.removeProperty("width"); + canvas.style.removeProperty("height"); + } + } + } + } +}; + +/** @type {!Int16Array} */ var HEAP16; + +/** @type {!Int32Array} */ var HEAP32; + +/** @type {!Int8Array} */ var HEAP8; + +/** @type {!Float32Array} */ var HEAPF32; + +/** @type {!Float64Array} */ var HEAPF64; + +/** @type {!Uint16Array} */ var HEAPU16; + +/** @type {!Uint32Array} */ var HEAPU32; + +/** @type {!Uint8Array} */ var HEAPU8; + +var callRuntimeCallbacks = callbacks => { + while (callbacks.length > 0) { + // Pass the module as the first argument. + callbacks.shift()(Module); + } +}; + +var onPostRuns = []; + +var addOnPostRun = cb => onPostRuns.push(cb); + +var onPreRuns = []; + +var addOnPreRun = cb => onPreRuns.push(cb); + +var noExitRuntime = true; + +var stackRestore = val => __emscripten_stack_restore(val); + +var stackSave = () => _emscripten_stack_get_current(); + +class ExceptionInfo { + // excPtr - Thrown object pointer to wrap. Metadata pointer is calculated from it. + constructor(excPtr) { + this.excPtr = excPtr; + this.ptr = excPtr - 24; + } + set_type(type) { + HEAPU32[(((this.ptr) + (4)) >> 2)] = type; + } + get_type() { + return HEAPU32[(((this.ptr) + (4)) >> 2)]; + } + set_destructor(destructor) { + HEAPU32[(((this.ptr) + (8)) >> 2)] = destructor; + } + get_destructor() { + return HEAPU32[(((this.ptr) + (8)) >> 2)]; + } + set_caught(caught) { + caught = caught ? 1 : 0; + HEAP8[(this.ptr) + (12)] = caught; + } + get_caught() { + return HEAP8[(this.ptr) + (12)] != 0; + } + set_rethrown(rethrown) { + rethrown = rethrown ? 1 : 0; + HEAP8[(this.ptr) + (13)] = rethrown; + } + get_rethrown() { + return HEAP8[(this.ptr) + (13)] != 0; + } + // Initialize native structure fields. Should be called once after allocated. + init(type, destructor) { + this.set_adjusted_ptr(0); + this.set_type(type); + this.set_destructor(destructor); + } + set_adjusted_ptr(adjustedPtr) { + HEAPU32[(((this.ptr) + (16)) >> 2)] = adjustedPtr; + } + get_adjusted_ptr() { + return HEAPU32[(((this.ptr) + (16)) >> 2)]; + } +} + +var uncaughtExceptionCount = 0; + +var ___cxa_throw = (ptr, type, destructor) => { + var info = new ExceptionInfo(ptr); + // Initialize ExceptionInfo content after it was allocated in __cxa_allocate_exception. + info.init(type, destructor); + uncaughtExceptionCount++; + abort(); +}; + +var PATH = { + isAbs: path => path.charAt(0) === "/", + splitPath: filename => { + var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + return splitPathRe.exec(filename).slice(1); + }, + normalizeArray: (parts, allowAboveRoot) => { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === ".") { + parts.splice(i, 1); + } else if (last === "..") { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (;up; up--) { + parts.unshift(".."); + } + } + return parts; + }, + normalize: path => { + var isAbsolute = PATH.isAbs(path), trailingSlash = path.slice(-1) === "/"; + // Normalize the path + path = PATH.normalizeArray(path.split("/").filter(p => !!p), !isAbsolute).join("/"); + if (!path && !isAbsolute) { + path = "."; + } + if (path && trailingSlash) { + path += "/"; + } + return (isAbsolute ? "/" : "") + path; + }, + dirname: path => { + var result = PATH.splitPath(path), root = result[0], dir = result[1]; + if (!root && !dir) { + // No dirname whatsoever + return "."; + } + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.slice(0, -1); + } + return root + dir; + }, + basename: path => path && path.match(/([^\/]+|\/)\/*$/)[1], + join: (...paths) => PATH.normalize(paths.join("/")), + join2: (l, r) => PATH.normalize(l + "/" + r) +}; + +var initRandomFill = () => { + // This block is not needed on v19+ since crypto.getRandomValues is builtin + if (ENVIRONMENT_IS_NODE) { + var nodeCrypto = require("node:crypto"); + return view => nodeCrypto.randomFillSync(view); + } + return view => (crypto.getRandomValues(view), 0); +}; + +var randomFill = view => (randomFill = initRandomFill())(view); + +var PATH_FS = { + resolve: (...args) => { + var resolvedPath = "", resolvedAbsolute = false; + for (var i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? args[i] : FS.cwd(); + // Skip empty and invalid entries + if (typeof path != "string") { + throw new TypeError("Arguments to path.resolve must be strings"); + } else if (!path) { + return ""; + } + resolvedPath = path + "/" + resolvedPath; + resolvedAbsolute = PATH.isAbs(path); + } + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + resolvedPath = PATH.normalizeArray(resolvedPath.split("/").filter(p => !!p), !resolvedAbsolute).join("/"); + return ((resolvedAbsolute ? "/" : "") + resolvedPath) || "."; + }, + relative: (from, to) => { + from = PATH_FS.resolve(from).slice(1); + to = PATH_FS.resolve(to).slice(1); + function trim(arr) { + var start = 0; + for (;start < arr.length; start++) { + if (arr[start] !== "") break; + } + var end = arr.length - 1; + for (;end >= 0; end--) { + if (arr[end] !== "") break; + } + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + var fromParts = trim(from.split("/")); + var toParts = trim(to.split("/")); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push(".."); + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join("/"); + } +}; + +var UTF8Decoder = new TextDecoder; + +var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on + // null terminator by itself. + // As a tiny code save trick, compare idx against maxIdx using a negation, + // so that maxBytesToRead=undefined/NaN means Infinity. + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; +}; + +/** + * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given + * array that contains uint8 values, returns a copy of that string as a + * Javascript String object. + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number=} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + return UTF8Decoder.decode(heapOrArray.buffer ? heapOrArray.subarray(idx, endPtr) : new Uint8Array(heapOrArray.slice(idx, endPtr))); +}; + +var FS_stdin_getChar_buffer = []; + +var lengthBytesUTF8 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code + // unit, not a Unicode code point of the character! So decode + // UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var c = str.charCodeAt(i); + // possibly a lead surrogate + if (c <= 127) { + len++; + } else if (c <= 2047) { + len += 2; + } else if (c >= 55296 && c <= 57343) { + len += 4; + ++i; + } else { + len += 3; + } + } + return len; +}; + +var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + // Parameter maxBytesToWrite is not optional. Negative values, 0, null, + // undefined and false each don't write out any bytes. + if (!(maxBytesToWrite > 0)) return 0; + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; + // -1 for string null terminator. + for (var i = 0; i < str.length; ++i) { + // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description + // and https://www.ietf.org/rfc/rfc2279.txt + // and https://tools.ietf.org/html/rfc3629 + var u = str.codePointAt(i); + if (u <= 127) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 2047) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 192 | (u >> 6); + heap[outIdx++] = 128 | (u & 63); + } else if (u <= 65535) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 224 | (u >> 12); + heap[outIdx++] = 128 | ((u >> 6) & 63); + heap[outIdx++] = 128 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + heap[outIdx++] = 240 | (u >> 18); + heap[outIdx++] = 128 | ((u >> 12) & 63); + heap[outIdx++] = 128 | ((u >> 6) & 63); + heap[outIdx++] = 128 | (u & 63); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + i++; + } + } + // Null-terminate the pointer to the buffer. + heap[outIdx] = 0; + return outIdx - startIdx; +}; + +/** @type {function(string, boolean=, number=)} */ var intArrayFromString = (stringy, dontAddNull, length) => { + var len = length > 0 ? length : lengthBytesUTF8(stringy) + 1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; +}; + +var FS_stdin_getChar = () => { + if (!FS_stdin_getChar_buffer.length) { + var result = null; + if (ENVIRONMENT_IS_NODE) { + // we will read data by chunks of BUFSIZE + var BUFSIZE = 256; + var buf = Buffer.alloc(BUFSIZE); + var bytesRead = 0; + // For some reason we must suppress a closure warning here, even though + // fd definitely exists on process.stdin, and is even the proper way to + // get the fd of stdin, + // https://github.com/nodejs/help/issues/2136#issuecomment-523649904 + // This started to happen after moving this logic out of library_tty.js, + // so it is related to the surrounding code in some unclear manner. + /** @suppress {missingProperties} */ var fd = process.stdin.fd; + try { + bytesRead = fs.readSync(fd, buf, 0, BUFSIZE); + } catch (e) { + // Cross-platform differences: on Windows, reading EOF throws an + // exception, but on other OSes, reading EOF returns 0. Uniformize + // behavior by treating the EOF exception to return 0. + if (e.toString().includes("EOF")) bytesRead = 0; else throw e; + } + if (bytesRead > 0) { + result = buf.slice(0, bytesRead).toString("utf-8"); + } + } else if (globalThis.window?.prompt) { + // Browser. + result = window.prompt("Input: "); + // returns null on cancel + if (result !== null) { + result += "\n"; + } + } else {} + if (!result) { + return null; + } + FS_stdin_getChar_buffer = intArrayFromString(result, true); + } + return FS_stdin_getChar_buffer.shift(); +}; + +var TTY = { + ttys: [], + init() {}, + shutdown() {}, + register(dev, ops) { + TTY.ttys[dev] = { + input: [], + output: [], + ops + }; + FS.registerDevice(dev, TTY.stream_ops); + }, + stream_ops: { + open(stream) { + var tty = TTY.ttys[stream.node.rdev]; + if (!tty) { + throw new FS.ErrnoError(43); + } + stream.tty = tty; + stream.seekable = false; + }, + close(stream) { + // flush any pending line data + stream.tty.ops.fsync(stream.tty); + }, + fsync(stream) { + stream.tty.ops.fsync(stream.tty); + }, + read(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.get_char) { + throw new FS.ErrnoError(60); + } + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = stream.tty.ops.get_char(stream.tty); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.put_char) { + throw new FS.ErrnoError(60); + } + try { + for (var i = 0; i < length; i++) { + stream.tty.ops.put_char(stream.tty, buffer[offset + i]); + } + } catch (e) { + throw new FS.ErrnoError(29); + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + } + }, + default_tty_ops: { + get_char(tty) { + return FS_stdin_getChar(); + }, + put_char(tty, val) { + if (val === null || val === 10) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } + }, + ioctl_tcgets(tty) { + // typical setting + return { + c_iflag: 25856, + c_oflag: 5, + c_cflag: 191, + c_lflag: 35387, + c_cc: [ 3, 28, 127, 21, 4, 0, 1, 0, 17, 19, 26, 0, 18, 15, 23, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] + }; + }, + ioctl_tcsets(tty, optional_actions, data) { + // currently just ignore + return 0; + }, + ioctl_tiocgwinsz(tty) { + return [ 24, 80 ]; + } + }, + default_tty1_ops: { + put_char(tty, val) { + if (val === null || val === 10) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } + } + } +}; + +var zeroMemory = (ptr, size) => HEAPU8.fill(0, ptr, ptr + size); + +var alignMemory = (size, alignment) => Math.ceil(size / alignment) * alignment; + +var mmapAlloc = size => { + size = alignMemory(size, 65536); + var ptr = _emscripten_builtin_memalign(65536, size); + if (ptr) zeroMemory(ptr, size); + return ptr; +}; + +var MEMFS = { + ops_table: null, + mount(mount) { + return MEMFS.createNode(null, "/", 16895, 0); + }, + createNode(parent, name, mode, dev) { + if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { + // not supported + throw new FS.ErrnoError(63); + } + MEMFS.ops_table ||= { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink + }, + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync + } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + }; + var node = FS.createNode(parent, name, mode, dev); + if (FS.isDir(node.mode)) { + node.node_ops = MEMFS.ops_table.dir.node; + node.stream_ops = MEMFS.ops_table.dir.stream; + node.contents = {}; + } else if (FS.isFile(node.mode)) { + node.node_ops = MEMFS.ops_table.file.node; + node.stream_ops = MEMFS.ops_table.file.stream; + // The actual number of bytes used in the typed array, as opposed to + // contents.length which gives the whole capacity. + node.usedBytes = 0; + // The byte data of the file is stored in a typed array. + // Note: typed arrays are not resizable like normal JS arrays are, so + // there is a small penalty involved for appending file writes that + // continuously grow a file similar to std::vector capacity vs used. + node.contents = MEMFS.emptyFileContents ??= new Uint8Array(0); + } else if (FS.isLink(node.mode)) { + node.node_ops = MEMFS.ops_table.link.node; + node.stream_ops = MEMFS.ops_table.link.stream; + } else if (FS.isChrdev(node.mode)) { + node.node_ops = MEMFS.ops_table.chrdev.node; + node.stream_ops = MEMFS.ops_table.chrdev.stream; + } + node.atime = node.mtime = node.ctime = Date.now(); + // add the new node to the parent + if (parent) { + parent.contents[name] = node; + parent.atime = parent.mtime = parent.ctime = node.atime; + } + return node; + }, + getFileDataAsTypedArray(node) { + return node.contents.subarray(0, node.usedBytes); + }, + expandFileStorage(node, newCapacity) { + var prevCapacity = node.contents.length; + if (prevCapacity >= newCapacity) return; + // No need to expand, the storage was already large enough. + // Don't expand strictly to the given requested limit if it's only a very + // small increase, but instead geometrically grow capacity. + // For small filesizes (<1MB), perform size*2 geometric increase, but for + // large sizes, do a much more conservative size*1.125 increase to avoid + // overshooting the allocation cap by a very large margin. + var CAPACITY_DOUBLING_MAX = 1024 * 1024; + newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2 : 1.125)) >>> 0); + if (prevCapacity) newCapacity = Math.max(newCapacity, 256); + // At minimum allocate 256b for each file when expanding. + var oldContents = MEMFS.getFileDataAsTypedArray(node); + node.contents = new Uint8Array(newCapacity); + // Allocate new storage. + node.contents.set(oldContents); + }, + resizeFileStorage(node, newSize) { + if (node.usedBytes == newSize) return; + var oldContents = node.contents; + node.contents = new Uint8Array(newSize); + // Allocate new storage. + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); + // Copy old data over to the new storage. + node.usedBytes = newSize; + }, + node_ops: { + getattr(node) { + var attr = {}; + // device numbers reuse inode numbers. + attr.dev = FS.isChrdev(node.mode) ? node.id : 1; + attr.ino = node.id; + attr.mode = node.mode; + attr.nlink = 1; + attr.uid = 0; + attr.gid = 0; + attr.rdev = node.rdev; + if (FS.isDir(node.mode)) { + attr.size = 4096; + } else if (FS.isFile(node.mode)) { + attr.size = node.usedBytes; + } else if (FS.isLink(node.mode)) { + attr.size = node.link.length; + } else { + attr.size = 0; + } + attr.atime = new Date(node.atime); + attr.mtime = new Date(node.mtime); + attr.ctime = new Date(node.ctime); + // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), + // but this is not required by the standard. + attr.blksize = 4096; + attr.blocks = Math.ceil(attr.size / attr.blksize); + return attr; + }, + setattr(node, attr) { + for (const key of [ "mode", "atime", "mtime", "ctime" ]) { + if (attr[key] != null) { + node[key] = attr[key]; + } + } + if (attr.size !== undefined) { + MEMFS.resizeFileStorage(node, attr.size); + } + }, + lookup(parent, name) { + // This error may happen quite a bit. To avoid overhead we reuse it (and + // suffer a lack of stack info). + if (!MEMFS.doesNotExistError) { + MEMFS.doesNotExistError = new FS.ErrnoError(44); + /** @suppress {checkTypes} */ MEMFS.doesNotExistError.stack = ""; + } + throw MEMFS.doesNotExistError; + }, + mknod(parent, name, mode, dev) { + return MEMFS.createNode(parent, name, mode, dev); + }, + rename(old_node, new_dir, new_name) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + if (new_node) { + if (FS.isDir(old_node.mode)) { + // if we're overwriting a directory at new_name, make sure it's empty. + for (var i in new_node.contents) { + throw new FS.ErrnoError(55); + } + } + FS.hashRemoveNode(new_node); + } + // do the internal rewiring + delete old_node.parent.contents[old_node.name]; + new_dir.contents[new_name] = old_node; + old_node.name = new_name; + new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now(); + }, + unlink(parent, name) { + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + rmdir(parent, name) { + var node = FS.lookupNode(parent, name); + for (var i in node.contents) { + throw new FS.ErrnoError(55); + } + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + readdir(node) { + return [ ".", "..", ...Object.keys(node.contents) ]; + }, + symlink(parent, newname, oldpath) { + var node = MEMFS.createNode(parent, newname, 511 | 40960, 0); + node.link = oldpath; + return node; + }, + readlink(node) { + if (!FS.isLink(node.mode)) { + throw new FS.ErrnoError(28); + } + return node.link; + } + }, + stream_ops: { + read(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= stream.node.usedBytes) return 0; + var size = Math.min(stream.node.usedBytes - position, length); + buffer.set(contents.subarray(position, position + size), offset); + return size; + }, + write(stream, buffer, offset, length, position, canOwn) { + // If the buffer is located in main memory (HEAP), and if + // memory can grow, we can't hold on to references of the + // memory buffer, as they may get invalidated. That means we + // need to copy its contents. + if (buffer.buffer === HEAP8.buffer) { + canOwn = false; + } + if (!length) return 0; + var node = stream.node; + node.mtime = node.ctime = Date.now(); + if (canOwn) { + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + } else if (node.usedBytes === 0 && position === 0) { + // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. + node.contents = buffer.slice(offset, offset + length); + node.usedBytes = length; + } else { + MEMFS.expandFileStorage(node, position + length); + // Use typed array write which is available. + node.contents.set(buffer.subarray(offset, offset + length), position); + node.usedBytes = Math.max(node.usedBytes, position + length); + } + return length; + }, + llseek(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position; + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + position += stream.node.usedBytes; + } + } + if (position < 0) { + throw new FS.ErrnoError(28); + } + return position; + }, + mmap(stream, length, position, prot, flags) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + var ptr; + var allocated; + var contents = stream.node.contents; + // Only make a new copy when MAP_PRIVATE is specified. + if (!(flags & 2) && contents.buffer === HEAP8.buffer) { + // We can't emulate MAP_SHARED when the file is not backed by the + // buffer we're mapping to (e.g. the HEAP buffer). + allocated = false; + ptr = contents.byteOffset; + } else { + allocated = true; + ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + if (contents) { + // Try to avoid unnecessary slices. + if (position > 0 || position + length < contents.length) { + if (contents.subarray) { + contents = contents.subarray(position, position + length); + } else { + contents = Array.prototype.slice.call(contents, position, position + length); + } + } + HEAP8.set(contents, ptr); + } + } + return { + ptr, + allocated + }; + }, + msync(stream, buffer, offset, length, mmapFlags) { + MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); + // should we check if bytesWritten and length are the same? + return 0; + } + } +}; + +var FS_modeStringToFlags = str => { + if (typeof str != "string") return str; + var flagModes = { + "r": 0, + "r+": 2, + "w": 512 | 64 | 1, + "w+": 512 | 64 | 2, + "a": 1024 | 64 | 1, + "a+": 1024 | 64 | 2 + }; + var flags = flagModes[str]; + if (typeof flags == "undefined") { + throw new Error(`Unknown file open mode: ${str}`); + } + return flags; +}; + +var FS_fileDataToTypedArray = data => { + if (typeof data == "string") { + data = intArrayFromString(data, true); + } + if (!data.subarray) { + data = new Uint8Array(data); + } + return data; +}; + +var FS_getMode = (canRead, canWrite) => { + var mode = 0; + if (canRead) mode |= 292 | 73; + if (canWrite) mode |= 146; + return mode; +}; + +var asyncLoad = async url => { + var arrayBuffer = await readAsync(url); + return new Uint8Array(arrayBuffer); +}; + +var FS_createDataFile = (...args) => FS.createDataFile(...args); + +var getUniqueRunDependency = id => id; + +var runDependencies = 0; + +var dependenciesFulfilled = null; + +var removeRunDependency = id => { + runDependencies--; + Module["monitorRunDependencies"]?.(runDependencies); + if (runDependencies == 0) { + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); + } + } +}; + +var addRunDependency = id => { + runDependencies++; + Module["monitorRunDependencies"]?.(runDependencies); +}; + +var FS_handledByPreloadPlugin = async (byteArray, fullname) => { + // Ensure plugins are ready. + if (typeof Browser != "undefined") Browser.init(); + for (var plugin of preloadPlugins) { + if (plugin["canHandle"](fullname)) { + return plugin["handle"](byteArray, fullname); + } + } + // If no plugin handled this file then return the original/unmodified + // byteArray. + return byteArray; +}; + +var FS_preloadFile = async (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => { + // TODO we should allow people to just pass in a complete filename instead + // of parent and name being that we just join them anyways + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency(`cp ${fullname}`); + // might have several active requests for the same fullname + addRunDependency(dep); + try { + var byteArray = url; + if (typeof url == "string") { + byteArray = await asyncLoad(url); + } + byteArray = await FS_handledByPreloadPlugin(byteArray, fullname); + preFinish?.(); + if (!dontCreateFile) { + FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); + } + } finally { + removeRunDependency(dep); + } +}; + +var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { + FS_preloadFile(parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish).then(onload).catch(onerror); +}; + +var FS = { + root: null, + mounts: [], + devices: {}, + streams: [], + nextInode: 1, + nameTable: null, + currentPath: "/", + initialized: false, + ignorePermissions: true, + filesystems: null, + syncFSRequests: 0, + ErrnoError: class { + name="ErrnoError"; + // We set the `name` property to be able to identify `FS.ErrnoError` + // - the `name` is a standard ECMA-262 property of error objects. Kind of good to have it anyway. + // - when using PROXYFS, an error can come from an underlying FS + // as different FS objects have their own FS.ErrnoError each, + // the test `err instanceof FS.ErrnoError` won't detect an error coming from another filesystem, causing bugs. + // we'll use the reliable test `err.name == "ErrnoError"` instead + constructor(errno) { + this.errno = errno; + } + }, + FSStream: class { + shared={}; + get object() { + return this.node; + } + set object(val) { + this.node = val; + } + get isRead() { + return (this.flags & 2097155) !== 1; + } + get isWrite() { + return (this.flags & 2097155) !== 0; + } + get isAppend() { + return (this.flags & 1024); + } + get flags() { + return this.shared.flags; + } + set flags(val) { + this.shared.flags = val; + } + get position() { + return this.shared.position; + } + set position(val) { + this.shared.position = val; + } + }, + FSNode: class { + node_ops={}; + stream_ops={}; + readMode=292 | 73; + writeMode=146; + mounted=null; + constructor(parent, name, mode, rdev) { + if (!parent) { + parent = this; + } + this.parent = parent; + this.mount = parent.mount; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.rdev = rdev; + this.atime = this.mtime = this.ctime = Date.now(); + } + get read() { + return (this.mode & this.readMode) === this.readMode; + } + set read(val) { + val ? this.mode |= this.readMode : this.mode &= ~this.readMode; + } + get write() { + return (this.mode & this.writeMode) === this.writeMode; + } + set write(val) { + val ? this.mode |= this.writeMode : this.mode &= ~this.writeMode; + } + get isFolder() { + return FS.isDir(this.mode); + } + get isDevice() { + return FS.isChrdev(this.mode); + } + }, + lookupPath(path, opts = {}) { + if (!path) { + throw new FS.ErrnoError(44); + } + opts.follow_mount ??= true; + if (!PATH.isAbs(path)) { + path = FS.cwd() + "/" + path; + } + // limit max consecutive symlinks to 40 (SYMLOOP_MAX). + linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { + // split the absolute path + var parts = path.split("/").filter(p => !!p); + // start at the root + var current = FS.root; + var current_path = "/"; + for (var i = 0; i < parts.length; i++) { + var islast = (i === parts.length - 1); + if (islast && opts.parent) { + // stop resolving + break; + } + if (parts[i] === ".") { + continue; + } + if (parts[i] === "..") { + current_path = PATH.dirname(current_path); + if (FS.isRoot(current)) { + path = current_path + "/" + parts.slice(i + 1).join("/"); + // We're making progress here, don't let many consecutive ..'s + // lead to ELOOP + nlinks--; + continue linkloop; + } else { + current = current.parent; + } + continue; + } + current_path = PATH.join2(current_path, parts[i]); + try { + current = FS.lookupNode(current, parts[i]); + } catch (e) { + // if noent_okay is true, suppress a ENOENT in the last component + // and return an object with an undefined node. This is needed for + // resolving symlinks in the path when creating a file. + if ((e?.errno === 44) && islast && opts.noent_okay) { + return { + path: current_path + }; + } + throw e; + } + // jump to the mount's root node if this is a mountpoint + if (FS.isMountpoint(current) && (!islast || opts.follow_mount)) { + current = current.mounted.root; + } + // by default, lookupPath will not follow a symlink if it is the final path component. + // setting opts.follow = true will override this behavior. + if (FS.isLink(current.mode) && (!islast || opts.follow)) { + if (!current.node_ops.readlink) { + throw new FS.ErrnoError(52); + } + var link = current.node_ops.readlink(current); + if (!PATH.isAbs(link)) { + link = PATH.dirname(current_path) + "/" + link; + } + path = link + "/" + parts.slice(i + 1).join("/"); + continue linkloop; + } + } + return { + path: current_path, + node: current + }; + } + throw new FS.ErrnoError(32); + }, + getPath(node) { + var path; + while (true) { + if (FS.isRoot(node)) { + var mount = node.mount.mountpoint; + if (!path) return mount; + return mount[mount.length - 1] !== "/" ? `${mount}/${path}` : mount + path; + } + path = path ? `${node.name}/${path}` : node.name; + node = node.parent; + } + }, + hashName(parentid, name) { + var hash = 0; + for (var i = 0; i < name.length; i++) { + hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; + } + return ((parentid + hash) >>> 0) % FS.nameTable.length; + }, + hashAddNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + node.name_next = FS.nameTable[hash]; + FS.nameTable[hash] = node; + }, + hashRemoveNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + if (FS.nameTable[hash] === node) { + FS.nameTable[hash] = node.name_next; + } else { + var current = FS.nameTable[hash]; + while (current) { + if (current.name_next === node) { + current.name_next = node.name_next; + break; + } + current = current.name_next; + } + } + }, + lookupNode(parent, name) { + var errCode = FS.mayLookup(parent); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + var hash = FS.hashName(parent.id, name); + for (var node = FS.nameTable[hash]; node; node = node.name_next) { + var nodeName = node.name; + if (node.parent.id === parent.id && nodeName === name) { + return node; + } + } + // if we failed to find it in the cache, call into the VFS + return FS.lookup(parent, name); + }, + createNode(parent, name, mode, rdev) { + var node = new FS.FSNode(parent, name, mode, rdev); + FS.hashAddNode(node); + return node; + }, + destroyNode(node) { + FS.hashRemoveNode(node); + }, + isRoot(node) { + return node === node.parent; + }, + isMountpoint(node) { + return !!node.mounted; + }, + isFile(mode) { + return (mode & 61440) === 32768; + }, + isDir(mode) { + return (mode & 61440) === 16384; + }, + isLink(mode) { + return (mode & 61440) === 40960; + }, + isChrdev(mode) { + return (mode & 61440) === 8192; + }, + isBlkdev(mode) { + return (mode & 61440) === 24576; + }, + isFIFO(mode) { + return (mode & 61440) === 4096; + }, + isSocket(mode) { + return (mode & 49152) === 49152; + }, + flagsToPermissionString(flag) { + var perms = [ "r", "w", "rw" ][flag & 3]; + if ((flag & 512)) { + perms += "w"; + } + return perms; + }, + nodePermissions(node, perms) { + if (FS.ignorePermissions) { + return 0; + } + // return 0 if any user, group or owner bits are set. + if (perms.includes("r") && !(node.mode & 292)) { + return 2; + } + if (perms.includes("w") && !(node.mode & 146)) { + return 2; + } + if (perms.includes("x") && !(node.mode & 73)) { + return 2; + } + return 0; + }, + mayLookup(dir) { + if (!FS.isDir(dir.mode)) return 54; + var errCode = FS.nodePermissions(dir, "x"); + if (errCode) return errCode; + if (!dir.node_ops.lookup) return 2; + return 0; + }, + mayCreate(dir, name) { + if (!FS.isDir(dir.mode)) { + return 54; + } + try { + var node = FS.lookupNode(dir, name); + return 20; + } catch (e) {} + return FS.nodePermissions(dir, "wx"); + }, + mayDelete(dir, name, isdir) { + var node; + try { + node = FS.lookupNode(dir, name); + } catch (e) { + return e.errno; + } + var errCode = FS.nodePermissions(dir, "wx"); + if (errCode) { + return errCode; + } + if (isdir) { + if (!FS.isDir(node.mode)) { + return 54; + } + if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { + return 10; + } + } else if (FS.isDir(node.mode)) { + return 31; + } + return 0; + }, + mayOpen(node, flags) { + if (!node) { + return 44; + } + if (FS.isLink(node.mode)) { + return 32; + } + var mode = FS.flagsToPermissionString(flags); + if (FS.isDir(node.mode)) { + // opening for write + // TODO: check for O_SEARCH? (== search for dir only) + if (mode !== "r" || (flags & (512 | 64))) { + return 31; + } + } + return FS.nodePermissions(node, mode); + }, + checkOpExists(op, err) { + if (!op) { + throw new FS.ErrnoError(err); + } + return op; + }, + MAX_OPEN_FDS: 4096, + nextfd() { + for (var fd = 0; fd <= FS.MAX_OPEN_FDS; fd++) { + if (!FS.streams[fd]) { + return fd; + } + } + throw new FS.ErrnoError(33); + }, + getStreamChecked(fd) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); + } + return stream; + }, + getStream: fd => FS.streams[fd], + createStream(stream, fd = -1) { + // clone it, so we can return an instance of FSStream + stream = Object.assign(new FS.FSStream, stream); + if (fd == -1) { + fd = FS.nextfd(); + } + stream.fd = fd; + FS.streams[fd] = stream; + return stream; + }, + closeStream(fd) { + FS.streams[fd] = null; + }, + dupStream(origStream, fd = -1) { + var stream = FS.createStream(origStream, fd); + stream.stream_ops?.dup?.(stream); + return stream; + }, + doSetAttr(stream, node, attr) { + var setattr = stream?.stream_ops.setattr; + var arg = setattr ? stream : node; + setattr ??= node.node_ops.setattr; + FS.checkOpExists(setattr, 63); + setattr(arg, attr); + }, + chrdev_stream_ops: { + open(stream) { + var device = FS.getDevice(stream.node.rdev); + // override node's stream ops with the device's + stream.stream_ops = device.stream_ops; + // forward the open call + stream.stream_ops.open?.(stream); + }, + llseek() { + throw new FS.ErrnoError(70); + } + }, + major: dev => ((dev) >> 8), + minor: dev => ((dev) & 255), + makedev: (ma, mi) => ((ma) << 8 | (mi)), + registerDevice(dev, ops) { + FS.devices[dev] = { + stream_ops: ops + }; + }, + getDevice: dev => FS.devices[dev], + getMounts(mount) { + var mounts = []; + var check = [ mount ]; + while (check.length) { + var m = check.pop(); + mounts.push(m); + check.push(...m.mounts); + } + return mounts; + }, + syncfs(populate, callback) { + if (typeof populate == "function") { + callback = populate; + populate = false; + } + FS.syncFSRequests++; + if (FS.syncFSRequests > 1) { + err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`); + } + var mounts = FS.getMounts(FS.root.mount); + var completed = 0; + function doCallback(errCode) { + FS.syncFSRequests--; + return callback(errCode); + } + function done(errCode) { + if (errCode) { + if (!done.errored) { + done.errored = true; + return doCallback(errCode); + } + return; + } + if (++completed >= mounts.length) { + doCallback(null); + } + } + // sync all mounts + for (var mount of mounts) { + if (mount.type.syncfs) { + mount.type.syncfs(mount, populate, done); + } else { + done(null); + } + } + }, + mount(type, opts, mountpoint) { + var root = mountpoint === "/"; + var pseudo = !mountpoint; + var node; + if (root && FS.root) { + throw new FS.ErrnoError(10); + } else if (!root && !pseudo) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + mountpoint = lookup.path; + // use the absolute path + node = lookup.node; + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + if (!FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + } + var mount = { + type, + opts, + mountpoint, + mounts: [] + }; + // create a root node for the fs + var mountRoot = type.mount(mount); + mountRoot.mount = mount; + mount.root = mountRoot; + if (root) { + FS.root = mountRoot; + } else if (node) { + // set as a mountpoint + node.mounted = mount; + // add the new mount to the current mount's children + if (node.mount) { + node.mount.mounts.push(mount); + } + } + return mountRoot; + }, + unmount(mountpoint) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + if (!FS.isMountpoint(lookup.node)) { + throw new FS.ErrnoError(28); + } + // destroy the nodes for this mount, and all its child mounts + var node = lookup.node; + var mount = node.mounted; + var mounts = FS.getMounts(mount); + for (var [hash, current] of Object.entries(FS.nameTable)) { + while (current) { + var next = current.name_next; + if (mounts.includes(current.mount)) { + FS.destroyNode(current); + } + current = next; + } + } + // no longer a mountpoint + node.mounted = null; + // remove this mount from the child mounts + var idx = node.mount.mounts.indexOf(mount); + node.mount.mounts.splice(idx, 1); + }, + lookup(parent, name) { + return parent.node_ops.lookup(parent, name); + }, + mknod(path, mode, dev) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + if (!name) { + throw new FS.ErrnoError(28); + } + if (name === "." || name === "..") { + throw new FS.ErrnoError(20); + } + var errCode = FS.mayCreate(parent, name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.mknod) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.mknod(parent, name, mode, dev); + }, + statfs(path) { + return FS.statfsNode(FS.lookupPath(path, { + follow: true + }).node); + }, + statfsStream(stream) { + // We keep a separate statfsStream function because noderawfs overrides + // it. In noderawfs, stream.node is sometimes null. Instead, we need to + // look at stream.path. + return FS.statfsNode(stream.node); + }, + statfsNode(node) { + // NOTE: None of the defaults here are true. We're just returning safe and + // sane values. Currently nodefs and rawfs replace these defaults, + // other file systems leave them alone. + var rtn = { + bsize: 4096, + frsize: 4096, + blocks: 1e6, + bfree: 5e5, + bavail: 5e5, + files: FS.nextInode, + ffree: FS.nextInode - 1, + fsid: 42, + flags: 2, + namelen: 255 + }; + if (node.node_ops.statfs) { + Object.assign(rtn, node.node_ops.statfs(node.mount.opts.root)); + } + return rtn; + }, + create(path, mode = 438) { + mode &= 4095; + mode |= 32768; + return FS.mknod(path, mode, 0); + }, + mkdir(path, mode = 511) { + mode &= 511 | 512; + mode |= 16384; + return FS.mknod(path, mode, 0); + }, + mkdirTree(path, mode) { + var dirs = path.split("/"); + var d = ""; + for (var dir of dirs) { + if (!dir) continue; + if (d || PATH.isAbs(path)) d += "/"; + d += dir; + try { + FS.mkdir(d, mode); + } catch (e) { + if (e.errno != 20) throw e; + } + } + }, + mkdev(path, mode, dev) { + if (typeof dev == "undefined") { + dev = mode; + mode = 438; + } + mode |= 8192; + return FS.mknod(path, mode, dev); + }, + symlink(oldpath, newpath) { + if (!PATH_FS.resolve(oldpath)) { + throw new FS.ErrnoError(44); + } + var lookup = FS.lookupPath(newpath, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.symlink) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.symlink(parent, newname, oldpath); + }, + rename(old_path, new_path) { + var old_dirname = PATH.dirname(old_path); + var new_dirname = PATH.dirname(new_path); + var old_name = PATH.basename(old_path); + var new_name = PATH.basename(new_path); + // parents must exist + var lookup, old_dir, new_dir; + // let the errors from non existent directories percolate up + lookup = FS.lookupPath(old_path, { + parent: true + }); + old_dir = lookup.node; + lookup = FS.lookupPath(new_path, { + parent: true + }); + new_dir = lookup.node; + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); + // need to be part of the same mount + if (old_dir.mount !== new_dir.mount) { + throw new FS.ErrnoError(75); + } + // source must exist + var old_node = FS.lookupNode(old_dir, old_name); + // old path should not be an ancestor of the new path + var relative = PATH_FS.relative(old_path, new_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(28); + } + // new path should not be an ancestor of the old path + relative = PATH_FS.relative(new_path, old_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(55); + } + // see if the new path already exists + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + // early out if nothing needs to change + if (old_node === new_node) { + return; + } + // we'll need to delete the old entry + var isdir = FS.isDir(old_node.mode); + var errCode = FS.mayDelete(old_dir, old_name, isdir); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + // need delete permissions if we'll be overwriting. + // need create permissions if new doesn't already exist. + errCode = new_node ? FS.mayDelete(new_dir, new_name, isdir) : FS.mayCreate(new_dir, new_name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!old_dir.node_ops.rename) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(old_node) || (new_node && FS.isMountpoint(new_node))) { + throw new FS.ErrnoError(10); + } + // if we are going to change the parent, check write permissions + if (new_dir !== old_dir) { + errCode = FS.nodePermissions(old_dir, "w"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + // remove the node from the lookup hash + FS.hashRemoveNode(old_node); + // do the underlying fs rename + try { + old_dir.node_ops.rename(old_node, new_dir, new_name); + // update old node (we do this here to avoid each backend + // needing to) + old_node.parent = new_dir; + } catch (e) { + throw e; + } finally { + // add the node back to the hash (in case node_ops.rename + // changed its name) + FS.hashAddNode(old_node); + } + }, + rmdir(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, true); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.rmdir) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.rmdir(parent, name); + FS.destroyNode(node); + }, + readdir(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + var readdir = FS.checkOpExists(node.node_ops.readdir, 54); + return readdir(node); + }, + unlink(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, false); + if (errCode) { + // According to POSIX, we should map EISDIR to EPERM, but + // we instead do what Linux does (and we must, as we use + // the musl linux libc). + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.unlink) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.unlink(parent, name); + FS.destroyNode(node); + }, + readlink(path) { + var lookup = FS.lookupPath(path); + var link = lookup.node; + if (!link) { + throw new FS.ErrnoError(44); + } + if (!link.node_ops.readlink) { + throw new FS.ErrnoError(28); + } + return link.node_ops.readlink(link); + }, + stat(path, dontFollow) { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + var node = lookup.node; + var getattr = FS.checkOpExists(node.node_ops.getattr, 63); + return getattr(node); + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + var node = stream.node; + var getattr = stream.stream_ops.getattr; + var arg = getattr ? stream : node; + getattr ??= node.node_ops.getattr; + FS.checkOpExists(getattr, 63); + return getattr(arg); + }, + lstat(path) { + return FS.stat(path, true); + }, + doChmod(stream, node, mode, dontFollow) { + FS.doSetAttr(stream, node, { + mode: (mode & 4095) | (node.mode & ~4095), + ctime: Date.now(), + dontFollow + }); + }, + chmod(path, mode, dontFollow) { + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node; + } else { + node = path; + } + FS.doChmod(null, node, mode, dontFollow); + }, + lchmod(path, mode) { + FS.chmod(path, mode, true); + }, + fchmod(fd, mode) { + var stream = FS.getStreamChecked(fd); + FS.doChmod(stream, stream.node, mode, false); + }, + doChown(stream, node, dontFollow) { + FS.doSetAttr(stream, node, { + timestamp: Date.now(), + dontFollow + }); + }, + chown(path, uid, gid, dontFollow) { + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node; + } else { + node = path; + } + FS.doChown(null, node, dontFollow); + }, + lchown(path, uid, gid) { + FS.chown(path, uid, gid, true); + }, + fchown(fd, uid, gid) { + var stream = FS.getStreamChecked(fd); + FS.doChown(stream, stream.node, false); + }, + doTruncate(stream, node, len) { + if (FS.isDir(node.mode)) { + throw new FS.ErrnoError(31); + } + if (!FS.isFile(node.mode)) { + throw new FS.ErrnoError(28); + } + var errCode = FS.nodePermissions(node, "w"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.doSetAttr(stream, node, { + size: len, + timestamp: Date.now() + }); + }, + truncate(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28); + } + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: true + }); + node = lookup.node; + } else { + node = path; + } + FS.doTruncate(null, node, len); + }, + ftruncate(fd, len) { + var stream = FS.getStreamChecked(fd); + if (len < 0 || (stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(28); + } + FS.doTruncate(stream, stream.node, len); + }, + utime(path, atime, mtime) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + var setattr = FS.checkOpExists(node.node_ops.setattr, 63); + setattr(node, { + atime, + mtime + }); + }, + open(path, flags, mode = 438) { + if (path === "") { + throw new FS.ErrnoError(44); + } + flags = FS_modeStringToFlags(flags); + if ((flags & 64)) { + mode = (mode & 4095) | 32768; + } else { + mode = 0; + } + var node; + var isDirPath; + if (typeof path == "object") { + node = path; + } else { + isDirPath = path.endsWith("/"); + // noent_okay makes it so that if the final component of the path + // doesn't exist, lookupPath returns `node: undefined`. `path` will be + // updated to point to the target of all symlinks. + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072), + noent_okay: true + }); + node = lookup.node; + path = lookup.path; + } + // perhaps we need to create the node + var created = false; + if ((flags & 64)) { + if (node) { + // if O_CREAT and O_EXCL are set, error out if the node already exists + if ((flags & 128)) { + throw new FS.ErrnoError(20); + } + } else if (isDirPath) { + throw new FS.ErrnoError(31); + } else { + // node doesn't exist, try to create it + // Ignore the permission bits here to ensure we can `open` this new + // file below. We use chmod below to apply the permissions once the + // file is open. + node = FS.mknod(path, mode | 511, 0); + created = true; + } + } + if (!node) { + throw new FS.ErrnoError(44); + } + // can't truncate a device + if (FS.isChrdev(node.mode)) { + flags &= ~512; + } + // if asked only for a directory, then this must be one + if ((flags & 65536) && !FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + // check permissions, if this is not a file we just created now (it is ok to + // create and write to a file with read-only permissions; it is read-only + // for later use) + if (!created) { + var errCode = FS.mayOpen(node, flags); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + // do truncation if necessary + if ((flags & 512) && !created) { + FS.truncate(node, 0); + } + // we've already handled these, don't pass down to the underlying vfs + flags &= ~(128 | 512 | 131072); + // register the stream with the filesystem + var stream = FS.createStream({ + node, + path: FS.getPath(node), + // we want the absolute path to the node + flags, + seekable: true, + position: 0, + stream_ops: node.stream_ops, + // used by the file family libc calls (fopen, fwrite, ferror, etc.) + ungotten: [], + error: false + }); + // call the new stream's open function + if (stream.stream_ops.open) { + stream.stream_ops.open(stream); + } + if (created) { + FS.chmod(node, mode & 511); + } + return stream; + }, + close(stream) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (stream.getdents) stream.getdents = null; + // free readdir state + try { + if (stream.stream_ops.close) { + stream.stream_ops.close(stream); + } + } catch (e) { + throw e; + } finally { + FS.closeStream(stream.fd); + } + stream.fd = null; + }, + isClosed(stream) { + return stream.fd === null; + }, + llseek(stream, offset, whence) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (!stream.seekable || !stream.stream_ops.llseek) { + throw new FS.ErrnoError(70); + } + if (whence != 0 && whence != 1 && whence != 2) { + throw new FS.ErrnoError(28); + } + stream.position = stream.stream_ops.llseek(stream, offset, whence); + stream.ungotten = []; + return stream.position; + }, + read(stream, buffer, offset, length, position) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.read) { + throw new FS.ErrnoError(28); + } + var seeking = typeof position != "undefined"; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); + if (!seeking) stream.position += bytesRead; + return bytesRead; + }, + write(stream, buffer, offset, length, position, canOwn) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.write) { + throw new FS.ErrnoError(28); + } + if (stream.seekable && stream.flags & 1024) { + // seek to the end before writing in append mode + FS.llseek(stream, 0, 2); + } + var seeking = typeof position != "undefined"; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); + if (!seeking) stream.position += bytesWritten; + return bytesWritten; + }, + mmap(stream, length, position, prot, flags) { + // User requests writing to file (prot & PROT_WRITE != 0). + // Checking if we have permissions to write to the file unless + // MAP_PRIVATE flag is set. According to POSIX spec it is possible + // to write to file opened in read-only mode with MAP_PRIVATE flag, + // as all modifications will be visible only in the memory of + // the current process. + if ((prot & 2) !== 0 && (flags & 2) === 0 && (stream.flags & 2097155) !== 2) { + throw new FS.ErrnoError(2); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(2); + } + if (!stream.stream_ops.mmap) { + throw new FS.ErrnoError(43); + } + if (!length) { + throw new FS.ErrnoError(28); + } + return stream.stream_ops.mmap(stream, length, position, prot, flags); + }, + msync(stream, buffer, offset, length, mmapFlags) { + if (!stream.stream_ops.msync) { + return 0; + } + return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); + }, + ioctl(stream, cmd, arg) { + if (!stream.stream_ops.ioctl) { + throw new FS.ErrnoError(59); + } + return stream.stream_ops.ioctl(stream, cmd, arg); + }, + readFile(path, opts = {}) { + opts.flags = opts.flags || 0; + opts.encoding = opts.encoding || "binary"; + if (opts.encoding !== "utf8" && opts.encoding !== "binary") { + abort(`Invalid encoding type "${opts.encoding}"`); + } + var stream = FS.open(path, opts.flags); + var stat = FS.stat(path); + var length = stat.size; + var buf = new Uint8Array(length); + FS.read(stream, buf, 0, length, 0); + if (opts.encoding === "utf8") { + buf = UTF8ArrayToString(buf); + } + FS.close(stream); + return buf; + }, + writeFile(path, data, opts = {}) { + opts.flags = opts.flags || 577; + var stream = FS.open(path, opts.flags, opts.mode); + data = FS_fileDataToTypedArray(data); + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); + FS.close(stream); + }, + cwd: () => FS.currentPath, + chdir(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + if (lookup.node === null) { + throw new FS.ErrnoError(44); + } + if (!FS.isDir(lookup.node.mode)) { + throw new FS.ErrnoError(54); + } + var errCode = FS.nodePermissions(lookup.node, "x"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.currentPath = lookup.path; + }, + createDefaultDirectories() { + FS.mkdir("/tmp"); + FS.mkdir("/home"); + FS.mkdir("/home/web_user"); + }, + createDefaultDevices() { + // create /dev + FS.mkdir("/dev"); + // setup /dev/null + FS.registerDevice(FS.makedev(1, 3), { + read: () => 0, + write: (stream, buffer, offset, length, pos) => length, + llseek: () => 0 + }); + FS.mkdev("/dev/null", FS.makedev(1, 3)); + // setup /dev/tty and /dev/tty1 + // stderr needs to print output using err() rather than out() + // so we register a second tty just for it. + TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); + TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); + FS.mkdev("/dev/tty", FS.makedev(5, 0)); + FS.mkdev("/dev/tty1", FS.makedev(6, 0)); + // setup /dev/[u]random + // use a buffer to avoid overhead of individual crypto calls per byte + var randomBuffer = new Uint8Array(1024), randomLeft = 0; + var randomByte = () => { + if (randomLeft === 0) { + randomFill(randomBuffer); + randomLeft = randomBuffer.byteLength; + } + return randomBuffer[--randomLeft]; + }; + FS.createDevice("/dev", "random", randomByte); + FS.createDevice("/dev", "urandom", randomByte); + // we're not going to emulate the actual shm device, + // just create the tmp dirs that reside in it commonly + FS.mkdir("/dev/shm"); + FS.mkdir("/dev/shm/tmp"); + }, + createSpecialDirectories() { + // create /proc/self/fd which allows /proc/self/fd/6 => readlink gives the + // name of the stream for fd 6 (see test_unistd_ttyname) + FS.mkdir("/proc"); + var proc_self = FS.mkdir("/proc/self"); + FS.mkdir("/proc/self/fd"); + FS.mount({ + mount() { + var node = FS.createNode(proc_self, "fd", 16895, 73); + node.stream_ops = { + llseek: MEMFS.stream_ops.llseek + }; + node.node_ops = { + lookup(parent, name) { + var fd = +name; + var stream = FS.getStreamChecked(fd); + var ret = { + parent: null, + mount: { + mountpoint: "fake" + }, + node_ops: { + readlink: () => stream.path + }, + id: fd + 1 + }; + ret.parent = ret; + // make it look like a simple root node + return ret; + }, + readdir() { + return Array.from(FS.streams.entries()).filter(([k, v]) => v).map(([k, v]) => k.toString()); + } + }; + return node; + } + }, {}, "/proc/self/fd"); + }, + createStandardStreams(input, output, error) { + // TODO deprecate the old functionality of a single + // input / output callback and that utilizes FS.createDevice + // and instead require a unique set of stream ops + // by default, we symlink the standard streams to the + // default tty devices. however, if the standard streams + // have been overwritten we create a unique device for + // them instead. + if (input) { + FS.createDevice("/dev", "stdin", input); + } else { + FS.symlink("/dev/tty", "/dev/stdin"); + } + if (output) { + FS.createDevice("/dev", "stdout", null, output); + } else { + FS.symlink("/dev/tty", "/dev/stdout"); + } + if (error) { + FS.createDevice("/dev", "stderr", null, error); + } else { + FS.symlink("/dev/tty1", "/dev/stderr"); + } + // open default streams for the stdin, stdout and stderr devices + var stdin = FS.open("/dev/stdin", 0); + var stdout = FS.open("/dev/stdout", 1); + var stderr = FS.open("/dev/stderr", 1); + }, + staticInit() { + FS.nameTable = new Array(4096); + FS.mount(MEMFS, {}, "/"); + FS.createDefaultDirectories(); + FS.createDefaultDevices(); + FS.createSpecialDirectories(); + FS.filesystems = { + "MEMFS": MEMFS + }; + }, + init(input, output, error) { + FS.initialized = true; + // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here + input ??= Module["stdin"]; + output ??= Module["stdout"]; + error ??= Module["stderr"]; + FS.createStandardStreams(input, output, error); + }, + quit() { + FS.initialized = false; + // force-flush all streams, so we get musl std streams printed out + // close all of our streams + for (var stream of FS.streams) { + if (stream) { + FS.close(stream); + } + } + }, + findObject(path, dontResolveLastLink) { + var ret = FS.analyzePath(path, dontResolveLastLink); + if (!ret.exists) { + return null; + } + return ret.object; + }, + analyzePath(path, dontResolveLastLink) { + // operate from within the context of the symlink's target + try { + var lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + path = lookup.path; + } catch (e) {} + var ret = { + isRoot: false, + exists: false, + error: 0, + name: null, + path: null, + object: null, + parentExists: false, + parentPath: null, + parentObject: null + }; + try { + var lookup = FS.lookupPath(path, { + parent: true + }); + ret.parentExists = true; + ret.parentPath = lookup.path; + ret.parentObject = lookup.node; + ret.name = PATH.basename(path); + lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + ret.exists = true; + ret.path = lookup.path; + ret.object = lookup.node; + ret.name = lookup.node.name; + ret.isRoot = lookup.path === "/"; + } catch (e) { + ret.error = e.errno; + } + return ret; + }, + createPath(parent, path, canRead, canWrite) { + parent = typeof parent == "string" ? parent : FS.getPath(parent); + var parts = path.split("/").reverse(); + while (parts.length) { + var part = parts.pop(); + if (!part) continue; + var current = PATH.join2(parent, part); + try { + FS.mkdir(current); + } catch (e) { + if (e.errno != 20) throw e; + } + parent = current; + } + return current; + }, + createFile(parent, name, properties, canRead, canWrite) { + var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name); + var mode = FS_getMode(canRead, canWrite); + return FS.create(path, mode); + }, + createDataFile(parent, name, data, canRead, canWrite, canOwn) { + var path = name; + if (parent) { + parent = typeof parent == "string" ? parent : FS.getPath(parent); + path = name ? PATH.join2(parent, name) : parent; + } + var mode = FS_getMode(canRead, canWrite); + var node = FS.create(path, mode); + if (data) { + data = FS_fileDataToTypedArray(data); + // make sure we can write to the file + FS.chmod(node, mode | 146); + var stream = FS.open(node, 577); + FS.write(stream, data, 0, data.length, 0, canOwn); + FS.close(stream); + FS.chmod(node, mode); + } + }, + createDevice(parent, name, input, output) { + var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name); + var mode = FS_getMode(!!input, !!output); + FS.createDevice.major ??= 64; + var dev = FS.makedev(FS.createDevice.major++, 0); + // Create a fake device that a set of stream ops to emulate + // the old behavior. + FS.registerDevice(dev, { + open(stream) { + stream.seekable = false; + }, + close(stream) { + // flush any pending line data + if (output?.buffer?.length) { + output(10); + } + }, + read(stream, buffer, offset, length, pos) { + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = input(); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + for (var i = 0; i < length; i++) { + try { + output(buffer[offset + i]); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + } + }); + return FS.mkdev(path, mode, dev); + }, + forceLoadFile(obj) { + if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; + if (globalThis.XMLHttpRequest) { + abort("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + } else { + // Command-line. + try { + obj.contents = readBinary(obj.url); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + }, + createLazyFile(parent, name, url, canRead, canWrite) { + // Lazy chunked Uint8Array (implements get and length from Uint8Array). + // Actual getting is abstracted away for eventual reuse. + class LazyUint8Array { + lengthKnown=false; + chunks=[]; + // Loaded chunks. Index is the chunk number + get(idx) { + if (idx > this.length - 1 || idx < 0) { + return undefined; + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = (idx / this.chunkSize) | 0; + return this.getter(chunkNum)[chunkOffset]; + } + setDataGetter(getter) { + this.getter = getter; + } + cacheLength() { + // Find length + var xhr = new XMLHttpRequest; + xhr.open("HEAD", url, false); + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + var chunkSize = 1024 * 1024; + // Chunk size in bytes + if (!hasByteServing) chunkSize = datalength; + // Function to get a range from the remote URL. + var doXHR = (from, to) => { + if (from > to) abort("invalid range (" + from + ", " + to + ") or no bytes requested!"); + if (to > datalength - 1) abort("only " + datalength + " bytes available! programmer error!"); + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + // Some hints to the browser that we want binary data. + xhr.responseType = "arraybuffer"; + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/plain; charset=x-user-defined"); + } + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array(/** @type{Array} */ (xhr.response || [])); + } + return intArrayFromString(xhr.responseText || "", true); + }; + var lazyArray = this; + lazyArray.setDataGetter(chunkNum => { + var start = chunkNum * chunkSize; + var end = (chunkNum + 1) * chunkSize - 1; + // including this byte + end = Math.min(end, datalength - 1); + // if datalength-1 is selected, this is the last block + if (typeof lazyArray.chunks[chunkNum] == "undefined") { + lazyArray.chunks[chunkNum] = doXHR(start, end); + } + if (typeof lazyArray.chunks[chunkNum] == "undefined") abort("doXHR failed!"); + return lazyArray.chunks[chunkNum]; + }); + if (usesGzip || !datalength) { + // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length + chunkSize = datalength = 1; + // this will force getter(0)/doXHR do download the whole file + datalength = this.getter(0).length; + chunkSize = datalength; + out("LazyFiles on gzip forces download of the whole file when length is accessed"); + } + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true; + } + get length() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._length; + } + get chunkSize() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._chunkSize; + } + } + if (globalThis.XMLHttpRequest) { + if (!ENVIRONMENT_IS_WORKER) abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc"); + var lazyArray = new LazyUint8Array; + var properties = { + isDevice: false, + contents: lazyArray + }; + } else { + var properties = { + isDevice: false, + url + }; + } + var node = FS.createFile(parent, name, properties, canRead, canWrite); + // This is a total hack, but I want to get this lazy file code out of the + // core of MEMFS. If we want to keep this lazy file concept I feel it should + // be its own thin LAZYFS proxying calls to MEMFS. + if (properties.contents) { + node.contents = properties.contents; + } else if (properties.url) { + node.contents = null; + node.url = properties.url; + } + // Add a function that defers querying the file size until it is asked the first time. + Object.defineProperties(node, { + usedBytes: { + get: function() { + return this.contents.length; + } + } + }); + // override each stream op with one that tries to force load the lazy file first + var stream_ops = {}; + for (const [key, fn] of Object.entries(node.stream_ops)) { + stream_ops[key] = (...args) => { + FS.forceLoadFile(node); + return fn(...args); + }; + } + function writeChunks(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= contents.length) return 0; + var size = Math.min(contents.length - position, length); + if (contents.slice) { + // normal array + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents[position + i]; + } + } else { + for (var i = 0; i < size; i++) { + // LazyUint8Array from sync binary XHR + buffer[offset + i] = contents.get(position + i); + } + } + return size; + } + // use a custom read function + stream_ops.read = (stream, buffer, offset, length, position) => { + FS.forceLoadFile(node); + return writeChunks(stream, buffer, offset, length, position); + }; + // use a custom mmap function + stream_ops.mmap = (stream, length, position, prot, flags) => { + FS.forceLoadFile(node); + var ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + writeChunks(stream, HEAP8, ptr, length, position); + return { + ptr, + allocated: true + }; + }; + node.stream_ops = stream_ops; + return node; + } +}; + +/** + * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the + * emscripten HEAP, returns a copy of that string as a Javascript String object. + * + * @param {number} ptr + * @param {number=} maxBytesToRead - An optional length that specifies the + * maximum number of bytes to read. You can omit this parameter to scan the + * string until the first 0 byte. If maxBytesToRead is passed, and the string + * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the + * string will cut short at that byte index. + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + if (!ptr) return ""; + var end = findStringEnd(HEAPU8, ptr, maxBytesToRead, ignoreNul); + return UTF8Decoder.decode(HEAPU8.subarray(ptr, end)); +}; + +var SYSCALLS = { + calculateAt(dirfd, path, allowEmpty) { + if (PATH.isAbs(path)) { + return path; + } + // relative path + var dir; + if (dirfd === -100) { + dir = FS.cwd(); + } else { + var dirstream = SYSCALLS.getStreamFromFD(dirfd); + dir = dirstream.path; + } + if (path.length == 0) { + if (!allowEmpty) { + throw new FS.ErrnoError(44); + } + return dir; + } + return dir + "/" + path; + }, + writeStat(buf, stat) { + HEAPU32[((buf) >> 2)] = stat.dev; + HEAPU32[(((buf) + (4)) >> 2)] = stat.mode; + HEAPU32[(((buf) + (8)) >> 2)] = stat.nlink; + HEAPU32[(((buf) + (12)) >> 2)] = stat.uid; + HEAPU32[(((buf) + (16)) >> 2)] = stat.gid; + HEAPU32[(((buf) + (20)) >> 2)] = stat.rdev; + (tempI64 = [ stat.size >>> 0, (tempDouble = stat.size, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (24)) >> 2)] = tempI64[0], HEAP32[(((buf) + (28)) >> 2)] = tempI64[1]); + HEAP32[(((buf) + (32)) >> 2)] = 4096; + HEAP32[(((buf) + (36)) >> 2)] = stat.blocks; + var atime = stat.atime.getTime(); + var mtime = stat.mtime.getTime(); + var ctime = stat.ctime.getTime(); + (tempI64 = [ Math.floor(atime / 1e3) >>> 0, (tempDouble = Math.floor(atime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (40)) >> 2)] = tempI64[0], HEAP32[(((buf) + (44)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (48)) >> 2)] = (atime % 1e3) * 1e3 * 1e3; + (tempI64 = [ Math.floor(mtime / 1e3) >>> 0, (tempDouble = Math.floor(mtime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (56)) >> 2)] = tempI64[0], HEAP32[(((buf) + (60)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (64)) >> 2)] = (mtime % 1e3) * 1e3 * 1e3; + (tempI64 = [ Math.floor(ctime / 1e3) >>> 0, (tempDouble = Math.floor(ctime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (72)) >> 2)] = tempI64[0], HEAP32[(((buf) + (76)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (80)) >> 2)] = (ctime % 1e3) * 1e3 * 1e3; + (tempI64 = [ stat.ino >>> 0, (tempDouble = stat.ino, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (88)) >> 2)] = tempI64[0], HEAP32[(((buf) + (92)) >> 2)] = tempI64[1]); + return 0; + }, + writeStatFs(buf, stats) { + HEAPU32[(((buf) + (4)) >> 2)] = stats.bsize; + HEAPU32[(((buf) + (60)) >> 2)] = stats.bsize; + (tempI64 = [ stats.blocks >>> 0, (tempDouble = stats.blocks, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (8)) >> 2)] = tempI64[0], HEAP32[(((buf) + (12)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.bfree >>> 0, (tempDouble = stats.bfree, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (16)) >> 2)] = tempI64[0], HEAP32[(((buf) + (20)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.bavail >>> 0, (tempDouble = stats.bavail, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (24)) >> 2)] = tempI64[0], HEAP32[(((buf) + (28)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.files >>> 0, (tempDouble = stats.files, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (32)) >> 2)] = tempI64[0], HEAP32[(((buf) + (36)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.ffree >>> 0, (tempDouble = stats.ffree, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (40)) >> 2)] = tempI64[0], HEAP32[(((buf) + (44)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (48)) >> 2)] = stats.fsid; + HEAPU32[(((buf) + (64)) >> 2)] = stats.flags; + // ST_NOSUID + HEAPU32[(((buf) + (56)) >> 2)] = stats.namelen; + }, + doMsync(addr, stream, len, flags, offset) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + if (flags & 2) { + // MAP_PRIVATE calls need not to be synced back to underlying fs + return 0; + } + var buffer = HEAPU8.slice(addr, addr + len); + FS.msync(stream, buffer, offset, len, flags); + }, + getStreamFromFD(fd) { + var stream = FS.getStreamChecked(fd); + return stream; + }, + varargs: undefined, + getStr(ptr) { + var ret = UTF8ToString(ptr); + return ret; + } +}; + +function ___syscall_dup(fd) { + try { + var old = SYSCALLS.getStreamFromFD(fd); + return FS.dupStream(old).fd; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_faccessat(dirfd, path, amode, flags) { + try { + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + if (amode & ~7) { + // need a valid mode + return -28; + } + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + if (!node) { + return -44; + } + var perms = ""; + if (amode & 4) perms += "r"; + if (amode & 2) perms += "w"; + if (amode & 1) perms += "x"; + if (perms && FS.nodePermissions(node, perms)) { + return -2; + } + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var syscallGetVarargI = () => { + // the `+` prepended here is necessary to convince the JSCompiler that varargs is indeed a number. + var ret = HEAP32[((+SYSCALLS.varargs) >> 2)]; + SYSCALLS.varargs += 4; + return ret; +}; + +var syscallGetVarargP = syscallGetVarargI; + +function ___syscall_fcntl64(fd, cmd, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(fd); + switch (cmd) { + case 0: + { + var arg = syscallGetVarargI(); + if (arg < 0) { + return -28; + } + while (FS.streams[arg]) { + arg++; + } + var newStream; + newStream = FS.dupStream(stream, arg); + return newStream.fd; + } + + case 1: + case 2: + return 0; + + // FD_CLOEXEC makes no sense for a single process. + case 3: + return stream.flags; + + case 4: + { + var arg = syscallGetVarargI(); + stream.flags |= arg; + return 0; + } + + case 12: + { + var arg = syscallGetVarargP(); + var offset = 0; + // We're always unlocked. + HEAP16[(((arg) + (offset)) >> 1)] = 2; + return 0; + } + + case 13: + case 14: + // Pretend that the locking is successful. These are process-level locks, + // and Emscripten programs are a single process. If we supported linking a + // filesystem between programs, we'd need to do more here. + // See https://github.com/emscripten-core/emscripten/issues/23697 + return 0; + } + return -28; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_fstat64(fd, buf) { + try { + return SYSCALLS.writeStat(buf, FS.fstat(fd)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var convertI32PairToI53Checked = (lo, hi) => ((hi + 2097152) >>> 0 < 4194305 - !!lo) ? (lo >>> 0) + hi * 4294967296 : NaN; + +function ___syscall_ftruncate64(fd, length_low, length_high) { + var length = convertI32PairToI53Checked(length_low, length_high); + try { + if (isNaN(length)) return -61; + FS.ftruncate(fd, length); + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_ioctl(fd, op, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(fd); + switch (op) { + case 21509: + { + if (!stream.tty) return -59; + return 0; + } + + case 21505: + { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcgets) { + var termios = stream.tty.ops.ioctl_tcgets(stream); + var argp = syscallGetVarargP(); + HEAP32[((argp) >> 2)] = termios.c_iflag || 0; + HEAP32[(((argp) + (4)) >> 2)] = termios.c_oflag || 0; + HEAP32[(((argp) + (8)) >> 2)] = termios.c_cflag || 0; + HEAP32[(((argp) + (12)) >> 2)] = termios.c_lflag || 0; + for (var i = 0; i < 32; i++) { + HEAP8[(argp + i) + (17)] = termios.c_cc[i] || 0; + } + return 0; + } + return 0; + } + + case 21510: + case 21511: + case 21512: + { + if (!stream.tty) return -59; + return 0; + } + + case 21506: + case 21507: + case 21508: + { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcsets) { + var argp = syscallGetVarargP(); + var c_iflag = HEAP32[((argp) >> 2)]; + var c_oflag = HEAP32[(((argp) + (4)) >> 2)]; + var c_cflag = HEAP32[(((argp) + (8)) >> 2)]; + var c_lflag = HEAP32[(((argp) + (12)) >> 2)]; + var c_cc = []; + for (var i = 0; i < 32; i++) { + c_cc.push(HEAP8[(argp + i) + (17)]); + } + return stream.tty.ops.ioctl_tcsets(stream.tty, op, { + c_iflag, + c_oflag, + c_cflag, + c_lflag, + c_cc + }); + } + return 0; + } + + case 21519: + { + if (!stream.tty) return -59; + var argp = syscallGetVarargP(); + HEAP32[((argp) >> 2)] = 0; + return 0; + } + + case 21520: + { + if (!stream.tty) return -59; + return -28; + } + + case 21537: + case 21531: + { + var argp = syscallGetVarargP(); + return FS.ioctl(stream, op, argp); + } + + case 21523: + { + // TODO: in theory we should write to the winsize struct that gets + // passed in, but for now musl doesn't read anything on it + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tiocgwinsz) { + var winsize = stream.tty.ops.ioctl_tiocgwinsz(stream.tty); + var argp = syscallGetVarargP(); + HEAP16[((argp) >> 1)] = winsize[0]; + HEAP16[(((argp) + (2)) >> 1)] = winsize[1]; + } + return 0; + } + + case 21524: + { + // TODO: technically, this ioctl call should change the window size. + // but, since emscripten doesn't have any concept of a terminal window + // yet, we'll just silently throw it away as we do TIOCGWINSZ + if (!stream.tty) return -59; + return 0; + } + + case 21515: + { + if (!stream.tty) return -59; + return 0; + } + + default: + return -28; + } + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_lstat64(path, buf) { + try { + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.lstat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_newfstatat(dirfd, path, buf, flags) { + try { + path = SYSCALLS.getStr(path); + var nofollow = flags & 256; + var allowEmpty = flags & 4096; + flags = flags & (~6400); + path = SYSCALLS.calculateAt(dirfd, path, allowEmpty); + return SYSCALLS.writeStat(buf, nofollow ? FS.lstat(path) : FS.stat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_openat(dirfd, path, flags, varargs) { + SYSCALLS.varargs = varargs; + try { + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + var mode = varargs ? syscallGetVarargI() : 0; + return FS.open(path, flags, mode).fd; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_stat64(path, buf) { + try { + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.stat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var __abort_js = () => abort(""); + +var __embind_register_bigint = (primitiveType, name, size, minRange, maxRange) => {}; + +var AsciiToString = ptr => { + var str = ""; + while (1) { + var ch = HEAPU8[ptr++]; + if (!ch) return str; + str += String.fromCharCode(ch); + } +}; + +var awaitingDependencies = {}; + +var registeredTypes = {}; + +var typeDependencies = {}; + +var BindingError = class BindingError extends Error { + constructor(message) { + super(message); + this.name = "BindingError"; + } +}; + +var throwBindingError = message => { + throw new BindingError(message); +}; + +/** @param {Object=} options */ function sharedRegisterType(rawType, registeredInstance, options = {}) { + var name = registeredInstance.name; + if (!rawType) { + throwBindingError(`type "${name}" must have a positive integer typeid pointer`); + } + if (registeredTypes.hasOwnProperty(rawType)) { + if (options.ignoreDuplicateRegistrations) { + return; + } else { + throwBindingError(`Cannot register type '${name}' twice`); + } + } + registeredTypes[rawType] = registeredInstance; + delete typeDependencies[rawType]; + if (awaitingDependencies.hasOwnProperty(rawType)) { + var callbacks = awaitingDependencies[rawType]; + delete awaitingDependencies[rawType]; + callbacks.forEach(cb => cb()); + } +} + +/** @param {Object=} options */ function registerType(rawType, registeredInstance, options = {}) { + return sharedRegisterType(rawType, registeredInstance, options); +} + +/** @suppress {globalThis} */ var __embind_register_bool = (rawType, name, trueValue, falseValue) => { + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: function(wt) { + // ambiguous emscripten ABI: sometimes return values are + // true or false, and sometimes integers (0 or 1) + return !!wt; + }, + toWireType: function(destructors, o) { + return o ? trueValue : falseValue; + }, + readValueFromPointer: function(pointer) { + return this.fromWireType(HEAPU8[pointer]); + }, + destructorFunction: null + }); +}; + +var emval_freelist = []; + +var emval_handles = [ 0, 1, , 1, null, 1, true, 1, false, 1 ]; + +var __emval_decref = handle => { + if (handle > 9 && 0 === --emval_handles[handle + 1]) { + var value = emval_handles[handle]; + emval_handles[handle] = undefined; + emval_freelist.push(handle); + } +}; + +var Emval = { + toValue: handle => { + if (!handle) { + throwBindingError(`Cannot use deleted val. handle = ${handle}`); + } + return emval_handles[handle]; + }, + toHandle: value => { + switch (value) { + case undefined: + return 2; + + case null: + return 4; + + case true: + return 6; + + case false: + return 8; + + default: + { + const handle = emval_freelist.pop() || emval_handles.length; + emval_handles[handle] = value; + emval_handles[handle + 1] = 1; + return handle; + } + } + } +}; + +/** @suppress {globalThis} */ function readPointer(pointer) { + return this.fromWireType(HEAPU32[((pointer) >> 2)]); +} + +var EmValType = { + name: "emscripten::val", + fromWireType: handle => { + var rv = Emval.toValue(handle); + __emval_decref(handle); + return rv; + }, + toWireType: (destructors, value) => Emval.toHandle(value), + readValueFromPointer: readPointer, + destructorFunction: null +}; + +var __embind_register_emval = rawType => registerType(rawType, EmValType); + +var floatReadValueFromPointer = (name, width) => { + switch (width) { + case 4: + return function(pointer) { + return this.fromWireType(HEAPF32[((pointer) >> 2)]); + }; + + case 8: + return function(pointer) { + return this.fromWireType(HEAPF64[((pointer) >> 3)]); + }; + + default: + throw new TypeError(`invalid float width (${width}): ${name}`); + } +}; + +var __embind_register_float = (rawType, name, size) => { + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: value => value, + toWireType: (destructors, value) => value, + readValueFromPointer: floatReadValueFromPointer(name, size), + destructorFunction: null + }); +}; + +var integerReadValueFromPointer = (name, width, signed) => { + // integers are quite common, so generate very specialized functions + switch (width) { + case 1: + return signed ? pointer => HEAP8[pointer] : pointer => HEAPU8[pointer]; + + case 2: + return signed ? pointer => HEAP16[((pointer) >> 1)] : pointer => HEAPU16[((pointer) >> 1)]; + + case 4: + return signed ? pointer => HEAP32[((pointer) >> 2)] : pointer => HEAPU32[((pointer) >> 2)]; + + default: + throw new TypeError(`invalid integer width (${width}): ${name}`); + } +}; + +/** @suppress {globalThis} */ var __embind_register_integer = (primitiveType, name, size, minRange, maxRange) => { + name = AsciiToString(name); + const isUnsignedType = minRange === 0; + let fromWireType = value => value; + if (isUnsignedType) { + var bitshift = 32 - 8 * size; + fromWireType = value => (value << bitshift) >>> bitshift; + maxRange = fromWireType(maxRange); + } + registerType(primitiveType, { + name, + fromWireType, + toWireType: (destructors, value) => value, + readValueFromPointer: integerReadValueFromPointer(name, size, minRange !== 0), + destructorFunction: null + }); +}; + +var __embind_register_memory_view = (rawType, dataTypeIndex, name) => { + var typeMapping = [ Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array ]; + var TA = typeMapping[dataTypeIndex]; + function decodeMemoryView(handle) { + var size = HEAPU32[((handle) >> 2)]; + var data = HEAPU32[(((handle) + (4)) >> 2)]; + return new TA(HEAP8.buffer, data, size); + } + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: decodeMemoryView, + readValueFromPointer: decodeMemoryView + }, { + ignoreDuplicateRegistrations: true + }); +}; + +var stringToUTF8 = (str, outPtr, maxBytesToWrite) => stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + +var __embind_register_std_string = (rawType, name) => { + name = AsciiToString(name); + var stdStringIsUTF8 = true; + registerType(rawType, { + name, + // For some method names we use string keys here since they are part of + // the public/external API and/or used by the runtime-generated code. + fromWireType(value) { + var length = HEAPU32[((value) >> 2)]; + var payload = value + 4; + var str; + if (stdStringIsUTF8) { + str = UTF8ToString(payload, length, true); + } else { + str = ""; + for (var i = 0; i < length; ++i) { + str += String.fromCharCode(HEAPU8[payload + i]); + } + } + _free(value); + return str; + }, + toWireType(destructors, value) { + if (value instanceof ArrayBuffer) { + value = new Uint8Array(value); + } + var length; + var valueIsOfTypeString = (typeof value == "string"); + // We accept `string` or array views with single byte elements + if (!(valueIsOfTypeString || (ArrayBuffer.isView(value) && value.BYTES_PER_ELEMENT == 1))) { + throwBindingError("Cannot pass non-string to std::string"); + } + if (stdStringIsUTF8 && valueIsOfTypeString) { + length = lengthBytesUTF8(value); + } else { + length = value.length; + } + // assumes POINTER_SIZE alignment + var base = _malloc(4 + length + 1); + var ptr = base + 4; + HEAPU32[((base) >> 2)] = length; + if (valueIsOfTypeString) { + if (stdStringIsUTF8) { + stringToUTF8(value, ptr, length + 1); + } else { + for (var i = 0; i < length; ++i) { + var charCode = value.charCodeAt(i); + if (charCode > 255) { + _free(base); + throwBindingError("String has UTF-16 code units that do not fit in 8 bits"); + } + HEAPU8[ptr + i] = charCode; + } + } + } else { + HEAPU8.set(value, ptr); + } + if (destructors !== null) { + destructors.push(_free, base); + } + return base; + }, + readValueFromPointer: readPointer, + destructorFunction(ptr) { + _free(ptr); + } + }); +}; + +var UTF16Decoder = new TextDecoder("utf-16le"); + +var UTF16ToString = (ptr, maxBytesToRead, ignoreNul) => { + var idx = ((ptr) >> 1); + var endIdx = findStringEnd(HEAPU16, idx, maxBytesToRead / 2, ignoreNul); + return UTF16Decoder.decode(HEAPU16.subarray(idx, endIdx)); +}; + +var stringToUTF16 = (str, outPtr, maxBytesToWrite) => { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + maxBytesToWrite ??= 2147483647; + if (maxBytesToWrite < 2) return 0; + maxBytesToWrite -= 2; + // Null terminator. + var startPtr = outPtr; + var numCharsToWrite = (maxBytesToWrite < str.length * 2) ? (maxBytesToWrite / 2) : str.length; + for (var i = 0; i < numCharsToWrite; ++i) { + // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. + var codeUnit = str.charCodeAt(i); + // possibly a lead surrogate + HEAP16[((outPtr) >> 1)] = codeUnit; + outPtr += 2; + } + // Null-terminate the pointer to the HEAP. + HEAP16[((outPtr) >> 1)] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF16 = str => str.length * 2; + +var UTF32ToString = (ptr, maxBytesToRead, ignoreNul) => { + var str = ""; + var startIdx = ((ptr) >> 2); + // If maxBytesToRead is not passed explicitly, it will be undefined, and this + // will always evaluate to true. This saves on code size. + for (var i = 0; !(i >= maxBytesToRead / 4); i++) { + var utf32 = HEAPU32[startIdx + i]; + if (!utf32 && !ignoreNul) break; + str += String.fromCodePoint(utf32); + } + return str; +}; + +var stringToUTF32 = (str, outPtr, maxBytesToWrite) => { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + maxBytesToWrite ??= 2147483647; + if (maxBytesToWrite < 4) return 0; + var startPtr = outPtr; + var endPtr = startPtr + maxBytesToWrite - 4; + for (var i = 0; i < str.length; ++i) { + var codePoint = str.codePointAt(i); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + if (codePoint > 65535) { + i++; + } + HEAP32[((outPtr) >> 2)] = codePoint; + outPtr += 4; + if (outPtr + 4 > endPtr) break; + } + // Null-terminate the pointer to the HEAP. + HEAP32[((outPtr) >> 2)] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF32 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var codePoint = str.codePointAt(i); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + if (codePoint > 65535) { + i++; + } + len += 4; + } + return len; +}; + +var __embind_register_std_wstring = (rawType, charSize, name) => { + name = AsciiToString(name); + var decodeString, encodeString, lengthBytesUTF; + if (charSize === 2) { + decodeString = UTF16ToString; + encodeString = stringToUTF16; + lengthBytesUTF = lengthBytesUTF16; + } else { + decodeString = UTF32ToString; + encodeString = stringToUTF32; + lengthBytesUTF = lengthBytesUTF32; + } + registerType(rawType, { + name, + fromWireType: value => { + // Code mostly taken from _embind_register_std_string fromWireType + var length = HEAPU32[((value) >> 2)]; + var str = decodeString(value + 4, length * charSize, true); + _free(value); + return str; + }, + toWireType: (destructors, value) => { + if (!(typeof value == "string")) { + throwBindingError(`Cannot pass non-string to C++ string type ${name}`); + } + // assumes POINTER_SIZE alignment + var length = lengthBytesUTF(value); + var ptr = _malloc(4 + length + charSize); + HEAPU32[((ptr) >> 2)] = length / charSize; + encodeString(value, ptr + 4, length + charSize); + if (destructors !== null) { + destructors.push(_free, ptr); + } + return ptr; + }, + readValueFromPointer: readPointer, + destructorFunction(ptr) { + _free(ptr); + } + }); +}; + +var __embind_register_void = (rawType, name) => { + name = AsciiToString(name); + registerType(rawType, { + isVoid: true, + // void return values can be optimized out sometimes + name, + fromWireType: () => undefined, + // TODO: assert if anything else is given? + toWireType: (destructors, o) => undefined + }); +}; + +var emval_methodCallers = []; + +var emval_addMethodCaller = caller => { + var id = emval_methodCallers.length; + emval_methodCallers.push(caller); + return id; +}; + +var getTypeName = type => { + var ptr = ___getTypeName(type); + var rv = AsciiToString(ptr); + _free(ptr); + return rv; +}; + +var requireRegisteredType = (rawType, humanName) => { + var impl = registeredTypes[rawType]; + if (undefined === impl) { + throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`); + } + return impl; +}; + +var emval_lookupTypes = (argCount, argTypes) => { + var a = new Array(argCount); + for (var i = 0; i < argCount; ++i) { + a[i] = requireRegisteredType(HEAPU32[(((argTypes) + (i * 4)) >> 2)], `parameter ${i}`); + } + return a; +}; + +var createNamedFunction = (name, func) => Object.defineProperty(func, "name", { + value: name +}); + +var emval_returnValue = (toReturnWire, destructorsRef, handle) => { + var destructors = []; + var result = toReturnWire(destructors, handle); + if (destructors.length) { + // void, primitives and any other types w/o destructors don't need to allocate a handle + HEAPU32[((destructorsRef) >> 2)] = Emval.toHandle(destructors); + } + return result; +}; + +var emval_symbols = {}; + +var getStringOrSymbol = address => { + var symbol = emval_symbols[address]; + if (symbol === undefined) { + return AsciiToString(address); + } + return symbol; +}; + +var __emval_create_invoker = (argCount, argTypesPtr, kind) => { + var GenericWireTypeSize = 8; + var [retType, ...argTypes] = emval_lookupTypes(argCount, argTypesPtr); + var toReturnWire = retType.toWireType.bind(retType); + var argFromPtr = argTypes.map(type => type.readValueFromPointer.bind(type)); + argCount--; + // remove the extracted return type + var argN = new Array(argCount); + var invokerFunction = (handle, methodName, destructorsRef, args) => { + var offset = 0; + for (var i = 0; i < argCount; ++i) { + argN[i] = argFromPtr[i](args + offset); + offset += GenericWireTypeSize; + } + var rv; + switch (kind) { + case 0: + rv = Emval.toValue(handle).apply(null, argN); + break; + + case 2: + rv = Reflect.construct(Emval.toValue(handle), argN); + break; + + case 3: + // no-op, just return the argument + rv = argN[0]; + break; + + case 1: + rv = Emval.toValue(handle)[getStringOrSymbol(methodName)](...argN); + break; + } + return emval_returnValue(toReturnWire, destructorsRef, rv); + }; + var functionName = `methodCaller<(${argTypes.map(t => t.name)}) => ${retType.name}>`; + return emval_addMethodCaller(createNamedFunction(functionName, invokerFunction)); +}; + +var __emval_get_global = name => { + if (!name) { + return Emval.toHandle(globalThis); + } + name = getStringOrSymbol(name); + return Emval.toHandle(globalThis[name]); +}; + +var __emval_get_property = (handle, key) => { + handle = Emval.toValue(handle); + key = Emval.toValue(key); + return Emval.toHandle(handle[key]); +}; + +var __emval_incref = handle => { + if (handle > 9) { + emval_handles[handle + 1] += 1; + } +}; + +var __emval_instanceof = (object, constructor) => { + object = Emval.toValue(object); + constructor = Emval.toValue(constructor); + return object instanceof constructor; +}; + +var __emval_invoke = (caller, handle, methodName, destructorsRef, args) => emval_methodCallers[caller](handle, methodName, destructorsRef, args); + +var __emval_new_cstring = v => Emval.toHandle(getStringOrSymbol(v)); + +var runDestructors = destructors => { + while (destructors.length) { + var ptr = destructors.pop(); + var del = destructors.pop(); + del(ptr); + } +}; + +var __emval_run_destructors = handle => { + var destructors = Emval.toValue(handle); + runDestructors(destructors); + __emval_decref(handle); +}; + +var __emval_set_property = (handle, key, value) => { + handle = Emval.toValue(handle); + key = Emval.toValue(key); + value = Emval.toValue(value); + handle[key] = value; +}; + +var __emval_typeof = handle => { + handle = Emval.toValue(handle); + return Emval.toHandle(typeof handle); +}; + +function __gmtime_js(time_low, time_high, tmPtr) { + var time = convertI32PairToI53Checked(time_low, time_high); + var date = new Date(time * 1e3); + HEAP32[((tmPtr) >> 2)] = date.getUTCSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getUTCMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getUTCHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getUTCDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getUTCMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getUTCFullYear() - 1900; + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getUTCDay(); + var start = Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0); + var yday = ((date.getTime() - start) / (1e3 * 60 * 60 * 24)) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; +} + +var isLeapYear = year => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + +var MONTH_DAYS_LEAP_CUMULATIVE = [ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 ]; + +var MONTH_DAYS_REGULAR_CUMULATIVE = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ]; + +var ydayFromDate = date => { + var leap = isLeapYear(date.getFullYear()); + var monthDaysCumulative = (leap ? MONTH_DAYS_LEAP_CUMULATIVE : MONTH_DAYS_REGULAR_CUMULATIVE); + var yday = monthDaysCumulative[date.getMonth()] + date.getDate() - 1; + // -1 since it's days since Jan 1 + return yday; +}; + +function __localtime_js(time_low, time_high, tmPtr) { + var time = convertI32PairToI53Checked(time_low, time_high); + var date = new Date(time * 1e3); + HEAP32[((tmPtr) >> 2)] = date.getSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getFullYear() - 1900; + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; + HEAP32[(((tmPtr) + (36)) >> 2)] = -(date.getTimezoneOffset() * 60); + // Attention: DST is in December in South, and some regions don't have DST at all. + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0; + HEAP32[(((tmPtr) + (32)) >> 2)] = dst; +} + +var setTempRet0 = val => __emscripten_tempret_set(val); + +var __mktime_js = function(tmPtr) { + var ret = (() => { + var date = new Date(HEAP32[(((tmPtr) + (20)) >> 2)] + 1900, HEAP32[(((tmPtr) + (16)) >> 2)], HEAP32[(((tmPtr) + (12)) >> 2)], HEAP32[(((tmPtr) + (8)) >> 2)], HEAP32[(((tmPtr) + (4)) >> 2)], HEAP32[((tmPtr) >> 2)], 0); + // There's an ambiguous hour when the time goes back; the tm_isdst field is + // used to disambiguate it. Date() basically guesses, so we fix it up if it + // guessed wrong, or fill in tm_isdst with the guess if it's -1. + var dst = HEAP32[(((tmPtr) + (32)) >> 2)]; + var guessedOffset = date.getTimezoneOffset(); + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dstOffset = Math.min(winterOffset, summerOffset); + // DST is in December in South + if (dst < 0) { + // Attention: some regions don't have DST at all. + HEAP32[(((tmPtr) + (32)) >> 2)] = Number(summerOffset != winterOffset && dstOffset == guessedOffset); + } else if ((dst > 0) != (dstOffset == guessedOffset)) { + var nonDstOffset = Math.max(winterOffset, summerOffset); + var trueOffset = dst > 0 ? dstOffset : nonDstOffset; + // Don't try setMinutes(date.getMinutes() + ...) -- it's messed up. + date.setTime(date.getTime() + (trueOffset - guessedOffset) * 6e4); + } + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; + // To match expected behavior, update fields from date + HEAP32[((tmPtr) >> 2)] = date.getSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getYear(); + var timeMs = date.getTime(); + if (isNaN(timeMs)) { + return -1; + } + // Return time in microseconds + return timeMs / 1e3; + })(); + return (setTempRet0((tempDouble = ret, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0)), + ret >>> 0); +}; + +function __mmap_js(len, prot, flags, fd, offset_low, offset_high, allocated, addr) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var res = FS.mmap(stream, len, offset, prot, flags); + var ptr = res.ptr; + HEAP32[((allocated) >> 2)] = res.allocated; + HEAPU32[((addr) >> 2)] = ptr; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function __munmap_js(addr, len, prot, flags, fd, offset_low, offset_high) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + var stream = SYSCALLS.getStreamFromFD(fd); + if (prot & 2) { + SYSCALLS.doMsync(addr, stream, len, flags, offset); + } + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var __tzset_js = (timezone, daylight, std_name, dst_name) => { + // TODO: Use (malleable) environment variables instead of system settings. + var currentYear = (new Date).getFullYear(); + var winter = new Date(currentYear, 0, 1); + var summer = new Date(currentYear, 6, 1); + var winterOffset = winter.getTimezoneOffset(); + var summerOffset = summer.getTimezoneOffset(); + // Local standard timezone offset. Local standard time is not adjusted for + // daylight savings. This code uses the fact that getTimezoneOffset returns + // a greater value during Standard Time versus Daylight Saving Time (DST). + // Thus it determines the expected output during Standard Time, and it + // compares whether the output of the given date the same (Standard) or less + // (DST). + var stdTimezoneOffset = Math.max(winterOffset, summerOffset); + // timezone is specified as seconds west of UTC ("The external variable + // `timezone` shall be set to the difference, in seconds, between + // Coordinated Universal Time (UTC) and local standard time."), the same + // as returned by stdTimezoneOffset. + // See http://pubs.opengroup.org/onlinepubs/009695399/functions/tzset.html + HEAPU32[((timezone) >> 2)] = stdTimezoneOffset * 60; + HEAP32[((daylight) >> 2)] = Number(winterOffset != summerOffset); + var extractZone = timezoneOffset => { + // Why inverse sign? + // Read here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset + var sign = timezoneOffset >= 0 ? "-" : "+"; + var absOffset = Math.abs(timezoneOffset); + var hours = String(Math.floor(absOffset / 60)).padStart(2, "0"); + var minutes = String(absOffset % 60).padStart(2, "0"); + return `UTC${sign}${hours}${minutes}`; + }; + var winterName = extractZone(winterOffset); + var summerName = extractZone(summerOffset); + if (summerOffset < winterOffset) { + // Northern hemisphere + stringToUTF8(winterName, std_name, 17); + stringToUTF8(summerName, dst_name, 17); + } else { + stringToUTF8(winterName, dst_name, 17); + stringToUTF8(summerName, std_name, 17); + } +}; + +var _emscripten_get_now = () => performance.now(); + +var _emscripten_date_now = () => Date.now(); + +var nowIsMonotonic = 1; + +var checkWasiClock = clock_id => clock_id >= 0 && clock_id <= 3; + +function _clock_time_get(clk_id, ignored_precision_low, ignored_precision_high, ptime) { + var ignored_precision = convertI32PairToI53Checked(ignored_precision_low, ignored_precision_high); + if (!checkWasiClock(clk_id)) { + return 28; + } + var now; + // all wasi clocks but realtime are monotonic + if (clk_id === 0) { + now = _emscripten_date_now(); + } else if (nowIsMonotonic) { + now = _emscripten_get_now(); + } else { + return 52; + } + // "now" is in ms, and wasi times are in ns. + var nsec = Math.round(now * 1e3 * 1e3); + (tempI64 = [ nsec >>> 0, (tempDouble = nsec, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[((ptime) >> 2)] = tempI64[0], HEAP32[(((ptime) + (4)) >> 2)] = tempI64[1]); + return 0; +} + +var readEmAsmArgsArray = []; + +var readEmAsmArgs = (sigPtr, buf) => { + readEmAsmArgsArray.length = 0; + var ch; + // Most arguments are i32s, so shift the buffer pointer so it is a plain + // index into HEAP32. + while (ch = HEAPU8[sigPtr++]) { + // Floats are always passed as doubles, so all types except for 'i' + // are 8 bytes and require alignment. + var wide = (ch != 105); + wide &= (ch != 112); + buf += wide && (buf % 8) ? 4 : 0; + readEmAsmArgsArray.push(// Special case for pointers under wasm64 or CAN_ADDRESS_2GB mode. + ch == 112 ? HEAPU32[((buf) >> 2)] : ch == 105 ? HEAP32[((buf) >> 2)] : HEAPF64[((buf) >> 3)]); + buf += wide ? 8 : 4; + } + return readEmAsmArgsArray; +}; + +var runEmAsmFunction = (code, sigPtr, argbuf) => { + var args = readEmAsmArgs(sigPtr, argbuf); + return ASM_CONSTS[code](...args); +}; + +var _emscripten_asm_const_int = (code, sigPtr, argbuf) => runEmAsmFunction(code, sigPtr, argbuf); + +var _emscripten_asm_const_ptr = (code, sigPtr, argbuf) => runEmAsmFunction(code, sigPtr, argbuf); + +var _emscripten_errn = (str, len) => err(UTF8ToString(str, len)); + +var getHeapMax = () => // Stay one Wasm page short of 4GB: while e.g. Chrome is able to allocate +// full 4GB Wasm memories, the size will wrap back to 0 bytes in Wasm side +// for any code that deals with heap sizes, which would require special +// casing all heap size related code to treat 0 specially. +2147483648; + +var _emscripten_get_heap_max = () => getHeapMax(); + +var _emscripten_has_asyncify = () => 0; + +var _emscripten_outn = (str, len) => out(UTF8ToString(str, len)); + +var UNWIND_CACHE = {}; + +var stringToNewUTF8 = str => { + var size = lengthBytesUTF8(str) + 1; + var ret = _malloc(size); + if (ret) stringToUTF8(str, ret, size); + return ret; +}; + +/** @returns {number} */ var convertFrameToPC = frame => { + var match; + if (match = /\bwasm-function\[\d+\]:(0x[0-9a-f]+)/.exec(frame)) { + // Wasm engines give the binary offset directly, so we use that as return address + return +match[1]; + } else if (match = /:(\d+):\d+(?:\)|$)/.exec(frame)) { + // If we are in js, we can use the js line number as the "return address". + // This should work for wasm2js. We tag the high bit to distinguish this + // from wasm addresses. + return 2147483648 | +match[1]; + } + // return 0 if we can't find any + return 0; +}; + +var saveInUnwindCache = callstack => { + for (var line of callstack) { + var pc = convertFrameToPC(line); + if (pc) { + UNWIND_CACHE[pc] = line; + } + } +}; + +var jsStackTrace = () => (new Error).stack.toString(); + +var _emscripten_stack_snapshot = () => { + var callstack = jsStackTrace().split("\n"); + if (callstack[0] == "Error") { + callstack.shift(); + } + saveInUnwindCache(callstack); + // Caches the stack snapshot so that emscripten_stack_unwind_buffer() can + // unwind from this spot. + UNWIND_CACHE.last_addr = convertFrameToPC(callstack[3]); + UNWIND_CACHE.last_stack = callstack; + return UNWIND_CACHE.last_addr; +}; + +var _emscripten_pc_get_function = pc => { + var frame = UNWIND_CACHE[pc]; + if (!frame) return 0; + var name; + var match; + // First try to match foo.wasm.sym files explcitly. e.g. + // at test_return_address.wasm.main (wasm://wasm/test_return_address.wasm-0012cc2a:wasm-function[26]:0x9f3 + // Then match JS symbols which don't include that module name: + // at invokeEntryPoint (.../test_return_address.js:1500:42) + // Finally match firefox format: + // Object._main@http://server.com:4324:12' + if (match = /^\s+at .*\.wasm\.(.*) \(.*\)$/.exec(frame)) { + name = match[1]; + } else if (match = /^\s+at (.*) \(.*\)$/.exec(frame)) { + name = match[1]; + } else if (match = /^(.+?)@/.exec(frame)) { + name = match[1]; + } else { + return 0; + } + _free(_emscripten_pc_get_function.ret ?? 0); + _emscripten_pc_get_function.ret = stringToNewUTF8(name); + return _emscripten_pc_get_function.ret; +}; + +var growMemory = size => { + var oldHeapSize = wasmMemory.buffer.byteLength; + var pages = ((size - oldHeapSize + 65535) / 65536) | 0; + try { + // round size grow request up to wasm page size (fixed 64KB per spec) + wasmMemory.grow(pages); + // .grow() takes a delta compared to the previous size + updateMemoryViews(); + return 1; + } catch (e) {} +}; + +var _emscripten_resize_heap = requestedSize => { + var oldSize = HEAPU8.length; + // With CAN_ADDRESS_2GB or MEMORY64, pointers are already unsigned. + requestedSize >>>= 0; + // With multithreaded builds, races can happen (another thread might increase the size + // in between), so return a failure, and let the caller retry. + // Memory resize rules: + // 1. Always increase heap size to at least the requested size, rounded up + // to next page multiple. + // 2a. If MEMORY_GROWTH_LINEAR_STEP == -1, excessively resize the heap + // geometrically: increase the heap size according to + // MEMORY_GROWTH_GEOMETRIC_STEP factor (default +20%), At most + // overreserve by MEMORY_GROWTH_GEOMETRIC_CAP bytes (default 96MB). + // 2b. If MEMORY_GROWTH_LINEAR_STEP != -1, excessively resize the heap + // linearly: increase the heap size by at least + // MEMORY_GROWTH_LINEAR_STEP bytes. + // 3. Max size for the heap is capped at 2048MB-WASM_PAGE_SIZE, or by + // MAXIMUM_MEMORY, or by ASAN limit, depending on which is smallest + // 4. If we were unable to allocate as much memory, it may be due to + // over-eager decision to excessively reserve due to (3) above. + // Hence if an allocation fails, cut down on the amount of excess + // growth, in an attempt to succeed to perform a smaller allocation. + // A limit is set for how much we can grow. We should not exceed that + // (the wasm binary specifies it, so if we tried, we'd fail anyhow). + var maxHeapSize = getHeapMax(); + if (requestedSize > maxHeapSize) { + return false; + } + // Loop through potential heap size increases. If we attempt a too eager + // reservation that fails, cut down on the attempted size and reserve a + // smaller bump instead. (max 3 times, chosen somewhat arbitrarily) + for (var cutDown = 1; cutDown <= 4; cutDown *= 2) { + var overGrownHeapSize = oldSize * (1 + .2 / cutDown); + // ensure geometric growth + // but limit overreserving (default to capping at +96MB overgrowth at most) + overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296); + var newSize = Math.min(maxHeapSize, alignMemory(Math.max(requestedSize, overGrownHeapSize), 65536)); + var replacement = growMemory(newSize); + if (replacement) { + return true; + } + } + return false; +}; + +var _emscripten_stack_unwind_buffer = (addr, buffer, count) => { + var stack; + if (UNWIND_CACHE.last_addr == addr) { + stack = UNWIND_CACHE.last_stack; + } else { + stack = jsStackTrace().split("\n"); + if (stack[0] == "Error") { + stack.shift(); + } + saveInUnwindCache(stack); + } + var offset = 3; + while (stack[offset] && convertFrameToPC(stack[offset]) != addr) { + ++offset; + } + for (var i = 0; i < count && stack[i + offset]; ++i) { + HEAP32[(((buffer) + (i * 4)) >> 2)] = convertFrameToPC(stack[i + offset]); + } + return i; +}; + +var GLctx; + +var webgl_enable_ANGLE_instanced_arrays = ctx => { + // Extension available in WebGL 1 from Firefox 26 and Google Chrome 30 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("ANGLE_instanced_arrays"); + // Because this extension is a core function in WebGL 2, assign the extension entry points in place of + // where the core functions will reside in WebGL 2. This way the calling code can call these without + // having to dynamically branch depending if running against WebGL 1 or WebGL 2. + if (ext) { + ctx["vertexAttribDivisor"] = (index, divisor) => ext["vertexAttribDivisorANGLE"](index, divisor); + ctx["drawArraysInstanced"] = (mode, first, count, primcount) => ext["drawArraysInstancedANGLE"](mode, first, count, primcount); + ctx["drawElementsInstanced"] = (mode, count, type, indices, primcount) => ext["drawElementsInstancedANGLE"](mode, count, type, indices, primcount); + return 1; + } +}; + +var webgl_enable_OES_vertex_array_object = ctx => { + // Extension available in WebGL 1 from Firefox 25 and WebKit 536.28/desktop Safari 6.0.3 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("OES_vertex_array_object"); + if (ext) { + ctx["createVertexArray"] = () => ext["createVertexArrayOES"](); + ctx["deleteVertexArray"] = vao => ext["deleteVertexArrayOES"](vao); + ctx["bindVertexArray"] = vao => ext["bindVertexArrayOES"](vao); + ctx["isVertexArray"] = vao => ext["isVertexArrayOES"](vao); + return 1; + } +}; + +var webgl_enable_WEBGL_draw_buffers = ctx => { + // Extension available in WebGL 1 from Firefox 28 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("WEBGL_draw_buffers"); + if (ext) { + ctx["drawBuffers"] = (n, bufs) => ext["drawBuffersWEBGL"](n, bufs); + return 1; + } +}; + +var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance = ctx => // Closure is expected to be allowed to minify the '.dibvbi' property, so not accessing it quoted. +!!(ctx.dibvbi = ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance")); + +var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance = ctx => !!(ctx.mdibvbi = ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance")); + +var webgl_enable_EXT_polygon_offset_clamp = ctx => !!(ctx.extPolygonOffsetClamp = ctx.getExtension("EXT_polygon_offset_clamp")); + +var webgl_enable_EXT_clip_control = ctx => !!(ctx.extClipControl = ctx.getExtension("EXT_clip_control")); + +var webgl_enable_WEBGL_polygon_mode = ctx => !!(ctx.webglPolygonMode = ctx.getExtension("WEBGL_polygon_mode")); + +var webgl_enable_WEBGL_multi_draw = ctx => // Closure is expected to be allowed to minify the '.multiDrawWebgl' property, so not accessing it quoted. +!!(ctx.multiDrawWebgl = ctx.getExtension("WEBGL_multi_draw")); + +var getEmscriptenSupportedExtensions = ctx => { + // Restrict the list of advertised extensions to those that we actually + // support. + var supportedExtensions = [ // WebGL 1 extensions + "ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_disjoint_timer_query", "EXT_frag_depth", "EXT_shader_texture_lod", "EXT_sRGB", "OES_element_index_uint", "OES_fbo_render_mipmap", "OES_standard_derivatives", "OES_texture_float", "OES_texture_half_float", "OES_texture_half_float_linear", "OES_vertex_array_object", "WEBGL_color_buffer_float", "WEBGL_depth_texture", "WEBGL_draw_buffers", // WebGL 2 extensions + "EXT_color_buffer_float", "EXT_conservative_depth", "EXT_disjoint_timer_query_webgl2", "EXT_texture_norm16", "NV_shader_noperspective_interpolation", "WEBGL_clip_cull_distance", // WebGL 1 and WebGL 2 extensions + "EXT_clip_control", "EXT_color_buffer_half_float", "EXT_depth_clamp", "EXT_float_blend", "EXT_polygon_offset_clamp", "EXT_texture_compression_bptc", "EXT_texture_compression_rgtc", "EXT_texture_filter_anisotropic", "KHR_parallel_shader_compile", "OES_texture_float_linear", "WEBGL_blend_func_extended", "WEBGL_compressed_texture_astc", "WEBGL_compressed_texture_etc", "WEBGL_compressed_texture_etc1", "WEBGL_compressed_texture_s3tc", "WEBGL_compressed_texture_s3tc_srgb", "WEBGL_debug_renderer_info", "WEBGL_debug_shaders", "WEBGL_lose_context", "WEBGL_multi_draw", "WEBGL_polygon_mode" ]; + // .getSupportedExtensions() can return null if context is lost, so coerce to empty array. + return (ctx.getSupportedExtensions() || []).filter(ext => supportedExtensions.includes(ext)); +}; + +var registerPreMainLoop = f => { + // Does nothing unless $MainLoop is included/used. + typeof MainLoop != "undefined" && MainLoop.preMainLoop.push(f); +}; + +var GL = { + counter: 1, + buffers: [], + mappedBuffers: {}, + programs: [], + framebuffers: [], + renderbuffers: [], + textures: [], + shaders: [], + vaos: [], + contexts: [], + offscreenCanvases: {}, + queries: [], + samplers: [], + transformFeedbacks: [], + syncs: [], + byteSizeByTypeRoot: 5120, + byteSizeByType: [ 1, 1, 2, 2, 4, 4, 4, 2, 3, 4, 8 ], + stringCache: {}, + stringiCache: {}, + unpackAlignment: 4, + unpackRowLength: 0, + recordError: errorCode => { + if (!GL.lastError) { + GL.lastError = errorCode; + } + }, + getNewId: table => { + var ret = GL.counter++; + for (var i = table.length; i < ret; i++) { + table[i] = null; + } + // Skip over any non-null elements that might have been created by + // glBindBuffer. + while (table[ret]) { + ret = GL.counter++; + } + return ret; + }, + genObject: (n, buffers, createFunction, objectTable) => { + for (var i = 0; i < n; i++) { + var buffer = GLctx[createFunction](); + var id = buffer && GL.getNewId(objectTable); + if (buffer) { + buffer.name = id; + objectTable[id] = buffer; + } else { + GL.recordError(1282); + } + HEAP32[(((buffers) + (i * 4)) >> 2)] = id; + } + }, + MAX_TEMP_BUFFER_SIZE: 2097152, + numTempVertexBuffersPerSize: 64, + log2ceilLookup: i => 32 - Math.clz32(i === 0 ? 0 : i - 1), + generateTempBuffers: (quads, context) => { + var largestIndex = GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE); + context.tempVertexBufferCounters1 = []; + context.tempVertexBufferCounters2 = []; + context.tempVertexBufferCounters1.length = context.tempVertexBufferCounters2.length = largestIndex + 1; + context.tempVertexBuffers1 = []; + context.tempVertexBuffers2 = []; + context.tempVertexBuffers1.length = context.tempVertexBuffers2.length = largestIndex + 1; + context.tempIndexBuffers = []; + context.tempIndexBuffers.length = largestIndex + 1; + for (var i = 0; i <= largestIndex; ++i) { + context.tempIndexBuffers[i] = null; + // Created on-demand + context.tempVertexBufferCounters1[i] = context.tempVertexBufferCounters2[i] = 0; + var ringbufferLength = GL.numTempVertexBuffersPerSize; + context.tempVertexBuffers1[i] = []; + context.tempVertexBuffers2[i] = []; + var ringbuffer1 = context.tempVertexBuffers1[i]; + var ringbuffer2 = context.tempVertexBuffers2[i]; + ringbuffer1.length = ringbuffer2.length = ringbufferLength; + for (var j = 0; j < ringbufferLength; ++j) { + ringbuffer1[j] = ringbuffer2[j] = null; + } + } + if (quads) { + // GL_QUAD indexes can be precalculated + context.tempQuadIndexBuffer = GLctx.createBuffer(); + context.GLctx.bindBuffer(34963, context.tempQuadIndexBuffer); + var numIndexes = GL.MAX_TEMP_BUFFER_SIZE >> 1; + var quadIndexes = new Uint16Array(numIndexes); + var i = 0, v = 0; + while (1) { + quadIndexes[i++] = v; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 1; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 2; + if (i >= numIndexes) break; + quadIndexes[i++] = v; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 2; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 3; + if (i >= numIndexes) break; + v += 4; + } + context.GLctx.bufferData(34963, quadIndexes, 35044); + context.GLctx.bindBuffer(34963, null); + } + }, + getTempVertexBuffer: sizeBytes => { + var idx = GL.log2ceilLookup(sizeBytes); + var ringbuffer = GL.currentContext.tempVertexBuffers1[idx]; + var nextFreeBufferIndex = GL.currentContext.tempVertexBufferCounters1[idx]; + GL.currentContext.tempVertexBufferCounters1[idx] = (GL.currentContext.tempVertexBufferCounters1[idx] + 1) & (GL.numTempVertexBuffersPerSize - 1); + var vbo = ringbuffer[nextFreeBufferIndex]; + if (vbo) { + return vbo; + } + var prevVBO = GLctx.getParameter(34964); + ringbuffer[nextFreeBufferIndex] = GLctx.createBuffer(); + GLctx.bindBuffer(34962, ringbuffer[nextFreeBufferIndex]); + GLctx.bufferData(34962, 1 << idx, 35048); + GLctx.bindBuffer(34962, prevVBO); + return ringbuffer[nextFreeBufferIndex]; + }, + getTempIndexBuffer: sizeBytes => { + var idx = GL.log2ceilLookup(sizeBytes); + var ibo = GL.currentContext.tempIndexBuffers[idx]; + if (ibo) { + return ibo; + } + var prevIBO = GLctx.getParameter(34965); + GL.currentContext.tempIndexBuffers[idx] = GLctx.createBuffer(); + GLctx.bindBuffer(34963, GL.currentContext.tempIndexBuffers[idx]); + GLctx.bufferData(34963, 1 << idx, 35048); + GLctx.bindBuffer(34963, prevIBO); + return GL.currentContext.tempIndexBuffers[idx]; + }, + newRenderingFrameStarted: () => { + if (!GL.currentContext) { + return; + } + var vb = GL.currentContext.tempVertexBuffers1; + GL.currentContext.tempVertexBuffers1 = GL.currentContext.tempVertexBuffers2; + GL.currentContext.tempVertexBuffers2 = vb; + vb = GL.currentContext.tempVertexBufferCounters1; + GL.currentContext.tempVertexBufferCounters1 = GL.currentContext.tempVertexBufferCounters2; + GL.currentContext.tempVertexBufferCounters2 = vb; + var largestIndex = GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE); + for (var i = 0; i <= largestIndex; ++i) { + GL.currentContext.tempVertexBufferCounters1[i] = 0; + } + }, + getSource: (shader, count, string, length) => { + var source = ""; + for (var i = 0; i < count; ++i) { + var len = length ? HEAPU32[(((length) + (i * 4)) >> 2)] : undefined; + source += UTF8ToString(HEAPU32[(((string) + (i * 4)) >> 2)], len); + } + return source; + }, + calcBufLength: (size, type, stride, count) => { + if (stride > 0) { + return count * stride; + } + var typeSize = GL.byteSizeByType[type - GL.byteSizeByTypeRoot]; + return size * typeSize * count; + }, + usedTempBuffers: [], + preDrawHandleClientVertexAttribBindings: count => { + GL.resetBufferBinding = false; + // TODO: initial pass to detect ranges we need to upload, might not need + // an upload per attrib + for (var i = 0; i < GL.currentContext.maxVertexAttribs; ++i) { + var cb = GL.currentContext.clientBuffers[i]; + if (!cb.clientside || !cb.enabled) continue; + GL.resetBufferBinding = true; + var size = GL.calcBufLength(cb.size, cb.type, cb.stride, count); + var buf = GL.getTempVertexBuffer(size); + GLctx.bindBuffer(34962, buf); + GLctx.bufferSubData(34962, 0, HEAPU8.subarray(cb.ptr, cb.ptr + size)); + cb.vertexAttribPointerAdaptor.call(GLctx, i, cb.size, cb.type, cb.normalized, cb.stride, 0); + } + }, + postDrawHandleClientVertexAttribBindings: () => { + if (GL.resetBufferBinding) { + GLctx.bindBuffer(34962, GL.buffers[GLctx.currentArrayBufferBinding]); + } + }, + createContext: (/** @type {HTMLCanvasElement} */ canvas, webGLContextAttributes) => { + // BUG: Workaround Safari WebGL issue: After successfully acquiring WebGL + // context on a canvas, calling .getContext() will always return that + // context independent of which 'webgl' or 'webgl2' + // context version was passed. See: + // https://webkit.org/b/222758 + // and: + // https://github.com/emscripten-core/emscripten/issues/13295. + // TODO: Once the bug is fixed and shipped in Safari, adjust the Safari + // version field in above check. + if (!canvas.getContextSafariWebGL2Fixed) { + canvas.getContextSafariWebGL2Fixed = canvas.getContext; + /** @type {function(this:HTMLCanvasElement, string, (Object|null)=): (Object|null)} */ function fixedGetContext(ver, attrs) { + var gl = canvas.getContextSafariWebGL2Fixed(ver, attrs); + return ((ver == "webgl") == (gl instanceof WebGLRenderingContext)) ? gl : null; + } + canvas.getContext = fixedGetContext; + } + var ctx = (webGLContextAttributes.majorVersion > 1) ? canvas.getContext("webgl2", webGLContextAttributes) : canvas.getContext("webgl", webGLContextAttributes); + if (!ctx) return 0; + var handle = GL.registerContext(ctx, webGLContextAttributes); + return handle; + }, + registerContext: (ctx, webGLContextAttributes) => { + // without pthreads a context is just an integer ID + var handle = GL.getNewId(GL.contexts); + var context = { + handle, + attributes: webGLContextAttributes, + version: webGLContextAttributes.majorVersion, + GLctx: ctx + }; + // Store the created context object so that we can access the context + // given a canvas without having to pass the parameters again. + if (ctx.canvas) ctx.canvas.GLctxObject = context; + GL.contexts[handle] = context; + if (typeof webGLContextAttributes.enableExtensionsByDefault == "undefined" || webGLContextAttributes.enableExtensionsByDefault) { + GL.initExtensions(context); + } + context.maxVertexAttribs = context.GLctx.getParameter(34921); + context.clientBuffers = []; + for (var i = 0; i < context.maxVertexAttribs; i++) { + context.clientBuffers[i] = { + enabled: false, + clientside: false, + size: 0, + type: 0, + normalized: 0, + stride: 0, + ptr: 0, + vertexAttribPointerAdaptor: null + }; + } + GL.generateTempBuffers(false, context); + return handle; + }, + makeContextCurrent: contextHandle => { + // Active Emscripten GL layer context object. + GL.currentContext = GL.contexts[contextHandle]; + // Active WebGL context object. + Module["ctx"] = GLctx = GL.currentContext?.GLctx; + return !(contextHandle && !GLctx); + }, + getContext: contextHandle => GL.contexts[contextHandle], + deleteContext: contextHandle => { + if (GL.currentContext === GL.contexts[contextHandle]) { + GL.currentContext = null; + } + if (typeof JSEvents == "object") { + // Release all JS event handlers on the DOM element that the GL context is + // associated with since the context is now deleted. + JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas); + } + // Make sure the canvas object no longer refers to the context object so + // there are no GC surprises. + if (GL.contexts[contextHandle]?.GLctx.canvas) { + GL.contexts[contextHandle].GLctx.canvas.GLctxObject = undefined; + } + GL.contexts[contextHandle] = null; + }, + initExtensions: context => { + // If this function is called without a specific context object, init the + // extensions of the currently active context. + context ||= GL.currentContext; + if (context.initExtensionsDone) return; + context.initExtensionsDone = true; + var GLctx = context.GLctx; + // Detect the presence of a few extensions manually, since the GL interop + // layer itself will need to know if they exist. + // Extensions that are available in both WebGL 1 and WebGL 2 + webgl_enable_WEBGL_multi_draw(GLctx); + webgl_enable_EXT_polygon_offset_clamp(GLctx); + webgl_enable_EXT_clip_control(GLctx); + webgl_enable_WEBGL_polygon_mode(GLctx); + // Extensions that are only available in WebGL 1 (the calls will be no-ops + // if called on a WebGL 2 context active) + webgl_enable_ANGLE_instanced_arrays(GLctx); + webgl_enable_OES_vertex_array_object(GLctx); + webgl_enable_WEBGL_draw_buffers(GLctx); + // Extensions that are available from WebGL >= 2 (no-op if called on a WebGL 1 context active) + webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx); + webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx); + // On WebGL 2, EXT_disjoint_timer_query is replaced with an alternative + // that's based on core APIs, and exposes only the queryCounterEXT() + // entrypoint. + if (context.version >= 2) { + GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query_webgl2"); + } + // However, Firefox exposes the WebGL 1 version on WebGL 2 as well and + // thus we look for the WebGL 1 version again if the WebGL 2 version + // isn't present. https://bugzil.la/1328882 + if (context.version < 2 || !GLctx.disjointTimerQueryExt) { + GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query"); + } + for (var ext of getEmscriptenSupportedExtensions(GLctx)) { + // WEBGL_lose_context, WEBGL_debug_renderer_info and WEBGL_debug_shaders + // are not enabled by default. + if (!ext.includes("lose_context") && !ext.includes("debug")) { + // Call .getExtension() to enable that extension permanently. + GLctx.getExtension(ext); + } + } + } +}; + +var webglPowerPreferences = [ "default", "low-power", "high-performance" ]; + +/** @type {Object} */ var specialHTMLTargets = [ 0, globalThis.document ?? 0, globalThis.window ?? 0 ]; + +var findEventTarget = target => { + // The sensible "default" target varies between events, but use window as the default + // since DOM events mostly can default to that. Specific callback registrations + // override their own defaults. + if (!target) return window; + if (typeof target == "number") target = specialHTMLTargets[target] || UTF8ToString(target); + if (target === "#window") return window; else if (target === "#document") return document; else if (target === "#screen") return screen; else if (target === "#canvas") return Module["canvas"]; else if (typeof target == "string") return globalThis.document?.getElementById(target); + return target; +}; + +var findCanvasEventTarget = target => { + if (typeof target == "number") target = UTF8ToString(target); + if (!target || target === "#canvas") { + if (typeof GL != "undefined" && GL.offscreenCanvases["canvas"]) return GL.offscreenCanvases["canvas"]; + // TODO: Remove this line, target '#canvas' should refer only to Module['canvas'], not to GL.offscreenCanvases['canvas'] - but need stricter tests to be able to remove this line. + return Module["canvas"]; + } + if (typeof GL != "undefined" && GL.offscreenCanvases[target]) return GL.offscreenCanvases[target]; + return findEventTarget(target); +}; + +var _emscripten_webgl_do_create_context = (target, attributes) => { + var attr32 = ((attributes) >> 2); + var powerPreference = HEAP32[attr32 + (8 >> 2)]; + var contextAttributes = { + "alpha": !!HEAP8[attributes + 0], + "depth": !!HEAP8[attributes + 1], + "stencil": !!HEAP8[attributes + 2], + "antialias": !!HEAP8[attributes + 3], + "premultipliedAlpha": !!HEAP8[attributes + 4], + "preserveDrawingBuffer": !!HEAP8[attributes + 5], + "powerPreference": webglPowerPreferences[powerPreference], + "failIfMajorPerformanceCaveat": !!HEAP8[attributes + 12], + // The following are not predefined WebGL context attributes in the WebGL specification, so the property names can be minified by Closure. + majorVersion: HEAP32[attr32 + (16 >> 2)], + minorVersion: HEAP32[attr32 + (20 >> 2)], + enableExtensionsByDefault: HEAP8[attributes + 24], + explicitSwapControl: HEAP8[attributes + 25], + proxyContextToMainThread: HEAP32[attr32 + (28 >> 2)], + renderViaOffscreenBackBuffer: HEAP8[attributes + 32] + }; + var canvas = findCanvasEventTarget(target); + if (!canvas) { + return 0; + } + if (contextAttributes.explicitSwapControl) { + return 0; + } + var contextHandle = GL.createContext(canvas, contextAttributes); + return contextHandle; +}; + +var _emscripten_webgl_create_context = _emscripten_webgl_do_create_context; + +var _emscripten_webgl_destroy_context = contextHandle => { + if (GL.currentContext == contextHandle) GL.currentContext = 0; + GL.deleteContext(contextHandle); +}; + +var _emscripten_webgl_get_context_attributes = (c, a) => { + if (!a) return -5; + c = GL.contexts[c]; + if (!c) return -3; + var t = c.GLctx?.getContextAttributes(); + if (!t) return -3; + HEAP8[a] = t.alpha; + HEAP8[(a) + (1)] = t.depth; + HEAP8[(a) + (2)] = t.stencil; + HEAP8[(a) + (3)] = t.antialias; + HEAP8[(a) + (4)] = t.premultipliedAlpha; + HEAP8[(a) + (5)] = t.preserveDrawingBuffer; + var power = t["powerPreference"] && webglPowerPreferences.indexOf(t["powerPreference"]); + HEAP32[(((a) + (8)) >> 2)] = power; + HEAP8[(a) + (12)] = t.failIfMajorPerformanceCaveat; + HEAP32[(((a) + (16)) >> 2)] = c.version; + HEAP32[(((a) + (20)) >> 2)] = 0; + HEAP8[(a) + (24)] = c.attributes.enableExtensionsByDefault; + return 0; +}; + +var _emscripten_webgl_do_get_current_context = () => GL.currentContext ? GL.currentContext.handle : 0; + +var _emscripten_webgl_get_current_context = _emscripten_webgl_do_get_current_context; + +var _emscripten_webgl_make_context_current = contextHandle => { + var success = GL.makeContextCurrent(contextHandle); + return success ? 0 : -5; +}; + +var stackAlloc = sz => __emscripten_stack_alloc(sz); + +var stringToUTF8OnStack = str => { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8(str, ret, size); + return ret; +}; + +var writeI53ToI64 = (ptr, num) => { + HEAPU32[((ptr) >> 2)] = num; + var lower = HEAPU32[((ptr) >> 2)]; + HEAPU32[(((ptr) + (4)) >> 2)] = (num - lower) / 4294967296; +}; + +var readI53FromI64 = ptr => HEAPU32[((ptr) >> 2)] + HEAP32[(((ptr) + (4)) >> 2)] * 4294967296; + +var wasmTableMirror = []; + +var getWasmTableEntry = funcPtr => { + var func = wasmTableMirror[funcPtr]; + if (!func) { + /** @suppress {checkTypes} */ wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr); + } + return func; +}; + +var WebGPU = { + Internals: { + jsObjects: [], + jsObjectInsert: (ptr, jsObject) => { + ptr >>>= 0; + WebGPU.Internals.jsObjects[ptr] = jsObject; + }, + bufferOnUnmaps: [], + futures: [], + futureInsert: (futureId, promise) => {} + }, + getJsObject: ptr => { + if (!ptr) return undefined; + ptr >>>= 0; + return WebGPU.Internals.jsObjects[ptr]; + }, + importJsAdapter: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateAdapter(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBindGroup: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateBindGroup(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBindGroupLayout: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateBindGroupLayout(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBuffer: (buffer, parentPtr = 0) => { + // At the moment, we do not allow importing pending buffers. + assert(buffer.mapState === "unmapped"); + var bufferPtr = _emwgpuImportBuffer(parentPtr); + WebGPU.Internals.jsObjectInsert(bufferPtr, buffer); + return bufferPtr; + }, + importJsCommandBuffer: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateCommandBuffer(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsCommandEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateCommandEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsComputePassEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateComputePassEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsComputePipeline: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateComputePipeline(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsDevice: (device, parentPtr = 0) => { + var queuePtr = _emwgpuCreateQueue(parentPtr); + var devicePtr = _emwgpuCreateDevice(parentPtr, queuePtr); + WebGPU.Internals.jsObjectInsert(queuePtr, device.queue); + WebGPU.Internals.jsObjectInsert(devicePtr, device); + return devicePtr; + }, + importJsExternalTexture: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateExternalTexture(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsPipelineLayout: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreatePipelineLayout(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsQuerySet: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateQuerySet(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsQueue: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateQueue(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderBundle: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderBundle(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderBundleEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderBundleEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderPassEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderPassEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderPipeline: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderPipeline(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsSampler: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateSampler(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsShaderModule: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateShaderModule(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsSurface: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateSurface(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsTexture: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateTexture(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsTextureView: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateTextureView(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + errorCallback: (callback, type, message, userdata) => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(message); + getWasmTableEntry(callback)(type, messagePtr, userdata); + stackRestore(sp); + }, + iterateExtensions: (root, handlers) => { + for (var ptr = HEAPU32[((root) >> 2)]; ptr; ptr = HEAPU32[((ptr) >> 2)]) { + var sType = HEAP32[(((ptr) + (4)) >> 2)]; + // This will crash if there's no handler indicating either a bogus + // sType, or one we haven't implemented yet. + var handler = handlers[sType](ptr); + } + }, + setStringView: (ptr, data, length) => { + HEAPU32[((ptr) >> 2)] = data; + HEAPU32[(((ptr) + (4)) >> 2)] = length; + }, + makeStringFromStringView: stringViewPtr => { + var ptr = HEAPU32[((stringViewPtr) >> 2)]; + var length = HEAPU32[(((stringViewPtr) + (4)) >> 2)]; + // UTF8ToString stops at the first null terminator character in the + // string regardless of the length. + return UTF8ToString(ptr, length); + }, + makeStringFromOptionalStringView: stringViewPtr => { + var ptr = HEAPU32[((stringViewPtr) >> 2)]; + var length = HEAPU32[(((stringViewPtr) + (4)) >> 2)]; + // If we don't have a valid string pointer, just return undefined when + // optional. + if (!ptr) { + if (length === 0) { + return ""; + } + return undefined; + } + // UTF8ToString stops at the first null terminator character in the + // string regardless of the length. + return UTF8ToString(ptr, length); + }, + makeColor: ptr => ({ + "r": HEAPF64[((ptr) >> 3)], + "g": HEAPF64[(((ptr) + (8)) >> 3)], + "b": HEAPF64[(((ptr) + (16)) >> 3)], + "a": HEAPF64[(((ptr) + (24)) >> 3)] + }), + makeExtent3D: ptr => ({ + "width": HEAPU32[((ptr) >> 2)], + "height": HEAPU32[(((ptr) + (4)) >> 2)], + "depthOrArrayLayers": HEAPU32[(((ptr) + (8)) >> 2)] + }), + makeOrigin3D: ptr => ({ + "x": HEAPU32[((ptr) >> 2)], + "y": HEAPU32[(((ptr) + (4)) >> 2)], + "z": HEAPU32[(((ptr) + (8)) >> 2)] + }), + makeTexelCopyTextureInfo: ptr => ({ + "texture": WebGPU.getJsObject(HEAPU32[((ptr) >> 2)]), + "mipLevel": HEAPU32[(((ptr) + (4)) >> 2)], + "origin": WebGPU.makeOrigin3D(ptr + 8), + "aspect": WebGPU.TextureAspect[HEAP32[(((ptr) + (20)) >> 2)]] + }), + makeTexelCopyBufferLayout: ptr => { + var bytesPerRow = HEAPU32[(((ptr) + (8)) >> 2)]; + var rowsPerImage = HEAPU32[(((ptr) + (12)) >> 2)]; + return { + "offset": readI53FromI64(ptr), + "bytesPerRow": bytesPerRow === 4294967295 ? undefined : bytesPerRow, + "rowsPerImage": rowsPerImage === 4294967295 ? undefined : rowsPerImage + }; + }, + makeTexelCopyBufferInfo: ptr => { + var layoutPtr = ptr + 0; + var bufferCopyView = WebGPU.makeTexelCopyBufferLayout(layoutPtr); + bufferCopyView["buffer"] = WebGPU.getJsObject(HEAPU32[(((ptr) + (16)) >> 2)]); + return bufferCopyView; + }, + makePassTimestampWrites: ptr => { + if (ptr === 0) return undefined; + return { + "querySet": WebGPU.getJsObject(HEAPU32[(((ptr) + (4)) >> 2)]), + "beginningOfPassWriteIndex": HEAPU32[(((ptr) + (8)) >> 2)], + "endOfPassWriteIndex": HEAPU32[(((ptr) + (12)) >> 2)] + }; + }, + makePipelineConstants: (constantCount, constantsPtr) => { + if (!constantCount) return; + var constants = {}; + for (var i = 0; i < constantCount; ++i) { + var entryPtr = constantsPtr + 24 * i; + var key = WebGPU.makeStringFromStringView(entryPtr + 4); + constants[key] = HEAPF64[(((entryPtr) + (16)) >> 3)]; + } + return constants; + }, + makePipelineLayout: layoutPtr => { + if (!layoutPtr) return "auto"; + return WebGPU.getJsObject(layoutPtr); + }, + makeComputeState: ptr => { + if (!ptr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((ptr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((ptr) + (16)) >> 2)], HEAPU32[(((ptr) + (20)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(ptr + 8) + }; + return desc; + }, + makeComputePipelineDesc: descriptor => { + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.makePipelineLayout(HEAPU32[(((descriptor) + (12)) >> 2)]), + "compute": WebGPU.makeComputeState(descriptor + 16) + }; + return desc; + }, + makeRenderPipelineDesc: descriptor => { + function makePrimitiveState(psPtr) { + if (!psPtr) return undefined; + return { + "topology": WebGPU.PrimitiveTopology[HEAP32[(((psPtr) + (4)) >> 2)]], + "stripIndexFormat": WebGPU.IndexFormat[HEAP32[(((psPtr) + (8)) >> 2)]], + "frontFace": WebGPU.FrontFace[HEAP32[(((psPtr) + (12)) >> 2)]], + "cullMode": WebGPU.CullMode[HEAP32[(((psPtr) + (16)) >> 2)]], + "unclippedDepth": !!(HEAPU32[(((psPtr) + (20)) >> 2)]) + }; + } + function makeBlendComponent(bdPtr) { + if (!bdPtr) return undefined; + return { + "operation": WebGPU.BlendOperation[HEAP32[((bdPtr) >> 2)]], + "srcFactor": WebGPU.BlendFactor[HEAP32[(((bdPtr) + (4)) >> 2)]], + "dstFactor": WebGPU.BlendFactor[HEAP32[(((bdPtr) + (8)) >> 2)]] + }; + } + function makeBlendState(bsPtr) { + if (!bsPtr) return undefined; + return { + "alpha": makeBlendComponent(bsPtr + 12), + "color": makeBlendComponent(bsPtr + 0) + }; + } + function makeColorState(csPtr) { + var format = WebGPU.TextureFormat[HEAP32[(((csPtr) + (4)) >> 2)]]; + return format ? { + "format": format, + "blend": makeBlendState(HEAPU32[(((csPtr) + (8)) >> 2)]), + "writeMask": HEAPU32[(((csPtr) + (16)) >> 2)] + } : undefined; + } + function makeColorStates(count, csArrayPtr) { + var states = []; + for (var i = 0; i < count; ++i) { + states.push(makeColorState(csArrayPtr + 24 * i)); + } + return states; + } + function makeStencilStateFace(ssfPtr) { + return { + "compare": WebGPU.CompareFunction[HEAP32[((ssfPtr) >> 2)]], + "failOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (4)) >> 2)]], + "depthFailOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (8)) >> 2)]], + "passOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (12)) >> 2)]] + }; + } + function makeDepthStencilState(dssPtr) { + if (!dssPtr) return undefined; + return { + "format": WebGPU.TextureFormat[HEAP32[(((dssPtr) + (4)) >> 2)]], + "depthWriteEnabled": !!(HEAPU32[(((dssPtr) + (8)) >> 2)]), + "depthCompare": WebGPU.CompareFunction[HEAP32[(((dssPtr) + (12)) >> 2)]], + "stencilFront": makeStencilStateFace(dssPtr + 16), + "stencilBack": makeStencilStateFace(dssPtr + 32), + "stencilReadMask": HEAPU32[(((dssPtr) + (48)) >> 2)], + "stencilWriteMask": HEAPU32[(((dssPtr) + (52)) >> 2)], + "depthBias": HEAP32[(((dssPtr) + (56)) >> 2)], + "depthBiasSlopeScale": HEAPF32[(((dssPtr) + (60)) >> 2)], + "depthBiasClamp": HEAPF32[(((dssPtr) + (64)) >> 2)] + }; + } + function makeVertexAttribute(vaPtr) { + return { + "format": WebGPU.VertexFormat[HEAP32[(((vaPtr) + (4)) >> 2)]], + "offset": readI53FromI64((vaPtr) + (8)), + "shaderLocation": HEAPU32[(((vaPtr) + (16)) >> 2)] + }; + } + function makeVertexAttributes(count, vaArrayPtr) { + var vas = []; + for (var i = 0; i < count; ++i) { + vas.push(makeVertexAttribute(vaArrayPtr + i * 24)); + } + return vas; + } + function makeVertexBuffer(vbPtr) { + if (!vbPtr) return undefined; + var stepMode = WebGPU.VertexStepMode[HEAP32[(((vbPtr) + (4)) >> 2)]]; + var attributeCount = HEAPU32[(((vbPtr) + (16)) >> 2)]; + if (!stepMode && !attributeCount) { + return null; + } + return { + "arrayStride": readI53FromI64((vbPtr) + (8)), + "stepMode": stepMode, + "attributes": makeVertexAttributes(attributeCount, HEAPU32[(((vbPtr) + (20)) >> 2)]) + }; + } + function makeVertexBuffers(count, vbArrayPtr) { + if (!count) return undefined; + var vbs = []; + for (var i = 0; i < count; ++i) { + vbs.push(makeVertexBuffer(vbArrayPtr + i * 24)); + } + return vbs; + } + function makeVertexState(viPtr) { + if (!viPtr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((viPtr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((viPtr) + (16)) >> 2)], HEAPU32[(((viPtr) + (20)) >> 2)]), + "buffers": makeVertexBuffers(HEAPU32[(((viPtr) + (24)) >> 2)], HEAPU32[(((viPtr) + (28)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(viPtr + 8) + }; + return desc; + } + function makeMultisampleState(msPtr) { + if (!msPtr) return undefined; + return { + "count": HEAPU32[(((msPtr) + (4)) >> 2)], + "mask": HEAPU32[(((msPtr) + (8)) >> 2)], + "alphaToCoverageEnabled": !!(HEAPU32[(((msPtr) + (12)) >> 2)]) + }; + } + function makeFragmentState(fsPtr) { + if (!fsPtr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((fsPtr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((fsPtr) + (16)) >> 2)], HEAPU32[(((fsPtr) + (20)) >> 2)]), + "targets": makeColorStates(HEAPU32[(((fsPtr) + (24)) >> 2)], HEAPU32[(((fsPtr) + (28)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(fsPtr + 8) + }; + return desc; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.makePipelineLayout(HEAPU32[(((descriptor) + (12)) >> 2)]), + "vertex": makeVertexState(descriptor + 16), + "primitive": makePrimitiveState(descriptor + 48), + "depthStencil": makeDepthStencilState(HEAPU32[(((descriptor) + (72)) >> 2)]), + "multisample": makeMultisampleState(descriptor + 76), + "fragment": makeFragmentState(HEAPU32[(((descriptor) + (92)) >> 2)]) + }; + return desc; + }, + fillLimitStruct: (limits, limitsOutPtr) => { + var nextInChainPtr = HEAPU32[((limitsOutPtr) >> 2)]; + function setLimitValueU32(name, basePtr, limitOffset, fallbackValue = 0) { + var limitValue = limits[name] ?? fallbackValue; + HEAPU32[(((basePtr) + (limitOffset)) >> 2)] = limitValue; + } + function setLimitValueU64(name, basePtr, limitOffset, fallbackValue = 0) { + var limitValue = limits[name] ?? fallbackValue; + // Limits are integer-valued JS `Number`s, so they fit in 'i53'. + writeI53ToI64((basePtr) + (limitOffset), limitValue); + } + setLimitValueU32("maxTextureDimension1D", limitsOutPtr, 4); + setLimitValueU32("maxTextureDimension2D", limitsOutPtr, 8); + setLimitValueU32("maxTextureDimension3D", limitsOutPtr, 12); + setLimitValueU32("maxTextureArrayLayers", limitsOutPtr, 16); + setLimitValueU32("maxBindGroups", limitsOutPtr, 20); + setLimitValueU32("maxBindGroupsPlusVertexBuffers", limitsOutPtr, 24); + setLimitValueU32("maxBindingsPerBindGroup", limitsOutPtr, 28); + setLimitValueU32("maxDynamicUniformBuffersPerPipelineLayout", limitsOutPtr, 32); + setLimitValueU32("maxDynamicStorageBuffersPerPipelineLayout", limitsOutPtr, 36); + setLimitValueU32("maxSampledTexturesPerShaderStage", limitsOutPtr, 40); + setLimitValueU32("maxSamplersPerShaderStage", limitsOutPtr, 44); + setLimitValueU32("maxStorageBuffersPerShaderStage", limitsOutPtr, 48); + setLimitValueU32("maxStorageTexturesPerShaderStage", limitsOutPtr, 52); + setLimitValueU32("maxUniformBuffersPerShaderStage", limitsOutPtr, 56); + setLimitValueU32("minUniformBufferOffsetAlignment", limitsOutPtr, 80); + setLimitValueU32("minStorageBufferOffsetAlignment", limitsOutPtr, 84); + setLimitValueU64("maxUniformBufferBindingSize", limitsOutPtr, 64); + setLimitValueU64("maxStorageBufferBindingSize", limitsOutPtr, 72); + setLimitValueU32("maxVertexBuffers", limitsOutPtr, 88); + setLimitValueU64("maxBufferSize", limitsOutPtr, 96); + setLimitValueU32("maxVertexAttributes", limitsOutPtr, 104); + setLimitValueU32("maxVertexBufferArrayStride", limitsOutPtr, 108); + setLimitValueU32("maxInterStageShaderVariables", limitsOutPtr, 112); + setLimitValueU32("maxColorAttachments", limitsOutPtr, 116); + setLimitValueU32("maxColorAttachmentBytesPerSample", limitsOutPtr, 120); + setLimitValueU32("maxComputeWorkgroupStorageSize", limitsOutPtr, 124); + setLimitValueU32("maxComputeInvocationsPerWorkgroup", limitsOutPtr, 128); + setLimitValueU32("maxComputeWorkgroupSizeX", limitsOutPtr, 132); + setLimitValueU32("maxComputeWorkgroupSizeY", limitsOutPtr, 136); + setLimitValueU32("maxComputeWorkgroupSizeZ", limitsOutPtr, 140); + setLimitValueU32("maxComputeWorkgroupsPerDimension", limitsOutPtr, 144); + // Note this limit is new and won't be present in all browsers for a while. Fall back to 0. + setLimitValueU32("maxImmediateSize", limitsOutPtr, 148); + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var compatibilityModeLimitsPtr = nextInChainPtr; + // Note these limits are new and won't be present in all browsers for a while. Fall back to exposing the PerShaderStage limit. + setLimitValueU32("maxStorageBuffersInVertexStage", compatibilityModeLimitsPtr, 8, limits.maxStorageBuffersPerShaderStage); + setLimitValueU32("maxStorageBuffersInFragmentStage", compatibilityModeLimitsPtr, 16, limits.maxStorageBuffersPerShaderStage); + setLimitValueU32("maxStorageTexturesInVertexStage", compatibilityModeLimitsPtr, 12, limits.maxStorageTexturesPerShaderStage); + setLimitValueU32("maxStorageTexturesInFragmentStage", compatibilityModeLimitsPtr, 20, limits.maxStorageTexturesPerShaderStage); + } + }, + fillAdapterInfoStruct: (info, infoStruct) => { + // Populate subgroup limits. + HEAPU32[(((infoStruct) + (52)) >> 2)] = info.subgroupMinSize; + HEAPU32[(((infoStruct) + (56)) >> 2)] = info.subgroupMaxSize; + // Append all the strings together to condense into a single malloc. + var strs = info.vendor + info.architecture + info.device + info.description; + var strPtr = stringToNewUTF8(strs); + var vendorLen = lengthBytesUTF8(info.vendor); + WebGPU.setStringView(infoStruct + 4, strPtr, vendorLen); + strPtr += vendorLen; + var architectureLen = lengthBytesUTF8(info.architecture); + WebGPU.setStringView(infoStruct + 12, strPtr, architectureLen); + strPtr += architectureLen; + var deviceLen = lengthBytesUTF8(info.device); + WebGPU.setStringView(infoStruct + 20, strPtr, deviceLen); + strPtr += deviceLen; + var descriptionLen = lengthBytesUTF8(info.description); + WebGPU.setStringView(infoStruct + 28, strPtr, descriptionLen); + strPtr += descriptionLen; + HEAP32[(((infoStruct) + (36)) >> 2)] = 2; + var adapterType = info.isFallbackAdapter ? 3 : 4; + HEAP32[(((infoStruct) + (40)) >> 2)] = adapterType; + HEAPU32[(((infoStruct) + (44)) >> 2)] = 0; + HEAPU32[(((infoStruct) + (48)) >> 2)] = 0; + }, + AddressMode: [ , "clamp-to-edge", "repeat", "mirror-repeat" ], + BlendFactor: [ , "zero", "one", "src", "one-minus-src", "src-alpha", "one-minus-src-alpha", "dst", "one-minus-dst", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated", "constant", "one-minus-constant", "src1", "one-minus-src1", "src1-alpha", "one-minus-src1-alpha" ], + BlendOperation: [ , "add", "subtract", "reverse-subtract", "min", "max" ], + BufferBindingType: [ , , "uniform", "storage", "read-only-storage" ], + BufferMapState: [ , "unmapped", "pending", "mapped" ], + CompareFunction: [ , "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always" ], + CompilationInfoRequestStatus: [ , "success", "callback-cancelled" ], + ComponentSwizzle: [ , "0", "1", "r", "g", "b", "a" ], + CompositeAlphaMode: [ , "opaque", "premultiplied", "unpremultiplied", "inherit" ], + CullMode: [ , "none", "front", "back" ], + ErrorFilter: [ , "validation", "out-of-memory", "internal" ], + FeatureLevel: [ , "compatibility", "core" ], + FeatureName: { + 1: "core-features-and-limits", + 2: "depth-clip-control", + 3: "depth32float-stencil8", + 4: "texture-compression-bc", + 5: "texture-compression-bc-sliced-3d", + 6: "texture-compression-etc2", + 7: "texture-compression-astc", + 8: "texture-compression-astc-sliced-3d", + 9: "timestamp-query", + 10: "indirect-first-instance", + 11: "shader-f16", + 12: "rg11b10ufloat-renderable", + 13: "bgra8unorm-storage", + 14: "float32-filterable", + 15: "float32-blendable", + 16: "clip-distances", + 17: "dual-source-blending", + 18: "subgroups", + 19: "texture-formats-tier1", + 20: "texture-formats-tier2", + 21: "primitive-index", + 22: "texture-component-swizzle", + 327692: "chromium-experimental-unorm16-texture-formats", + 327729: "chromium-experimental-multi-draw-indirect" + }, + FilterMode: [ , "nearest", "linear" ], + FrontFace: [ , "ccw", "cw" ], + IndexFormat: [ , "uint16", "uint32" ], + InstanceFeatureName: [ , "timed-wait-any", "shader-source-spirv", "multiple-devices-per-adapter" ], + LoadOp: [ , "load", "clear" ], + MipmapFilterMode: [ , "nearest", "linear" ], + OptionalBool: [ "false", "true" ], + PowerPreference: [ , "low-power", "high-performance" ], + PredefinedColorSpace: [ , "srgb", "display-p3" ], + PrimitiveTopology: [ , "point-list", "line-list", "line-strip", "triangle-list", "triangle-strip" ], + QueryType: [ , "occlusion", "timestamp" ], + SamplerBindingType: [ , , "filtering", "non-filtering", "comparison" ], + Status: [ , "success", "error" ], + StencilOperation: [ , "keep", "zero", "replace", "invert", "increment-clamp", "decrement-clamp", "increment-wrap", "decrement-wrap" ], + StorageTextureAccess: [ , , "write-only", "read-only", "read-write" ], + StoreOp: [ , "store", "discard" ], + SurfaceGetCurrentTextureStatus: [ , "success-optimal", "success-suboptimal", "timeout", "outdated", "lost", "error" ], + TextureAspect: [ , "all", "stencil-only", "depth-only" ], + TextureDimension: [ , "1d", "2d", "3d" ], + TextureFormat: [ , "r8unorm", "r8snorm", "r8uint", "r8sint", "r16unorm", "r16snorm", "r16uint", "r16sint", "r16float", "rg8unorm", "rg8snorm", "rg8uint", "rg8sint", "r32float", "r32uint", "r32sint", "rg16unorm", "rg16snorm", "rg16uint", "rg16sint", "rg16float", "rgba8unorm", "rgba8unorm-srgb", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rgb9e5ufloat", "rg32float", "rg32uint", "rg32sint", "rgba16unorm", "rgba16snorm", "rgba16uint", "rgba16sint", "rgba16float", "rgba32float", "rgba32uint", "rgba32sint", "stencil8", "depth16unorm", "depth24plus", "depth24plus-stencil8", "depth32float", "depth32float-stencil8", "bc1-rgba-unorm", "bc1-rgba-unorm-srgb", "bc2-rgba-unorm", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm", "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb", "etc2-rgb8unorm", "etc2-rgb8unorm-srgb", "etc2-rgb8a1unorm", "etc2-rgb8a1unorm-srgb", "etc2-rgba8unorm", "etc2-rgba8unorm-srgb", "eac-r11unorm", "eac-r11snorm", "eac-rg11unorm", "eac-rg11snorm", "astc-4x4-unorm", "astc-4x4-unorm-srgb", "astc-5x4-unorm", "astc-5x4-unorm-srgb", "astc-5x5-unorm", "astc-5x5-unorm-srgb", "astc-6x5-unorm", "astc-6x5-unorm-srgb", "astc-6x6-unorm", "astc-6x6-unorm-srgb", "astc-8x5-unorm", "astc-8x5-unorm-srgb", "astc-8x6-unorm", "astc-8x6-unorm-srgb", "astc-8x8-unorm", "astc-8x8-unorm-srgb", "astc-10x5-unorm", "astc-10x5-unorm-srgb", "astc-10x6-unorm", "astc-10x6-unorm-srgb", "astc-10x8-unorm", "astc-10x8-unorm-srgb", "astc-10x10-unorm", "astc-10x10-unorm-srgb", "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", "astc-12x12-unorm-srgb" ], + TextureSampleType: [ , , "float", "unfilterable-float", "depth", "sint", "uint" ], + TextureViewDimension: [ , "1d", "2d", "2d-array", "cube", "cube-array", "3d" ], + ToneMappingMode: [ , "standard", "extended" ], + VertexFormat: [ , "uint8", "uint8x2", "uint8x4", "sint8", "sint8x2", "sint8x4", "unorm8", "unorm8x2", "unorm8x4", "snorm8", "snorm8x2", "snorm8x4", "uint16", "uint16x2", "uint16x4", "sint16", "sint16x2", "sint16x4", "unorm16", "unorm16x2", "unorm16x4", "snorm16", "snorm16x2", "snorm16x4", "float16", "float16x2", "float16x4", "float32", "float32x2", "float32x3", "float32x4", "uint32", "uint32x2", "uint32x3", "uint32x4", "sint32", "sint32x2", "sint32x3", "sint32x4", "unorm10-10-10-2", "unorm8x4-bgra" ], + VertexStepMode: [ , "vertex", "instance" ], + WGSLLanguageFeatureName: [ , "readonly_and_readwrite_storage_textures", "packed_4x8_integer_dot_product", "unrestricted_pointer_parameters", "pointer_composite_access", "uniform_buffer_standard_layout", "subgroup_id", "texture_and_sampler_let", "subgroup_uniformity", "texture_formats_tier1", "linear_indexing" ] +}; + +var _emscripten_webgpu_get_device = () => { + if (WebGPU.preinitializedDeviceId === undefined) { + WebGPU.preinitializedDeviceId = WebGPU.importJsDevice(Module["preinitializedWebGPUDevice"]); + // Some users depend on this keeping the device alive, so we add an + // additional reference when we first initialize it. + _wgpuDeviceAddRef(WebGPU.preinitializedDeviceId); + } + _wgpuDeviceAddRef(WebGPU.preinitializedDeviceId); + return WebGPU.preinitializedDeviceId; +}; + +var _emwgpuBufferDestroy = bufferPtr => { + var buffer = WebGPU.getJsObject(bufferPtr); + var onUnmap = WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + if (onUnmap) { + for (var i = 0; i < onUnmap.length; ++i) { + onUnmap[i](); + } + delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + } + buffer.destroy(); +}; + +var _emwgpuBufferGetMappedRange = (bufferPtr, offset, size) => { + var buffer = WebGPU.getJsObject(bufferPtr); + if (size == -1) size = undefined; + var mapped; + try { + mapped = buffer.getMappedRange(offset, size); + } catch (ex) { + return 0; + } + var data = _memalign(16, mapped.byteLength); + HEAPU8.fill(0, data, mapped.byteLength); + WebGPU.Internals.bufferOnUnmaps[bufferPtr].push(() => { + new Uint8Array(mapped).set(HEAPU8.subarray(data, data + mapped.byteLength)); + _free(data); + }); + return data; +}; + +var _emwgpuBufferUnmap = bufferPtr => { + var buffer = WebGPU.getJsObject(bufferPtr); + var onUnmap = WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + if (!onUnmap) { + // Already unmapped + return; + } + for (var i = 0; i < onUnmap.length; ++i) { + onUnmap[i](); + } + delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + buffer.unmap(); +}; + +var _emwgpuDelete = ptr => { + delete WebGPU.Internals.jsObjects[ptr]; +}; + +var _emwgpuDeviceCreateBuffer = (devicePtr, descriptor, bufferPtr) => { + var mappedAtCreation = !!(HEAPU32[(((descriptor) + (32)) >> 2)]); + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "usage": HEAPU32[(((descriptor) + (16)) >> 2)], + "size": readI53FromI64((descriptor) + (24)), + "mappedAtCreation": mappedAtCreation + }; + var device = WebGPU.getJsObject(devicePtr); + var buffer; + try { + buffer = device.createBuffer(desc); + } catch (ex) { + // The only exception should be RangeError if mapping at creation ran out of memory. + return false; + } + WebGPU.Internals.jsObjectInsert(bufferPtr, buffer); + if (mappedAtCreation) { + WebGPU.Internals.bufferOnUnmaps[bufferPtr] = []; + } + return true; +}; + +var _emwgpuDeviceCreateComputePipelineAsync = function(devicePtr, futureId_low, futureId_high, descriptor, pipelinePtr) { + var futureId = convertI32PairToI53Checked(futureId_low, futureId_high); + var desc = WebGPU.makeComputePipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + // createComputePipelineAsync + WebGPU.Internals.futureInsert(futureId, device.createComputePipelineAsync(desc).then(pipeline => { + // createComputePipelineAsync fulfilled + callUserCallback(() => { + WebGPU.Internals.jsObjectInsert(pipelinePtr, pipeline); + _emwgpuOnCreateComputePipelineCompleted(futureId, 1, pipelinePtr, 0); + }); + }, pipelineError => { + // createComputePipelineAsync rejected + callUserCallback(() => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(pipelineError.message); + var status = pipelineError.reason === "validation" ? 3 : pipelineError.reason === "internal" ? 4 : 0; + _emwgpuOnCreateComputePipelineCompleted(futureId, status, pipelinePtr, messagePtr); + stackRestore(sp); + }); + })); +}; + +var _emwgpuDeviceCreateRenderPipelineAsync = function(devicePtr, futureId_low, futureId_high, descriptor, pipelinePtr) { + var futureId = convertI32PairToI53Checked(futureId_low, futureId_high); + var desc = WebGPU.makeRenderPipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + // createRenderPipelineAsync + WebGPU.Internals.futureInsert(futureId, device.createRenderPipelineAsync(desc).then(pipeline => { + // createRenderPipelineAsync fulfilled + callUserCallback(() => { + WebGPU.Internals.jsObjectInsert(pipelinePtr, pipeline); + _emwgpuOnCreateRenderPipelineCompleted(futureId, 1, pipelinePtr, 0); + }); + }, pipelineError => { + // createRenderPipelineAsync rejected + callUserCallback(() => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(pipelineError.message); + var status = pipelineError.reason === "validation" ? 3 : pipelineError.reason === "internal" ? 4 : 0; + _emwgpuOnCreateRenderPipelineCompleted(futureId, status, pipelinePtr, messagePtr); + stackRestore(sp); + }); + })); +}; + +var _emwgpuDeviceCreateShaderModule = (devicePtr, descriptor, shaderModulePtr) => { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "code": "" + }; + switch (sType) { + case 2: + { + desc["code"] = WebGPU.makeStringFromStringView(nextInChainPtr + 8); + break; + } + } + var device = WebGPU.getJsObject(devicePtr); + WebGPU.Internals.jsObjectInsert(shaderModulePtr, device.createShaderModule(desc)); +}; + +var _emwgpuDeviceDestroy = devicePtr => { + const device = WebGPU.getJsObject(devicePtr); + // Remove the onuncapturederror handler which holds a pointer to the WGPUDevice. + device.onuncapturederror = null; + device.destroy(); +}; + +var _emwgpuWaitAny = (futurePtr, futureCount, timeoutMSPtr) => { + abort("TODO: Implement asyncify-free WaitAny for timeout=0"); +}; + +var ENV = {}; + +var getExecutableName = () => thisProgram || "./this.program"; + +var getEnvStrings = () => { + if (!getEnvStrings.strings) { + // Default values. + // Browser language detection #8751 + var lang = (globalThis.navigator?.language ?? "C").replace("-", "_") + ".UTF-8"; + var env = { + "USER": "web_user", + "LOGNAME": "web_user", + "PATH": "/", + "PWD": "/", + "HOME": "/home/web_user", + "LANG": lang, + "_": getExecutableName() + }; + // Apply the user-provided values, if any. + for (var x in ENV) { + // x is a key in ENV; if ENV[x] is undefined, that means it was + // explicitly set to be so. We allow user code to do that to + // force variables with default values to remain unset. + if (ENV[x] === undefined) delete env[x]; else env[x] = ENV[x]; + } + var strings = []; + for (var x in env) { + strings.push(`${x}=${env[x]}`); + } + getEnvStrings.strings = strings; + } + return getEnvStrings.strings; +}; + +var _environ_get = (__environ, environ_buf) => { + var bufSize = 0; + var envp = 0; + for (var string of getEnvStrings()) { + var ptr = environ_buf + bufSize; + HEAPU32[(((__environ) + (envp)) >> 2)] = ptr; + bufSize += stringToUTF8(string, ptr, Infinity) + 1; + envp += 4; + } + return 0; +}; + +var _environ_sizes_get = (penviron_count, penviron_buf_size) => { + var strings = getEnvStrings(); + HEAPU32[((penviron_count) >> 2)] = strings.length; + var bufSize = 0; + for (var string of strings) { + bufSize += lengthBytesUTF8(string) + 1; + } + HEAPU32[((penviron_buf_size) >> 2)] = bufSize; + return 0; +}; + +function _fd_close(fd) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + FS.close(stream); + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +/** @param {number=} offset */ var doReadv = (stream, iov, iovcnt, offset) => { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[((iov) >> 2)]; + var len = HEAPU32[(((iov) + (4)) >> 2)]; + iov += 8; + var curr = FS.read(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) break; + // nothing more to read + if (typeof offset != "undefined") { + offset += curr; + } + } + return ret; +}; + +function _fd_read(fd, iov, iovcnt, pnum) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doReadv(stream, iov, iovcnt); + HEAPU32[((pnum) >> 2)] = num; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +function _fd_seek(fd, offset_low, offset_high, whence, newOffset) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + if (isNaN(offset)) return 61; + var stream = SYSCALLS.getStreamFromFD(fd); + FS.llseek(stream, offset, whence); + (tempI64 = [ stream.position >>> 0, (tempDouble = stream.position, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[((newOffset) >> 2)] = tempI64[0], HEAP32[(((newOffset) + (4)) >> 2)] = tempI64[1]); + if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; + // reset readdir state + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +/** @param {number=} offset */ var doWritev = (stream, iov, iovcnt, offset) => { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[((iov) >> 2)]; + var len = HEAPU32[(((iov) + (4)) >> 2)]; + iov += 8; + var curr = FS.write(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) { + // No more space to write. + break; + } + if (typeof offset != "undefined") { + offset += curr; + } + } + return ret; +}; + +function _fd_write(fd, iov, iovcnt, pnum) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doWritev(stream, iov, iovcnt); + HEAPU32[((pnum) >> 2)] = num; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +var _emscripten_glActiveTexture = x0 => GLctx.activeTexture(x0); + +var _glActiveTexture = _emscripten_glActiveTexture; + +var _emscripten_glAttachShader = (program, shader) => { + GLctx.attachShader(GL.programs[program], GL.shaders[shader]); +}; + +var _glAttachShader = _emscripten_glAttachShader; + +var _emscripten_glBindAttribLocation = (program, index, name) => { + GLctx.bindAttribLocation(GL.programs[program], index, UTF8ToString(name)); +}; + +var _glBindAttribLocation = _emscripten_glBindAttribLocation; + +var _emscripten_glBindBuffer = (target, buffer) => { + // Calling glBindBuffer with an unknown buffer will implicitly create a + // new one. Here we bypass `GL.counter` and directly using the ID passed + // in. + if (buffer && !GL.buffers[buffer]) { + var b = GLctx.createBuffer(); + b.name = buffer; + GL.buffers[buffer] = b; + } + if (target == 34962) { + GLctx.currentArrayBufferBinding = buffer; + } else if (target == 34963) { + GLctx.currentElementArrayBufferBinding = buffer; + } + if (target == 35051) { + // In WebGL 2 glReadPixels entry point, we need to use a different WebGL 2 + // API function call when a buffer is bound to + // GL_PIXEL_PACK_BUFFER_BINDING point, so must keep track whether that + // binding point is non-null to know what is the proper API function to + // call. + GLctx.currentPixelPackBufferBinding = buffer; + } else if (target == 35052) { + // In WebGL 2 gl(Compressed)Tex(Sub)Image[23]D entry points, we need to + // use a different WebGL 2 API function call when a buffer is bound to + // GL_PIXEL_UNPACK_BUFFER_BINDING point, so must keep track whether that + // binding point is non-null to know what is the proper API function to + // call. + GLctx.currentPixelUnpackBufferBinding = buffer; + } + GLctx.bindBuffer(target, GL.buffers[buffer]); +}; + +var _glBindBuffer = _emscripten_glBindBuffer; + +var _emscripten_glBindBufferBase = (target, index, buffer) => { + GLctx.bindBufferBase(target, index, GL.buffers[buffer]); +}; + +var _glBindBufferBase = _emscripten_glBindBufferBase; + +var _emscripten_glBindFramebuffer = (target, framebuffer) => { + GLctx.bindFramebuffer(target, GL.framebuffers[framebuffer]); +}; + +var _glBindFramebuffer = _emscripten_glBindFramebuffer; + +var _emscripten_glBindTexture = (target, texture) => { + GLctx.bindTexture(target, GL.textures[texture]); +}; + +var _glBindTexture = _emscripten_glBindTexture; + +var _emscripten_glBindVertexArray = vao => { + GLctx.bindVertexArray(GL.vaos[vao]); + var ibo = GLctx.getParameter(34965); + GLctx.currentElementArrayBufferBinding = ibo ? (ibo.name | 0) : 0; +}; + +var _glBindVertexArray = _emscripten_glBindVertexArray; + +var _emscripten_glBlendEquation = x0 => GLctx.blendEquation(x0); + +var _glBlendEquation = _emscripten_glBlendEquation; + +var _emscripten_glBlendFunc = (x0, x1) => GLctx.blendFunc(x0, x1); + +var _glBlendFunc = _emscripten_glBlendFunc; + +var _emscripten_glBufferData = (target, size, data, usage) => { + if (GL.currentContext.version >= 2) { + // If size is zero, WebGL would interpret uploading the whole input + // arraybuffer (starting from given offset), which would not make sense in + // WebAssembly, so avoid uploading if size is zero. However we must still + // call bufferData to establish a backing storage of zero bytes. + if (data && size) { + GLctx.bufferData(target, HEAPU8, usage, data, size); + } else { + GLctx.bufferData(target, size, usage); + } + return; + } + // N.b. here first form specifies a heap subarray, second form an integer + // size, so the ?: code here is polymorphic. It is advised to avoid + // randomly mixing both uses in calling code, to avoid any potential JS + // engine JIT issues. + GLctx.bufferData(target, data ? HEAPU8.subarray(data, data + size) : size, usage); +}; + +var _glBufferData = _emscripten_glBufferData; + +var _emscripten_glClear = x0 => GLctx.clear(x0); + +var _glClear = _emscripten_glClear; + +var _emscripten_glClearColor = (x0, x1, x2, x3) => GLctx.clearColor(x0, x1, x2, x3); + +var _glClearColor = _emscripten_glClearColor; + +var convertI32PairToI53 = (lo, hi) => (lo >>> 0) + hi * 4294967296; + +var _emscripten_glClientWaitSync = (sync, flags, timeout_low, timeout_high) => { + // WebGL2 vs GLES3 differences: in GLES3, the timeout parameter is a uint64, where 0xFFFFFFFFFFFFFFFFULL means GL_TIMEOUT_IGNORED. + // In JS, there's no 64-bit value types, so instead timeout is taken to be signed, and GL_TIMEOUT_IGNORED is given value -1. + // Inherently the value accepted in the timeout is lossy, and can't take in arbitrary u64 bit pattern (but most likely doesn't matter) + // See https://www.khronos.org/registry/webgl/specs/latest/2.0/#5.15 + var timeout = convertI32PairToI53(timeout_low, timeout_high); + return GLctx.clientWaitSync(GL.syncs[sync], flags, timeout); +}; + +var _glClientWaitSync = _emscripten_glClientWaitSync; + +var _emscripten_glColorMask = (red, green, blue, alpha) => { + GLctx.colorMask(!!red, !!green, !!blue, !!alpha); +}; + +var _glColorMask = _emscripten_glColorMask; + +var _emscripten_glCompileShader = shader => { + GLctx.compileShader(GL.shaders[shader]); +}; + +var _glCompileShader = _emscripten_glCompileShader; + +var _emscripten_glCreateProgram = () => { + var id = GL.getNewId(GL.programs); + var program = GLctx.createProgram(); + // Store additional information needed for each shader program: + program.name = id; + // Lazy cache results of + // glGetProgramiv(GL_ACTIVE_UNIFORM_MAX_LENGTH/GL_ACTIVE_ATTRIBUTE_MAX_LENGTH/GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH) + program.maxUniformLength = program.maxAttributeLength = program.maxUniformBlockNameLength = 0; + program.uniformIdCounter = 1; + GL.programs[id] = program; + return id; +}; + +var _glCreateProgram = _emscripten_glCreateProgram; + +var _emscripten_glCreateShader = shaderType => { + var id = GL.getNewId(GL.shaders); + GL.shaders[id] = GLctx.createShader(shaderType); + return id; +}; + +var _glCreateShader = _emscripten_glCreateShader; + +var _emscripten_glDeleteBuffers = (n, buffers) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((buffers) + (i * 4)) >> 2)]; + var buffer = GL.buffers[id]; + // From spec: "glDeleteBuffers silently ignores 0's and names that do not + // correspond to existing buffer objects." + if (!buffer) continue; + GLctx.deleteBuffer(buffer); + buffer.name = 0; + GL.buffers[id] = null; + if (id == GLctx.currentArrayBufferBinding) GLctx.currentArrayBufferBinding = 0; + if (id == GLctx.currentElementArrayBufferBinding) GLctx.currentElementArrayBufferBinding = 0; + if (id == GLctx.currentPixelPackBufferBinding) GLctx.currentPixelPackBufferBinding = 0; + if (id == GLctx.currentPixelUnpackBufferBinding) GLctx.currentPixelUnpackBufferBinding = 0; + } +}; + +var _glDeleteBuffers = _emscripten_glDeleteBuffers; + +var _emscripten_glDeleteFramebuffers = (n, framebuffers) => { + for (var i = 0; i < n; ++i) { + var id = HEAP32[(((framebuffers) + (i * 4)) >> 2)]; + var framebuffer = GL.framebuffers[id]; + if (!framebuffer) continue; + // GL spec: "glDeleteFramebuffers silently ignores 0s and names that do not correspond to existing framebuffer objects". + GLctx.deleteFramebuffer(framebuffer); + framebuffer.name = 0; + GL.framebuffers[id] = null; + } +}; + +var _glDeleteFramebuffers = _emscripten_glDeleteFramebuffers; + +var _emscripten_glDeleteProgram = id => { + if (!id) return; + var program = GL.programs[id]; + if (!program) { + // glDeleteProgram actually signals an error when deleting a nonexisting + // object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteProgram(program); + program.name = 0; + GL.programs[id] = null; +}; + +var _glDeleteProgram = _emscripten_glDeleteProgram; + +var _emscripten_glDeleteShader = id => { + if (!id) return; + var shader = GL.shaders[id]; + if (!shader) { + // glDeleteShader actually signals an error when deleting a nonexisting + // object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteShader(shader); + GL.shaders[id] = null; +}; + +var _glDeleteShader = _emscripten_glDeleteShader; + +var _emscripten_glDeleteSync = id => { + if (!id) return; + var sync = GL.syncs[id]; + if (!sync) { + // glDeleteSync signals an error when deleting a nonexisting object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteSync(sync); + sync.name = 0; + GL.syncs[id] = null; +}; + +var _glDeleteSync = _emscripten_glDeleteSync; + +var _emscripten_glDeleteTextures = (n, textures) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((textures) + (i * 4)) >> 2)]; + var texture = GL.textures[id]; + // GL spec: "glDeleteTextures silently ignores 0s and names that do not + // correspond to existing textures". + if (!texture) continue; + GLctx.deleteTexture(texture); + texture.name = 0; + GL.textures[id] = null; + } +}; + +var _glDeleteTextures = _emscripten_glDeleteTextures; + +var _emscripten_glDeleteVertexArrays = (n, vaos) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((vaos) + (i * 4)) >> 2)]; + GLctx.deleteVertexArray(GL.vaos[id]); + GL.vaos[id] = null; + } +}; + +var _glDeleteVertexArrays = _emscripten_glDeleteVertexArrays; + +var _emscripten_glDetachShader = (program, shader) => { + GLctx.detachShader(GL.programs[program], GL.shaders[shader]); +}; + +var _glDetachShader = _emscripten_glDetachShader; + +var _emscripten_glDisable = x0 => GLctx.disable(x0); + +var _glDisable = _emscripten_glDisable; + +var _emscripten_glDisableVertexAttribArray = index => { + var cb = GL.currentContext.clientBuffers[index]; + cb.enabled = false; + GLctx.disableVertexAttribArray(index); +}; + +var _glDisableVertexAttribArray = _emscripten_glDisableVertexAttribArray; + +var _emscripten_glDrawArrays = (mode, first, count) => { + // bind any client-side buffers + GL.preDrawHandleClientVertexAttribBindings(first + count); + GLctx.drawArrays(mode, first, count); + GL.postDrawHandleClientVertexAttribBindings(); +}; + +var _glDrawArrays = _emscripten_glDrawArrays; + +var tempFixedLengthArray = []; + +var _emscripten_glDrawBuffers = (n, bufs) => { + var bufArray = tempFixedLengthArray[n]; + for (var i = 0; i < n; i++) { + bufArray[i] = HEAP32[(((bufs) + (i * 4)) >> 2)]; + } + GLctx.drawBuffers(bufArray); +}; + +var _glDrawBuffers = _emscripten_glDrawBuffers; + +var _emscripten_glEnable = x0 => GLctx.enable(x0); + +var _glEnable = _emscripten_glEnable; + +var _emscripten_glEnableVertexAttribArray = index => { + var cb = GL.currentContext.clientBuffers[index]; + cb.enabled = true; + GLctx.enableVertexAttribArray(index); +}; + +var _glEnableVertexAttribArray = _emscripten_glEnableVertexAttribArray; + +var _emscripten_glFenceSync = (condition, flags) => { + var sync = GLctx.fenceSync(condition, flags); + if (sync) { + var id = GL.getNewId(GL.syncs); + sync.name = id; + GL.syncs[id] = sync; + return id; + } + return 0; +}; + +var _glFenceSync = _emscripten_glFenceSync; + +var _emscripten_glFinish = () => GLctx.finish(); + +var _glFinish = _emscripten_glFinish; + +var _emscripten_glFlush = () => GLctx.flush(); + +var _glFlush = _emscripten_glFlush; + +var _emscripten_glFramebufferTexture2D = (target, attachment, textarget, texture, level) => { + GLctx.framebufferTexture2D(target, attachment, textarget, GL.textures[texture], level); +}; + +var _glFramebufferTexture2D = _emscripten_glFramebufferTexture2D; + +var _emscripten_glFramebufferTextureLayer = (target, attachment, texture, level, layer) => { + GLctx.framebufferTextureLayer(target, attachment, GL.textures[texture], level, layer); +}; + +var _glFramebufferTextureLayer = _emscripten_glFramebufferTextureLayer; + +var _emscripten_glGenBuffers = (n, buffers) => { + GL.genObject(n, buffers, "createBuffer", GL.buffers); +}; + +var _glGenBuffers = _emscripten_glGenBuffers; + +var _emscripten_glGenFramebuffers = (n, ids) => { + GL.genObject(n, ids, "createFramebuffer", GL.framebuffers); +}; + +var _glGenFramebuffers = _emscripten_glGenFramebuffers; + +var _emscripten_glGenTextures = (n, textures) => { + GL.genObject(n, textures, "createTexture", GL.textures); +}; + +var _glGenTextures = _emscripten_glGenTextures; + +var _emscripten_glGenVertexArrays = (n, arrays) => { + GL.genObject(n, arrays, "createVertexArray", GL.vaos); +}; + +var _glGenVertexArrays = _emscripten_glGenVertexArrays; + +var _emscripten_glGetAttribLocation = (program, name) => GLctx.getAttribLocation(GL.programs[program], UTF8ToString(name)); + +var _glGetAttribLocation = _emscripten_glGetAttribLocation; + +var _emscripten_glGetError = () => { + var error = GLctx.getError() || GL.lastError; + GL.lastError = 0; + return error; +}; + +var _glGetError = _emscripten_glGetError; + +var webglGetExtensions = () => { + var exts = getEmscriptenSupportedExtensions(GLctx); + exts = exts.concat(exts.map(e => "GL_" + e)); + return exts; +}; + +var emscriptenWebGLGet = (name_, p, type) => { + // Guard against user passing a null pointer. + // Note that GLES2 spec does not say anything about how passing a null + // pointer should be treated. Testing on desktop core GL 3, the application + // crashes on glGetIntegerv to a null pointer, but better to report an error + // instead of doing anything random. + if (!p) { + GL.recordError(1281); + return; + } + var ret = undefined; + switch (name_) { + // Handle a few trivial GLES values + case 36346: + // GL_SHADER_COMPILER + ret = 1; + break; + + case 36344: + // GL_SHADER_BINARY_FORMATS + if (type != 0 && type != 1) { + GL.recordError(1280); + } + // Do not write anything to the out pointer, since no binary formats are + // supported. + return; + + case 34814: + // GL_NUM_PROGRAM_BINARY_FORMATS + case 36345: + // GL_NUM_SHADER_BINARY_FORMATS + ret = 0; + break; + + case 34466: + // GL_NUM_COMPRESSED_TEXTURE_FORMATS + // WebGL doesn't have GL_NUM_COMPRESSED_TEXTURE_FORMATS (it's obsolete + // since GL_COMPRESSED_TEXTURE_FORMATS returns a JS array that can be + // queried for length), so implement it ourselves to allow C++ GLES2 + // code to get the length. + var formats = GLctx.getParameter(34467); + ret = formats ? formats.length : 0; + break; + + case 33309: + // GL_NUM_EXTENSIONS + if (GL.currentContext.version < 2) { + // Calling GLES3/WebGL2 function with a GLES2/WebGL1 context + GL.recordError(1282); + return; + } + ret = webglGetExtensions().length; + break; + + case 33307: + // GL_MAJOR_VERSION + case 33308: + // GL_MINOR_VERSION + if (GL.currentContext.version < 2) { + GL.recordError(1280); + // GL_INVALID_ENUM + return; + } + ret = name_ == 33307 ? 3 : 0; + // return version 3.0 + break; + } + if (ret === undefined) { + var result = GLctx.getParameter(name_); + switch (typeof result) { + case "number": + ret = result; + break; + + case "boolean": + ret = result ? 1 : 0; + break; + + case "string": + GL.recordError(1280); + // GL_INVALID_ENUM + return; + + case "object": + if (result === null) { + // null is a valid result for some (e.g., which buffer is bound - + // perhaps nothing is bound), but otherwise can mean an invalid + // name_, which we need to report as an error + switch (name_) { + case 34964: + // ARRAY_BUFFER_BINDING + case 35725: + // CURRENT_PROGRAM + case 34965: + // ELEMENT_ARRAY_BUFFER_BINDING + case 36006: + // FRAMEBUFFER_BINDING or DRAW_FRAMEBUFFER_BINDING + case 36007: + // RENDERBUFFER_BINDING + case 32873: + // TEXTURE_BINDING_2D + case 34229: + // WebGL 2 GL_VERTEX_ARRAY_BINDING, or WebGL 1 extension OES_vertex_array_object GL_VERTEX_ARRAY_BINDING_OES + case 36662: + // COPY_READ_BUFFER_BINDING or COPY_READ_BUFFER + case 36663: + // COPY_WRITE_BUFFER_BINDING or COPY_WRITE_BUFFER + case 35053: + // PIXEL_PACK_BUFFER_BINDING + case 35055: + // PIXEL_UNPACK_BUFFER_BINDING + case 36010: + // READ_FRAMEBUFFER_BINDING + case 35097: + // SAMPLER_BINDING + case 35869: + // TEXTURE_BINDING_2D_ARRAY + case 32874: + // TEXTURE_BINDING_3D + case 36389: + // TRANSFORM_FEEDBACK_BINDING + case 35983: + // TRANSFORM_FEEDBACK_BUFFER_BINDING + case 35368: + // UNIFORM_BUFFER_BINDING + case 34068: + { + // TEXTURE_BINDING_CUBE_MAP + ret = 0; + break; + } + + default: + { + GL.recordError(1280); + // GL_INVALID_ENUM + return; + } + } + } else if (result instanceof Float32Array || result instanceof Uint32Array || result instanceof Int32Array || result instanceof Array) { + for (var i = 0; i < result.length; ++i) { + switch (type) { + case 0: + HEAP32[(((p) + (i * 4)) >> 2)] = result[i]; + break; + + case 2: + HEAPF32[(((p) + (i * 4)) >> 2)] = result[i]; + break; + + case 4: + HEAP8[(p) + (i)] = result[i] ? 1 : 0; + break; + } + } + return; + } else { + try { + ret = result.name | 0; + } catch (e) { + GL.recordError(1280); + // GL_INVALID_ENUM + err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`); + return; + } + } + break; + + default: + GL.recordError(1280); + // GL_INVALID_ENUM + err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof (result)}!`); + return; + } + } + switch (type) { + case 1: + writeI53ToI64(p, ret); + break; + + case 0: + HEAP32[((p) >> 2)] = ret; + break; + + case 2: + HEAPF32[((p) >> 2)] = ret; + break; + + case 4: + HEAP8[p] = ret ? 1 : 0; + break; + } +}; + +var _emscripten_glGetFloatv = (name_, p) => emscriptenWebGLGet(name_, p, 2); + +var _glGetFloatv = _emscripten_glGetFloatv; + +var _emscripten_glGetIntegerv = (name_, p) => emscriptenWebGLGet(name_, p, 0); + +var _glGetIntegerv = _emscripten_glGetIntegerv; + +var _emscripten_glGetProgramiv = (program, pname, p) => { + if (!p) { + // GLES2 specification does not specify how to behave if p is a null + // pointer. Since calling this function does not make sense if p == null, + // issue a GL error to notify user about it. + GL.recordError(1281); + return; + } + if (program >= GL.counter) { + GL.recordError(1281); + return; + } + program = GL.programs[program]; + if (pname == 35716) { + // GL_INFO_LOG_LENGTH + var log = GLctx.getProgramInfoLog(program); + if (log === null) log = "(unknown error)"; + HEAP32[((p) >> 2)] = log.length + 1; + } else if (pname == 35719) { + if (!program.maxUniformLength) { + var numActiveUniforms = GLctx.getProgramParameter(program, 35718); + for (var i = 0; i < numActiveUniforms; ++i) { + program.maxUniformLength = Math.max(program.maxUniformLength, GLctx.getActiveUniform(program, i).name.length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxUniformLength; + } else if (pname == 35722) { + if (!program.maxAttributeLength) { + var numActiveAttributes = GLctx.getProgramParameter(program, 35721); + for (var i = 0; i < numActiveAttributes; ++i) { + program.maxAttributeLength = Math.max(program.maxAttributeLength, GLctx.getActiveAttrib(program, i).name.length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxAttributeLength; + } else if (pname == 35381) { + if (!program.maxUniformBlockNameLength) { + var numActiveUniformBlocks = GLctx.getProgramParameter(program, 35382); + for (var i = 0; i < numActiveUniformBlocks; ++i) { + program.maxUniformBlockNameLength = Math.max(program.maxUniformBlockNameLength, GLctx.getActiveUniformBlockName(program, i).length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxUniformBlockNameLength; + } else { + HEAP32[((p) >> 2)] = GLctx.getProgramParameter(program, pname); + } +}; + +var _glGetProgramiv = _emscripten_glGetProgramiv; + +var _emscripten_glGetShaderInfoLog = (shader, maxLength, length, infoLog) => { + var log = GLctx.getShaderInfoLog(GL.shaders[shader]); + if (log === null) log = "(unknown error)"; + var numBytesWrittenExclNull = (maxLength > 0 && infoLog) ? stringToUTF8(log, infoLog, maxLength) : 0; + if (length) HEAP32[((length) >> 2)] = numBytesWrittenExclNull; +}; + +var _glGetShaderInfoLog = _emscripten_glGetShaderInfoLog; + +var _emscripten_glGetShaderiv = (shader, pname, p) => { + if (!p) { + // GLES2 specification does not specify how to behave if p is a null + // pointer. Since calling this function does not make sense if p == null, + // issue a GL error to notify user about it. + GL.recordError(1281); + return; + } + if (pname == 35716) { + // GL_INFO_LOG_LENGTH + var log = GLctx.getShaderInfoLog(GL.shaders[shader]); + if (log === null) log = "(unknown error)"; + // The GLES2 specification says that if the shader has an empty info log, + // a value of 0 is returned. Otherwise the log has a null char appended. + // (An empty string is falsey, so we can just check that instead of + // looking at log.length.) + var logLength = log ? log.length + 1 : 0; + HEAP32[((p) >> 2)] = logLength; + } else if (pname == 35720) { + // GL_SHADER_SOURCE_LENGTH + var source = GLctx.getShaderSource(GL.shaders[shader]); + // source may be a null, or the empty string, both of which are falsey + // values that we report a 0 length for. + var sourceLength = source ? source.length + 1 : 0; + HEAP32[((p) >> 2)] = sourceLength; + } else { + HEAP32[((p) >> 2)] = GLctx.getShaderParameter(GL.shaders[shader], pname); + } +}; + +var _glGetShaderiv = _emscripten_glGetShaderiv; + +var _emscripten_glGetString = name_ => { + var ret = GL.stringCache[name_]; + if (!ret) { + switch (name_) { + case 7939: + ret = stringToNewUTF8(webglGetExtensions().join(" ")); + break; + + case 7936: + case 7937: + case 37445: + case 37446: + var s = GLctx.getParameter(name_); + if (!s) { + GL.recordError(1280); + } + ret = s ? stringToNewUTF8(s) : 0; + break; + + case 7938: + var webGLVersion = GLctx.getParameter(7938); + // return GLES version string corresponding to the version of the WebGL context + var glVersion = `OpenGL ES 2.0 (${webGLVersion})`; + if (GL.currentContext.version >= 2) glVersion = `OpenGL ES 3.0 (${webGLVersion})`; + ret = stringToNewUTF8(glVersion); + break; + + case 35724: + var glslVersion = GLctx.getParameter(35724); + // extract the version number 'N.M' from the string 'WebGL GLSL ES N.M ...' + var ver_re = /^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/; + var ver_num = glslVersion.match(ver_re); + if (ver_num !== null) { + if (ver_num[1].length == 3) ver_num[1] = ver_num[1] + "0"; + // ensure minor version has 2 digits + glslVersion = `OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`; + } + ret = stringToNewUTF8(glslVersion); + break; + + default: + GL.recordError(1280); + } + GL.stringCache[name_] = ret; + } + return ret; +}; + +var _glGetString = _emscripten_glGetString; + +var _emscripten_glGetUniformBlockIndex = (program, uniformBlockName) => GLctx.getUniformBlockIndex(GL.programs[program], UTF8ToString(uniformBlockName)); + +var _glGetUniformBlockIndex = _emscripten_glGetUniformBlockIndex; + +/** @suppress {checkTypes} */ var jstoi_q = str => parseInt(str); + +/** @noinline */ var webglGetLeftBracePos = name => name.slice(-1) == "]" && name.lastIndexOf("["); + +var webglPrepareUniformLocationsBeforeFirstUse = program => { + var uniformLocsById = program.uniformLocsById, // Maps GLuint -> WebGLUniformLocation + uniformSizeAndIdsByName = program.uniformSizeAndIdsByName, // Maps name -> [uniform array length, GLuint] + i, j; + // On the first time invocation of glGetUniformLocation on this shader program: + // initialize cache data structures and discover which uniforms are arrays. + if (!uniformLocsById) { + // maps GLint integer locations to WebGLUniformLocations + program.uniformLocsById = uniformLocsById = {}; + // maps integer locations back to uniform name strings, so that we can lazily fetch uniform array locations + program.uniformArrayNamesById = {}; + var numActiveUniforms = GLctx.getProgramParameter(program, 35718); + for (i = 0; i < numActiveUniforms; ++i) { + var u = GLctx.getActiveUniform(program, i); + var nm = u.name; + var sz = u.size; + var lb = webglGetLeftBracePos(nm); + var arrayName = lb > 0 ? nm.slice(0, lb) : nm; + // Assign a new location. + var id = program.uniformIdCounter; + program.uniformIdCounter += sz; + // Eagerly get the location of the uniformArray[0] base element. + // The remaining indices >0 will be left for lazy evaluation to + // improve performance. Those may never be needed to fetch, if the + // application fills arrays always in full starting from the first + // element of the array. + uniformSizeAndIdsByName[arrayName] = [ sz, id ]; + // Store placeholder integers in place that highlight that these + // >0 index locations are array indices pending population. + for (j = 0; j < sz; ++j) { + uniformLocsById[id] = j; + program.uniformArrayNamesById[id++] = arrayName; + } + } + } +}; + +var _emscripten_glGetUniformLocation = (program, name) => { + name = UTF8ToString(name); + if (program = GL.programs[program]) { + webglPrepareUniformLocationsBeforeFirstUse(program); + var uniformLocsById = program.uniformLocsById; + // Maps GLuint -> WebGLUniformLocation + var arrayIndex = 0; + var uniformBaseName = name; + // Invariant: when populating integer IDs for uniform locations, we must + // maintain the precondition that arrays reside in contiguous addresses, + // i.e. for a 'vec4 colors[10];', colors[4] must be at location + // colors[0]+4. However, user might call glGetUniformLocation(program, + // "colors") for an array, so we cannot discover based on the user input + // arguments whether the uniform we are dealing with is an array. The only + // way to discover which uniforms are arrays is to enumerate over all the + // active uniforms in the program. + var leftBrace = webglGetLeftBracePos(name); + // If user passed an array accessor "[index]", parse the array index off the accessor. + if (leftBrace > 0) { + arrayIndex = jstoi_q(name.slice(leftBrace + 1)) >>> 0; + // "index]", coerce parseInt(']') with >>>0 to treat "foo[]" as "foo[0]" and foo[-1] as unsigned out-of-bounds. + uniformBaseName = name.slice(0, leftBrace); + } + // Have we cached the location of this uniform before? + // A pair [array length, GLint of the uniform location] + var sizeAndId = program.uniformSizeAndIdsByName[uniformBaseName]; + // If a uniform with this name exists, and if its index is within the + // array limits (if it's even an array), query the WebGLlocation, or + // return an existing cached location. + if (sizeAndId && arrayIndex < sizeAndId[0]) { + arrayIndex += sizeAndId[1]; + // Add the base location of the uniform to the array index offset. + if ((uniformLocsById[arrayIndex] = uniformLocsById[arrayIndex] || GLctx.getUniformLocation(program, name))) { + return arrayIndex; + } + } + } else { + // N.b. we are currently unable to distinguish between GL program IDs that + // never existed vs GL program IDs that have been deleted, so report + // GL_INVALID_VALUE in both cases. + GL.recordError(1281); + } + return -1; +}; + +var _glGetUniformLocation = _emscripten_glGetUniformLocation; + +var _emscripten_glLineWidth = x0 => GLctx.lineWidth(x0); + +var _glLineWidth = _emscripten_glLineWidth; + +var _emscripten_glLinkProgram = program => { + program = GL.programs[program]; + GLctx.linkProgram(program); + // Invalidate earlier computed uniform->ID mappings, those have now become stale + program.uniformLocsById = 0; + // Mark as null-like so that glGetUniformLocation() knows to populate this again. + program.uniformSizeAndIdsByName = {}; +}; + +var _glLinkProgram = _emscripten_glLinkProgram; + +var _emscripten_glPixelStorei = (pname, param) => { + if (pname == 3317) { + GL.unpackAlignment = param; + } else if (pname == 3314) { + GL.unpackRowLength = param; + } + GLctx.pixelStorei(pname, param); +}; + +var _glPixelStorei = _emscripten_glPixelStorei; + +var computeUnpackAlignedImageSize = (width, height, sizePerPixel) => { + function roundedToNextMultipleOf(x, y) { + return (x + y - 1) & -y; + } + var plainRowSize = (GL.unpackRowLength || width) * sizePerPixel; + var alignedRowSize = roundedToNextMultipleOf(plainRowSize, GL.unpackAlignment); + return height * alignedRowSize; +}; + +var colorChannelsInGlTextureFormat = format => { + // Micro-optimizations for size: map format to size by subtracting smallest + // enum value (0x1902) from all values first. Also omit the most common + // size value (1) from the list, which is assumed by formats not on the + // list. + var colorChannels = { + // 0x1902 /* GL_DEPTH_COMPONENT */ - 0x1902: 1, + // 0x1906 /* GL_ALPHA */ - 0x1902: 1, + 5: 3, + 6: 4, + // 0x1909 /* GL_LUMINANCE */ - 0x1902: 1, + 8: 2, + 29502: 3, + 29504: 4, + // 0x1903 /* GL_RED */ - 0x1902: 1, + 26917: 2, + 26918: 2, + // 0x8D94 /* GL_RED_INTEGER */ - 0x1902: 1, + 29846: 3, + 29847: 4 + }; + return colorChannels[format - 6402] || 1; +}; + +var heapObjectForWebGLType = type => { + // Micro-optimization for size: Subtract lowest GL enum number (0x1400/* GL_BYTE */) from type to compare + // smaller values for the heap, for shorter generated code size. + // Also the type HEAPU16 is not tested for explicitly, but any unrecognized type will return out HEAPU16. + // (since most types are HEAPU16) + type -= 5120; + if (type == 0) return HEAP8; + if (type == 1) return HEAPU8; + if (type == 2) return HEAP16; + if (type == 4) return HEAP32; + if (type == 6) return HEAPF32; + if (type == 5 || type == 28922 || type == 28520 || type == 30779 || type == 30782) return HEAPU32; + return HEAPU16; +}; + +var toTypedArrayIndex = (pointer, heap) => pointer >>> (31 - Math.clz32(heap.BYTES_PER_ELEMENT)); + +var emscriptenWebGLGetTexPixelData = (type, format, width, height, pixels, internalFormat) => { + var heap = heapObjectForWebGLType(type); + var sizePerPixel = colorChannelsInGlTextureFormat(format) * heap.BYTES_PER_ELEMENT; + var bytes = computeUnpackAlignedImageSize(width, height, sizePerPixel); + return heap.subarray(toTypedArrayIndex(pixels, heap), toTypedArrayIndex(pixels + bytes, heap)); +}; + +var _emscripten_glReadPixels = (x, y, width, height, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelPackBufferBinding) { + GLctx.readPixels(x, y, width, height, format, type, pixels); + return; + } + var heap = heapObjectForWebGLType(type); + var target = toTypedArrayIndex(pixels, heap); + GLctx.readPixels(x, y, width, height, format, type, heap, target); + return; + } + var pixelData = emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, format); + if (!pixelData) { + GL.recordError(1280); + return; + } + GLctx.readPixels(x, y, width, height, format, type, pixelData); +}; + +var _glReadPixels = _emscripten_glReadPixels; + +var _emscripten_glShaderSource = (shader, count, string, length) => { + var source = GL.getSource(shader, count, string, length); + GLctx.shaderSource(GL.shaders[shader], source); +}; + +var _glShaderSource = _emscripten_glShaderSource; + +var _emscripten_glTexImage2D = (target, level, internalFormat, width, height, border, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixels); + return; + } + if (pixels) { + var heap = heapObjectForWebGLType(type); + var index = toTypedArrayIndex(pixels, heap); + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, heap, index); + return; + } + } + var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, internalFormat) : null; + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixelData); +}; + +var _glTexImage2D = _emscripten_glTexImage2D; + +var _emscripten_glTexParameterf = (x0, x1, x2) => GLctx.texParameterf(x0, x1, x2); + +var _glTexParameterf = _emscripten_glTexParameterf; + +var _emscripten_glTexParameterfv = (target, pname, params) => { + var param = HEAPF32[((params) >> 2)]; + GLctx.texParameterf(target, pname, param); +}; + +var _glTexParameterfv = _emscripten_glTexParameterfv; + +var _emscripten_glTexParameteri = (x0, x1, x2) => GLctx.texParameteri(x0, x1, x2); + +var _glTexParameteri = _emscripten_glTexParameteri; + +var _emscripten_glTexStorage2D = (x0, x1, x2, x3, x4) => GLctx.texStorage2D(x0, x1, x2, x3, x4); + +var _glTexStorage2D = _emscripten_glTexStorage2D; + +var _emscripten_glTexStorage3D = (x0, x1, x2, x3, x4, x5) => GLctx.texStorage3D(x0, x1, x2, x3, x4, x5); + +var _glTexStorage3D = _emscripten_glTexStorage3D; + +var _emscripten_glTexSubImage2D = (target, level, xoffset, yoffset, width, height, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + return; + } + if (pixels) { + var heap = heapObjectForWebGLType(type); + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, heap, toTypedArrayIndex(pixels, heap)); + return; + } + } + var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, 0) : null; + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixelData); +}; + +var _glTexSubImage2D = _emscripten_glTexSubImage2D; + +var _emscripten_glTexSubImage3D = (target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels) => { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels); + } else if (pixels) { + var heap = heapObjectForWebGLType(type); + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, heap, toTypedArrayIndex(pixels, heap)); + } else { + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, null); + } +}; + +var _glTexSubImage3D = _emscripten_glTexSubImage3D; + +var webglGetUniformLocation = location => { + var p = GLctx.currentProgram; + if (p) { + var webglLoc = p.uniformLocsById[location]; + // p.uniformLocsById[location] stores either an integer, or a + // WebGLUniformLocation. + // If an integer, we have not yet bound the location, so do it now. The + // integer value specifies the array index we should bind to. + if (typeof webglLoc == "number") { + p.uniformLocsById[location] = webglLoc = GLctx.getUniformLocation(p, p.uniformArrayNamesById[location] + (webglLoc > 0 ? `[${webglLoc}]` : "")); + } + // Else an already cached WebGLUniformLocation, return it. + return webglLoc; + } else { + GL.recordError(1282); + } +}; + +var _emscripten_glUniform1f = (location, v0) => { + GLctx.uniform1f(webglGetUniformLocation(location), v0); +}; + +var _glUniform1f = _emscripten_glUniform1f; + +var miniTempWebGLFloatBuffers = []; + +var _emscripten_glUniform1fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform1fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count); + return; + } + if (count <= 288) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; ++i) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 4) >> 2)); + } + GLctx.uniform1fv(webglGetUniformLocation(location), view); +}; + +var _glUniform1fv = _emscripten_glUniform1fv; + +var _emscripten_glUniform1i = (location, v0) => { + GLctx.uniform1i(webglGetUniformLocation(location), v0); +}; + +var _glUniform1i = _emscripten_glUniform1i; + +var _emscripten_glUniform2f = (location, v0, v1) => { + GLctx.uniform2f(webglGetUniformLocation(location), v0, v1); +}; + +var _glUniform2f = _emscripten_glUniform2f; + +var _emscripten_glUniform2fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform2fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count * 2); + return; + } + if (count <= 144) { + // avoid allocation when uploading few enough uniforms + count *= 2; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 2) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 8) >> 2)); + } + GLctx.uniform2fv(webglGetUniformLocation(location), view); +}; + +var _glUniform2fv = _emscripten_glUniform2fv; + +var _emscripten_glUniform3f = (location, v0, v1, v2) => { + GLctx.uniform3f(webglGetUniformLocation(location), v0, v1, v2); +}; + +var _glUniform3f = _emscripten_glUniform3f; + +var _emscripten_glUniform4f = (location, v0, v1, v2, v3) => { + GLctx.uniform4f(webglGetUniformLocation(location), v0, v1, v2, v3); +}; + +var _glUniform4f = _emscripten_glUniform4f; + +var _emscripten_glUniform4fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform4fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[4 * count]; + // hoist the heap out of the loop for size and for pthreads+growth. + var heap = HEAPF32; + value = ((value) >> 2); + count *= 4; + for (var i = 0; i < count; i += 4) { + var dst = value + i; + view[i] = heap[dst]; + view[i + 1] = heap[dst + 1]; + view[i + 2] = heap[dst + 2]; + view[i + 3] = heap[dst + 3]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniform4fv(webglGetUniformLocation(location), view); +}; + +var _glUniform4fv = _emscripten_glUniform4fv; + +var miniTempWebGLIntBuffers = []; + +var _emscripten_glUniform4iv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform4iv(webglGetUniformLocation(location), HEAP32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + count *= 4; + var view = miniTempWebGLIntBuffers[count]; + for (var i = 0; i < count; i += 4) { + view[i] = HEAP32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAP32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAP32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAP32[(((value) + (4 * i + 12)) >> 2)]; + } + } else { + var view = HEAP32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniform4iv(webglGetUniformLocation(location), view); +}; + +var _glUniform4iv = _emscripten_glUniform4iv; + +var _emscripten_glUniformBlockBinding = (program, uniformBlockIndex, uniformBlockBinding) => { + program = GL.programs[program]; + GLctx.uniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); +}; + +var _glUniformBlockBinding = _emscripten_glUniformBlockBinding; + +var _emscripten_glUniformMatrix2fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + count *= 4; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 4) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAPF32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAPF32[(((value) + (4 * i + 12)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix2fv = _emscripten_glUniformMatrix2fv; + +var _emscripten_glUniformMatrix3fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 9); + return; + } + if (count <= 32) { + // avoid allocation when uploading few enough uniforms + count *= 9; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 9) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAPF32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAPF32[(((value) + (4 * i + 12)) >> 2)]; + view[i + 4] = HEAPF32[(((value) + (4 * i + 16)) >> 2)]; + view[i + 5] = HEAPF32[(((value) + (4 * i + 20)) >> 2)]; + view[i + 6] = HEAPF32[(((value) + (4 * i + 24)) >> 2)]; + view[i + 7] = HEAPF32[(((value) + (4 * i + 28)) >> 2)]; + view[i + 8] = HEAPF32[(((value) + (4 * i + 32)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 36) >> 2)); + } + GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix3fv = _emscripten_glUniformMatrix3fv; + +var _emscripten_glUniformMatrix4fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 16); + return; + } + if (count <= 18) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[16 * count]; + // hoist the heap out of the loop for size and for pthreads+growth. + var heap = HEAPF32; + value = ((value) >> 2); + count *= 16; + for (var i = 0; i < count; i += 16) { + var dst = value + i; + view[i] = heap[dst]; + view[i + 1] = heap[dst + 1]; + view[i + 2] = heap[dst + 2]; + view[i + 3] = heap[dst + 3]; + view[i + 4] = heap[dst + 4]; + view[i + 5] = heap[dst + 5]; + view[i + 6] = heap[dst + 6]; + view[i + 7] = heap[dst + 7]; + view[i + 8] = heap[dst + 8]; + view[i + 9] = heap[dst + 9]; + view[i + 10] = heap[dst + 10]; + view[i + 11] = heap[dst + 11]; + view[i + 12] = heap[dst + 12]; + view[i + 13] = heap[dst + 13]; + view[i + 14] = heap[dst + 14]; + view[i + 15] = heap[dst + 15]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 64) >> 2)); + } + GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix4fv = _emscripten_glUniformMatrix4fv; + +var _emscripten_glUseProgram = program => { + program = GL.programs[program]; + GLctx.useProgram(program); + // Record the currently active program so that we can access the uniform + // mapping table of that program. + GLctx.currentProgram = program; +}; + +var _glUseProgram = _emscripten_glUseProgram; + +var _emscripten_glVertexAttribPointer = (index, size, type, normalized, stride, ptr) => { + var cb = GL.currentContext.clientBuffers[index]; + if (!GLctx.currentArrayBufferBinding) { + cb.size = size; + cb.type = type; + cb.normalized = normalized; + cb.stride = stride; + cb.ptr = ptr; + cb.clientside = true; + cb.vertexAttribPointerAdaptor = function(index, size, type, normalized, stride, ptr) { + this.vertexAttribPointer(index, size, type, normalized, stride, ptr); + }; + return; + } + cb.clientside = false; + GLctx.vertexAttribPointer(index, size, type, !!normalized, stride, ptr); +}; + +var _glVertexAttribPointer = _emscripten_glVertexAttribPointer; + +var _emscripten_glViewport = (x0, x1, x2, x3) => GLctx.viewport(x0, x1, x2, x3); + +var _glViewport = _emscripten_glViewport; + +function _mediapipe_find_canvas_event_target(canvasSelector) { + let target = findCanvasEventTarget(canvasSelector); + // WebGPU-on-worker uses this function to try to grab the canvas, but + // doesn't have a DOM element to find. So as a quick patch, if the default + // behavior is unsuccessful here then we try a webgpu canvas property + // which is set by the user directly on the Module, much like how our old + // pipeline used the Module.canvas property. See b/265271517 for details. + if (Module && !target) { + target = Module.canvasWebGpu; + } + return Emval.toHandle(target); +} + +function _mediapipe_webgl_tex_image_drawable(drawableHandle) { + const drawable = Emval.toValue(drawableHandle); + GLctx.texImage2D(GLctx.TEXTURE_2D, 0, GLctx.RGBA, GLctx.RGBA, GLctx.UNSIGNED_BYTE, drawable); +} + +var _random_get = (buffer, size) => randomFill(HEAPU8.subarray(buffer, buffer + size)); + +var _wgpuCommandEncoderBeginComputePass = (encoderPtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "timestampWrites": WebGPU.makePassTimestampWrites(HEAPU32[(((descriptor) + (12)) >> 2)]) + }; + } + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateComputePassEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.beginComputePass(desc)); + return ptr; +}; + +var _wgpuCommandEncoderBeginRenderPass = (encoderPtr, descriptor) => { + function makeColorAttachment(caPtr) { + var viewPtr = HEAPU32[(((caPtr) + (4)) >> 2)]; + if (viewPtr === 0) { + // Null `view` means no attachment in this slot. + return undefined; + } + var depthSlice = HEAPU32[(((caPtr) + (8)) >> 2)]; + if (depthSlice == 4294967295) depthSlice = undefined; + return { + "view": WebGPU.getJsObject(viewPtr), + "depthSlice": depthSlice, + "resolveTarget": WebGPU.getJsObject(HEAPU32[(((caPtr) + (12)) >> 2)]), + "clearValue": WebGPU.makeColor(caPtr + 24), + "loadOp": WebGPU.LoadOp[HEAP32[(((caPtr) + (16)) >> 2)]], + "storeOp": WebGPU.StoreOp[HEAP32[(((caPtr) + (20)) >> 2)]] + }; + } + function makeColorAttachments(count, caPtr) { + var attachments = []; + for (var i = 0; i < count; ++i) { + attachments.push(makeColorAttachment(caPtr + 56 * i)); + } + return attachments; + } + function makeDepthStencilAttachment(dsaPtr) { + if (dsaPtr === 0) return undefined; + return { + "view": WebGPU.getJsObject(HEAPU32[(((dsaPtr) + (4)) >> 2)]), + "depthClearValue": HEAPF32[(((dsaPtr) + (16)) >> 2)], + "depthLoadOp": WebGPU.LoadOp[HEAP32[(((dsaPtr) + (8)) >> 2)]], + "depthStoreOp": WebGPU.StoreOp[HEAP32[(((dsaPtr) + (12)) >> 2)]], + "depthReadOnly": !!(HEAPU32[(((dsaPtr) + (20)) >> 2)]), + "stencilClearValue": HEAPU32[(((dsaPtr) + (32)) >> 2)], + "stencilLoadOp": WebGPU.LoadOp[HEAP32[(((dsaPtr) + (24)) >> 2)]], + "stencilStoreOp": WebGPU.StoreOp[HEAP32[(((dsaPtr) + (28)) >> 2)]], + "stencilReadOnly": !!(HEAPU32[(((dsaPtr) + (36)) >> 2)]) + }; + } + function makeRenderPassDescriptor(descriptor) { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var maxDrawCount = undefined; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var renderPassMaxDrawCount = nextInChainPtr; + // Note: The user could have passed a really huge value here, which is technically valid in + // C but will not be allowed by WebGPU in JS because of [EnforceRange]. We intentionally + // ignore that case because it's not useful - apps can just pick a smaller maxDrawCount. + maxDrawCount = readI53FromI64((renderPassMaxDrawCount) + (8)); + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "colorAttachments": makeColorAttachments(HEAPU32[(((descriptor) + (12)) >> 2)], HEAPU32[(((descriptor) + (16)) >> 2)]), + "depthStencilAttachment": makeDepthStencilAttachment(HEAPU32[(((descriptor) + (20)) >> 2)]), + "occlusionQuerySet": WebGPU.getJsObject(HEAPU32[(((descriptor) + (24)) >> 2)]), + "timestampWrites": WebGPU.makePassTimestampWrites(HEAPU32[(((descriptor) + (28)) >> 2)]), + "maxDrawCount": maxDrawCount + }; + return desc; + } + var desc = makeRenderPassDescriptor(descriptor); + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateRenderPassEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.beginRenderPass(desc)); + return ptr; +}; + +var _wgpuCommandEncoderCopyBufferToTexture = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyBufferToTexture(WebGPU.makeTexelCopyBufferInfo(srcPtr), WebGPU.makeTexelCopyTextureInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderCopyTextureToBuffer = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyTextureToBuffer(WebGPU.makeTexelCopyTextureInfo(srcPtr), WebGPU.makeTexelCopyBufferInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderCopyTextureToTexture = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyTextureToTexture(WebGPU.makeTexelCopyTextureInfo(srcPtr), WebGPU.makeTexelCopyTextureInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderFinish = (encoderPtr, descriptor) => { + // TODO: Use the descriptor. + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateCommandBuffer(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.finish()); + return ptr; +}; + +var _wgpuComputePassEncoderDispatchWorkgroups = (passPtr, x, y, z) => { + var pass = WebGPU.getJsObject(passPtr); + pass.dispatchWorkgroups(x, y, z); +}; + +var _wgpuComputePassEncoderEnd = passPtr => { + var pass = WebGPU.getJsObject(passPtr); + pass.end(); +}; + +var _wgpuComputePassEncoderSetBindGroup = (passPtr, groupIndex, groupPtr, dynamicOffsetCount, dynamicOffsetsPtr) => { + var pass = WebGPU.getJsObject(passPtr); + var group = WebGPU.getJsObject(groupPtr); + if (dynamicOffsetCount == 0) { + pass.setBindGroup(groupIndex, group); + } else { + pass.setBindGroup(groupIndex, group, HEAPU32, ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount); + } +}; + +var _wgpuComputePassEncoderSetPipeline = (passPtr, pipelinePtr) => { + var pass = WebGPU.getJsObject(passPtr); + var pipeline = WebGPU.getJsObject(pipelinePtr); + pass.setPipeline(pipeline); +}; + +var _wgpuComputePipelineGetBindGroupLayout = (pipelinePtr, groupIndex) => { + var pipeline = WebGPU.getJsObject(pipelinePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, pipeline.getBindGroupLayout(groupIndex)); + return ptr; +}; + +var _wgpuDeviceCreateBindGroup = (devicePtr, descriptor) => { + function makeEntry(entryPtr) { + var bufferPtr = HEAPU32[(((entryPtr) + (8)) >> 2)]; + var samplerPtr = HEAPU32[(((entryPtr) + (32)) >> 2)]; + var textureViewPtr = HEAPU32[(((entryPtr) + (36)) >> 2)]; + var externalTexturePtr = 0; + WebGPU.iterateExtensions(entryPtr, { + 14: ptr => { + externalTexturePtr = HEAPU32[(((ptr) + (8)) >> 2)]; + } + }); + var resource; + if (bufferPtr) { + // Note the sentinel UINT64_MAX will be read as -1. + var size = readI53FromI64((entryPtr) + (24)); + if (size == -1) size = undefined; + resource = { + "buffer": WebGPU.getJsObject(bufferPtr), + "offset": readI53FromI64((entryPtr) + (16)), + "size": size + }; + } else { + resource = WebGPU.getJsObject(samplerPtr || textureViewPtr || externalTexturePtr); + } + return { + "binding": HEAPU32[(((entryPtr) + (4)) >> 2)], + "resource": resource + }; + } + function makeEntries(count, entriesPtrs) { + var entries = []; + for (var i = 0; i < count; ++i) { + entries.push(makeEntry(entriesPtrs + 40 * i)); + } + return entries; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.getJsObject(HEAPU32[(((descriptor) + (12)) >> 2)]), + "entries": makeEntries(HEAPU32[(((descriptor) + (16)) >> 2)], HEAPU32[(((descriptor) + (20)) >> 2)]) + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateBindGroup(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createBindGroup(desc)); + return ptr; +}; + +var _wgpuDeviceCreateBindGroupLayout = (devicePtr, descriptor) => { + function makeBufferEntry(substructPtr) { + var typeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!typeInt) return undefined; + return { + "type": WebGPU.BufferBindingType[typeInt], + "hasDynamicOffset": !!(HEAPU32[(((substructPtr) + (8)) >> 2)]), + "minBindingSize": readI53FromI64((substructPtr) + (16)) + }; + } + function makeSamplerEntry(substructPtr) { + var typeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!typeInt) return undefined; + return { + "type": WebGPU.SamplerBindingType[typeInt] + }; + } + function makeTextureEntry(substructPtr) { + var sampleTypeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!sampleTypeInt) return undefined; + return { + "sampleType": WebGPU.TextureSampleType[sampleTypeInt], + "viewDimension": WebGPU.TextureViewDimension[HEAP32[(((substructPtr) + (8)) >> 2)]], + "multisampled": !!(HEAPU32[(((substructPtr) + (12)) >> 2)]) + }; + } + function makeStorageTextureEntry(substructPtr) { + var accessInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!accessInt) return undefined; + return { + "access": WebGPU.StorageTextureAccess[accessInt], + "format": WebGPU.TextureFormat[HEAP32[(((substructPtr) + (8)) >> 2)]], + "viewDimension": WebGPU.TextureViewDimension[HEAP32[(((substructPtr) + (12)) >> 2)]] + }; + } + function makeEntry(entryPtr) { + var entry = { + "binding": HEAPU32[(((entryPtr) + (4)) >> 2)], + "visibility": HEAPU32[(((entryPtr) + (8)) >> 2)], + "buffer": makeBufferEntry(entryPtr + 24), + "sampler": makeSamplerEntry(entryPtr + 48), + "texture": makeTextureEntry(entryPtr + 56), + "storageTexture": makeStorageTextureEntry(entryPtr + 72) + }; + WebGPU.iterateExtensions(entryPtr, { + 13: ptr => { + entry["externalTexture"] = {}; + } + }); + return entry; + } + function makeEntries(count, entriesPtrs) { + var entries = []; + for (var i = 0; i < count; ++i) { + entries.push(makeEntry(entriesPtrs + 88 * i)); + } + return entries; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "entries": makeEntries(HEAPU32[(((descriptor) + (12)) >> 2)], HEAPU32[(((descriptor) + (16)) >> 2)]) + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createBindGroupLayout(desc)); + return ptr; +}; + +var _wgpuDeviceCreateCommandEncoder = (devicePtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4) + }; + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateCommandEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createCommandEncoder(desc)); + return ptr; +}; + +var _wgpuDeviceCreateComputePipeline = (devicePtr, descriptor) => { + var desc = WebGPU.makeComputePipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateComputePipeline(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createComputePipeline(desc)); + return ptr; +}; + +var _wgpuDeviceCreatePipelineLayout = (devicePtr, descriptor) => { + var bglCount = HEAPU32[(((descriptor) + (12)) >> 2)]; + var bglPtr = HEAPU32[(((descriptor) + (16)) >> 2)]; + var bgls = []; + for (var i = 0; i < bglCount; ++i) { + bgls.push(WebGPU.getJsObject(HEAPU32[(((bglPtr) + (4 * i)) >> 2)])); + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "bindGroupLayouts": bgls + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreatePipelineLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createPipelineLayout(desc)); + return ptr; +}; + +var _wgpuDeviceCreateRenderPipeline = (devicePtr, descriptor) => { + var desc = WebGPU.makeRenderPipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateRenderPipeline(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createRenderPipeline(desc)); + return ptr; +}; + +var _wgpuDeviceCreateSampler = (devicePtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "addressModeU": WebGPU.AddressMode[HEAP32[(((descriptor) + (12)) >> 2)]], + "addressModeV": WebGPU.AddressMode[HEAP32[(((descriptor) + (16)) >> 2)]], + "addressModeW": WebGPU.AddressMode[HEAP32[(((descriptor) + (20)) >> 2)]], + "magFilter": WebGPU.FilterMode[HEAP32[(((descriptor) + (24)) >> 2)]], + "minFilter": WebGPU.FilterMode[HEAP32[(((descriptor) + (28)) >> 2)]], + "mipmapFilter": WebGPU.MipmapFilterMode[HEAP32[(((descriptor) + (32)) >> 2)]], + "lodMinClamp": HEAPF32[(((descriptor) + (36)) >> 2)], + "lodMaxClamp": HEAPF32[(((descriptor) + (40)) >> 2)], + "compare": WebGPU.CompareFunction[HEAP32[(((descriptor) + (44)) >> 2)]], + "maxAnisotropy": HEAPU16[(((descriptor) + (48)) >> 1)] + }; + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateSampler(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createSampler(desc)); + return ptr; +}; + +var _wgpuDeviceCreateTexture = (devicePtr, descriptor) => { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var textureBindingViewDimension; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var textureBindingViewDimensionDescriptor = nextInChainPtr; + textureBindingViewDimension = WebGPU.TextureViewDimension[HEAP32[(((textureBindingViewDimensionDescriptor) + (8)) >> 2)]]; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "size": WebGPU.makeExtent3D(descriptor + 28), + "mipLevelCount": HEAPU32[(((descriptor) + (44)) >> 2)], + "sampleCount": HEAPU32[(((descriptor) + (48)) >> 2)], + "dimension": WebGPU.TextureDimension[HEAP32[(((descriptor) + (24)) >> 2)]], + "format": WebGPU.TextureFormat[HEAP32[(((descriptor) + (40)) >> 2)]], + "usage": HEAPU32[(((descriptor) + (16)) >> 2)], + "textureBindingViewDimension": textureBindingViewDimension + }; + var viewFormatCount = HEAPU32[(((descriptor) + (52)) >> 2)]; + if (viewFormatCount) { + var viewFormatsPtr = HEAPU32[(((descriptor) + (56)) >> 2)]; + // viewFormatsPtr pointer to an array of TextureFormat which is an enum of size uint32_t + desc["viewFormats"] = Array.from(HEAP32.subarray((((viewFormatsPtr) >> 2)), ((viewFormatsPtr + viewFormatCount * 4) >> 2)), format => WebGPU.TextureFormat[format]); + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateTexture(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createTexture(desc)); + return ptr; +}; + +var _wgpuQueueSubmit = (queuePtr, commandCount, commands) => { + var queue = WebGPU.getJsObject(queuePtr); + var cmds = Array.from(HEAP32.subarray((((commands) >> 2)), ((commands + commandCount * 4) >> 2)), id => WebGPU.getJsObject(id)); + queue.submit(cmds); +}; + +function _wgpuQueueWriteBuffer(queuePtr, bufferPtr, bufferOffset_low, bufferOffset_high, data, size) { + var bufferOffset = convertI32PairToI53Checked(bufferOffset_low, bufferOffset_high); + var queue = WebGPU.getJsObject(queuePtr); + var buffer = WebGPU.getJsObject(bufferPtr); + // There is a size limitation for ArrayBufferView. Work around by passing in a subarray + // instead of the whole heap. crbug.com/1201109 + var subarray = HEAPU8.subarray(data, data + size); + queue.writeBuffer(buffer, bufferOffset, subarray, 0, size); +} + +var _wgpuQueueWriteTexture = (queuePtr, destinationPtr, data, dataSize, dataLayoutPtr, writeSizePtr) => { + var queue = WebGPU.getJsObject(queuePtr); + var destination = WebGPU.makeTexelCopyTextureInfo(destinationPtr); + var dataLayout = WebGPU.makeTexelCopyBufferLayout(dataLayoutPtr); + var writeSize = WebGPU.makeExtent3D(writeSizePtr); + // This subarray isn't strictly necessary, but helps work around an issue + // where Chromium makes a copy of the entire heap. crbug.com/1134457 + var subarray = HEAPU8.subarray(data, data + dataSize); + queue.writeTexture(destination, subarray, dataLayout, writeSize); +}; + +var _wgpuRenderPassEncoderDraw = (passPtr, vertexCount, instanceCount, firstVertex, firstInstance) => { + firstVertex >>>= 0; + firstInstance >>>= 0; + var pass = WebGPU.getJsObject(passPtr); + pass.draw(vertexCount, instanceCount, firstVertex, firstInstance); +}; + +var _wgpuRenderPassEncoderEnd = encoderPtr => { + var encoder = WebGPU.getJsObject(encoderPtr); + encoder.end(); +}; + +var _wgpuRenderPassEncoderSetBindGroup = (passPtr, groupIndex, groupPtr, dynamicOffsetCount, dynamicOffsetsPtr) => { + var pass = WebGPU.getJsObject(passPtr); + var group = WebGPU.getJsObject(groupPtr); + if (dynamicOffsetCount == 0) { + pass.setBindGroup(groupIndex, group); + } else { + pass.setBindGroup(groupIndex, group, HEAPU32, ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount); + } +}; + +var _wgpuRenderPassEncoderSetPipeline = (passPtr, pipelinePtr) => { + var pass = WebGPU.getJsObject(passPtr); + var pipeline = WebGPU.getJsObject(pipelinePtr); + pass.setPipeline(pipeline); +}; + +var _wgpuRenderPipelineGetBindGroupLayout = (pipelinePtr, groupIndex) => { + var pipeline = WebGPU.getJsObject(pipelinePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, pipeline.getBindGroupLayout(groupIndex)); + return ptr; +}; + +var _wgpuTextureCreateView = (texturePtr, descriptor) => { + var desc; + if (descriptor) { + var swizzle; + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var swizzleDescriptor = nextInChainPtr; + var swizzlePtr = swizzleDescriptor + 8; + var r = WebGPU.ComponentSwizzle[HEAP32[((swizzlePtr) >> 2)]] || "r"; + var g = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (4)) >> 2)]] || "g"; + var b = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (8)) >> 2)]] || "b"; + var a = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (12)) >> 2)]] || "a"; + swizzle = `${r}${g}${b}${a}`; + } + var mipLevelCount = HEAPU32[(((descriptor) + (24)) >> 2)]; + var arrayLayerCount = HEAPU32[(((descriptor) + (32)) >> 2)]; + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "format": WebGPU.TextureFormat[HEAP32[(((descriptor) + (12)) >> 2)]], + "dimension": WebGPU.TextureViewDimension[HEAP32[(((descriptor) + (16)) >> 2)]], + "baseMipLevel": HEAPU32[(((descriptor) + (20)) >> 2)], + "mipLevelCount": mipLevelCount === 4294967295 ? undefined : mipLevelCount, + "baseArrayLayer": HEAPU32[(((descriptor) + (28)) >> 2)], + "arrayLayerCount": arrayLayerCount === 4294967295 ? undefined : arrayLayerCount, + "aspect": WebGPU.TextureAspect[HEAP32[(((descriptor) + (36)) >> 2)]], + "usage": HEAPU32[(((descriptor) + (40)) >> 2)], + "swizzle": swizzle + }; + } + var texture = WebGPU.getJsObject(texturePtr); + var ptr = _emwgpuCreateTextureView(0); + WebGPU.Internals.jsObjectInsert(ptr, texture.createView(desc)); + return ptr; +}; + +var _wgpuTextureDestroy = texturePtr => { + WebGPU.getJsObject(texturePtr).destroy(); +}; + +var _wgpuTextureGetFormat = texturePtr => { + var texture = WebGPU.getJsObject(texturePtr); + // Should return the enum integer instead of string. + return WebGPU.TextureFormat.indexOf(texture.format); +}; + +var getCFunc = ident => { + var func = Module["_" + ident]; + // closure exported function + return func; +}; + +var writeArrayToMemory = (array, buffer) => { + HEAP8.set(array, buffer); +}; + +/** + * @param {string|null=} returnType + * @param {Array=} argTypes + * @param {Array=} args + * @param {Object=} opts + */ var ccall = (ident, returnType, argTypes, args, opts) => { + // For fast lookup of conversion functions + var toC = { + "string": str => { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { + // null string + ret = stringToUTF8OnStack(str); + } + return ret; + }, + "array": arr => { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret; + } + }; + function convertReturnValue(ret) { + if (returnType === "string") { + return UTF8ToString(ret); + } + if (returnType === "boolean") return Boolean(ret); + return ret; + } + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func(...cArgs); + function onDone(ret) { + if (stack !== 0) stackRestore(stack); + return convertReturnValue(ret); + } + ret = onDone(ret); + return ret; +}; + +var FS_createPath = (...args) => FS.createPath(...args); + +var FS_unlink = (...args) => FS.unlink(...args); + +var FS_createLazyFile = (...args) => FS.createLazyFile(...args); + +var FS_createDevice = (...args) => FS.createDevice(...args); + +FS.createPreloadedFile = FS_createPreloadedFile; + +FS.preloadFile = FS_preloadFile; + +FS.staticInit(); + +// Signal GL rendering layer that processing of a new frame is about to +// start. This helps it optimize VBO double-buffering and reduce GPU stalls. +registerPreMainLoop(() => GL.newRenderingFrameStarted()); + +for (let i = 0; i < 32; ++i) tempFixedLengthArray.push(new Array(i)); + +var miniTempWebGLFloatBuffersStorage = new Float32Array(288); + +// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive +for (/**@suppress{duplicate}*/ var i = 0; i <= 288; ++i) { + miniTempWebGLFloatBuffers[i] = miniTempWebGLFloatBuffersStorage.subarray(0, i); +} + +var miniTempWebGLIntBuffersStorage = new Int32Array(288); + +// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive +for (/**@suppress{duplicate}*/ var i = 0; i <= 288; ++i) { + miniTempWebGLIntBuffers[i] = miniTempWebGLIntBuffersStorage.subarray(0, i); +} + +// End JS library code +// include: postlibrary.js +// This file is included after the automatically-generated JS library code +// but before the wasm module is created. +{ + // Begin ATMODULES hooks + if (Module["preloadPlugins"]) preloadPlugins = Module["preloadPlugins"]; + if (Module["noExitRuntime"]) noExitRuntime = Module["noExitRuntime"]; + if (Module["print"]) out = Module["print"]; + if (Module["printErr"]) err = Module["printErr"]; + if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; + // End ATMODULES hooks + if (Module["arguments"]) arguments_ = Module["arguments"]; + if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; + if (Module["preInit"]) { + if (typeof Module["preInit"] == "function") Module["preInit"] = [ Module["preInit"] ]; + while (Module["preInit"].length > 0) { + Module["preInit"].shift()(); + } + } +} + +// Begin runtime exports +Module["addRunDependency"] = addRunDependency; + +Module["removeRunDependency"] = removeRunDependency; + +Module["ccall"] = ccall; + +Module["stringToNewUTF8"] = stringToNewUTF8; + +Module["FS_preloadFile"] = FS_preloadFile; + +Module["FS_unlink"] = FS_unlink; + +Module["FS_createPath"] = FS_createPath; + +Module["FS_createDevice"] = FS_createDevice; + +Module["FS_createDataFile"] = FS_createDataFile; + +Module["FS_createLazyFile"] = FS_createLazyFile; + +// End runtime exports +// Begin JS library exports +// End JS library exports +// end include: postlibrary.js +var ASM_CONSTS = { + 1462283: $0 => { + const canvas = Emval.toValue($0); + const context = canvas.getContext("webgpu"); + return WebGPU.importJsTexture(context.getCurrentTexture()); + }, + 1462426: ($0, $1, $2, $3, $4) => { + const drawable = Emval.toValue($0); + const device = WebGPU.getJsObject($1); + const texture = WebGPU.getJsObject($2); + const width = $3; + const height = $4; + device.queue.copyExternalImageToTexture({ + source: drawable + }, { + texture + }, [ width, height ]); + }, + 1462685: ($0, $1, $2, $3) => { + const sourceExtTex = Emval.toValue($0); + const device = WebGPU.getJsObject($1); + const sampler = WebGPU.getJsObject($2); + const bgLayout = WebGPU.getJsObject($3); + const bindGroup = device.createBindGroup({ + layout: bgLayout, + entries: [ { + binding: 0, + resource: sampler + }, { + binding: 1, + resource: sourceExtTex + } ] + }); + return WebGPU.importJsBindGroup(bindGroup); + }, + 1463055: ($0, $1) => { + const input = Emval.toValue($0); + const output = Emval.toValue($1); + const ctx = output.getContext("2d"); + ctx.drawImage(input, 0, 0, output.width, output.height); + }, + 1463220: ($0, $1) => { + const inputArray = Emval.toValue($0); + const output = Emval.toValue($1); + const ctx = output.getContext("2d"); + const image_data = new ImageData(inputArray, output.width, output.height); + ctx.putImageData(image_data, 0, 0); + }, + 1463444: ($0, $1) => { + const input = Emval.toValue($0); + const outputArray = Emval.toValue($1); + const ctx = input.getContext("2d"); + const data = ctx.getImageData(0, 0, input.width, input.height); + outputArray.set(data.data); + }, + 1463648: () => (typeof HTMLCanvasElement !== "undefined"), + 1463703: () => !!Module["preinitializedWebGPUDevice"], + 1463754: () => { + specialHTMLTargets["#canvas"] = Module.canvas; + } +}; + +function BeginGlQueryTiming(calc_name, num_repetitions) { + const gl = Module.canvas.getContext("webgl2"); + const query = gl.createQuery(); + Module.WEBGL_SHADER_CALC_METRICS = Module.WEBGL_SHADER_CALC_METRICS || {}; + Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)] = { + query, + repetitions: num_repetitions + }; + Module.WEBGL_QUERY_TIMER_EXT = Module.WEBGL_QUERY_TIMER_EXT || gl.getExtension("EXT_disjoint_timer_query_webgl2"); + gl.beginQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT, query); +} + +function EndGlQueryTiming(calc_name) { + const gl = Module.canvas.getContext("webgl2"); + gl.endQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT, Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)].query); +} + +function JsWrapImageConverter() { + if (!Module._imageConverter) { + Module._imageConverter = (binaryPtr, binarySize, width, height, numChannels, makeDeepCopy, outputType) => { + const imageData = new outputType(makeDeepCopy ? Module.HEAPU8.slice(binaryPtr, binaryPtr + binarySize).buffer : Module.HEAPU8.buffer, binaryPtr, width * height * numChannels); + return { + data: imageData, + width, + height + }; + }; + } +} + +function JsOnUint8ArrayImageListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Uint8Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, timestamp_ms); +} + +function JsOnFloat32ArrayImageListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Float32Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, timestamp_ms); +} + +function JsOnWebGLTextureListener(output_stream_name, name, width, height, timestamp_ms) { + Module._wrapSimpleListenerOutput(output_stream_name, { + data: GL.textures[name], + width, + height + }, timestamp_ms); +} + +function JsOnUint8ArrayImageVectorListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Uint8Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, false, timestamp_ms); +} + +function JsOnFloat32ArrayImageVectorListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Float32Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, false, timestamp_ms); +} + +function JsOnWebGLTextureVectorListener(output_stream_name, name, width, height, timestamp_ms) { + Module._wrapSimpleListenerOutput(output_stream_name, { + data: GL.textures[name], + width, + height + }, false, timestamp_ms); +} + +function JsOnEmptyPacketListener(output_stream_name, timestamp) { + Module._wrapEmptyPacketListenerOutput(output_stream_name, timestamp); +} + +function JsOnVectorFinishedListener(output_stream_name, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, undefined, true, timestamp); +} + +function JsOnSimpleListenerBool(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerBool(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerInt(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerInt(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerUint(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerUint(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerDouble(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerDouble(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerFloat(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerFloat(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerString(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, UTF8ToString(out_data), timestamp); +} + +function JsOnVectorListenerString(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, UTF8ToString(out_data), false, timestamp); +} + +function JsOnVectorListenerProto(output_stream_name, proto_ptr, proto_size, make_deep_copy, timestamp) { + const newProtoArray = make_deep_copy ? Module.HEAPU8.slice(proto_ptr, proto_ptr + proto_size) : new Uint8Array(Module.HEAPU8.buffer, proto_ptr, proto_size); + Module._wrapSimpleListenerOutput(output_stream_name, newProtoArray, false, timestamp); +} + +function JsWrapSimpleListeners() { + if (!Module._wrapSimpleListenerOutput) { + Module._wrapSimpleListenerOutput = (outputStreamName, ...args) => { + if (Module.simpleListeners) { + const streamName = UTF8ToString(outputStreamName); + if (Module.simpleListeners[streamName]) { + Module.simpleListeners[streamName](...args); + } + } + }; + } + if (!Module._wrapEmptyPacketListenerOutput) { + Module._wrapEmptyPacketListenerOutput = (outputStreamName, timestamp) => { + if (Module.emptyPacketListeners) { + const streamName = UTF8ToString(outputStreamName); + if (Module.emptyPacketListeners[streamName]) { + Module.emptyPacketListeners[streamName](timestamp); + } + } + }; + } +} + +function JsOnSimpleListenerBinaryArray(output_stream_name, binary_ptr, binary_size, make_deep_copy, timestamp) { + const newProtoArray = make_deep_copy ? Module.HEAPU8.slice(binary_ptr, binary_ptr + binary_size) : new Uint8Array(Module.HEAPU8.buffer, binary_ptr, binary_size); + Module._wrapSimpleListenerOutput(output_stream_name, newProtoArray, timestamp); +} + +function mediapipe_import_external_texture(device_handle, source_handle) { + const device = WebGPU.getJsObject(device_handle); + const source = Emval.toValue(source_handle); + const externalTexture = device.importExternalTexture({ + source + }); + return Emval.toHandle(externalTexture); +} + +function mediapipe_create_utility_canvas2d() { + let canvas; + if (typeof HTMLCanvasElement !== "undefined") { + canvas = document.createElement("canvas"); + canvas.style.display = "none"; + } else { + canvas = new OffscreenCanvas(0, 0); + } + return Emval.toHandle(canvas); +} + +function GetAdapterArchitecture() { + const device = Module["preinitializedWebGPUDevice"]; + const architecture = device.adapterInfo ? device.adapterInfo.architecture : "Unknown"; + return stringToNewUTF8(architecture); +} + +function GetAdapterDescription() { + const device = Module["preinitializedWebGPUDevice"]; + const description = device.adapterInfo ? device.adapterInfo.description : "Unknown"; + return stringToNewUTF8(description); +} + +function GetAdapterDeviceName() { + const device = Module["preinitializedWebGPUDevice"]; + const deviceName = device.adapterInfo ? device.adapterInfo.device : "Unknown"; + return stringToNewUTF8(deviceName); +} + +function GetAdapterVendor() { + const device = Module["preinitializedWebGPUDevice"]; + const vendor = device.adapterInfo ? device.adapterInfo.vendor : "Unknown"; + return stringToNewUTF8(vendor); +} + +function __asyncjs__mediapipe_map_buffer_jspi(buffer_handle, data) { + return Asyncify.handleAsync(async () => { + const buffer = WebGPU.getJsObject(buffer_handle); + if ("mapSync" in buffer) { + buffer.mapSync(GPUMapMode.READ); + } else { + await buffer.mapAsync(GPUMapMode.READ); + } + const mapped = buffer.getMappedRange(); + HEAPU8.set(new Uint8Array(mapped), data); + buffer.unmap(); + }); +} + +function hardware_concurrency() { + var concurrency = 1; + try { + concurrency = self.navigator.hardwareConcurrency; + } catch (e) {} + return concurrency; +} + +function JsWrapErrorListener(code, message) { + if (Module.errorListener) { + const stringMessage = UTF8ToString(message); + Module.errorListener(code, stringMessage); + } +} + +function UseBottomLeftGpuOrigin() { + return (Module && Module.gpuOriginForWebTexturesIsBottomLeft); +} + +function custom_emscripten_dbgn(str, len) { + if (typeof (dbg) !== "undefined") { + dbg(UTF8ToString(str, len)); + } else { + if (typeof (custom_dbg) === "undefined") { + function custom_dbg(text) { + console.warn.apply(console, arguments); + } + } + custom_dbg(UTF8ToString(str, len)); + } +} + +// Imports from the Wasm binary. +var _free, _malloc, _wgpuDeviceAddRef, _addBoundTextureAsImageToStream, _attachImageListener, _attachImageVectorListener, _registerModelResourcesGraphService, _bindTextureToStream, _addBoundTextureToStream, _addDoubleToInputStream, _addFloatToInputStream, _addBoolToInputStream, _addIntToInputStream, _addUintToInputStream, _addStringToInputStream, _addRawDataSpanToInputStream, _allocateBoolVector, _allocateFloatVector, _allocateDoubleVector, _allocateIntVector, _allocateUintVector, _allocateStringVector, _addBoolVectorEntry, _addFloatVectorEntry, _addDoubleVectorEntry, _addIntVectorEntry, _addUintVectorEntry, _addStringVectorEntry, _addBoolVectorToInputStream, _addFloatVectorToInputStream, _addDoubleVectorToInputStream, _addIntVectorToInputStream, _addUintVectorToInputStream, _addStringVectorToInputStream, _addFlatHashMapToInputStream, _addProtoToInputStream, _addEmptyPacketToInputStream, _addBoolToInputSidePacket, _addDoubleToInputSidePacket, _addFloatToInputSidePacket, _addIntToInputSidePacket, _addUintToInputSidePacket, _addStringToInputSidePacket, _addRawDataSpanToInputSidePacket, _addProtoToInputSidePacket, _addBoolVectorToInputSidePacket, _addDoubleVectorToInputSidePacket, _addFloatVectorToInputSidePacket, _addIntVectorToInputSidePacket, _addUintVectorToInputSidePacket, _addStringVectorToInputSidePacket, _attachBoolListener, _attachBoolVectorListener, _attachDoubleListener, _attachDoubleVectorListener, _attachFloatListener, _attachFloatVectorListener, _attachIntListener, _attachIntVectorListener, _attachUintListener, _attachUintVectorListener, _attachStringListener, _attachStringVectorListener, _attachProtoListener, _attachProtoVectorListener, _getGraphConfig, ___getTypeName, _emwgpuCreateBindGroup, _emwgpuCreateBindGroupLayout, _emwgpuCreateCommandBuffer, _emwgpuCreateCommandEncoder, _emwgpuCreateComputePassEncoder, _emwgpuCreateComputePipeline, _emwgpuCreateExternalTexture, _emwgpuCreatePipelineLayout, _emwgpuCreateQuerySet, _emwgpuCreateRenderBundle, _emwgpuCreateRenderBundleEncoder, _emwgpuCreateRenderPassEncoder, _emwgpuCreateRenderPipeline, _emwgpuCreateSampler, _emwgpuCreateSurface, _emwgpuCreateTexture, _emwgpuCreateTextureView, _emwgpuCreateAdapter, _emwgpuImportBuffer, _emwgpuCreateDevice, _emwgpuCreateQueue, _emwgpuCreateShaderModule, _emwgpuOnCreateComputePipelineCompleted, _emwgpuOnCreateRenderPipelineCompleted, _clearSubgraphs, _pushBinarySubgraph, _pushTextSubgraph, _changeBinaryGraph, _changeTextGraph, _processGl, _process, _bindTextureToCanvas, _requestShaderRefreshOnGraphChange, _waitUntilIdle, _closeGraph, _setAutoRenderToScreen, _emscripten_builtin_memalign, _memalign, __emscripten_tempret_set, __emscripten_stack_restore, __emscripten_stack_alloc, _emscripten_stack_get_current, dynCall_ji, dynCall_jii, dynCall_iiiijij, dynCall_viiji, dynCall_viji, dynCall_iiiji, dynCall_jjj, dynCall_iiiijj, dynCall_viijj, dynCall_viiijjj, dynCall_vij, dynCall_viiiji, dynCall_viijii, dynCall_vijjj, dynCall_vj, dynCall_viij, dynCall_jiji, dynCall_iiiiij, dynCall_iiiiijj, dynCall_iiiiiijj, memory, _kVersionStampBuildChangelistStr, _kVersionStampCitcSnapshotStr, _kVersionStampCitcWorkspaceIdStr, _kVersionStampSourceUriStr, _kVersionStampBuildClientStr, _kVersionStampBuildClientMintStatusStr, _kVersionStampBuildCompilerStr, _kVersionStampBuildDateTimePstStr, _kVersionStampBuildDepotPathStr, _kVersionStampBuildIdStr, _kVersionStampBuildInfoStr, _kVersionStampBuildLabelStr, _kVersionStampBuildTargetStr, _kVersionStampBuildTimestampStr, _kVersionStampBuildToolStr, _kVersionStampG3BuildTargetStr, _kVersionStampVerifiableStr, _kVersionStampBuildFdoTypeStr, _kVersionStampBuildBaselineChangelistStr, _kVersionStampBuildLtoTypeStr, _kVersionStampBuildPropellerTypeStr, _kVersionStampBuildPghoTypeStr, _kVersionStampBuildUsernameStr, _kVersionStampBuildHostnameStr, _kVersionStampBuildDirectoryStr, _kVersionStampBuildChangelistInt, _kVersionStampCitcSnapshotInt, _kVersionStampBuildClientMintStatusInt, _kVersionStampBuildTimestampInt, _kVersionStampVerifiableInt, _kVersionStampBuildCoverageEnabledInt, _kVersionStampBuildBaselineChangelistInt, _kVersionStampPrecookedTimestampStr, _kVersionStampPrecookedClientInfoStr, __indirect_function_table, wasmMemory, wasmTable; + +function assignWasmExports(wasmExports) { + _free = Module["_free"] = wasmExports["Td"]; + _malloc = Module["_malloc"] = wasmExports["Ud"]; + _wgpuDeviceAddRef = wasmExports["Vd"]; + _addBoundTextureAsImageToStream = Module["_addBoundTextureAsImageToStream"] = wasmExports["Wd"]; + _attachImageListener = Module["_attachImageListener"] = wasmExports["Xd"]; + _attachImageVectorListener = Module["_attachImageVectorListener"] = wasmExports["Yd"]; + _registerModelResourcesGraphService = Module["_registerModelResourcesGraphService"] = wasmExports["Zd"]; + _bindTextureToStream = Module["_bindTextureToStream"] = wasmExports["_d"]; + _addBoundTextureToStream = Module["_addBoundTextureToStream"] = wasmExports["$d"]; + _addDoubleToInputStream = Module["_addDoubleToInputStream"] = wasmExports["ae"]; + _addFloatToInputStream = Module["_addFloatToInputStream"] = wasmExports["be"]; + _addBoolToInputStream = Module["_addBoolToInputStream"] = wasmExports["ce"]; + _addIntToInputStream = Module["_addIntToInputStream"] = wasmExports["de"]; + _addUintToInputStream = Module["_addUintToInputStream"] = wasmExports["ee"]; + _addStringToInputStream = Module["_addStringToInputStream"] = wasmExports["fe"]; + _addRawDataSpanToInputStream = Module["_addRawDataSpanToInputStream"] = wasmExports["ge"]; + _allocateBoolVector = Module["_allocateBoolVector"] = wasmExports["he"]; + _allocateFloatVector = Module["_allocateFloatVector"] = wasmExports["ie"]; + _allocateDoubleVector = Module["_allocateDoubleVector"] = wasmExports["je"]; + _allocateIntVector = Module["_allocateIntVector"] = wasmExports["ke"]; + _allocateUintVector = Module["_allocateUintVector"] = wasmExports["le"]; + _allocateStringVector = Module["_allocateStringVector"] = wasmExports["me"]; + _addBoolVectorEntry = Module["_addBoolVectorEntry"] = wasmExports["ne"]; + _addFloatVectorEntry = Module["_addFloatVectorEntry"] = wasmExports["oe"]; + _addDoubleVectorEntry = Module["_addDoubleVectorEntry"] = wasmExports["pe"]; + _addIntVectorEntry = Module["_addIntVectorEntry"] = wasmExports["qe"]; + _addUintVectorEntry = Module["_addUintVectorEntry"] = wasmExports["re"]; + _addStringVectorEntry = Module["_addStringVectorEntry"] = wasmExports["se"]; + _addBoolVectorToInputStream = Module["_addBoolVectorToInputStream"] = wasmExports["te"]; + _addFloatVectorToInputStream = Module["_addFloatVectorToInputStream"] = wasmExports["ue"]; + _addDoubleVectorToInputStream = Module["_addDoubleVectorToInputStream"] = wasmExports["ve"]; + _addIntVectorToInputStream = Module["_addIntVectorToInputStream"] = wasmExports["we"]; + _addUintVectorToInputStream = Module["_addUintVectorToInputStream"] = wasmExports["xe"]; + _addStringVectorToInputStream = Module["_addStringVectorToInputStream"] = wasmExports["ye"]; + _addFlatHashMapToInputStream = Module["_addFlatHashMapToInputStream"] = wasmExports["ze"]; + _addProtoToInputStream = Module["_addProtoToInputStream"] = wasmExports["Ae"]; + _addEmptyPacketToInputStream = Module["_addEmptyPacketToInputStream"] = wasmExports["Be"]; + _addBoolToInputSidePacket = Module["_addBoolToInputSidePacket"] = wasmExports["Ce"]; + _addDoubleToInputSidePacket = Module["_addDoubleToInputSidePacket"] = wasmExports["De"]; + _addFloatToInputSidePacket = Module["_addFloatToInputSidePacket"] = wasmExports["Ee"]; + _addIntToInputSidePacket = Module["_addIntToInputSidePacket"] = wasmExports["Fe"]; + _addUintToInputSidePacket = Module["_addUintToInputSidePacket"] = wasmExports["Ge"]; + _addStringToInputSidePacket = Module["_addStringToInputSidePacket"] = wasmExports["He"]; + _addRawDataSpanToInputSidePacket = Module["_addRawDataSpanToInputSidePacket"] = wasmExports["Ie"]; + _addProtoToInputSidePacket = Module["_addProtoToInputSidePacket"] = wasmExports["Je"]; + _addBoolVectorToInputSidePacket = Module["_addBoolVectorToInputSidePacket"] = wasmExports["Ke"]; + _addDoubleVectorToInputSidePacket = Module["_addDoubleVectorToInputSidePacket"] = wasmExports["Le"]; + _addFloatVectorToInputSidePacket = Module["_addFloatVectorToInputSidePacket"] = wasmExports["Me"]; + _addIntVectorToInputSidePacket = Module["_addIntVectorToInputSidePacket"] = wasmExports["Ne"]; + _addUintVectorToInputSidePacket = Module["_addUintVectorToInputSidePacket"] = wasmExports["Oe"]; + _addStringVectorToInputSidePacket = Module["_addStringVectorToInputSidePacket"] = wasmExports["Pe"]; + _attachBoolListener = Module["_attachBoolListener"] = wasmExports["Qe"]; + _attachBoolVectorListener = Module["_attachBoolVectorListener"] = wasmExports["Re"]; + _attachDoubleListener = Module["_attachDoubleListener"] = wasmExports["Se"]; + _attachDoubleVectorListener = Module["_attachDoubleVectorListener"] = wasmExports["Te"]; + _attachFloatListener = Module["_attachFloatListener"] = wasmExports["Ue"]; + _attachFloatVectorListener = Module["_attachFloatVectorListener"] = wasmExports["Ve"]; + _attachIntListener = Module["_attachIntListener"] = wasmExports["We"]; + _attachIntVectorListener = Module["_attachIntVectorListener"] = wasmExports["Xe"]; + _attachUintListener = Module["_attachUintListener"] = wasmExports["Ye"]; + _attachUintVectorListener = Module["_attachUintVectorListener"] = wasmExports["Ze"]; + _attachStringListener = Module["_attachStringListener"] = wasmExports["_e"]; + _attachStringVectorListener = Module["_attachStringVectorListener"] = wasmExports["$e"]; + _attachProtoListener = Module["_attachProtoListener"] = wasmExports["af"]; + _attachProtoVectorListener = Module["_attachProtoVectorListener"] = wasmExports["bf"]; + _getGraphConfig = Module["_getGraphConfig"] = wasmExports["cf"]; + ___getTypeName = wasmExports["df"]; + _emwgpuCreateBindGroup = wasmExports["ef"]; + _emwgpuCreateBindGroupLayout = wasmExports["ff"]; + _emwgpuCreateCommandBuffer = wasmExports["gf"]; + _emwgpuCreateCommandEncoder = wasmExports["hf"]; + _emwgpuCreateComputePassEncoder = wasmExports["jf"]; + _emwgpuCreateComputePipeline = wasmExports["kf"]; + _emwgpuCreateExternalTexture = wasmExports["lf"]; + _emwgpuCreatePipelineLayout = wasmExports["mf"]; + _emwgpuCreateQuerySet = wasmExports["nf"]; + _emwgpuCreateRenderBundle = wasmExports["of"]; + _emwgpuCreateRenderBundleEncoder = wasmExports["pf"]; + _emwgpuCreateRenderPassEncoder = wasmExports["qf"]; + _emwgpuCreateRenderPipeline = wasmExports["rf"]; + _emwgpuCreateSampler = wasmExports["sf"]; + _emwgpuCreateSurface = wasmExports["tf"]; + _emwgpuCreateTexture = wasmExports["uf"]; + _emwgpuCreateTextureView = wasmExports["vf"]; + _emwgpuCreateAdapter = wasmExports["wf"]; + _emwgpuImportBuffer = wasmExports["xf"]; + _emwgpuCreateDevice = wasmExports["yf"]; + _emwgpuCreateQueue = wasmExports["zf"]; + _emwgpuCreateShaderModule = wasmExports["Af"]; + _emwgpuOnCreateComputePipelineCompleted = wasmExports["Bf"]; + _emwgpuOnCreateRenderPipelineCompleted = wasmExports["Cf"]; + _clearSubgraphs = Module["_clearSubgraphs"] = wasmExports["Df"]; + _pushBinarySubgraph = Module["_pushBinarySubgraph"] = wasmExports["Ef"]; + _pushTextSubgraph = Module["_pushTextSubgraph"] = wasmExports["Ff"]; + _changeBinaryGraph = Module["_changeBinaryGraph"] = wasmExports["Gf"]; + _changeTextGraph = Module["_changeTextGraph"] = wasmExports["Hf"]; + _processGl = Module["_processGl"] = wasmExports["If"]; + _process = Module["_process"] = wasmExports["Jf"]; + _bindTextureToCanvas = Module["_bindTextureToCanvas"] = wasmExports["Kf"]; + _requestShaderRefreshOnGraphChange = Module["_requestShaderRefreshOnGraphChange"] = wasmExports["Lf"]; + _waitUntilIdle = Module["_waitUntilIdle"] = wasmExports["Mf"]; + _closeGraph = Module["_closeGraph"] = wasmExports["Nf"]; + _setAutoRenderToScreen = Module["_setAutoRenderToScreen"] = wasmExports["Of"]; + _emscripten_builtin_memalign = wasmExports["Pf"]; + _memalign = wasmExports["Qf"]; + __emscripten_tempret_set = wasmExports["Rf"]; + __emscripten_stack_restore = wasmExports["Sf"]; + __emscripten_stack_alloc = wasmExports["Tf"]; + _emscripten_stack_get_current = wasmExports["Uf"]; + dynCall_ji = wasmExports["dynCall_ji"]; + dynCall_jii = wasmExports["dynCall_jii"]; + dynCall_iiiijij = wasmExports["dynCall_iiiijij"]; + dynCall_viiji = wasmExports["dynCall_viiji"]; + dynCall_viji = wasmExports["dynCall_viji"]; + dynCall_iiiji = wasmExports["dynCall_iiiji"]; + dynCall_jjj = wasmExports["dynCall_jjj"]; + dynCall_iiiijj = wasmExports["dynCall_iiiijj"]; + dynCall_viijj = wasmExports["dynCall_viijj"]; + dynCall_viiijjj = wasmExports["dynCall_viiijjj"]; + dynCall_vij = wasmExports["dynCall_vij"]; + dynCall_viiiji = wasmExports["dynCall_viiiji"]; + dynCall_viijii = wasmExports["dynCall_viijii"]; + dynCall_vijjj = wasmExports["dynCall_vijjj"]; + dynCall_vj = wasmExports["dynCall_vj"]; + dynCall_viij = wasmExports["dynCall_viij"]; + dynCall_jiji = wasmExports["dynCall_jiji"]; + dynCall_iiiiij = wasmExports["dynCall_iiiiij"]; + dynCall_iiiiijj = wasmExports["dynCall_iiiiijj"]; + dynCall_iiiiiijj = wasmExports["dynCall_iiiiiijj"]; + memory = wasmMemory = wasmExports["id"]; + _kVersionStampBuildChangelistStr = Module["_kVersionStampBuildChangelistStr"] = wasmExports["kd"].value; + _kVersionStampCitcSnapshotStr = Module["_kVersionStampCitcSnapshotStr"] = wasmExports["ld"].value; + _kVersionStampCitcWorkspaceIdStr = Module["_kVersionStampCitcWorkspaceIdStr"] = wasmExports["md"].value; + _kVersionStampSourceUriStr = Module["_kVersionStampSourceUriStr"] = wasmExports["nd"].value; + _kVersionStampBuildClientStr = Module["_kVersionStampBuildClientStr"] = wasmExports["od"].value; + _kVersionStampBuildClientMintStatusStr = Module["_kVersionStampBuildClientMintStatusStr"] = wasmExports["pd"].value; + _kVersionStampBuildCompilerStr = Module["_kVersionStampBuildCompilerStr"] = wasmExports["qd"].value; + _kVersionStampBuildDateTimePstStr = Module["_kVersionStampBuildDateTimePstStr"] = wasmExports["rd"].value; + _kVersionStampBuildDepotPathStr = Module["_kVersionStampBuildDepotPathStr"] = wasmExports["sd"].value; + _kVersionStampBuildIdStr = Module["_kVersionStampBuildIdStr"] = wasmExports["td"].value; + _kVersionStampBuildInfoStr = Module["_kVersionStampBuildInfoStr"] = wasmExports["ud"].value; + _kVersionStampBuildLabelStr = Module["_kVersionStampBuildLabelStr"] = wasmExports["vd"].value; + _kVersionStampBuildTargetStr = Module["_kVersionStampBuildTargetStr"] = wasmExports["wd"].value; + _kVersionStampBuildTimestampStr = Module["_kVersionStampBuildTimestampStr"] = wasmExports["xd"].value; + _kVersionStampBuildToolStr = Module["_kVersionStampBuildToolStr"] = wasmExports["yd"].value; + _kVersionStampG3BuildTargetStr = Module["_kVersionStampG3BuildTargetStr"] = wasmExports["zd"].value; + _kVersionStampVerifiableStr = Module["_kVersionStampVerifiableStr"] = wasmExports["Ad"].value; + _kVersionStampBuildFdoTypeStr = Module["_kVersionStampBuildFdoTypeStr"] = wasmExports["Bd"].value; + _kVersionStampBuildBaselineChangelistStr = Module["_kVersionStampBuildBaselineChangelistStr"] = wasmExports["Cd"].value; + _kVersionStampBuildLtoTypeStr = Module["_kVersionStampBuildLtoTypeStr"] = wasmExports["Dd"].value; + _kVersionStampBuildPropellerTypeStr = Module["_kVersionStampBuildPropellerTypeStr"] = wasmExports["Ed"].value; + _kVersionStampBuildPghoTypeStr = Module["_kVersionStampBuildPghoTypeStr"] = wasmExports["Fd"].value; + _kVersionStampBuildUsernameStr = Module["_kVersionStampBuildUsernameStr"] = wasmExports["Gd"].value; + _kVersionStampBuildHostnameStr = Module["_kVersionStampBuildHostnameStr"] = wasmExports["Hd"].value; + _kVersionStampBuildDirectoryStr = Module["_kVersionStampBuildDirectoryStr"] = wasmExports["Id"].value; + _kVersionStampBuildChangelistInt = Module["_kVersionStampBuildChangelistInt"] = wasmExports["Jd"].value; + _kVersionStampCitcSnapshotInt = Module["_kVersionStampCitcSnapshotInt"] = wasmExports["Kd"].value; + _kVersionStampBuildClientMintStatusInt = Module["_kVersionStampBuildClientMintStatusInt"] = wasmExports["Ld"].value; + _kVersionStampBuildTimestampInt = Module["_kVersionStampBuildTimestampInt"] = wasmExports["Md"].value; + _kVersionStampVerifiableInt = Module["_kVersionStampVerifiableInt"] = wasmExports["Nd"].value; + _kVersionStampBuildCoverageEnabledInt = Module["_kVersionStampBuildCoverageEnabledInt"] = wasmExports["Od"].value; + _kVersionStampBuildBaselineChangelistInt = Module["_kVersionStampBuildBaselineChangelistInt"] = wasmExports["Pd"].value; + _kVersionStampPrecookedTimestampStr = Module["_kVersionStampPrecookedTimestampStr"] = wasmExports["Qd"].value; + _kVersionStampPrecookedClientInfoStr = Module["_kVersionStampPrecookedClientInfoStr"] = wasmExports["Rd"].value; + __indirect_function_table = wasmTable = wasmExports["Sd"]; +} + +var wasmImports = { + /** @export */ hd: BeginGlQueryTiming, + /** @export */ gd: EndGlQueryTiming, + /** @export */ fd: GetAdapterArchitecture, + /** @export */ ed: GetAdapterDescription, + /** @export */ dd: GetAdapterDeviceName, + /** @export */ cd: GetAdapterVendor, + /** @export */ bd: JsOnEmptyPacketListener, + /** @export */ ad: JsOnFloat32ArrayImageListener, + /** @export */ $c: JsOnFloat32ArrayImageVectorListener, + /** @export */ pb: JsOnSimpleListenerBinaryArray, + /** @export */ _c: JsOnSimpleListenerBool, + /** @export */ Zc: JsOnSimpleListenerDouble, + /** @export */ Yc: JsOnSimpleListenerFloat, + /** @export */ Xc: JsOnSimpleListenerInt, + /** @export */ Wc: JsOnSimpleListenerString, + /** @export */ Vc: JsOnSimpleListenerUint, + /** @export */ Uc: JsOnUint8ArrayImageListener, + /** @export */ Tc: JsOnUint8ArrayImageVectorListener, + /** @export */ P: JsOnVectorFinishedListener, + /** @export */ Sc: JsOnVectorListenerBool, + /** @export */ Rc: JsOnVectorListenerDouble, + /** @export */ Qc: JsOnVectorListenerFloat, + /** @export */ Pc: JsOnVectorListenerInt, + /** @export */ Oc: JsOnVectorListenerProto, + /** @export */ Nc: JsOnVectorListenerString, + /** @export */ Mc: JsOnVectorListenerUint, + /** @export */ Lc: JsOnWebGLTextureListener, + /** @export */ Kc: JsOnWebGLTextureVectorListener, + /** @export */ Pa: JsWrapErrorListener, + /** @export */ ob: JsWrapImageConverter, + /** @export */ u: JsWrapSimpleListeners, + /** @export */ nb: UseBottomLeftGpuOrigin, + /** @export */ yb: __asyncjs__mediapipe_map_buffer_jspi, + /** @export */ r: ___cxa_throw, + /** @export */ Jc: ___syscall_dup, + /** @export */ Ic: ___syscall_faccessat, + /** @export */ mb: ___syscall_fcntl64, + /** @export */ Hc: ___syscall_fstat64, + /** @export */ Mb: ___syscall_ftruncate64, + /** @export */ Gc: ___syscall_ioctl, + /** @export */ Fc: ___syscall_lstat64, + /** @export */ Ec: ___syscall_newfstatat, + /** @export */ lb: ___syscall_openat, + /** @export */ Dc: ___syscall_stat64, + /** @export */ yc: __abort_js, + /** @export */ Jb: __embind_register_bigint, + /** @export */ xc: __embind_register_bool, + /** @export */ wc: __embind_register_emval, + /** @export */ jb: __embind_register_float, + /** @export */ J: __embind_register_integer, + /** @export */ q: __embind_register_memory_view, + /** @export */ vc: __embind_register_std_string, + /** @export */ Ma: __embind_register_std_wstring, + /** @export */ uc: __embind_register_void, + /** @export */ $: __emval_create_invoker, + /** @export */ p: __emval_decref, + /** @export */ La: __emval_get_global, + /** @export */ ib: __emval_get_property, + /** @export */ ja: __emval_incref, + /** @export */ Ka: __emval_instanceof, + /** @export */ _: __emval_invoke, + /** @export */ ua: __emval_new_cstring, + /** @export */ Z: __emval_run_destructors, + /** @export */ hb: __emval_set_property, + /** @export */ tc: __emval_typeof, + /** @export */ Ib: __gmtime_js, + /** @export */ Hb: __localtime_js, + /** @export */ Gb: __mktime_js, + /** @export */ Fb: __mmap_js, + /** @export */ Eb: __munmap_js, + /** @export */ sc: __tzset_js, + /** @export */ Lb: _clock_time_get, + /** @export */ rc: custom_emscripten_dbgn, + /** @export */ Y: _emscripten_asm_const_int, + /** @export */ gb: _emscripten_asm_const_ptr, + /** @export */ Ja: _emscripten_errn, + /** @export */ qc: _emscripten_get_heap_max, + /** @export */ y: _emscripten_get_now, + /** @export */ ia: _emscripten_has_asyncify, + /** @export */ pc: _emscripten_outn, + /** @export */ oc: _emscripten_pc_get_function, + /** @export */ nc: _emscripten_resize_heap, + /** @export */ fb: _emscripten_stack_snapshot, + /** @export */ mc: _emscripten_stack_unwind_buffer, + /** @export */ lc: _emscripten_webgl_create_context, + /** @export */ kc: _emscripten_webgl_destroy_context, + /** @export */ jc: _emscripten_webgl_get_context_attributes, + /** @export */ ha: _emscripten_webgl_get_current_context, + /** @export */ ic: _emscripten_webgl_make_context_current, + /** @export */ R: _emscripten_webgpu_get_device, + /** @export */ hc: _emwgpuBufferDestroy, + /** @export */ gc: _emwgpuBufferGetMappedRange, + /** @export */ fc: _emwgpuBufferUnmap, + /** @export */ x: _emwgpuDelete, + /** @export */ ec: _emwgpuDeviceCreateBuffer, + /** @export */ Db: _emwgpuDeviceCreateComputePipelineAsync, + /** @export */ Cb: _emwgpuDeviceCreateRenderPipelineAsync, + /** @export */ dc: _emwgpuDeviceCreateShaderModule, + /** @export */ cc: _emwgpuDeviceDestroy, + /** @export */ bc: _emwgpuWaitAny, + /** @export */ Cc: _environ_get, + /** @export */ Bc: _environ_sizes_get, + /** @export */ eb: _exit, + /** @export */ Oa: _fd_close, + /** @export */ kb: _fd_read, + /** @export */ Kb: _fd_seek, + /** @export */ Na: _fd_write, + /** @export */ b: _glActiveTexture, + /** @export */ ta: _glAttachShader, + /** @export */ ac: _glBindAttribLocation, + /** @export */ c: _glBindBuffer, + /** @export */ db: _glBindBufferBase, + /** @export */ t: _glBindFramebuffer, + /** @export */ a: _glBindTexture, + /** @export */ m: _glBindVertexArray, + /** @export */ cb: _glBlendEquation, + /** @export */ $b: _glBlendFunc, + /** @export */ j: _glBufferData, + /** @export */ I: _glClear, + /** @export */ H: _glClearColor, + /** @export */ da: _glClientWaitSync, + /** @export */ ga: _glColorMask, + /** @export */ bb: _glCompileShader, + /** @export */ ab: _glCreateProgram, + /** @export */ $a: _glCreateShader, + /** @export */ o: _glDeleteBuffers, + /** @export */ Q: _glDeleteFramebuffers, + /** @export */ h: _glDeleteProgram, + /** @export */ sa: _glDeleteShader, + /** @export */ ra: _glDeleteSync, + /** @export */ D: _glDeleteTextures, + /** @export */ A: _glDeleteVertexArrays, + /** @export */ _a: _glDetachShader, + /** @export */ G: _glDisable, + /** @export */ n: _glDisableVertexAttribArray, + /** @export */ i: _glDrawArrays, + /** @export */ fa: _glDrawBuffers, + /** @export */ _b: _glEnable, + /** @export */ l: _glEnableVertexAttribArray, + /** @export */ Za: _glFenceSync, + /** @export */ qa: _glFinish, + /** @export */ v: _glFlush, + /** @export */ C: _glFramebufferTexture2D, + /** @export */ Ya: _glFramebufferTextureLayer, + /** @export */ s: _glGenBuffers, + /** @export */ X: _glGenFramebuffers, + /** @export */ F: _glGenTextures, + /** @export */ B: _glGenVertexArrays, + /** @export */ Xa: _glGetAttribLocation, + /** @export */ ea: _glGetError, + /** @export */ Zb: _glGetFloatv, + /** @export */ w: _glGetIntegerv, + /** @export */ Yb: _glGetProgramiv, + /** @export */ Xb: _glGetShaderInfoLog, + /** @export */ Wb: _glGetShaderiv, + /** @export */ O: _glGetString, + /** @export */ Vb: _glGetUniformBlockIndex, + /** @export */ d: _glGetUniformLocation, + /** @export */ Ub: _glLineWidth, + /** @export */ Wa: _glLinkProgram, + /** @export */ pa: _glPixelStorei, + /** @export */ oa: _glReadPixels, + /** @export */ Va: _glShaderSource, + /** @export */ E: _glTexImage2D, + /** @export */ na: _glTexParameterf, + /** @export */ Ua: _glTexParameterfv, + /** @export */ f: _glTexParameteri, + /** @export */ ma: _glTexStorage2D, + /** @export */ Tb: _glTexStorage3D, + /** @export */ W: _glTexSubImage2D, + /** @export */ Sb: _glTexSubImage3D, + /** @export */ N: _glUniform1f, + /** @export */ la: _glUniform1fv, + /** @export */ e: _glUniform1i, + /** @export */ V: _glUniform2f, + /** @export */ Rb: _glUniform2fv, + /** @export */ Ia: _glUniform3f, + /** @export */ Ta: _glUniform4f, + /** @export */ U: _glUniform4fv, + /** @export */ Qb: _glUniform4iv, + /** @export */ Pb: _glUniformBlockBinding, + /** @export */ Ob: _glUniformMatrix2fv, + /** @export */ Nb: _glUniformMatrix3fv, + /** @export */ Ha: _glUniformMatrix4fv, + /** @export */ g: _glUseProgram, + /** @export */ k: _glVertexAttribPointer, + /** @export */ T: _glViewport, + /** @export */ Ga: hardware_concurrency, + /** @export */ Bb: mediapipe_create_utility_canvas2d, + /** @export */ Ab: _mediapipe_find_canvas_event_target, + /** @export */ zb: mediapipe_import_external_texture, + /** @export */ xb: _mediapipe_webgl_tex_image_drawable, + /** @export */ Ac: _proc_exit, + /** @export */ zc: _random_get, + /** @export */ Fa: _wgpuCommandEncoderBeginComputePass, + /** @export */ Ea: _wgpuCommandEncoderBeginRenderPass, + /** @export */ wb: _wgpuCommandEncoderCopyBufferToTexture, + /** @export */ vb: _wgpuCommandEncoderCopyTextureToBuffer, + /** @export */ ub: _wgpuCommandEncoderCopyTextureToTexture, + /** @export */ M: _wgpuCommandEncoderFinish, + /** @export */ Da: _wgpuComputePassEncoderDispatchWorkgroups, + /** @export */ Ca: _wgpuComputePassEncoderEnd, + /** @export */ Ba: _wgpuComputePassEncoderSetBindGroup, + /** @export */ Aa: _wgpuComputePassEncoderSetPipeline, + /** @export */ za: _wgpuComputePipelineGetBindGroupLayout, + /** @export */ ca: _wgpuDeviceCreateBindGroup, + /** @export */ tb: _wgpuDeviceCreateBindGroupLayout, + /** @export */ L: _wgpuDeviceCreateCommandEncoder, + /** @export */ sb: _wgpuDeviceCreateComputePipeline, + /** @export */ rb: _wgpuDeviceCreatePipelineLayout, + /** @export */ Sa: _wgpuDeviceCreateRenderPipeline, + /** @export */ S: _wgpuDeviceCreateSampler, + /** @export */ ba: _wgpuDeviceCreateTexture, + /** @export */ K: _wgpuQueueSubmit, + /** @export */ ka: _wgpuQueueWriteBuffer, + /** @export */ qb: _wgpuQueueWriteTexture, + /** @export */ ya: _wgpuRenderPassEncoderDraw, + /** @export */ xa: _wgpuRenderPassEncoderEnd, + /** @export */ wa: _wgpuRenderPassEncoderSetBindGroup, + /** @export */ va: _wgpuRenderPassEncoderSetPipeline, + /** @export */ Ra: _wgpuRenderPipelineGetBindGroupLayout, + /** @export */ z: _wgpuTextureCreateView, + /** @export */ Qa: _wgpuTextureDestroy, + /** @export */ aa: _wgpuTextureGetFormat +}; + +// include: postamble.js +// === Auto-generated postamble setup entry stuff === +function run() { + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + preRun(); + // a preRun added a dependency, run will be called later + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + function doRun() { + // run may have just been called through dependencies being fulfilled just in this very frame, + // or while the async setStatus time below was happening + Module["calledRun"] = true; + if (ABORT) return; + initRuntime(); + readyPromiseResolve?.(Module); + Module["onRuntimeInitialized"]?.(); + postRun(); + } + if (Module["setStatus"]) { + Module["setStatus"]("Running..."); + setTimeout(() => { + setTimeout(() => Module["setStatus"](""), 1); + doRun(); + }, 1); + } else { + doRun(); + } +} + +var wasmExports; + +// In modularize mode the generated code is within a factory function so we +// can use await here (since it's not top-level-await). +wasmExports = await (createWasm()); + +run(); + +// end include: postamble.js +// include: postamble_modularize.js +// In MODULARIZE mode we wrap the generated code in a factory function +// and return either the Module itself, or a promise of the module. +// We assign to the `moduleRtn` global here and configure closure to see +// this as an extern so it won't get minified. +if (runtimeInitialized) { + moduleRtn = Module; +} else { + // Set up the promise that indicates the Module is initialized + moduleRtn = new Promise((resolve, reject) => { + readyPromiseResolve = resolve; + readyPromiseReject = reject; + }); +} + + + return moduleRtn; +} + +// Export using a UMD style export, or ES6 exports if selected +globalThis.ModuleFactory = ModuleFactory; globalThis.custom_dbg = console.warn.bind(console); export default ModuleFactory; + diff --git a/public/models/mediapipe/wasm/vision_wasm_module_internal.wasm b/public/models/mediapipe/wasm/vision_wasm_module_internal.wasm new file mode 100644 index 0000000..9c82447 Binary files /dev/null and b/public/models/mediapipe/wasm/vision_wasm_module_internal.wasm differ diff --git a/public/models/mediapipe/wasm/vision_wasm_nosimd_internal.js b/public/models/mediapipe/wasm/vision_wasm_nosimd_internal.js new file mode 100644 index 0000000..3dffe5f --- /dev/null +++ b/public/models/mediapipe/wasm/vision_wasm_nosimd_internal.js @@ -0,0 +1,8818 @@ +// This code implements the `-sMODULARIZE` settings by taking the generated +// JS program code (INNER_JS_CODE) and wrapping it in a factory function. + +// Single threaded MINIMAL_RUNTIME programs do not need access to +// document.currentScript, so a simple export declaration is enough. +var ModuleFactory = (() => { + // When MODULARIZE this JS may be executed later, + // after document.currentScript is gone, so we save it. + // In EXPORT_ES6 mode we can just use 'import.meta.url'. + var _scriptName = globalThis.document?.currentScript?.src; + return async function(moduleArg = {}) { + var moduleRtn; + +// include: shell.js +// include: minimum_runtime_check.js +// end include: minimum_runtime_check.js +// The Module object: Our interface to the outside world. We import +// and export values on it. There are various ways Module can be used: +// 1. Not defined. We create it here +// 2. A function parameter, function(moduleArg) => Promise +// 3. pre-run appended it, var Module = {}; ..generated code.. +// 4. External script tag defines var Module. +// We need to check if Module already exists (e.g. case 3 above). +// Substitution will be replaced with actual code on later stage of the build, +// this way Closure Compiler will not mangle it (e.g. case 4. above). +// Note that if you want to run closure, and also to use Module +// after the generated code, you will need to define var Module = {}; +// before the code. Then that object will be used in the code, and you +// can continue to use Module afterwards as well. +var Module = moduleArg; + +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). +// Attempt to auto-detect the environment +var ENVIRONMENT_IS_WEB = !!globalThis.window; + +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; + +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != "renderer"; + +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) +var arguments_ = []; + +var thisProgram = "./this.program"; + +var quit_ = (status, toThrow) => { + throw toThrow; +}; + +if (typeof __filename != "undefined") { + // Node + _scriptName = __filename; +} else if (ENVIRONMENT_IS_WORKER) { + _scriptName = self.location.href; +} + +// `/` should be present at the end if `scriptDirectory` is not empty +var scriptDirectory = ""; + +function locateFile(path) { + if (Module["locateFile"]) { + return Module["locateFile"](path, scriptDirectory); + } + return scriptDirectory + path; +} + +// Hooks that are implemented differently in different runtime environments. +var readAsync, readBinary; + +if (ENVIRONMENT_IS_NODE) { + // These modules will usually be used on Node.js. Load them eagerly to avoid + // the complexity of lazy-loading. + var fs = require("node:fs"); + scriptDirectory = __dirname + "/"; + // include: node_shell_read.js + readBinary = filename => { + // We need to re-wrap `file://` strings to URLs. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename); + return ret; + }; + readAsync = async (filename, binary = true) => { + // See the comment in the `readBinary` function. + filename = isFileURI(filename) ? new URL(filename) : filename; + var ret = fs.readFileSync(filename, binary ? undefined : "utf8"); + return ret; + }; + // end include: node_shell_read.js + if (process.argv.length > 1) { + thisProgram = process.argv[1].replace(/\\/g, "/"); + } + arguments_ = process.argv.slice(2); + quit_ = (status, toThrow) => { + process.exitCode = status; + throw toThrow; + }; +} else // Note that this includes Node.js workers when relevant (pthreads is enabled). +// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and +// ENVIRONMENT_IS_NODE. +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + try { + scriptDirectory = new URL(".", _scriptName).href; + } catch {} + { + // include: web_or_worker_shell_read.js + if (ENVIRONMENT_IS_WORKER) { + readBinary = url => { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + xhr.responseType = "arraybuffer"; + xhr.send(null); + return new Uint8Array(/** @type{!ArrayBuffer} */ (xhr.response)); + }; + } + readAsync = async url => { + // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. + // See https://github.com/github/fetch/pull/92#issuecomment-140665932 + // Cordova or Electron apps are typically loaded from a file:// url. + // So use XHR on webview if URL is a file URL. + if (isFileURI(url)) { + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest; + xhr.open("GET", url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = () => { + if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { + // file URLs can return 0 + resolve(xhr.response); + return; + } + reject(xhr.status); + }; + xhr.onerror = reject; + xhr.send(null); + }); + } + var response = await fetch(url, { + credentials: "same-origin" + }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + " : " + response.url); + }; + } +} else {} + +var out = console.log.bind(console); + +var err = console.error.bind(console); + +// end include: shell.js +// include: preamble.js +// === Preamble library stuff === +// Documentation for the public APIs defined in this file must be updated in: +// site/source/docs/api_reference/preamble.js.rst +// A prebuilt local version of the documentation is available at: +// site/build/text/docs/api_reference/preamble.js.txt +// You can also build docs locally as HTML or other formats in site/ +// An online HTML version (which may be of a different version of Emscripten) +// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html +var wasmBinary; + +// Wasm globals +//======================================== +// Runtime essentials +//======================================== +// whether we are quitting the application. no code should run after this. +// set in exit() and abort() +var ABORT = false; + +// set by exit() and abort(). Passed to 'onExit' handler. +// NOTE: This is also used as the process return code in shell environments +// but only when noExitRuntime is false. +var EXITSTATUS; + +// In STRICT mode, we only define assert() when ASSERTIONS is set. i.e. we +// don't define it at all in release modes. This matches the behaviour of +// MINIMAL_RUNTIME. +// TODO(sbc): Make this the default even without STRICT enabled. +/** @type {function(*, string=)} */ function assert(condition, text) { + if (!condition) { + // This build was created without ASSERTIONS defined. `assert()` should not + // ever be called in this configuration but in case there are callers in + // the wild leave this simple abort() implementation here for now. + abort(text); + } +} + +/** + * Indicates whether filename is delivered via file protocol (as opposed to http/https) + * @noinline + */ var isFileURI = filename => filename.startsWith("file://"); + +// include: runtime_common.js +// include: runtime_stack_check.js +// end include: runtime_stack_check.js +// include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + +// end include: runtime_exceptions.js +// include: runtime_debug.js +// end include: runtime_debug.js +var readyPromiseResolve, readyPromiseReject; + +// Memory management +var runtimeInitialized = false; + +function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + Module["HEAPU8"] = HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + Module["HEAPU32"] = HEAPU32 = new Uint32Array(b); + Module["HEAPF32"] = HEAPF32 = new Float32Array(b); + Module["HEAPF64"] = HEAPF64 = new Float64Array(b); +} + +// include: memoryprofiler.js +// end include: memoryprofiler.js +// end include: runtime_common.js +function preRun() { + if (Module["preRun"]) { + if (typeof Module["preRun"] == "function") Module["preRun"] = [ Module["preRun"] ]; + while (Module["preRun"].length) { + addOnPreRun(Module["preRun"].shift()); + } + } + // Begin ATPRERUNS hooks + callRuntimeCallbacks(onPreRuns); +} + +function initRuntime() { + runtimeInitialized = true; + // Begin ATINITS hooks + if (!Module["noFSInit"] && !FS.initialized) FS.init(); + TTY.init(); + // End ATINITS hooks + wasmExports["id"](); + // Begin ATPOSTCTORS hooks + FS.ignorePermissions = false; +} + +function postRun() { + // PThreads reuse the runtime from the main thread. + if (Module["postRun"]) { + if (typeof Module["postRun"] == "function") Module["postRun"] = [ Module["postRun"] ]; + while (Module["postRun"].length) { + addOnPostRun(Module["postRun"].shift()); + } + } + // Begin ATPOSTRUNS hooks + callRuntimeCallbacks(onPostRuns); +} + +/** + * @param {string|number=} what + */ function abort(what) { + Module["onAbort"]?.(what); + what = `Aborted(${what})`; + // TODO(sbc): Should we remove printing and leave it up to whoever + // catches the exception? + err(what); + ABORT = true; + what += ". Build with -sASSERTIONS for more info."; + // Use a wasm runtime error, because a JS error might be seen as a foreign + // exception, which means we'd run destructors on it. We need the error to + // simply make the program stop. + // FIXME This approach does not work in Wasm EH because it currently does not assume + // all RuntimeErrors are from traps; it decides whether a RuntimeError is from + // a trap or not based on a hidden field within the object. So at the moment + // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that + // allows this in the wasm spec. + // Suppress closure compiler warning here. Closure compiler's builtin extern + // definition for WebAssembly.RuntimeError claims it takes no arguments even + // though it can. + // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. + /** @suppress {checkTypes} */ var e = new WebAssembly.RuntimeError(what); + readyPromiseReject?.(e); + // Throw the error whether or not MODULARIZE is set because abort is used + // in code paths apart from instantiation where an exception is expected + // to be thrown when abort is called. + throw e; +} + +var wasmBinaryFile; + +function findWasmBinary() { + return locateFile("vision_wasm_nosimd_internal.wasm"); +} + +function getBinarySync(file) { + if (file == wasmBinaryFile && wasmBinary) { + return new Uint8Array(wasmBinary); + } + if (readBinary) { + return readBinary(file); + } + // Throwing a plain string here, even though it not normally advisable since + // this gets turning into an `abort` in instantiateArrayBuffer. + throw "both async and sync fetching of the wasm failed"; +} + +async function getWasmBinary(binaryFile) { + // If we don't have the binary yet, load it asynchronously using readAsync. + if (!wasmBinary) { + // Fetch the binary using readAsync + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch {} + } + // Otherwise, getBinarySync should be able to get it synchronously + return getBinarySync(binaryFile); +} + +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); + return instance; + } catch (reason) { + err(`failed to asynchronously prepare wasm: ${reason}`); + abort(reason); + } +} + +async function instantiateAsync(binary, binaryFile, imports) { + if (!binary && !isFileURI(binaryFile) && !ENVIRONMENT_IS_NODE) { + try { + var response = fetch(binaryFile, { + credentials: "same-origin" + }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err(`wasm streaming compile failed: ${reason}`); + err("falling back to ArrayBuffer instantiation"); + } + } + return instantiateArrayBuffer(binaryFile, imports); +} + +function getWasmImports() { + // prepare imports + var imports = { + "a": wasmImports + }; + return imports; +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +async function createWasm() { + // Load the wasm module and create an instance of using native support in the JS engine. + // handle a generated wasm instance, receiving its exports and + // performing other necessary setup + /** @param {WebAssembly.Module=} module*/ function receiveInstance(instance, module) { + wasmExports = instance.exports; + assignWasmExports(wasmExports); + updateMemoryViews(); + return wasmExports; + } + // Prefer streaming instantiation if available. + function receiveInstantiationResult(result) { + // 'result' is a ResultObject object which has both the module and instance. + // receiveInstance() will swap in the exports (to Module.asm) so they can be called + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. + // When the regression is fixed, can restore the above PTHREADS-enabled path. + return receiveInstance(result["instance"]); + } + var info = getWasmImports(); + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback + // to manually instantiate the Wasm module themselves. This allows pages to + // run the instantiation parallel to any other async startup actions they are + // performing. + // Also pthreads and wasm workers initialize the wasm instance through this + // path. + if (Module["instantiateWasm"]) { + return new Promise((resolve, reject) => { + Module["instantiateWasm"](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + }); + } + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; +} + +// Globals used by JS i64 conversions (see makeSetValue) +var tempDouble; + +var tempI64; + +// end include: preamble.js +// Begin JS library code +var handleException = e => { + // Certain exception types we do not treat as errors since they are used for + // internal control flow. + // 1. ExitStatus, which is thrown by exit() + // 2. "unwind", which is thrown by emscripten_unwind_to_js_event_loop() and others + // that wish to return to JS event loop. + if (e instanceof ExitStatus || e == "unwind") { + return EXITSTATUS; + } + quit_(1, e); +}; + +class ExitStatus { + name="ExitStatus"; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; + } +} + +var runtimeKeepaliveCounter = 0; + +var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0; + +var _proc_exit = code => { + EXITSTATUS = code; + if (!keepRuntimeAlive()) { + Module["onExit"]?.(code); + ABORT = true; + } + quit_(code, new ExitStatus(code)); +}; + +/** @param {boolean|number=} implicit */ var exitJS = (status, implicit) => { + EXITSTATUS = status; + _proc_exit(status); +}; + +var _exit = exitJS; + +var maybeExit = () => { + if (!keepRuntimeAlive()) { + try { + _exit(EXITSTATUS); + } catch (e) { + handleException(e); + } + } +}; + +var callUserCallback = func => { + if (ABORT) { + return; + } + try { + return func(); + } catch (e) { + handleException(e); + } finally { + maybeExit(); + } +}; + +function getFullscreenElement() { + return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.webkitCurrentFullScreenElement || document.msFullscreenElement; +} + +/** @param {number=} timeout */ var safeSetTimeout = (func, timeout) => setTimeout(() => { + callUserCallback(func); +}, timeout); + +var warnOnce = text => { + warnOnce.shown ||= {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + if (ENVIRONMENT_IS_NODE) text = "warning: " + text; + err(text); + } +}; + +var preloadPlugins = []; + +var Browser = { + useWebGL: false, + isFullscreen: false, + pointerLock: false, + moduleContextCreatedCallbacks: [], + workers: [], + preloadedImages: {}, + preloadedAudios: {}, + getCanvas: () => Module["canvas"], + init() { + if (Browser.initted) return; + Browser.initted = true; + // Support for plugins that can process preloaded files. You can add more of these to + // your app by creating and appending to preloadPlugins. + // Each plugin is asked if it can handle a file based on the file's name. If it can, + // it is given the file's raw data. When it is done, it calls a callback with the file's + // (possibly modified) data. For example, a plugin might decompress a file, or it + // might create some side data structure for use later (like an Image element, etc.). + var imagePlugin = {}; + imagePlugin["canHandle"] = name => !Module["noImageDecoding"] && /\.(jpg|jpeg|png|bmp|webp)$/i.test(name); + imagePlugin["handle"] = async (byteArray, name) => { + var b = new Blob([ byteArray ], { + type: Browser.getMimetype(name) + }); + if (b.size !== byteArray.length) { + // Safari bug #118630 + // Safari's Blob can only take an ArrayBuffer + b = new Blob([ (new Uint8Array(byteArray)).buffer ], { + type: Browser.getMimetype(name) + }); + } + var url = URL.createObjectURL(b); + return new Promise((resolve, reject) => { + var img = new Image; + img.onload = () => { + var canvas = /** @type {!HTMLCanvasElement} */ (document.createElement("canvas")); + canvas.width = img.width; + canvas.height = img.height; + var ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + Browser.preloadedImages[name] = canvas; + URL.revokeObjectURL(url); + resolve(byteArray); + }; + img.onerror = event => { + err(`Image ${url} could not be decoded`); + reject(); + }; + img.src = url; + }); + }; + preloadPlugins.push(imagePlugin); + var audioPlugin = {}; + audioPlugin["canHandle"] = name => !Module["noAudioDecoding"] && name.slice(-4) in { + ".ogg": 1, + ".wav": 1, + ".mp3": 1 + }; + audioPlugin["handle"] = async (byteArray, name) => new Promise((resolve, reject) => { + var done = false; + function finish(audio) { + if (done) return; + done = true; + Browser.preloadedAudios[name] = audio; + resolve(byteArray); + } + var b = new Blob([ byteArray ], { + type: Browser.getMimetype(name) + }); + var url = URL.createObjectURL(b); + // XXX we never revoke this! + var audio = new Audio; + audio.addEventListener("canplaythrough", () => finish(audio), false); + // use addEventListener due to chromium bug 124926 + audio.onerror = event => { + if (done) return; + err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`); + function encode64(data) { + var BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var PAD = "="; + var ret = ""; + var leftchar = 0; + var leftbits = 0; + for (var i = 0; i < data.length; i++) { + leftchar = (leftchar << 8) | data[i]; + leftbits += 8; + while (leftbits >= 6) { + var curr = (leftchar >> (leftbits - 6)) & 63; + leftbits -= 6; + ret += BASE[curr]; + } + } + if (leftbits == 2) { + ret += BASE[(leftchar & 3) << 4]; + ret += PAD + PAD; + } else if (leftbits == 4) { + ret += BASE[(leftchar & 15) << 2]; + ret += PAD; + } + return ret; + } + audio.src = "data:audio/x-" + name.slice(-3) + ";base64," + encode64(byteArray); + finish(audio); + }; + audio.src = url; + // workaround for chrome bug 124926 - we do not always get oncanplaythrough or onerror + safeSetTimeout(() => { + finish(audio); + }, 1e4); + }); + preloadPlugins.push(audioPlugin); + // Canvas event setup + function pointerLockChange() { + var canvas = Browser.getCanvas(); + Browser.pointerLock = document.pointerLockElement === canvas; + } + var canvas = Browser.getCanvas(); + if (canvas) { + // forced aspect ratio can be enabled by defining 'forcedAspectRatio' on Module + // Module['forcedAspectRatio'] = 4 / 3; + document.addEventListener("pointerlockchange", pointerLockChange, false); + if (Module["elementPointerLock"]) { + canvas.addEventListener("click", ev => { + if (!Browser.pointerLock && Browser.getCanvas().requestPointerLock) { + Browser.getCanvas().requestPointerLock(); + ev.preventDefault(); + } + }, false); + } + } + }, + createContext(/** @type {HTMLCanvasElement} */ canvas, useWebGL, setInModule, webGLContextAttributes) { + if (useWebGL && Module["ctx"] && canvas == Browser.getCanvas()) return Module["ctx"]; + // no need to recreate GL context if it's already been created for this canvas. + var ctx; + var contextHandle; + if (useWebGL) { + // For GLES2/desktop GL compatibility, adjust a few defaults to be different to WebGL defaults, so that they align better with the desktop defaults. + var contextAttributes = { + antialias: false, + alpha: false, + majorVersion: (typeof WebGL2RenderingContext != "undefined") ? 2 : 1 + }; + if (webGLContextAttributes) { + for (var attribute in webGLContextAttributes) { + contextAttributes[attribute] = webGLContextAttributes[attribute]; + } + } + // This check of existence of GL is here to satisfy Closure compiler, which yells if variable GL is referenced below but GL object is not + // actually compiled in because application is not doing any GL operations. TODO: Ideally if GL is not being used, this function + // Browser.createContext() should not even be emitted. + if (typeof GL != "undefined") { + contextHandle = GL.createContext(canvas, contextAttributes); + if (contextHandle) { + ctx = GL.getContext(contextHandle).GLctx; + } + } + } else { + ctx = canvas.getContext("2d"); + } + if (!ctx) return null; + if (setInModule) { + Module["ctx"] = ctx; + if (useWebGL) GL.makeContextCurrent(contextHandle); + Browser.useWebGL = useWebGL; + Browser.moduleContextCreatedCallbacks.forEach(callback => callback()); + Browser.init(); + } + return ctx; + }, + fullscreenHandlersInstalled: false, + lockPointer: undefined, + resizeCanvas: undefined, + requestFullscreen(lockPointer, resizeCanvas) { + Browser.lockPointer = lockPointer; + Browser.resizeCanvas = resizeCanvas; + if (typeof Browser.lockPointer == "undefined") Browser.lockPointer = true; + if (typeof Browser.resizeCanvas == "undefined") Browser.resizeCanvas = false; + var canvas = Browser.getCanvas(); + function fullscreenChange() { + Browser.isFullscreen = false; + var canvasContainer = canvas.parentNode; + if (getFullscreenElement() === canvasContainer) { + canvas.exitFullscreen = Browser.exitFullscreen; + if (Browser.lockPointer) canvas.requestPointerLock(); + Browser.isFullscreen = true; + if (Browser.resizeCanvas) { + Browser.setFullscreenCanvasSize(); + } else { + Browser.updateCanvasDimensions(canvas); + } + } else { + // remove the full screen specific parent of the canvas again to restore the HTML structure from before going full screen + canvasContainer.parentNode.insertBefore(canvas, canvasContainer); + canvasContainer.parentNode.removeChild(canvasContainer); + if (Browser.resizeCanvas) { + Browser.setWindowedCanvasSize(); + } else { + Browser.updateCanvasDimensions(canvas); + } + } + Module["onFullScreen"]?.(Browser.isFullscreen); + Module["onFullscreen"]?.(Browser.isFullscreen); + } + if (!Browser.fullscreenHandlersInstalled) { + Browser.fullscreenHandlersInstalled = true; + document.addEventListener("fullscreenchange", fullscreenChange, false); + document.addEventListener("mozfullscreenchange", fullscreenChange, false); + document.addEventListener("webkitfullscreenchange", fullscreenChange, false); + document.addEventListener("MSFullscreenChange", fullscreenChange, false); + } + // create a new parent to ensure the canvas has no siblings. this allows browsers to optimize full screen performance when its parent is the full screen root + var canvasContainer = document.createElement("div"); + canvas.parentNode.insertBefore(canvasContainer, canvas); + canvasContainer.appendChild(canvas); + // use parent of canvas as full screen root to allow aspect ratio correction (Firefox stretches the root to screen size) + canvasContainer.requestFullscreen = canvasContainer["requestFullscreen"] || canvasContainer["mozRequestFullScreen"] || canvasContainer["msRequestFullscreen"] || (canvasContainer["webkitRequestFullscreen"] ? () => canvasContainer["webkitRequestFullscreen"](Element["ALLOW_KEYBOARD_INPUT"]) : null) || (canvasContainer["webkitRequestFullScreen"] ? () => canvasContainer["webkitRequestFullScreen"](Element["ALLOW_KEYBOARD_INPUT"]) : null); + canvasContainer.requestFullscreen(); + }, + exitFullscreen() { + // This is workaround for chrome. Trying to exit from fullscreen + // not in fullscreen state will cause "TypeError: Document not active" + // in chrome. See https://github.com/emscripten-core/emscripten/pull/8236 + if (!Browser.isFullscreen) { + return false; + } + var CFS = document["exitFullscreen"] || document["cancelFullScreen"] || document["mozCancelFullScreen"] || document["msExitFullscreen"] || document["webkitCancelFullScreen"] || (() => {}); + CFS.apply(document, []); + return true; + }, + safeSetTimeout(func, timeout) { + // Legacy function, this is used by the SDL2 port so we need to keep it + // around at least until that is updated. + // See https://github.com/libsdl-org/SDL/pull/6304 + return safeSetTimeout(func, timeout); + }, + getMimetype(name) { + return { + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "png": "image/png", + "bmp": "image/bmp", + "ogg": "audio/ogg", + "wav": "audio/wav", + "mp3": "audio/mpeg" + }[name.slice(name.lastIndexOf(".") + 1)]; + }, + getUserMedia(func) { + window.getUserMedia ||= navigator["getUserMedia"] || navigator["mozGetUserMedia"]; + window.getUserMedia(func); + }, + getMovementX(event) { + return event["movementX"] || event["mozMovementX"] || event["webkitMovementX"] || 0; + }, + getMovementY(event) { + return event["movementY"] || event["mozMovementY"] || event["webkitMovementY"] || 0; + }, + getMouseWheelDelta(event) { + var delta = 0; + switch (event.type) { + case "DOMMouseScroll": + // 3 lines make up a step + delta = event.detail / 3; + break; + + case "mousewheel": + // 120 units make up a step + delta = event.wheelDelta / 120; + break; + + case "wheel": + delta = event.deltaY; + switch (event.deltaMode) { + case 0: + // DOM_DELTA_PIXEL: 100 pixels make up a step + delta /= 100; + break; + + case 1: + // DOM_DELTA_LINE: 3 lines make up a step + delta /= 3; + break; + + case 2: + // DOM_DELTA_PAGE: A page makes up 80 steps + delta *= 80; + break; + + default: + abort("unrecognized mouse wheel delta mode: " + event.deltaMode); + } + break; + + default: + abort("unrecognized mouse wheel event: " + event.type); + } + return delta; + }, + mouseX: 0, + mouseY: 0, + mouseMovementX: 0, + mouseMovementY: 0, + touches: {}, + lastTouches: {}, + calculateMouseCoords(pageX, pageY) { + // Calculate the movement based on the changes + // in the coordinates. + var canvas = Browser.getCanvas(); + var rect = canvas.getBoundingClientRect(); + // Neither .scrollX or .pageXOffset are defined in a spec, but + // we prefer .scrollX because it is currently in a spec draft. + // (see: http://www.w3.org/TR/2013/WD-cssom-view-20131217/) + var scrollX = ((typeof window.scrollX != "undefined") ? window.scrollX : window.pageXOffset); + var scrollY = ((typeof window.scrollY != "undefined") ? window.scrollY : window.pageYOffset); + var adjustedX = pageX - (scrollX + rect.left); + var adjustedY = pageY - (scrollY + rect.top); + // the canvas might be CSS-scaled compared to its backbuffer; + // SDL-using content will want mouse coordinates in terms + // of backbuffer units. + adjustedX = adjustedX * (canvas.width / rect.width); + adjustedY = adjustedY * (canvas.height / rect.height); + return { + x: adjustedX, + y: adjustedY + }; + }, + setMouseCoords(pageX, pageY) { + const {x, y} = Browser.calculateMouseCoords(pageX, pageY); + Browser.mouseMovementX = x - Browser.mouseX; + Browser.mouseMovementY = y - Browser.mouseY; + Browser.mouseX = x; + Browser.mouseY = y; + }, + calculateMouseEvent(event) { + // event should be mousemove, mousedown or mouseup + if (Browser.pointerLock) { + // When the pointer is locked, calculate the coordinates + // based on the movement of the mouse. + // Workaround for Firefox bug 764498 + if (event.type != "mousemove" && ("mozMovementX" in event)) { + Browser.mouseMovementX = Browser.mouseMovementY = 0; + } else { + Browser.mouseMovementX = Browser.getMovementX(event); + Browser.mouseMovementY = Browser.getMovementY(event); + } + // add the mouse delta to the current absolute mouse position + Browser.mouseX += Browser.mouseMovementX; + Browser.mouseY += Browser.mouseMovementY; + } else { + if (event.type === "touchstart" || event.type === "touchend" || event.type === "touchmove") { + var touch = event.touch; + if (touch === undefined) { + return; + } + var coords = Browser.calculateMouseCoords(touch.pageX, touch.pageY); + if (event.type === "touchstart") { + Browser.lastTouches[touch.identifier] = coords; + Browser.touches[touch.identifier] = coords; + } else if (event.type === "touchend" || event.type === "touchmove") { + var last = Browser.touches[touch.identifier]; + last ||= coords; + Browser.lastTouches[touch.identifier] = last; + Browser.touches[touch.identifier] = coords; + } + return; + } + Browser.setMouseCoords(event.pageX, event.pageY); + } + }, + resizeListeners: [], + updateResizeListeners() { + var canvas = Browser.getCanvas(); + Browser.resizeListeners.forEach(listener => listener(canvas.width, canvas.height)); + }, + setCanvasSize(width, height, noUpdates) { + var canvas = Browser.getCanvas(); + Browser.updateCanvasDimensions(canvas, width, height); + if (!noUpdates) Browser.updateResizeListeners(); + }, + windowedWidth: 0, + windowedHeight: 0, + setFullscreenCanvasSize() { + // check if SDL is available + if (typeof SDL != "undefined") { + var flags = HEAPU32[((SDL.screen) >> 2)]; + flags = flags | 8388608; + // set SDL_FULLSCREEN flag + HEAP32[((SDL.screen) >> 2)] = flags; + } + Browser.updateCanvasDimensions(Browser.getCanvas()); + Browser.updateResizeListeners(); + }, + setWindowedCanvasSize() { + // check if SDL is available + if (typeof SDL != "undefined") { + var flags = HEAPU32[((SDL.screen) >> 2)]; + flags = flags & ~8388608; + // clear SDL_FULLSCREEN flag + HEAP32[((SDL.screen) >> 2)] = flags; + } + Browser.updateCanvasDimensions(Browser.getCanvas()); + Browser.updateResizeListeners(); + }, + updateCanvasDimensions(canvas, wNative, hNative) { + if (wNative && hNative) { + canvas.widthNative = wNative; + canvas.heightNative = hNative; + } else { + wNative = canvas.widthNative; + hNative = canvas.heightNative; + } + var w = wNative; + var h = hNative; + if (Module["forcedAspectRatio"] > 0) { + if (w / h < Module["forcedAspectRatio"]) { + w = Math.round(h * Module["forcedAspectRatio"]); + } else { + h = Math.round(w / Module["forcedAspectRatio"]); + } + } + if ((getFullscreenElement() === canvas.parentNode) && (typeof screen != "undefined")) { + var factor = Math.min(screen.width / w, screen.height / h); + w = Math.round(w * factor); + h = Math.round(h * factor); + } + if (Browser.resizeCanvas) { + if (canvas.width != w) canvas.width = w; + if (canvas.height != h) canvas.height = h; + if (typeof canvas.style != "undefined") { + canvas.style.removeProperty("width"); + canvas.style.removeProperty("height"); + } + } else { + if (canvas.width != wNative) canvas.width = wNative; + if (canvas.height != hNative) canvas.height = hNative; + if (typeof canvas.style != "undefined") { + if (w != wNative || h != hNative) { + canvas.style.setProperty("width", w + "px", "important"); + canvas.style.setProperty("height", h + "px", "important"); + } else { + canvas.style.removeProperty("width"); + canvas.style.removeProperty("height"); + } + } + } + } +}; + +/** @type {!Int16Array} */ var HEAP16; + +/** @type {!Int32Array} */ var HEAP32; + +/** @type {!Int8Array} */ var HEAP8; + +/** @type {!Float32Array} */ var HEAPF32; + +/** @type {!Float64Array} */ var HEAPF64; + +/** @type {!Uint16Array} */ var HEAPU16; + +/** @type {!Uint32Array} */ var HEAPU32; + +/** @type {!Uint8Array} */ var HEAPU8; + +var callRuntimeCallbacks = callbacks => { + while (callbacks.length > 0) { + // Pass the module as the first argument. + callbacks.shift()(Module); + } +}; + +var onPostRuns = []; + +var addOnPostRun = cb => onPostRuns.push(cb); + +var onPreRuns = []; + +var addOnPreRun = cb => onPreRuns.push(cb); + +var noExitRuntime = true; + +var stackRestore = val => __emscripten_stack_restore(val); + +var stackSave = () => _emscripten_stack_get_current(); + +class ExceptionInfo { + // excPtr - Thrown object pointer to wrap. Metadata pointer is calculated from it. + constructor(excPtr) { + this.excPtr = excPtr; + this.ptr = excPtr - 24; + } + set_type(type) { + HEAPU32[(((this.ptr) + (4)) >> 2)] = type; + } + get_type() { + return HEAPU32[(((this.ptr) + (4)) >> 2)]; + } + set_destructor(destructor) { + HEAPU32[(((this.ptr) + (8)) >> 2)] = destructor; + } + get_destructor() { + return HEAPU32[(((this.ptr) + (8)) >> 2)]; + } + set_caught(caught) { + caught = caught ? 1 : 0; + HEAP8[(this.ptr) + (12)] = caught; + } + get_caught() { + return HEAP8[(this.ptr) + (12)] != 0; + } + set_rethrown(rethrown) { + rethrown = rethrown ? 1 : 0; + HEAP8[(this.ptr) + (13)] = rethrown; + } + get_rethrown() { + return HEAP8[(this.ptr) + (13)] != 0; + } + // Initialize native structure fields. Should be called once after allocated. + init(type, destructor) { + this.set_adjusted_ptr(0); + this.set_type(type); + this.set_destructor(destructor); + } + set_adjusted_ptr(adjustedPtr) { + HEAPU32[(((this.ptr) + (16)) >> 2)] = adjustedPtr; + } + get_adjusted_ptr() { + return HEAPU32[(((this.ptr) + (16)) >> 2)]; + } +} + +var uncaughtExceptionCount = 0; + +var ___cxa_throw = (ptr, type, destructor) => { + var info = new ExceptionInfo(ptr); + // Initialize ExceptionInfo content after it was allocated in __cxa_allocate_exception. + info.init(type, destructor); + uncaughtExceptionCount++; + abort(); +}; + +var PATH = { + isAbs: path => path.charAt(0) === "/", + splitPath: filename => { + var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + return splitPathRe.exec(filename).slice(1); + }, + normalizeArray: (parts, allowAboveRoot) => { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === ".") { + parts.splice(i, 1); + } else if (last === "..") { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (;up; up--) { + parts.unshift(".."); + } + } + return parts; + }, + normalize: path => { + var isAbsolute = PATH.isAbs(path), trailingSlash = path.slice(-1) === "/"; + // Normalize the path + path = PATH.normalizeArray(path.split("/").filter(p => !!p), !isAbsolute).join("/"); + if (!path && !isAbsolute) { + path = "."; + } + if (path && trailingSlash) { + path += "/"; + } + return (isAbsolute ? "/" : "") + path; + }, + dirname: path => { + var result = PATH.splitPath(path), root = result[0], dir = result[1]; + if (!root && !dir) { + // No dirname whatsoever + return "."; + } + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.slice(0, -1); + } + return root + dir; + }, + basename: path => path && path.match(/([^\/]+|\/)\/*$/)[1], + join: (...paths) => PATH.normalize(paths.join("/")), + join2: (l, r) => PATH.normalize(l + "/" + r) +}; + +var initRandomFill = () => { + // This block is not needed on v19+ since crypto.getRandomValues is builtin + if (ENVIRONMENT_IS_NODE) { + var nodeCrypto = require("node:crypto"); + return view => nodeCrypto.randomFillSync(view); + } + return view => (crypto.getRandomValues(view), 0); +}; + +var randomFill = view => (randomFill = initRandomFill())(view); + +var PATH_FS = { + resolve: (...args) => { + var resolvedPath = "", resolvedAbsolute = false; + for (var i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? args[i] : FS.cwd(); + // Skip empty and invalid entries + if (typeof path != "string") { + throw new TypeError("Arguments to path.resolve must be strings"); + } else if (!path) { + return ""; + } + resolvedPath = path + "/" + resolvedPath; + resolvedAbsolute = PATH.isAbs(path); + } + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + resolvedPath = PATH.normalizeArray(resolvedPath.split("/").filter(p => !!p), !resolvedAbsolute).join("/"); + return ((resolvedAbsolute ? "/" : "") + resolvedPath) || "."; + }, + relative: (from, to) => { + from = PATH_FS.resolve(from).slice(1); + to = PATH_FS.resolve(to).slice(1); + function trim(arr) { + var start = 0; + for (;start < arr.length; start++) { + if (arr[start] !== "") break; + } + var end = arr.length - 1; + for (;end >= 0; end--) { + if (arr[end] !== "") break; + } + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + var fromParts = trim(from.split("/")); + var toParts = trim(to.split("/")); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push(".."); + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join("/"); + } +}; + +var UTF8Decoder = new TextDecoder; + +var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on + // null terminator by itself. + // As a tiny code save trick, compare idx against maxIdx using a negation, + // so that maxBytesToRead=undefined/NaN means Infinity. + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; +}; + +/** + * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given + * array that contains uint8 values, returns a copy of that string as a + * Javascript String object. + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number=} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + return UTF8Decoder.decode(heapOrArray.buffer ? heapOrArray.subarray(idx, endPtr) : new Uint8Array(heapOrArray.slice(idx, endPtr))); +}; + +var FS_stdin_getChar_buffer = []; + +var lengthBytesUTF8 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code + // unit, not a Unicode code point of the character! So decode + // UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var c = str.charCodeAt(i); + // possibly a lead surrogate + if (c <= 127) { + len++; + } else if (c <= 2047) { + len += 2; + } else if (c >= 55296 && c <= 57343) { + len += 4; + ++i; + } else { + len += 3; + } + } + return len; +}; + +var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + // Parameter maxBytesToWrite is not optional. Negative values, 0, null, + // undefined and false each don't write out any bytes. + if (!(maxBytesToWrite > 0)) return 0; + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; + // -1 for string null terminator. + for (var i = 0; i < str.length; ++i) { + // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description + // and https://www.ietf.org/rfc/rfc2279.txt + // and https://tools.ietf.org/html/rfc3629 + var u = str.codePointAt(i); + if (u <= 127) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 2047) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 192 | (u >> 6); + heap[outIdx++] = 128 | (u & 63); + } else if (u <= 65535) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 224 | (u >> 12); + heap[outIdx++] = 128 | ((u >> 6) & 63); + heap[outIdx++] = 128 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + heap[outIdx++] = 240 | (u >> 18); + heap[outIdx++] = 128 | ((u >> 12) & 63); + heap[outIdx++] = 128 | ((u >> 6) & 63); + heap[outIdx++] = 128 | (u & 63); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + i++; + } + } + // Null-terminate the pointer to the buffer. + heap[outIdx] = 0; + return outIdx - startIdx; +}; + +/** @type {function(string, boolean=, number=)} */ var intArrayFromString = (stringy, dontAddNull, length) => { + var len = length > 0 ? length : lengthBytesUTF8(stringy) + 1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; +}; + +var FS_stdin_getChar = () => { + if (!FS_stdin_getChar_buffer.length) { + var result = null; + if (ENVIRONMENT_IS_NODE) { + // we will read data by chunks of BUFSIZE + var BUFSIZE = 256; + var buf = Buffer.alloc(BUFSIZE); + var bytesRead = 0; + // For some reason we must suppress a closure warning here, even though + // fd definitely exists on process.stdin, and is even the proper way to + // get the fd of stdin, + // https://github.com/nodejs/help/issues/2136#issuecomment-523649904 + // This started to happen after moving this logic out of library_tty.js, + // so it is related to the surrounding code in some unclear manner. + /** @suppress {missingProperties} */ var fd = process.stdin.fd; + try { + bytesRead = fs.readSync(fd, buf, 0, BUFSIZE); + } catch (e) { + // Cross-platform differences: on Windows, reading EOF throws an + // exception, but on other OSes, reading EOF returns 0. Uniformize + // behavior by treating the EOF exception to return 0. + if (e.toString().includes("EOF")) bytesRead = 0; else throw e; + } + if (bytesRead > 0) { + result = buf.slice(0, bytesRead).toString("utf-8"); + } + } else if (globalThis.window?.prompt) { + // Browser. + result = window.prompt("Input: "); + // returns null on cancel + if (result !== null) { + result += "\n"; + } + } else {} + if (!result) { + return null; + } + FS_stdin_getChar_buffer = intArrayFromString(result, true); + } + return FS_stdin_getChar_buffer.shift(); +}; + +var TTY = { + ttys: [], + init() {}, + shutdown() {}, + register(dev, ops) { + TTY.ttys[dev] = { + input: [], + output: [], + ops + }; + FS.registerDevice(dev, TTY.stream_ops); + }, + stream_ops: { + open(stream) { + var tty = TTY.ttys[stream.node.rdev]; + if (!tty) { + throw new FS.ErrnoError(43); + } + stream.tty = tty; + stream.seekable = false; + }, + close(stream) { + // flush any pending line data + stream.tty.ops.fsync(stream.tty); + }, + fsync(stream) { + stream.tty.ops.fsync(stream.tty); + }, + read(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.get_char) { + throw new FS.ErrnoError(60); + } + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = stream.tty.ops.get_char(stream.tty); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.put_char) { + throw new FS.ErrnoError(60); + } + try { + for (var i = 0; i < length; i++) { + stream.tty.ops.put_char(stream.tty, buffer[offset + i]); + } + } catch (e) { + throw new FS.ErrnoError(29); + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + } + }, + default_tty_ops: { + get_char(tty) { + return FS_stdin_getChar(); + }, + put_char(tty, val) { + if (val === null || val === 10) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } + }, + ioctl_tcgets(tty) { + // typical setting + return { + c_iflag: 25856, + c_oflag: 5, + c_cflag: 191, + c_lflag: 35387, + c_cc: [ 3, 28, 127, 21, 4, 0, 1, 0, 17, 19, 26, 0, 18, 15, 23, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] + }; + }, + ioctl_tcsets(tty, optional_actions, data) { + // currently just ignore + return 0; + }, + ioctl_tiocgwinsz(tty) { + return [ 24, 80 ]; + } + }, + default_tty1_ops: { + put_char(tty, val) { + if (val === null || val === 10) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } + } + } +}; + +var zeroMemory = (ptr, size) => HEAPU8.fill(0, ptr, ptr + size); + +var alignMemory = (size, alignment) => Math.ceil(size / alignment) * alignment; + +var mmapAlloc = size => { + size = alignMemory(size, 65536); + var ptr = _emscripten_builtin_memalign(65536, size); + if (ptr) zeroMemory(ptr, size); + return ptr; +}; + +var MEMFS = { + ops_table: null, + mount(mount) { + return MEMFS.createNode(null, "/", 16895, 0); + }, + createNode(parent, name, mode, dev) { + if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { + // not supported + throw new FS.ErrnoError(63); + } + MEMFS.ops_table ||= { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink + }, + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync + } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + }; + var node = FS.createNode(parent, name, mode, dev); + if (FS.isDir(node.mode)) { + node.node_ops = MEMFS.ops_table.dir.node; + node.stream_ops = MEMFS.ops_table.dir.stream; + node.contents = {}; + } else if (FS.isFile(node.mode)) { + node.node_ops = MEMFS.ops_table.file.node; + node.stream_ops = MEMFS.ops_table.file.stream; + // The actual number of bytes used in the typed array, as opposed to + // contents.length which gives the whole capacity. + node.usedBytes = 0; + // The byte data of the file is stored in a typed array. + // Note: typed arrays are not resizable like normal JS arrays are, so + // there is a small penalty involved for appending file writes that + // continuously grow a file similar to std::vector capacity vs used. + node.contents = MEMFS.emptyFileContents ??= new Uint8Array(0); + } else if (FS.isLink(node.mode)) { + node.node_ops = MEMFS.ops_table.link.node; + node.stream_ops = MEMFS.ops_table.link.stream; + } else if (FS.isChrdev(node.mode)) { + node.node_ops = MEMFS.ops_table.chrdev.node; + node.stream_ops = MEMFS.ops_table.chrdev.stream; + } + node.atime = node.mtime = node.ctime = Date.now(); + // add the new node to the parent + if (parent) { + parent.contents[name] = node; + parent.atime = parent.mtime = parent.ctime = node.atime; + } + return node; + }, + getFileDataAsTypedArray(node) { + return node.contents.subarray(0, node.usedBytes); + }, + expandFileStorage(node, newCapacity) { + var prevCapacity = node.contents.length; + if (prevCapacity >= newCapacity) return; + // No need to expand, the storage was already large enough. + // Don't expand strictly to the given requested limit if it's only a very + // small increase, but instead geometrically grow capacity. + // For small filesizes (<1MB), perform size*2 geometric increase, but for + // large sizes, do a much more conservative size*1.125 increase to avoid + // overshooting the allocation cap by a very large margin. + var CAPACITY_DOUBLING_MAX = 1024 * 1024; + newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2 : 1.125)) >>> 0); + if (prevCapacity) newCapacity = Math.max(newCapacity, 256); + // At minimum allocate 256b for each file when expanding. + var oldContents = MEMFS.getFileDataAsTypedArray(node); + node.contents = new Uint8Array(newCapacity); + // Allocate new storage. + node.contents.set(oldContents); + }, + resizeFileStorage(node, newSize) { + if (node.usedBytes == newSize) return; + var oldContents = node.contents; + node.contents = new Uint8Array(newSize); + // Allocate new storage. + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); + // Copy old data over to the new storage. + node.usedBytes = newSize; + }, + node_ops: { + getattr(node) { + var attr = {}; + // device numbers reuse inode numbers. + attr.dev = FS.isChrdev(node.mode) ? node.id : 1; + attr.ino = node.id; + attr.mode = node.mode; + attr.nlink = 1; + attr.uid = 0; + attr.gid = 0; + attr.rdev = node.rdev; + if (FS.isDir(node.mode)) { + attr.size = 4096; + } else if (FS.isFile(node.mode)) { + attr.size = node.usedBytes; + } else if (FS.isLink(node.mode)) { + attr.size = node.link.length; + } else { + attr.size = 0; + } + attr.atime = new Date(node.atime); + attr.mtime = new Date(node.mtime); + attr.ctime = new Date(node.ctime); + // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), + // but this is not required by the standard. + attr.blksize = 4096; + attr.blocks = Math.ceil(attr.size / attr.blksize); + return attr; + }, + setattr(node, attr) { + for (const key of [ "mode", "atime", "mtime", "ctime" ]) { + if (attr[key] != null) { + node[key] = attr[key]; + } + } + if (attr.size !== undefined) { + MEMFS.resizeFileStorage(node, attr.size); + } + }, + lookup(parent, name) { + // This error may happen quite a bit. To avoid overhead we reuse it (and + // suffer a lack of stack info). + if (!MEMFS.doesNotExistError) { + MEMFS.doesNotExistError = new FS.ErrnoError(44); + /** @suppress {checkTypes} */ MEMFS.doesNotExistError.stack = ""; + } + throw MEMFS.doesNotExistError; + }, + mknod(parent, name, mode, dev) { + return MEMFS.createNode(parent, name, mode, dev); + }, + rename(old_node, new_dir, new_name) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + if (new_node) { + if (FS.isDir(old_node.mode)) { + // if we're overwriting a directory at new_name, make sure it's empty. + for (var i in new_node.contents) { + throw new FS.ErrnoError(55); + } + } + FS.hashRemoveNode(new_node); + } + // do the internal rewiring + delete old_node.parent.contents[old_node.name]; + new_dir.contents[new_name] = old_node; + old_node.name = new_name; + new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now(); + }, + unlink(parent, name) { + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + rmdir(parent, name) { + var node = FS.lookupNode(parent, name); + for (var i in node.contents) { + throw new FS.ErrnoError(55); + } + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + readdir(node) { + return [ ".", "..", ...Object.keys(node.contents) ]; + }, + symlink(parent, newname, oldpath) { + var node = MEMFS.createNode(parent, newname, 511 | 40960, 0); + node.link = oldpath; + return node; + }, + readlink(node) { + if (!FS.isLink(node.mode)) { + throw new FS.ErrnoError(28); + } + return node.link; + } + }, + stream_ops: { + read(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= stream.node.usedBytes) return 0; + var size = Math.min(stream.node.usedBytes - position, length); + buffer.set(contents.subarray(position, position + size), offset); + return size; + }, + write(stream, buffer, offset, length, position, canOwn) { + // If the buffer is located in main memory (HEAP), and if + // memory can grow, we can't hold on to references of the + // memory buffer, as they may get invalidated. That means we + // need to copy its contents. + if (buffer.buffer === HEAP8.buffer) { + canOwn = false; + } + if (!length) return 0; + var node = stream.node; + node.mtime = node.ctime = Date.now(); + if (canOwn) { + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + } else if (node.usedBytes === 0 && position === 0) { + // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. + node.contents = buffer.slice(offset, offset + length); + node.usedBytes = length; + } else { + MEMFS.expandFileStorage(node, position + length); + // Use typed array write which is available. + node.contents.set(buffer.subarray(offset, offset + length), position); + node.usedBytes = Math.max(node.usedBytes, position + length); + } + return length; + }, + llseek(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position; + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + position += stream.node.usedBytes; + } + } + if (position < 0) { + throw new FS.ErrnoError(28); + } + return position; + }, + mmap(stream, length, position, prot, flags) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + var ptr; + var allocated; + var contents = stream.node.contents; + // Only make a new copy when MAP_PRIVATE is specified. + if (!(flags & 2) && contents.buffer === HEAP8.buffer) { + // We can't emulate MAP_SHARED when the file is not backed by the + // buffer we're mapping to (e.g. the HEAP buffer). + allocated = false; + ptr = contents.byteOffset; + } else { + allocated = true; + ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + if (contents) { + // Try to avoid unnecessary slices. + if (position > 0 || position + length < contents.length) { + if (contents.subarray) { + contents = contents.subarray(position, position + length); + } else { + contents = Array.prototype.slice.call(contents, position, position + length); + } + } + HEAP8.set(contents, ptr); + } + } + return { + ptr, + allocated + }; + }, + msync(stream, buffer, offset, length, mmapFlags) { + MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); + // should we check if bytesWritten and length are the same? + return 0; + } + } +}; + +var FS_modeStringToFlags = str => { + if (typeof str != "string") return str; + var flagModes = { + "r": 0, + "r+": 2, + "w": 512 | 64 | 1, + "w+": 512 | 64 | 2, + "a": 1024 | 64 | 1, + "a+": 1024 | 64 | 2 + }; + var flags = flagModes[str]; + if (typeof flags == "undefined") { + throw new Error(`Unknown file open mode: ${str}`); + } + return flags; +}; + +var FS_fileDataToTypedArray = data => { + if (typeof data == "string") { + data = intArrayFromString(data, true); + } + if (!data.subarray) { + data = new Uint8Array(data); + } + return data; +}; + +var FS_getMode = (canRead, canWrite) => { + var mode = 0; + if (canRead) mode |= 292 | 73; + if (canWrite) mode |= 146; + return mode; +}; + +var asyncLoad = async url => { + var arrayBuffer = await readAsync(url); + return new Uint8Array(arrayBuffer); +}; + +var FS_createDataFile = (...args) => FS.createDataFile(...args); + +var getUniqueRunDependency = id => id; + +var runDependencies = 0; + +var dependenciesFulfilled = null; + +var removeRunDependency = id => { + runDependencies--; + Module["monitorRunDependencies"]?.(runDependencies); + if (runDependencies == 0) { + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); + } + } +}; + +var addRunDependency = id => { + runDependencies++; + Module["monitorRunDependencies"]?.(runDependencies); +}; + +var FS_handledByPreloadPlugin = async (byteArray, fullname) => { + // Ensure plugins are ready. + if (typeof Browser != "undefined") Browser.init(); + for (var plugin of preloadPlugins) { + if (plugin["canHandle"](fullname)) { + return plugin["handle"](byteArray, fullname); + } + } + // If no plugin handled this file then return the original/unmodified + // byteArray. + return byteArray; +}; + +var FS_preloadFile = async (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => { + // TODO we should allow people to just pass in a complete filename instead + // of parent and name being that we just join them anyways + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency(`cp ${fullname}`); + // might have several active requests for the same fullname + addRunDependency(dep); + try { + var byteArray = url; + if (typeof url == "string") { + byteArray = await asyncLoad(url); + } + byteArray = await FS_handledByPreloadPlugin(byteArray, fullname); + preFinish?.(); + if (!dontCreateFile) { + FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); + } + } finally { + removeRunDependency(dep); + } +}; + +var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { + FS_preloadFile(parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish).then(onload).catch(onerror); +}; + +var FS = { + root: null, + mounts: [], + devices: {}, + streams: [], + nextInode: 1, + nameTable: null, + currentPath: "/", + initialized: false, + ignorePermissions: true, + filesystems: null, + syncFSRequests: 0, + ErrnoError: class { + name="ErrnoError"; + // We set the `name` property to be able to identify `FS.ErrnoError` + // - the `name` is a standard ECMA-262 property of error objects. Kind of good to have it anyway. + // - when using PROXYFS, an error can come from an underlying FS + // as different FS objects have their own FS.ErrnoError each, + // the test `err instanceof FS.ErrnoError` won't detect an error coming from another filesystem, causing bugs. + // we'll use the reliable test `err.name == "ErrnoError"` instead + constructor(errno) { + this.errno = errno; + } + }, + FSStream: class { + shared={}; + get object() { + return this.node; + } + set object(val) { + this.node = val; + } + get isRead() { + return (this.flags & 2097155) !== 1; + } + get isWrite() { + return (this.flags & 2097155) !== 0; + } + get isAppend() { + return (this.flags & 1024); + } + get flags() { + return this.shared.flags; + } + set flags(val) { + this.shared.flags = val; + } + get position() { + return this.shared.position; + } + set position(val) { + this.shared.position = val; + } + }, + FSNode: class { + node_ops={}; + stream_ops={}; + readMode=292 | 73; + writeMode=146; + mounted=null; + constructor(parent, name, mode, rdev) { + if (!parent) { + parent = this; + } + this.parent = parent; + this.mount = parent.mount; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.rdev = rdev; + this.atime = this.mtime = this.ctime = Date.now(); + } + get read() { + return (this.mode & this.readMode) === this.readMode; + } + set read(val) { + val ? this.mode |= this.readMode : this.mode &= ~this.readMode; + } + get write() { + return (this.mode & this.writeMode) === this.writeMode; + } + set write(val) { + val ? this.mode |= this.writeMode : this.mode &= ~this.writeMode; + } + get isFolder() { + return FS.isDir(this.mode); + } + get isDevice() { + return FS.isChrdev(this.mode); + } + }, + lookupPath(path, opts = {}) { + if (!path) { + throw new FS.ErrnoError(44); + } + opts.follow_mount ??= true; + if (!PATH.isAbs(path)) { + path = FS.cwd() + "/" + path; + } + // limit max consecutive symlinks to 40 (SYMLOOP_MAX). + linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { + // split the absolute path + var parts = path.split("/").filter(p => !!p); + // start at the root + var current = FS.root; + var current_path = "/"; + for (var i = 0; i < parts.length; i++) { + var islast = (i === parts.length - 1); + if (islast && opts.parent) { + // stop resolving + break; + } + if (parts[i] === ".") { + continue; + } + if (parts[i] === "..") { + current_path = PATH.dirname(current_path); + if (FS.isRoot(current)) { + path = current_path + "/" + parts.slice(i + 1).join("/"); + // We're making progress here, don't let many consecutive ..'s + // lead to ELOOP + nlinks--; + continue linkloop; + } else { + current = current.parent; + } + continue; + } + current_path = PATH.join2(current_path, parts[i]); + try { + current = FS.lookupNode(current, parts[i]); + } catch (e) { + // if noent_okay is true, suppress a ENOENT in the last component + // and return an object with an undefined node. This is needed for + // resolving symlinks in the path when creating a file. + if ((e?.errno === 44) && islast && opts.noent_okay) { + return { + path: current_path + }; + } + throw e; + } + // jump to the mount's root node if this is a mountpoint + if (FS.isMountpoint(current) && (!islast || opts.follow_mount)) { + current = current.mounted.root; + } + // by default, lookupPath will not follow a symlink if it is the final path component. + // setting opts.follow = true will override this behavior. + if (FS.isLink(current.mode) && (!islast || opts.follow)) { + if (!current.node_ops.readlink) { + throw new FS.ErrnoError(52); + } + var link = current.node_ops.readlink(current); + if (!PATH.isAbs(link)) { + link = PATH.dirname(current_path) + "/" + link; + } + path = link + "/" + parts.slice(i + 1).join("/"); + continue linkloop; + } + } + return { + path: current_path, + node: current + }; + } + throw new FS.ErrnoError(32); + }, + getPath(node) { + var path; + while (true) { + if (FS.isRoot(node)) { + var mount = node.mount.mountpoint; + if (!path) return mount; + return mount[mount.length - 1] !== "/" ? `${mount}/${path}` : mount + path; + } + path = path ? `${node.name}/${path}` : node.name; + node = node.parent; + } + }, + hashName(parentid, name) { + var hash = 0; + for (var i = 0; i < name.length; i++) { + hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; + } + return ((parentid + hash) >>> 0) % FS.nameTable.length; + }, + hashAddNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + node.name_next = FS.nameTable[hash]; + FS.nameTable[hash] = node; + }, + hashRemoveNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + if (FS.nameTable[hash] === node) { + FS.nameTable[hash] = node.name_next; + } else { + var current = FS.nameTable[hash]; + while (current) { + if (current.name_next === node) { + current.name_next = node.name_next; + break; + } + current = current.name_next; + } + } + }, + lookupNode(parent, name) { + var errCode = FS.mayLookup(parent); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + var hash = FS.hashName(parent.id, name); + for (var node = FS.nameTable[hash]; node; node = node.name_next) { + var nodeName = node.name; + if (node.parent.id === parent.id && nodeName === name) { + return node; + } + } + // if we failed to find it in the cache, call into the VFS + return FS.lookup(parent, name); + }, + createNode(parent, name, mode, rdev) { + var node = new FS.FSNode(parent, name, mode, rdev); + FS.hashAddNode(node); + return node; + }, + destroyNode(node) { + FS.hashRemoveNode(node); + }, + isRoot(node) { + return node === node.parent; + }, + isMountpoint(node) { + return !!node.mounted; + }, + isFile(mode) { + return (mode & 61440) === 32768; + }, + isDir(mode) { + return (mode & 61440) === 16384; + }, + isLink(mode) { + return (mode & 61440) === 40960; + }, + isChrdev(mode) { + return (mode & 61440) === 8192; + }, + isBlkdev(mode) { + return (mode & 61440) === 24576; + }, + isFIFO(mode) { + return (mode & 61440) === 4096; + }, + isSocket(mode) { + return (mode & 49152) === 49152; + }, + flagsToPermissionString(flag) { + var perms = [ "r", "w", "rw" ][flag & 3]; + if ((flag & 512)) { + perms += "w"; + } + return perms; + }, + nodePermissions(node, perms) { + if (FS.ignorePermissions) { + return 0; + } + // return 0 if any user, group or owner bits are set. + if (perms.includes("r") && !(node.mode & 292)) { + return 2; + } + if (perms.includes("w") && !(node.mode & 146)) { + return 2; + } + if (perms.includes("x") && !(node.mode & 73)) { + return 2; + } + return 0; + }, + mayLookup(dir) { + if (!FS.isDir(dir.mode)) return 54; + var errCode = FS.nodePermissions(dir, "x"); + if (errCode) return errCode; + if (!dir.node_ops.lookup) return 2; + return 0; + }, + mayCreate(dir, name) { + if (!FS.isDir(dir.mode)) { + return 54; + } + try { + var node = FS.lookupNode(dir, name); + return 20; + } catch (e) {} + return FS.nodePermissions(dir, "wx"); + }, + mayDelete(dir, name, isdir) { + var node; + try { + node = FS.lookupNode(dir, name); + } catch (e) { + return e.errno; + } + var errCode = FS.nodePermissions(dir, "wx"); + if (errCode) { + return errCode; + } + if (isdir) { + if (!FS.isDir(node.mode)) { + return 54; + } + if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { + return 10; + } + } else if (FS.isDir(node.mode)) { + return 31; + } + return 0; + }, + mayOpen(node, flags) { + if (!node) { + return 44; + } + if (FS.isLink(node.mode)) { + return 32; + } + var mode = FS.flagsToPermissionString(flags); + if (FS.isDir(node.mode)) { + // opening for write + // TODO: check for O_SEARCH? (== search for dir only) + if (mode !== "r" || (flags & (512 | 64))) { + return 31; + } + } + return FS.nodePermissions(node, mode); + }, + checkOpExists(op, err) { + if (!op) { + throw new FS.ErrnoError(err); + } + return op; + }, + MAX_OPEN_FDS: 4096, + nextfd() { + for (var fd = 0; fd <= FS.MAX_OPEN_FDS; fd++) { + if (!FS.streams[fd]) { + return fd; + } + } + throw new FS.ErrnoError(33); + }, + getStreamChecked(fd) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); + } + return stream; + }, + getStream: fd => FS.streams[fd], + createStream(stream, fd = -1) { + // clone it, so we can return an instance of FSStream + stream = Object.assign(new FS.FSStream, stream); + if (fd == -1) { + fd = FS.nextfd(); + } + stream.fd = fd; + FS.streams[fd] = stream; + return stream; + }, + closeStream(fd) { + FS.streams[fd] = null; + }, + dupStream(origStream, fd = -1) { + var stream = FS.createStream(origStream, fd); + stream.stream_ops?.dup?.(stream); + return stream; + }, + doSetAttr(stream, node, attr) { + var setattr = stream?.stream_ops.setattr; + var arg = setattr ? stream : node; + setattr ??= node.node_ops.setattr; + FS.checkOpExists(setattr, 63); + setattr(arg, attr); + }, + chrdev_stream_ops: { + open(stream) { + var device = FS.getDevice(stream.node.rdev); + // override node's stream ops with the device's + stream.stream_ops = device.stream_ops; + // forward the open call + stream.stream_ops.open?.(stream); + }, + llseek() { + throw new FS.ErrnoError(70); + } + }, + major: dev => ((dev) >> 8), + minor: dev => ((dev) & 255), + makedev: (ma, mi) => ((ma) << 8 | (mi)), + registerDevice(dev, ops) { + FS.devices[dev] = { + stream_ops: ops + }; + }, + getDevice: dev => FS.devices[dev], + getMounts(mount) { + var mounts = []; + var check = [ mount ]; + while (check.length) { + var m = check.pop(); + mounts.push(m); + check.push(...m.mounts); + } + return mounts; + }, + syncfs(populate, callback) { + if (typeof populate == "function") { + callback = populate; + populate = false; + } + FS.syncFSRequests++; + if (FS.syncFSRequests > 1) { + err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`); + } + var mounts = FS.getMounts(FS.root.mount); + var completed = 0; + function doCallback(errCode) { + FS.syncFSRequests--; + return callback(errCode); + } + function done(errCode) { + if (errCode) { + if (!done.errored) { + done.errored = true; + return doCallback(errCode); + } + return; + } + if (++completed >= mounts.length) { + doCallback(null); + } + } + // sync all mounts + for (var mount of mounts) { + if (mount.type.syncfs) { + mount.type.syncfs(mount, populate, done); + } else { + done(null); + } + } + }, + mount(type, opts, mountpoint) { + var root = mountpoint === "/"; + var pseudo = !mountpoint; + var node; + if (root && FS.root) { + throw new FS.ErrnoError(10); + } else if (!root && !pseudo) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + mountpoint = lookup.path; + // use the absolute path + node = lookup.node; + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + if (!FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + } + var mount = { + type, + opts, + mountpoint, + mounts: [] + }; + // create a root node for the fs + var mountRoot = type.mount(mount); + mountRoot.mount = mount; + mount.root = mountRoot; + if (root) { + FS.root = mountRoot; + } else if (node) { + // set as a mountpoint + node.mounted = mount; + // add the new mount to the current mount's children + if (node.mount) { + node.mount.mounts.push(mount); + } + } + return mountRoot; + }, + unmount(mountpoint) { + var lookup = FS.lookupPath(mountpoint, { + follow_mount: false + }); + if (!FS.isMountpoint(lookup.node)) { + throw new FS.ErrnoError(28); + } + // destroy the nodes for this mount, and all its child mounts + var node = lookup.node; + var mount = node.mounted; + var mounts = FS.getMounts(mount); + for (var [hash, current] of Object.entries(FS.nameTable)) { + while (current) { + var next = current.name_next; + if (mounts.includes(current.mount)) { + FS.destroyNode(current); + } + current = next; + } + } + // no longer a mountpoint + node.mounted = null; + // remove this mount from the child mounts + var idx = node.mount.mounts.indexOf(mount); + node.mount.mounts.splice(idx, 1); + }, + lookup(parent, name) { + return parent.node_ops.lookup(parent, name); + }, + mknod(path, mode, dev) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + if (!name) { + throw new FS.ErrnoError(28); + } + if (name === "." || name === "..") { + throw new FS.ErrnoError(20); + } + var errCode = FS.mayCreate(parent, name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.mknod) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.mknod(parent, name, mode, dev); + }, + statfs(path) { + return FS.statfsNode(FS.lookupPath(path, { + follow: true + }).node); + }, + statfsStream(stream) { + // We keep a separate statfsStream function because noderawfs overrides + // it. In noderawfs, stream.node is sometimes null. Instead, we need to + // look at stream.path. + return FS.statfsNode(stream.node); + }, + statfsNode(node) { + // NOTE: None of the defaults here are true. We're just returning safe and + // sane values. Currently nodefs and rawfs replace these defaults, + // other file systems leave them alone. + var rtn = { + bsize: 4096, + frsize: 4096, + blocks: 1e6, + bfree: 5e5, + bavail: 5e5, + files: FS.nextInode, + ffree: FS.nextInode - 1, + fsid: 42, + flags: 2, + namelen: 255 + }; + if (node.node_ops.statfs) { + Object.assign(rtn, node.node_ops.statfs(node.mount.opts.root)); + } + return rtn; + }, + create(path, mode = 438) { + mode &= 4095; + mode |= 32768; + return FS.mknod(path, mode, 0); + }, + mkdir(path, mode = 511) { + mode &= 511 | 512; + mode |= 16384; + return FS.mknod(path, mode, 0); + }, + mkdirTree(path, mode) { + var dirs = path.split("/"); + var d = ""; + for (var dir of dirs) { + if (!dir) continue; + if (d || PATH.isAbs(path)) d += "/"; + d += dir; + try { + FS.mkdir(d, mode); + } catch (e) { + if (e.errno != 20) throw e; + } + } + }, + mkdev(path, mode, dev) { + if (typeof dev == "undefined") { + dev = mode; + mode = 438; + } + mode |= 8192; + return FS.mknod(path, mode, dev); + }, + symlink(oldpath, newpath) { + if (!PATH_FS.resolve(oldpath)) { + throw new FS.ErrnoError(44); + } + var lookup = FS.lookupPath(newpath, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.symlink) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.symlink(parent, newname, oldpath); + }, + rename(old_path, new_path) { + var old_dirname = PATH.dirname(old_path); + var new_dirname = PATH.dirname(new_path); + var old_name = PATH.basename(old_path); + var new_name = PATH.basename(new_path); + // parents must exist + var lookup, old_dir, new_dir; + // let the errors from non existent directories percolate up + lookup = FS.lookupPath(old_path, { + parent: true + }); + old_dir = lookup.node; + lookup = FS.lookupPath(new_path, { + parent: true + }); + new_dir = lookup.node; + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); + // need to be part of the same mount + if (old_dir.mount !== new_dir.mount) { + throw new FS.ErrnoError(75); + } + // source must exist + var old_node = FS.lookupNode(old_dir, old_name); + // old path should not be an ancestor of the new path + var relative = PATH_FS.relative(old_path, new_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(28); + } + // new path should not be an ancestor of the old path + relative = PATH_FS.relative(new_path, old_dirname); + if (relative.charAt(0) !== ".") { + throw new FS.ErrnoError(55); + } + // see if the new path already exists + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + // early out if nothing needs to change + if (old_node === new_node) { + return; + } + // we'll need to delete the old entry + var isdir = FS.isDir(old_node.mode); + var errCode = FS.mayDelete(old_dir, old_name, isdir); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + // need delete permissions if we'll be overwriting. + // need create permissions if new doesn't already exist. + errCode = new_node ? FS.mayDelete(new_dir, new_name, isdir) : FS.mayCreate(new_dir, new_name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!old_dir.node_ops.rename) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(old_node) || (new_node && FS.isMountpoint(new_node))) { + throw new FS.ErrnoError(10); + } + // if we are going to change the parent, check write permissions + if (new_dir !== old_dir) { + errCode = FS.nodePermissions(old_dir, "w"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + // remove the node from the lookup hash + FS.hashRemoveNode(old_node); + // do the underlying fs rename + try { + old_dir.node_ops.rename(old_node, new_dir, new_name); + // update old node (we do this here to avoid each backend + // needing to) + old_node.parent = new_dir; + } catch (e) { + throw e; + } finally { + // add the node back to the hash (in case node_ops.rename + // changed its name) + FS.hashAddNode(old_node); + } + }, + rmdir(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, true); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.rmdir) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.rmdir(parent, name); + FS.destroyNode(node); + }, + readdir(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + var readdir = FS.checkOpExists(node.node_ops.readdir, 54); + return readdir(node); + }, + unlink(path) { + var lookup = FS.lookupPath(path, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, false); + if (errCode) { + // According to POSIX, we should map EISDIR to EPERM, but + // we instead do what Linux does (and we must, as we use + // the musl linux libc). + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.unlink) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.unlink(parent, name); + FS.destroyNode(node); + }, + readlink(path) { + var lookup = FS.lookupPath(path); + var link = lookup.node; + if (!link) { + throw new FS.ErrnoError(44); + } + if (!link.node_ops.readlink) { + throw new FS.ErrnoError(28); + } + return link.node_ops.readlink(link); + }, + stat(path, dontFollow) { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + var node = lookup.node; + var getattr = FS.checkOpExists(node.node_ops.getattr, 63); + return getattr(node); + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + var node = stream.node; + var getattr = stream.stream_ops.getattr; + var arg = getattr ? stream : node; + getattr ??= node.node_ops.getattr; + FS.checkOpExists(getattr, 63); + return getattr(arg); + }, + lstat(path) { + return FS.stat(path, true); + }, + doChmod(stream, node, mode, dontFollow) { + FS.doSetAttr(stream, node, { + mode: (mode & 4095) | (node.mode & ~4095), + ctime: Date.now(), + dontFollow + }); + }, + chmod(path, mode, dontFollow) { + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node; + } else { + node = path; + } + FS.doChmod(null, node, mode, dontFollow); + }, + lchmod(path, mode) { + FS.chmod(path, mode, true); + }, + fchmod(fd, mode) { + var stream = FS.getStreamChecked(fd); + FS.doChmod(stream, stream.node, mode, false); + }, + doChown(stream, node, dontFollow) { + FS.doSetAttr(stream, node, { + timestamp: Date.now(), + dontFollow + }); + }, + chown(path, uid, gid, dontFollow) { + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: !dontFollow + }); + node = lookup.node; + } else { + node = path; + } + FS.doChown(null, node, dontFollow); + }, + lchown(path, uid, gid) { + FS.chown(path, uid, gid, true); + }, + fchown(fd, uid, gid) { + var stream = FS.getStreamChecked(fd); + FS.doChown(stream, stream.node, false); + }, + doTruncate(stream, node, len) { + if (FS.isDir(node.mode)) { + throw new FS.ErrnoError(31); + } + if (!FS.isFile(node.mode)) { + throw new FS.ErrnoError(28); + } + var errCode = FS.nodePermissions(node, "w"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.doSetAttr(stream, node, { + size: len, + timestamp: Date.now() + }); + }, + truncate(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28); + } + var node; + if (typeof path == "string") { + var lookup = FS.lookupPath(path, { + follow: true + }); + node = lookup.node; + } else { + node = path; + } + FS.doTruncate(null, node, len); + }, + ftruncate(fd, len) { + var stream = FS.getStreamChecked(fd); + if (len < 0 || (stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(28); + } + FS.doTruncate(stream, stream.node, len); + }, + utime(path, atime, mtime) { + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + var setattr = FS.checkOpExists(node.node_ops.setattr, 63); + setattr(node, { + atime, + mtime + }); + }, + open(path, flags, mode = 438) { + if (path === "") { + throw new FS.ErrnoError(44); + } + flags = FS_modeStringToFlags(flags); + if ((flags & 64)) { + mode = (mode & 4095) | 32768; + } else { + mode = 0; + } + var node; + var isDirPath; + if (typeof path == "object") { + node = path; + } else { + isDirPath = path.endsWith("/"); + // noent_okay makes it so that if the final component of the path + // doesn't exist, lookupPath returns `node: undefined`. `path` will be + // updated to point to the target of all symlinks. + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072), + noent_okay: true + }); + node = lookup.node; + path = lookup.path; + } + // perhaps we need to create the node + var created = false; + if ((flags & 64)) { + if (node) { + // if O_CREAT and O_EXCL are set, error out if the node already exists + if ((flags & 128)) { + throw new FS.ErrnoError(20); + } + } else if (isDirPath) { + throw new FS.ErrnoError(31); + } else { + // node doesn't exist, try to create it + // Ignore the permission bits here to ensure we can `open` this new + // file below. We use chmod below to apply the permissions once the + // file is open. + node = FS.mknod(path, mode | 511, 0); + created = true; + } + } + if (!node) { + throw new FS.ErrnoError(44); + } + // can't truncate a device + if (FS.isChrdev(node.mode)) { + flags &= ~512; + } + // if asked only for a directory, then this must be one + if ((flags & 65536) && !FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + // check permissions, if this is not a file we just created now (it is ok to + // create and write to a file with read-only permissions; it is read-only + // for later use) + if (!created) { + var errCode = FS.mayOpen(node, flags); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + // do truncation if necessary + if ((flags & 512) && !created) { + FS.truncate(node, 0); + } + // we've already handled these, don't pass down to the underlying vfs + flags &= ~(128 | 512 | 131072); + // register the stream with the filesystem + var stream = FS.createStream({ + node, + path: FS.getPath(node), + // we want the absolute path to the node + flags, + seekable: true, + position: 0, + stream_ops: node.stream_ops, + // used by the file family libc calls (fopen, fwrite, ferror, etc.) + ungotten: [], + error: false + }); + // call the new stream's open function + if (stream.stream_ops.open) { + stream.stream_ops.open(stream); + } + if (created) { + FS.chmod(node, mode & 511); + } + return stream; + }, + close(stream) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (stream.getdents) stream.getdents = null; + // free readdir state + try { + if (stream.stream_ops.close) { + stream.stream_ops.close(stream); + } + } catch (e) { + throw e; + } finally { + FS.closeStream(stream.fd); + } + stream.fd = null; + }, + isClosed(stream) { + return stream.fd === null; + }, + llseek(stream, offset, whence) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (!stream.seekable || !stream.stream_ops.llseek) { + throw new FS.ErrnoError(70); + } + if (whence != 0 && whence != 1 && whence != 2) { + throw new FS.ErrnoError(28); + } + stream.position = stream.stream_ops.llseek(stream, offset, whence); + stream.ungotten = []; + return stream.position; + }, + read(stream, buffer, offset, length, position) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.read) { + throw new FS.ErrnoError(28); + } + var seeking = typeof position != "undefined"; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); + if (!seeking) stream.position += bytesRead; + return bytesRead; + }, + write(stream, buffer, offset, length, position, canOwn) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.write) { + throw new FS.ErrnoError(28); + } + if (stream.seekable && stream.flags & 1024) { + // seek to the end before writing in append mode + FS.llseek(stream, 0, 2); + } + var seeking = typeof position != "undefined"; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); + if (!seeking) stream.position += bytesWritten; + return bytesWritten; + }, + mmap(stream, length, position, prot, flags) { + // User requests writing to file (prot & PROT_WRITE != 0). + // Checking if we have permissions to write to the file unless + // MAP_PRIVATE flag is set. According to POSIX spec it is possible + // to write to file opened in read-only mode with MAP_PRIVATE flag, + // as all modifications will be visible only in the memory of + // the current process. + if ((prot & 2) !== 0 && (flags & 2) === 0 && (stream.flags & 2097155) !== 2) { + throw new FS.ErrnoError(2); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(2); + } + if (!stream.stream_ops.mmap) { + throw new FS.ErrnoError(43); + } + if (!length) { + throw new FS.ErrnoError(28); + } + return stream.stream_ops.mmap(stream, length, position, prot, flags); + }, + msync(stream, buffer, offset, length, mmapFlags) { + if (!stream.stream_ops.msync) { + return 0; + } + return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); + }, + ioctl(stream, cmd, arg) { + if (!stream.stream_ops.ioctl) { + throw new FS.ErrnoError(59); + } + return stream.stream_ops.ioctl(stream, cmd, arg); + }, + readFile(path, opts = {}) { + opts.flags = opts.flags || 0; + opts.encoding = opts.encoding || "binary"; + if (opts.encoding !== "utf8" && opts.encoding !== "binary") { + abort(`Invalid encoding type "${opts.encoding}"`); + } + var stream = FS.open(path, opts.flags); + var stat = FS.stat(path); + var length = stat.size; + var buf = new Uint8Array(length); + FS.read(stream, buf, 0, length, 0); + if (opts.encoding === "utf8") { + buf = UTF8ArrayToString(buf); + } + FS.close(stream); + return buf; + }, + writeFile(path, data, opts = {}) { + opts.flags = opts.flags || 577; + var stream = FS.open(path, opts.flags, opts.mode); + data = FS_fileDataToTypedArray(data); + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); + FS.close(stream); + }, + cwd: () => FS.currentPath, + chdir(path) { + var lookup = FS.lookupPath(path, { + follow: true + }); + if (lookup.node === null) { + throw new FS.ErrnoError(44); + } + if (!FS.isDir(lookup.node.mode)) { + throw new FS.ErrnoError(54); + } + var errCode = FS.nodePermissions(lookup.node, "x"); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.currentPath = lookup.path; + }, + createDefaultDirectories() { + FS.mkdir("/tmp"); + FS.mkdir("/home"); + FS.mkdir("/home/web_user"); + }, + createDefaultDevices() { + // create /dev + FS.mkdir("/dev"); + // setup /dev/null + FS.registerDevice(FS.makedev(1, 3), { + read: () => 0, + write: (stream, buffer, offset, length, pos) => length, + llseek: () => 0 + }); + FS.mkdev("/dev/null", FS.makedev(1, 3)); + // setup /dev/tty and /dev/tty1 + // stderr needs to print output using err() rather than out() + // so we register a second tty just for it. + TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); + TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); + FS.mkdev("/dev/tty", FS.makedev(5, 0)); + FS.mkdev("/dev/tty1", FS.makedev(6, 0)); + // setup /dev/[u]random + // use a buffer to avoid overhead of individual crypto calls per byte + var randomBuffer = new Uint8Array(1024), randomLeft = 0; + var randomByte = () => { + if (randomLeft === 0) { + randomFill(randomBuffer); + randomLeft = randomBuffer.byteLength; + } + return randomBuffer[--randomLeft]; + }; + FS.createDevice("/dev", "random", randomByte); + FS.createDevice("/dev", "urandom", randomByte); + // we're not going to emulate the actual shm device, + // just create the tmp dirs that reside in it commonly + FS.mkdir("/dev/shm"); + FS.mkdir("/dev/shm/tmp"); + }, + createSpecialDirectories() { + // create /proc/self/fd which allows /proc/self/fd/6 => readlink gives the + // name of the stream for fd 6 (see test_unistd_ttyname) + FS.mkdir("/proc"); + var proc_self = FS.mkdir("/proc/self"); + FS.mkdir("/proc/self/fd"); + FS.mount({ + mount() { + var node = FS.createNode(proc_self, "fd", 16895, 73); + node.stream_ops = { + llseek: MEMFS.stream_ops.llseek + }; + node.node_ops = { + lookup(parent, name) { + var fd = +name; + var stream = FS.getStreamChecked(fd); + var ret = { + parent: null, + mount: { + mountpoint: "fake" + }, + node_ops: { + readlink: () => stream.path + }, + id: fd + 1 + }; + ret.parent = ret; + // make it look like a simple root node + return ret; + }, + readdir() { + return Array.from(FS.streams.entries()).filter(([k, v]) => v).map(([k, v]) => k.toString()); + } + }; + return node; + } + }, {}, "/proc/self/fd"); + }, + createStandardStreams(input, output, error) { + // TODO deprecate the old functionality of a single + // input / output callback and that utilizes FS.createDevice + // and instead require a unique set of stream ops + // by default, we symlink the standard streams to the + // default tty devices. however, if the standard streams + // have been overwritten we create a unique device for + // them instead. + if (input) { + FS.createDevice("/dev", "stdin", input); + } else { + FS.symlink("/dev/tty", "/dev/stdin"); + } + if (output) { + FS.createDevice("/dev", "stdout", null, output); + } else { + FS.symlink("/dev/tty", "/dev/stdout"); + } + if (error) { + FS.createDevice("/dev", "stderr", null, error); + } else { + FS.symlink("/dev/tty1", "/dev/stderr"); + } + // open default streams for the stdin, stdout and stderr devices + var stdin = FS.open("/dev/stdin", 0); + var stdout = FS.open("/dev/stdout", 1); + var stderr = FS.open("/dev/stderr", 1); + }, + staticInit() { + FS.nameTable = new Array(4096); + FS.mount(MEMFS, {}, "/"); + FS.createDefaultDirectories(); + FS.createDefaultDevices(); + FS.createSpecialDirectories(); + FS.filesystems = { + "MEMFS": MEMFS + }; + }, + init(input, output, error) { + FS.initialized = true; + // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here + input ??= Module["stdin"]; + output ??= Module["stdout"]; + error ??= Module["stderr"]; + FS.createStandardStreams(input, output, error); + }, + quit() { + FS.initialized = false; + // force-flush all streams, so we get musl std streams printed out + // close all of our streams + for (var stream of FS.streams) { + if (stream) { + FS.close(stream); + } + } + }, + findObject(path, dontResolveLastLink) { + var ret = FS.analyzePath(path, dontResolveLastLink); + if (!ret.exists) { + return null; + } + return ret.object; + }, + analyzePath(path, dontResolveLastLink) { + // operate from within the context of the symlink's target + try { + var lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + path = lookup.path; + } catch (e) {} + var ret = { + isRoot: false, + exists: false, + error: 0, + name: null, + path: null, + object: null, + parentExists: false, + parentPath: null, + parentObject: null + }; + try { + var lookup = FS.lookupPath(path, { + parent: true + }); + ret.parentExists = true; + ret.parentPath = lookup.path; + ret.parentObject = lookup.node; + ret.name = PATH.basename(path); + lookup = FS.lookupPath(path, { + follow: !dontResolveLastLink + }); + ret.exists = true; + ret.path = lookup.path; + ret.object = lookup.node; + ret.name = lookup.node.name; + ret.isRoot = lookup.path === "/"; + } catch (e) { + ret.error = e.errno; + } + return ret; + }, + createPath(parent, path, canRead, canWrite) { + parent = typeof parent == "string" ? parent : FS.getPath(parent); + var parts = path.split("/").reverse(); + while (parts.length) { + var part = parts.pop(); + if (!part) continue; + var current = PATH.join2(parent, part); + try { + FS.mkdir(current); + } catch (e) { + if (e.errno != 20) throw e; + } + parent = current; + } + return current; + }, + createFile(parent, name, properties, canRead, canWrite) { + var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name); + var mode = FS_getMode(canRead, canWrite); + return FS.create(path, mode); + }, + createDataFile(parent, name, data, canRead, canWrite, canOwn) { + var path = name; + if (parent) { + parent = typeof parent == "string" ? parent : FS.getPath(parent); + path = name ? PATH.join2(parent, name) : parent; + } + var mode = FS_getMode(canRead, canWrite); + var node = FS.create(path, mode); + if (data) { + data = FS_fileDataToTypedArray(data); + // make sure we can write to the file + FS.chmod(node, mode | 146); + var stream = FS.open(node, 577); + FS.write(stream, data, 0, data.length, 0, canOwn); + FS.close(stream); + FS.chmod(node, mode); + } + }, + createDevice(parent, name, input, output) { + var path = PATH.join2(typeof parent == "string" ? parent : FS.getPath(parent), name); + var mode = FS_getMode(!!input, !!output); + FS.createDevice.major ??= 64; + var dev = FS.makedev(FS.createDevice.major++, 0); + // Create a fake device that a set of stream ops to emulate + // the old behavior. + FS.registerDevice(dev, { + open(stream) { + stream.seekable = false; + }, + close(stream) { + // flush any pending line data + if (output?.buffer?.length) { + output(10); + } + }, + read(stream, buffer, offset, length, pos) { + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = input(); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset + i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + for (var i = 0; i < length; i++) { + try { + output(buffer[offset + i]); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + } + }); + return FS.mkdev(path, mode, dev); + }, + forceLoadFile(obj) { + if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; + if (globalThis.XMLHttpRequest) { + abort("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + } else { + // Command-line. + try { + obj.contents = readBinary(obj.url); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + }, + createLazyFile(parent, name, url, canRead, canWrite) { + // Lazy chunked Uint8Array (implements get and length from Uint8Array). + // Actual getting is abstracted away for eventual reuse. + class LazyUint8Array { + lengthKnown=false; + chunks=[]; + // Loaded chunks. Index is the chunk number + get(idx) { + if (idx > this.length - 1 || idx < 0) { + return undefined; + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = (idx / this.chunkSize) | 0; + return this.getter(chunkNum)[chunkOffset]; + } + setDataGetter(getter) { + this.getter = getter; + } + cacheLength() { + // Find length + var xhr = new XMLHttpRequest; + xhr.open("HEAD", url, false); + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + var chunkSize = 1024 * 1024; + // Chunk size in bytes + if (!hasByteServing) chunkSize = datalength; + // Function to get a range from the remote URL. + var doXHR = (from, to) => { + if (from > to) abort("invalid range (" + from + ", " + to + ") or no bytes requested!"); + if (to > datalength - 1) abort("only " + datalength + " bytes available! programmer error!"); + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. + var xhr = new XMLHttpRequest; + xhr.open("GET", url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + // Some hints to the browser that we want binary data. + xhr.responseType = "arraybuffer"; + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/plain; charset=x-user-defined"); + } + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array(/** @type{Array} */ (xhr.response || [])); + } + return intArrayFromString(xhr.responseText || "", true); + }; + var lazyArray = this; + lazyArray.setDataGetter(chunkNum => { + var start = chunkNum * chunkSize; + var end = (chunkNum + 1) * chunkSize - 1; + // including this byte + end = Math.min(end, datalength - 1); + // if datalength-1 is selected, this is the last block + if (typeof lazyArray.chunks[chunkNum] == "undefined") { + lazyArray.chunks[chunkNum] = doXHR(start, end); + } + if (typeof lazyArray.chunks[chunkNum] == "undefined") abort("doXHR failed!"); + return lazyArray.chunks[chunkNum]; + }); + if (usesGzip || !datalength) { + // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length + chunkSize = datalength = 1; + // this will force getter(0)/doXHR do download the whole file + datalength = this.getter(0).length; + chunkSize = datalength; + out("LazyFiles on gzip forces download of the whole file when length is accessed"); + } + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true; + } + get length() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._length; + } + get chunkSize() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._chunkSize; + } + } + if (globalThis.XMLHttpRequest) { + if (!ENVIRONMENT_IS_WORKER) abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc"); + var lazyArray = new LazyUint8Array; + var properties = { + isDevice: false, + contents: lazyArray + }; + } else { + var properties = { + isDevice: false, + url + }; + } + var node = FS.createFile(parent, name, properties, canRead, canWrite); + // This is a total hack, but I want to get this lazy file code out of the + // core of MEMFS. If we want to keep this lazy file concept I feel it should + // be its own thin LAZYFS proxying calls to MEMFS. + if (properties.contents) { + node.contents = properties.contents; + } else if (properties.url) { + node.contents = null; + node.url = properties.url; + } + // Add a function that defers querying the file size until it is asked the first time. + Object.defineProperties(node, { + usedBytes: { + get: function() { + return this.contents.length; + } + } + }); + // override each stream op with one that tries to force load the lazy file first + var stream_ops = {}; + for (const [key, fn] of Object.entries(node.stream_ops)) { + stream_ops[key] = (...args) => { + FS.forceLoadFile(node); + return fn(...args); + }; + } + function writeChunks(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= contents.length) return 0; + var size = Math.min(contents.length - position, length); + if (contents.slice) { + // normal array + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents[position + i]; + } + } else { + for (var i = 0; i < size; i++) { + // LazyUint8Array from sync binary XHR + buffer[offset + i] = contents.get(position + i); + } + } + return size; + } + // use a custom read function + stream_ops.read = (stream, buffer, offset, length, position) => { + FS.forceLoadFile(node); + return writeChunks(stream, buffer, offset, length, position); + }; + // use a custom mmap function + stream_ops.mmap = (stream, length, position, prot, flags) => { + FS.forceLoadFile(node); + var ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + writeChunks(stream, HEAP8, ptr, length, position); + return { + ptr, + allocated: true + }; + }; + node.stream_ops = stream_ops; + return node; + } +}; + +/** + * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the + * emscripten HEAP, returns a copy of that string as a Javascript String object. + * + * @param {number} ptr + * @param {number=} maxBytesToRead - An optional length that specifies the + * maximum number of bytes to read. You can omit this parameter to scan the + * string until the first 0 byte. If maxBytesToRead is passed, and the string + * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the + * string will cut short at that byte index. + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + if (!ptr) return ""; + var end = findStringEnd(HEAPU8, ptr, maxBytesToRead, ignoreNul); + return UTF8Decoder.decode(HEAPU8.subarray(ptr, end)); +}; + +var SYSCALLS = { + calculateAt(dirfd, path, allowEmpty) { + if (PATH.isAbs(path)) { + return path; + } + // relative path + var dir; + if (dirfd === -100) { + dir = FS.cwd(); + } else { + var dirstream = SYSCALLS.getStreamFromFD(dirfd); + dir = dirstream.path; + } + if (path.length == 0) { + if (!allowEmpty) { + throw new FS.ErrnoError(44); + } + return dir; + } + return dir + "/" + path; + }, + writeStat(buf, stat) { + HEAPU32[((buf) >> 2)] = stat.dev; + HEAPU32[(((buf) + (4)) >> 2)] = stat.mode; + HEAPU32[(((buf) + (8)) >> 2)] = stat.nlink; + HEAPU32[(((buf) + (12)) >> 2)] = stat.uid; + HEAPU32[(((buf) + (16)) >> 2)] = stat.gid; + HEAPU32[(((buf) + (20)) >> 2)] = stat.rdev; + (tempI64 = [ stat.size >>> 0, (tempDouble = stat.size, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (24)) >> 2)] = tempI64[0], HEAP32[(((buf) + (28)) >> 2)] = tempI64[1]); + HEAP32[(((buf) + (32)) >> 2)] = 4096; + HEAP32[(((buf) + (36)) >> 2)] = stat.blocks; + var atime = stat.atime.getTime(); + var mtime = stat.mtime.getTime(); + var ctime = stat.ctime.getTime(); + (tempI64 = [ Math.floor(atime / 1e3) >>> 0, (tempDouble = Math.floor(atime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (40)) >> 2)] = tempI64[0], HEAP32[(((buf) + (44)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (48)) >> 2)] = (atime % 1e3) * 1e3 * 1e3; + (tempI64 = [ Math.floor(mtime / 1e3) >>> 0, (tempDouble = Math.floor(mtime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (56)) >> 2)] = tempI64[0], HEAP32[(((buf) + (60)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (64)) >> 2)] = (mtime % 1e3) * 1e3 * 1e3; + (tempI64 = [ Math.floor(ctime / 1e3) >>> 0, (tempDouble = Math.floor(ctime / 1e3), + (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (72)) >> 2)] = tempI64[0], HEAP32[(((buf) + (76)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (80)) >> 2)] = (ctime % 1e3) * 1e3 * 1e3; + (tempI64 = [ stat.ino >>> 0, (tempDouble = stat.ino, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (88)) >> 2)] = tempI64[0], HEAP32[(((buf) + (92)) >> 2)] = tempI64[1]); + return 0; + }, + writeStatFs(buf, stats) { + HEAPU32[(((buf) + (4)) >> 2)] = stats.bsize; + HEAPU32[(((buf) + (60)) >> 2)] = stats.bsize; + (tempI64 = [ stats.blocks >>> 0, (tempDouble = stats.blocks, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (8)) >> 2)] = tempI64[0], HEAP32[(((buf) + (12)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.bfree >>> 0, (tempDouble = stats.bfree, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (16)) >> 2)] = tempI64[0], HEAP32[(((buf) + (20)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.bavail >>> 0, (tempDouble = stats.bavail, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (24)) >> 2)] = tempI64[0], HEAP32[(((buf) + (28)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.files >>> 0, (tempDouble = stats.files, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (32)) >> 2)] = tempI64[0], HEAP32[(((buf) + (36)) >> 2)] = tempI64[1]); + (tempI64 = [ stats.ffree >>> 0, (tempDouble = stats.ffree, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[(((buf) + (40)) >> 2)] = tempI64[0], HEAP32[(((buf) + (44)) >> 2)] = tempI64[1]); + HEAPU32[(((buf) + (48)) >> 2)] = stats.fsid; + HEAPU32[(((buf) + (64)) >> 2)] = stats.flags; + // ST_NOSUID + HEAPU32[(((buf) + (56)) >> 2)] = stats.namelen; + }, + doMsync(addr, stream, len, flags, offset) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + if (flags & 2) { + // MAP_PRIVATE calls need not to be synced back to underlying fs + return 0; + } + var buffer = HEAPU8.slice(addr, addr + len); + FS.msync(stream, buffer, offset, len, flags); + }, + getStreamFromFD(fd) { + var stream = FS.getStreamChecked(fd); + return stream; + }, + varargs: undefined, + getStr(ptr) { + var ret = UTF8ToString(ptr); + return ret; + } +}; + +function ___syscall_dup(fd) { + try { + var old = SYSCALLS.getStreamFromFD(fd); + return FS.dupStream(old).fd; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_faccessat(dirfd, path, amode, flags) { + try { + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + if (amode & ~7) { + // need a valid mode + return -28; + } + var lookup = FS.lookupPath(path, { + follow: true + }); + var node = lookup.node; + if (!node) { + return -44; + } + var perms = ""; + if (amode & 4) perms += "r"; + if (amode & 2) perms += "w"; + if (amode & 1) perms += "x"; + if (perms && FS.nodePermissions(node, perms)) { + return -2; + } + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var syscallGetVarargI = () => { + // the `+` prepended here is necessary to convince the JSCompiler that varargs is indeed a number. + var ret = HEAP32[((+SYSCALLS.varargs) >> 2)]; + SYSCALLS.varargs += 4; + return ret; +}; + +var syscallGetVarargP = syscallGetVarargI; + +function ___syscall_fcntl64(fd, cmd, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(fd); + switch (cmd) { + case 0: + { + var arg = syscallGetVarargI(); + if (arg < 0) { + return -28; + } + while (FS.streams[arg]) { + arg++; + } + var newStream; + newStream = FS.dupStream(stream, arg); + return newStream.fd; + } + + case 1: + case 2: + return 0; + + // FD_CLOEXEC makes no sense for a single process. + case 3: + return stream.flags; + + case 4: + { + var arg = syscallGetVarargI(); + stream.flags |= arg; + return 0; + } + + case 12: + { + var arg = syscallGetVarargP(); + var offset = 0; + // We're always unlocked. + HEAP16[(((arg) + (offset)) >> 1)] = 2; + return 0; + } + + case 13: + case 14: + // Pretend that the locking is successful. These are process-level locks, + // and Emscripten programs are a single process. If we supported linking a + // filesystem between programs, we'd need to do more here. + // See https://github.com/emscripten-core/emscripten/issues/23697 + return 0; + } + return -28; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_fstat64(fd, buf) { + try { + return SYSCALLS.writeStat(buf, FS.fstat(fd)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var convertI32PairToI53Checked = (lo, hi) => ((hi + 2097152) >>> 0 < 4194305 - !!lo) ? (lo >>> 0) + hi * 4294967296 : NaN; + +function ___syscall_ftruncate64(fd, length_low, length_high) { + var length = convertI32PairToI53Checked(length_low, length_high); + try { + if (isNaN(length)) return -61; + FS.ftruncate(fd, length); + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_ioctl(fd, op, varargs) { + SYSCALLS.varargs = varargs; + try { + var stream = SYSCALLS.getStreamFromFD(fd); + switch (op) { + case 21509: + { + if (!stream.tty) return -59; + return 0; + } + + case 21505: + { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcgets) { + var termios = stream.tty.ops.ioctl_tcgets(stream); + var argp = syscallGetVarargP(); + HEAP32[((argp) >> 2)] = termios.c_iflag || 0; + HEAP32[(((argp) + (4)) >> 2)] = termios.c_oflag || 0; + HEAP32[(((argp) + (8)) >> 2)] = termios.c_cflag || 0; + HEAP32[(((argp) + (12)) >> 2)] = termios.c_lflag || 0; + for (var i = 0; i < 32; i++) { + HEAP8[(argp + i) + (17)] = termios.c_cc[i] || 0; + } + return 0; + } + return 0; + } + + case 21510: + case 21511: + case 21512: + { + if (!stream.tty) return -59; + return 0; + } + + case 21506: + case 21507: + case 21508: + { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcsets) { + var argp = syscallGetVarargP(); + var c_iflag = HEAP32[((argp) >> 2)]; + var c_oflag = HEAP32[(((argp) + (4)) >> 2)]; + var c_cflag = HEAP32[(((argp) + (8)) >> 2)]; + var c_lflag = HEAP32[(((argp) + (12)) >> 2)]; + var c_cc = []; + for (var i = 0; i < 32; i++) { + c_cc.push(HEAP8[(argp + i) + (17)]); + } + return stream.tty.ops.ioctl_tcsets(stream.tty, op, { + c_iflag, + c_oflag, + c_cflag, + c_lflag, + c_cc + }); + } + return 0; + } + + case 21519: + { + if (!stream.tty) return -59; + var argp = syscallGetVarargP(); + HEAP32[((argp) >> 2)] = 0; + return 0; + } + + case 21520: + { + if (!stream.tty) return -59; + return -28; + } + + case 21537: + case 21531: + { + var argp = syscallGetVarargP(); + return FS.ioctl(stream, op, argp); + } + + case 21523: + { + // TODO: in theory we should write to the winsize struct that gets + // passed in, but for now musl doesn't read anything on it + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tiocgwinsz) { + var winsize = stream.tty.ops.ioctl_tiocgwinsz(stream.tty); + var argp = syscallGetVarargP(); + HEAP16[((argp) >> 1)] = winsize[0]; + HEAP16[(((argp) + (2)) >> 1)] = winsize[1]; + } + return 0; + } + + case 21524: + { + // TODO: technically, this ioctl call should change the window size. + // but, since emscripten doesn't have any concept of a terminal window + // yet, we'll just silently throw it away as we do TIOCGWINSZ + if (!stream.tty) return -59; + return 0; + } + + case 21515: + { + if (!stream.tty) return -59; + return 0; + } + + default: + return -28; + } + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_lstat64(path, buf) { + try { + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.lstat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_newfstatat(dirfd, path, buf, flags) { + try { + path = SYSCALLS.getStr(path); + var nofollow = flags & 256; + var allowEmpty = flags & 4096; + flags = flags & (~6400); + path = SYSCALLS.calculateAt(dirfd, path, allowEmpty); + return SYSCALLS.writeStat(buf, nofollow ? FS.lstat(path) : FS.stat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_openat(dirfd, path, flags, varargs) { + SYSCALLS.varargs = varargs; + try { + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + var mode = varargs ? syscallGetVarargI() : 0; + return FS.open(path, flags, mode).fd; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function ___syscall_stat64(path, buf) { + try { + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.stat(path)); + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var __abort_js = () => abort(""); + +var __embind_register_bigint = (primitiveType, name, size, minRange, maxRange) => {}; + +var AsciiToString = ptr => { + var str = ""; + while (1) { + var ch = HEAPU8[ptr++]; + if (!ch) return str; + str += String.fromCharCode(ch); + } +}; + +var awaitingDependencies = {}; + +var registeredTypes = {}; + +var typeDependencies = {}; + +var BindingError = class BindingError extends Error { + constructor(message) { + super(message); + this.name = "BindingError"; + } +}; + +var throwBindingError = message => { + throw new BindingError(message); +}; + +/** @param {Object=} options */ function sharedRegisterType(rawType, registeredInstance, options = {}) { + var name = registeredInstance.name; + if (!rawType) { + throwBindingError(`type "${name}" must have a positive integer typeid pointer`); + } + if (registeredTypes.hasOwnProperty(rawType)) { + if (options.ignoreDuplicateRegistrations) { + return; + } else { + throwBindingError(`Cannot register type '${name}' twice`); + } + } + registeredTypes[rawType] = registeredInstance; + delete typeDependencies[rawType]; + if (awaitingDependencies.hasOwnProperty(rawType)) { + var callbacks = awaitingDependencies[rawType]; + delete awaitingDependencies[rawType]; + callbacks.forEach(cb => cb()); + } +} + +/** @param {Object=} options */ function registerType(rawType, registeredInstance, options = {}) { + return sharedRegisterType(rawType, registeredInstance, options); +} + +/** @suppress {globalThis} */ var __embind_register_bool = (rawType, name, trueValue, falseValue) => { + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: function(wt) { + // ambiguous emscripten ABI: sometimes return values are + // true or false, and sometimes integers (0 or 1) + return !!wt; + }, + toWireType: function(destructors, o) { + return o ? trueValue : falseValue; + }, + readValueFromPointer: function(pointer) { + return this.fromWireType(HEAPU8[pointer]); + }, + destructorFunction: null + }); +}; + +var emval_freelist = []; + +var emval_handles = [ 0, 1, , 1, null, 1, true, 1, false, 1 ]; + +var __emval_decref = handle => { + if (handle > 9 && 0 === --emval_handles[handle + 1]) { + var value = emval_handles[handle]; + emval_handles[handle] = undefined; + emval_freelist.push(handle); + } +}; + +var Emval = { + toValue: handle => { + if (!handle) { + throwBindingError(`Cannot use deleted val. handle = ${handle}`); + } + return emval_handles[handle]; + }, + toHandle: value => { + switch (value) { + case undefined: + return 2; + + case null: + return 4; + + case true: + return 6; + + case false: + return 8; + + default: + { + const handle = emval_freelist.pop() || emval_handles.length; + emval_handles[handle] = value; + emval_handles[handle + 1] = 1; + return handle; + } + } + } +}; + +/** @suppress {globalThis} */ function readPointer(pointer) { + return this.fromWireType(HEAPU32[((pointer) >> 2)]); +} + +var EmValType = { + name: "emscripten::val", + fromWireType: handle => { + var rv = Emval.toValue(handle); + __emval_decref(handle); + return rv; + }, + toWireType: (destructors, value) => Emval.toHandle(value), + readValueFromPointer: readPointer, + destructorFunction: null +}; + +var __embind_register_emval = rawType => registerType(rawType, EmValType); + +var floatReadValueFromPointer = (name, width) => { + switch (width) { + case 4: + return function(pointer) { + return this.fromWireType(HEAPF32[((pointer) >> 2)]); + }; + + case 8: + return function(pointer) { + return this.fromWireType(HEAPF64[((pointer) >> 3)]); + }; + + default: + throw new TypeError(`invalid float width (${width}): ${name}`); + } +}; + +var __embind_register_float = (rawType, name, size) => { + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: value => value, + toWireType: (destructors, value) => value, + readValueFromPointer: floatReadValueFromPointer(name, size), + destructorFunction: null + }); +}; + +var integerReadValueFromPointer = (name, width, signed) => { + // integers are quite common, so generate very specialized functions + switch (width) { + case 1: + return signed ? pointer => HEAP8[pointer] : pointer => HEAPU8[pointer]; + + case 2: + return signed ? pointer => HEAP16[((pointer) >> 1)] : pointer => HEAPU16[((pointer) >> 1)]; + + case 4: + return signed ? pointer => HEAP32[((pointer) >> 2)] : pointer => HEAPU32[((pointer) >> 2)]; + + default: + throw new TypeError(`invalid integer width (${width}): ${name}`); + } +}; + +/** @suppress {globalThis} */ var __embind_register_integer = (primitiveType, name, size, minRange, maxRange) => { + name = AsciiToString(name); + const isUnsignedType = minRange === 0; + let fromWireType = value => value; + if (isUnsignedType) { + var bitshift = 32 - 8 * size; + fromWireType = value => (value << bitshift) >>> bitshift; + maxRange = fromWireType(maxRange); + } + registerType(primitiveType, { + name, + fromWireType, + toWireType: (destructors, value) => value, + readValueFromPointer: integerReadValueFromPointer(name, size, minRange !== 0), + destructorFunction: null + }); +}; + +var __embind_register_memory_view = (rawType, dataTypeIndex, name) => { + var typeMapping = [ Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array ]; + var TA = typeMapping[dataTypeIndex]; + function decodeMemoryView(handle) { + var size = HEAPU32[((handle) >> 2)]; + var data = HEAPU32[(((handle) + (4)) >> 2)]; + return new TA(HEAP8.buffer, data, size); + } + name = AsciiToString(name); + registerType(rawType, { + name, + fromWireType: decodeMemoryView, + readValueFromPointer: decodeMemoryView + }, { + ignoreDuplicateRegistrations: true + }); +}; + +var stringToUTF8 = (str, outPtr, maxBytesToWrite) => stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + +var __embind_register_std_string = (rawType, name) => { + name = AsciiToString(name); + var stdStringIsUTF8 = true; + registerType(rawType, { + name, + // For some method names we use string keys here since they are part of + // the public/external API and/or used by the runtime-generated code. + fromWireType(value) { + var length = HEAPU32[((value) >> 2)]; + var payload = value + 4; + var str; + if (stdStringIsUTF8) { + str = UTF8ToString(payload, length, true); + } else { + str = ""; + for (var i = 0; i < length; ++i) { + str += String.fromCharCode(HEAPU8[payload + i]); + } + } + _free(value); + return str; + }, + toWireType(destructors, value) { + if (value instanceof ArrayBuffer) { + value = new Uint8Array(value); + } + var length; + var valueIsOfTypeString = (typeof value == "string"); + // We accept `string` or array views with single byte elements + if (!(valueIsOfTypeString || (ArrayBuffer.isView(value) && value.BYTES_PER_ELEMENT == 1))) { + throwBindingError("Cannot pass non-string to std::string"); + } + if (stdStringIsUTF8 && valueIsOfTypeString) { + length = lengthBytesUTF8(value); + } else { + length = value.length; + } + // assumes POINTER_SIZE alignment + var base = _malloc(4 + length + 1); + var ptr = base + 4; + HEAPU32[((base) >> 2)] = length; + if (valueIsOfTypeString) { + if (stdStringIsUTF8) { + stringToUTF8(value, ptr, length + 1); + } else { + for (var i = 0; i < length; ++i) { + var charCode = value.charCodeAt(i); + if (charCode > 255) { + _free(base); + throwBindingError("String has UTF-16 code units that do not fit in 8 bits"); + } + HEAPU8[ptr + i] = charCode; + } + } + } else { + HEAPU8.set(value, ptr); + } + if (destructors !== null) { + destructors.push(_free, base); + } + return base; + }, + readValueFromPointer: readPointer, + destructorFunction(ptr) { + _free(ptr); + } + }); +}; + +var UTF16Decoder = new TextDecoder("utf-16le"); + +var UTF16ToString = (ptr, maxBytesToRead, ignoreNul) => { + var idx = ((ptr) >> 1); + var endIdx = findStringEnd(HEAPU16, idx, maxBytesToRead / 2, ignoreNul); + return UTF16Decoder.decode(HEAPU16.subarray(idx, endIdx)); +}; + +var stringToUTF16 = (str, outPtr, maxBytesToWrite) => { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + maxBytesToWrite ??= 2147483647; + if (maxBytesToWrite < 2) return 0; + maxBytesToWrite -= 2; + // Null terminator. + var startPtr = outPtr; + var numCharsToWrite = (maxBytesToWrite < str.length * 2) ? (maxBytesToWrite / 2) : str.length; + for (var i = 0; i < numCharsToWrite; ++i) { + // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. + var codeUnit = str.charCodeAt(i); + // possibly a lead surrogate + HEAP16[((outPtr) >> 1)] = codeUnit; + outPtr += 2; + } + // Null-terminate the pointer to the HEAP. + HEAP16[((outPtr) >> 1)] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF16 = str => str.length * 2; + +var UTF32ToString = (ptr, maxBytesToRead, ignoreNul) => { + var str = ""; + var startIdx = ((ptr) >> 2); + // If maxBytesToRead is not passed explicitly, it will be undefined, and this + // will always evaluate to true. This saves on code size. + for (var i = 0; !(i >= maxBytesToRead / 4); i++) { + var utf32 = HEAPU32[startIdx + i]; + if (!utf32 && !ignoreNul) break; + str += String.fromCodePoint(utf32); + } + return str; +}; + +var stringToUTF32 = (str, outPtr, maxBytesToWrite) => { + // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. + maxBytesToWrite ??= 2147483647; + if (maxBytesToWrite < 4) return 0; + var startPtr = outPtr; + var endPtr = startPtr + maxBytesToWrite - 4; + for (var i = 0; i < str.length; ++i) { + var codePoint = str.codePointAt(i); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + if (codePoint > 65535) { + i++; + } + HEAP32[((outPtr) >> 2)] = codePoint; + outPtr += 4; + if (outPtr + 4 > endPtr) break; + } + // Null-terminate the pointer to the HEAP. + HEAP32[((outPtr) >> 2)] = 0; + return outPtr - startPtr; +}; + +var lengthBytesUTF32 = str => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var codePoint = str.codePointAt(i); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + if (codePoint > 65535) { + i++; + } + len += 4; + } + return len; +}; + +var __embind_register_std_wstring = (rawType, charSize, name) => { + name = AsciiToString(name); + var decodeString, encodeString, lengthBytesUTF; + if (charSize === 2) { + decodeString = UTF16ToString; + encodeString = stringToUTF16; + lengthBytesUTF = lengthBytesUTF16; + } else { + decodeString = UTF32ToString; + encodeString = stringToUTF32; + lengthBytesUTF = lengthBytesUTF32; + } + registerType(rawType, { + name, + fromWireType: value => { + // Code mostly taken from _embind_register_std_string fromWireType + var length = HEAPU32[((value) >> 2)]; + var str = decodeString(value + 4, length * charSize, true); + _free(value); + return str; + }, + toWireType: (destructors, value) => { + if (!(typeof value == "string")) { + throwBindingError(`Cannot pass non-string to C++ string type ${name}`); + } + // assumes POINTER_SIZE alignment + var length = lengthBytesUTF(value); + var ptr = _malloc(4 + length + charSize); + HEAPU32[((ptr) >> 2)] = length / charSize; + encodeString(value, ptr + 4, length + charSize); + if (destructors !== null) { + destructors.push(_free, ptr); + } + return ptr; + }, + readValueFromPointer: readPointer, + destructorFunction(ptr) { + _free(ptr); + } + }); +}; + +var __embind_register_void = (rawType, name) => { + name = AsciiToString(name); + registerType(rawType, { + isVoid: true, + // void return values can be optimized out sometimes + name, + fromWireType: () => undefined, + // TODO: assert if anything else is given? + toWireType: (destructors, o) => undefined + }); +}; + +var emval_methodCallers = []; + +var emval_addMethodCaller = caller => { + var id = emval_methodCallers.length; + emval_methodCallers.push(caller); + return id; +}; + +var getTypeName = type => { + var ptr = ___getTypeName(type); + var rv = AsciiToString(ptr); + _free(ptr); + return rv; +}; + +var requireRegisteredType = (rawType, humanName) => { + var impl = registeredTypes[rawType]; + if (undefined === impl) { + throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`); + } + return impl; +}; + +var emval_lookupTypes = (argCount, argTypes) => { + var a = new Array(argCount); + for (var i = 0; i < argCount; ++i) { + a[i] = requireRegisteredType(HEAPU32[(((argTypes) + (i * 4)) >> 2)], `parameter ${i}`); + } + return a; +}; + +var createNamedFunction = (name, func) => Object.defineProperty(func, "name", { + value: name +}); + +var emval_returnValue = (toReturnWire, destructorsRef, handle) => { + var destructors = []; + var result = toReturnWire(destructors, handle); + if (destructors.length) { + // void, primitives and any other types w/o destructors don't need to allocate a handle + HEAPU32[((destructorsRef) >> 2)] = Emval.toHandle(destructors); + } + return result; +}; + +var emval_symbols = {}; + +var getStringOrSymbol = address => { + var symbol = emval_symbols[address]; + if (symbol === undefined) { + return AsciiToString(address); + } + return symbol; +}; + +var __emval_create_invoker = (argCount, argTypesPtr, kind) => { + var GenericWireTypeSize = 8; + var [retType, ...argTypes] = emval_lookupTypes(argCount, argTypesPtr); + var toReturnWire = retType.toWireType.bind(retType); + var argFromPtr = argTypes.map(type => type.readValueFromPointer.bind(type)); + argCount--; + // remove the extracted return type + var argN = new Array(argCount); + var invokerFunction = (handle, methodName, destructorsRef, args) => { + var offset = 0; + for (var i = 0; i < argCount; ++i) { + argN[i] = argFromPtr[i](args + offset); + offset += GenericWireTypeSize; + } + var rv; + switch (kind) { + case 0: + rv = Emval.toValue(handle).apply(null, argN); + break; + + case 2: + rv = Reflect.construct(Emval.toValue(handle), argN); + break; + + case 3: + // no-op, just return the argument + rv = argN[0]; + break; + + case 1: + rv = Emval.toValue(handle)[getStringOrSymbol(methodName)](...argN); + break; + } + return emval_returnValue(toReturnWire, destructorsRef, rv); + }; + var functionName = `methodCaller<(${argTypes.map(t => t.name)}) => ${retType.name}>`; + return emval_addMethodCaller(createNamedFunction(functionName, invokerFunction)); +}; + +var __emval_get_global = name => { + if (!name) { + return Emval.toHandle(globalThis); + } + name = getStringOrSymbol(name); + return Emval.toHandle(globalThis[name]); +}; + +var __emval_get_property = (handle, key) => { + handle = Emval.toValue(handle); + key = Emval.toValue(key); + return Emval.toHandle(handle[key]); +}; + +var __emval_incref = handle => { + if (handle > 9) { + emval_handles[handle + 1] += 1; + } +}; + +var __emval_instanceof = (object, constructor) => { + object = Emval.toValue(object); + constructor = Emval.toValue(constructor); + return object instanceof constructor; +}; + +var __emval_invoke = (caller, handle, methodName, destructorsRef, args) => emval_methodCallers[caller](handle, methodName, destructorsRef, args); + +var __emval_new_cstring = v => Emval.toHandle(getStringOrSymbol(v)); + +var runDestructors = destructors => { + while (destructors.length) { + var ptr = destructors.pop(); + var del = destructors.pop(); + del(ptr); + } +}; + +var __emval_run_destructors = handle => { + var destructors = Emval.toValue(handle); + runDestructors(destructors); + __emval_decref(handle); +}; + +var __emval_set_property = (handle, key, value) => { + handle = Emval.toValue(handle); + key = Emval.toValue(key); + value = Emval.toValue(value); + handle[key] = value; +}; + +var __emval_typeof = handle => { + handle = Emval.toValue(handle); + return Emval.toHandle(typeof handle); +}; + +function __gmtime_js(time_low, time_high, tmPtr) { + var time = convertI32PairToI53Checked(time_low, time_high); + var date = new Date(time * 1e3); + HEAP32[((tmPtr) >> 2)] = date.getUTCSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getUTCMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getUTCHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getUTCDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getUTCMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getUTCFullYear() - 1900; + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getUTCDay(); + var start = Date.UTC(date.getUTCFullYear(), 0, 1, 0, 0, 0, 0); + var yday = ((date.getTime() - start) / (1e3 * 60 * 60 * 24)) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; +} + +var isLeapYear = year => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + +var MONTH_DAYS_LEAP_CUMULATIVE = [ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 ]; + +var MONTH_DAYS_REGULAR_CUMULATIVE = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ]; + +var ydayFromDate = date => { + var leap = isLeapYear(date.getFullYear()); + var monthDaysCumulative = (leap ? MONTH_DAYS_LEAP_CUMULATIVE : MONTH_DAYS_REGULAR_CUMULATIVE); + var yday = monthDaysCumulative[date.getMonth()] + date.getDate() - 1; + // -1 since it's days since Jan 1 + return yday; +}; + +function __localtime_js(time_low, time_high, tmPtr) { + var time = convertI32PairToI53Checked(time_low, time_high); + var date = new Date(time * 1e3); + HEAP32[((tmPtr) >> 2)] = date.getSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getFullYear() - 1900; + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; + HEAP32[(((tmPtr) + (36)) >> 2)] = -(date.getTimezoneOffset() * 60); + // Attention: DST is in December in South, and some regions don't have DST at all. + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset)) | 0; + HEAP32[(((tmPtr) + (32)) >> 2)] = dst; +} + +var setTempRet0 = val => __emscripten_tempret_set(val); + +var __mktime_js = function(tmPtr) { + var ret = (() => { + var date = new Date(HEAP32[(((tmPtr) + (20)) >> 2)] + 1900, HEAP32[(((tmPtr) + (16)) >> 2)], HEAP32[(((tmPtr) + (12)) >> 2)], HEAP32[(((tmPtr) + (8)) >> 2)], HEAP32[(((tmPtr) + (4)) >> 2)], HEAP32[((tmPtr) >> 2)], 0); + // There's an ambiguous hour when the time goes back; the tm_isdst field is + // used to disambiguate it. Date() basically guesses, so we fix it up if it + // guessed wrong, or fill in tm_isdst with the guess if it's -1. + var dst = HEAP32[(((tmPtr) + (32)) >> 2)]; + var guessedOffset = date.getTimezoneOffset(); + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dstOffset = Math.min(winterOffset, summerOffset); + // DST is in December in South + if (dst < 0) { + // Attention: some regions don't have DST at all. + HEAP32[(((tmPtr) + (32)) >> 2)] = Number(summerOffset != winterOffset && dstOffset == guessedOffset); + } else if ((dst > 0) != (dstOffset == guessedOffset)) { + var nonDstOffset = Math.max(winterOffset, summerOffset); + var trueOffset = dst > 0 ? dstOffset : nonDstOffset; + // Don't try setMinutes(date.getMinutes() + ...) -- it's messed up. + date.setTime(date.getTime() + (trueOffset - guessedOffset) * 6e4); + } + HEAP32[(((tmPtr) + (24)) >> 2)] = date.getDay(); + var yday = ydayFromDate(date) | 0; + HEAP32[(((tmPtr) + (28)) >> 2)] = yday; + // To match expected behavior, update fields from date + HEAP32[((tmPtr) >> 2)] = date.getSeconds(); + HEAP32[(((tmPtr) + (4)) >> 2)] = date.getMinutes(); + HEAP32[(((tmPtr) + (8)) >> 2)] = date.getHours(); + HEAP32[(((tmPtr) + (12)) >> 2)] = date.getDate(); + HEAP32[(((tmPtr) + (16)) >> 2)] = date.getMonth(); + HEAP32[(((tmPtr) + (20)) >> 2)] = date.getYear(); + var timeMs = date.getTime(); + if (isNaN(timeMs)) { + return -1; + } + // Return time in microseconds + return timeMs / 1e3; + })(); + return (setTempRet0((tempDouble = ret, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0)), + ret >>> 0); +}; + +function __mmap_js(len, prot, flags, fd, offset_low, offset_high, allocated, addr) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var res = FS.mmap(stream, len, offset, prot, flags); + var ptr = res.ptr; + HEAP32[((allocated) >> 2)] = res.allocated; + HEAPU32[((addr) >> 2)] = ptr; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +function __munmap_js(addr, len, prot, flags, fd, offset_low, offset_high) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + var stream = SYSCALLS.getStreamFromFD(fd); + if (prot & 2) { + SYSCALLS.doMsync(addr, stream, len, flags, offset); + } + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return -e.errno; + } +} + +var __tzset_js = (timezone, daylight, std_name, dst_name) => { + // TODO: Use (malleable) environment variables instead of system settings. + var currentYear = (new Date).getFullYear(); + var winter = new Date(currentYear, 0, 1); + var summer = new Date(currentYear, 6, 1); + var winterOffset = winter.getTimezoneOffset(); + var summerOffset = summer.getTimezoneOffset(); + // Local standard timezone offset. Local standard time is not adjusted for + // daylight savings. This code uses the fact that getTimezoneOffset returns + // a greater value during Standard Time versus Daylight Saving Time (DST). + // Thus it determines the expected output during Standard Time, and it + // compares whether the output of the given date the same (Standard) or less + // (DST). + var stdTimezoneOffset = Math.max(winterOffset, summerOffset); + // timezone is specified as seconds west of UTC ("The external variable + // `timezone` shall be set to the difference, in seconds, between + // Coordinated Universal Time (UTC) and local standard time."), the same + // as returned by stdTimezoneOffset. + // See http://pubs.opengroup.org/onlinepubs/009695399/functions/tzset.html + HEAPU32[((timezone) >> 2)] = stdTimezoneOffset * 60; + HEAP32[((daylight) >> 2)] = Number(winterOffset != summerOffset); + var extractZone = timezoneOffset => { + // Why inverse sign? + // Read here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset + var sign = timezoneOffset >= 0 ? "-" : "+"; + var absOffset = Math.abs(timezoneOffset); + var hours = String(Math.floor(absOffset / 60)).padStart(2, "0"); + var minutes = String(absOffset % 60).padStart(2, "0"); + return `UTC${sign}${hours}${minutes}`; + }; + var winterName = extractZone(winterOffset); + var summerName = extractZone(summerOffset); + if (summerOffset < winterOffset) { + // Northern hemisphere + stringToUTF8(winterName, std_name, 17); + stringToUTF8(summerName, dst_name, 17); + } else { + stringToUTF8(winterName, dst_name, 17); + stringToUTF8(summerName, std_name, 17); + } +}; + +var _emscripten_get_now = () => performance.now(); + +var _emscripten_date_now = () => Date.now(); + +var nowIsMonotonic = 1; + +var checkWasiClock = clock_id => clock_id >= 0 && clock_id <= 3; + +function _clock_time_get(clk_id, ignored_precision_low, ignored_precision_high, ptime) { + var ignored_precision = convertI32PairToI53Checked(ignored_precision_low, ignored_precision_high); + if (!checkWasiClock(clk_id)) { + return 28; + } + var now; + // all wasi clocks but realtime are monotonic + if (clk_id === 0) { + now = _emscripten_date_now(); + } else if (nowIsMonotonic) { + now = _emscripten_get_now(); + } else { + return 52; + } + // "now" is in ms, and wasi times are in ns. + var nsec = Math.round(now * 1e3 * 1e3); + (tempI64 = [ nsec >>> 0, (tempDouble = nsec, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[((ptime) >> 2)] = tempI64[0], HEAP32[(((ptime) + (4)) >> 2)] = tempI64[1]); + return 0; +} + +var readEmAsmArgsArray = []; + +var readEmAsmArgs = (sigPtr, buf) => { + readEmAsmArgsArray.length = 0; + var ch; + // Most arguments are i32s, so shift the buffer pointer so it is a plain + // index into HEAP32. + while (ch = HEAPU8[sigPtr++]) { + // Floats are always passed as doubles, so all types except for 'i' + // are 8 bytes and require alignment. + var wide = (ch != 105); + wide &= (ch != 112); + buf += wide && (buf % 8) ? 4 : 0; + readEmAsmArgsArray.push(// Special case for pointers under wasm64 or CAN_ADDRESS_2GB mode. + ch == 112 ? HEAPU32[((buf) >> 2)] : ch == 105 ? HEAP32[((buf) >> 2)] : HEAPF64[((buf) >> 3)]); + buf += wide ? 8 : 4; + } + return readEmAsmArgsArray; +}; + +var runEmAsmFunction = (code, sigPtr, argbuf) => { + var args = readEmAsmArgs(sigPtr, argbuf); + return ASM_CONSTS[code](...args); +}; + +var _emscripten_asm_const_int = (code, sigPtr, argbuf) => runEmAsmFunction(code, sigPtr, argbuf); + +var _emscripten_asm_const_ptr = (code, sigPtr, argbuf) => runEmAsmFunction(code, sigPtr, argbuf); + +var _emscripten_errn = (str, len) => err(UTF8ToString(str, len)); + +var getHeapMax = () => // Stay one Wasm page short of 4GB: while e.g. Chrome is able to allocate +// full 4GB Wasm memories, the size will wrap back to 0 bytes in Wasm side +// for any code that deals with heap sizes, which would require special +// casing all heap size related code to treat 0 specially. +2147483648; + +var _emscripten_get_heap_max = () => getHeapMax(); + +var _emscripten_has_asyncify = () => 0; + +var _emscripten_outn = (str, len) => out(UTF8ToString(str, len)); + +var UNWIND_CACHE = {}; + +var stringToNewUTF8 = str => { + var size = lengthBytesUTF8(str) + 1; + var ret = _malloc(size); + if (ret) stringToUTF8(str, ret, size); + return ret; +}; + +/** @returns {number} */ var convertFrameToPC = frame => { + var match; + if (match = /\bwasm-function\[\d+\]:(0x[0-9a-f]+)/.exec(frame)) { + // Wasm engines give the binary offset directly, so we use that as return address + return +match[1]; + } else if (match = /:(\d+):\d+(?:\)|$)/.exec(frame)) { + // If we are in js, we can use the js line number as the "return address". + // This should work for wasm2js. We tag the high bit to distinguish this + // from wasm addresses. + return 2147483648 | +match[1]; + } + // return 0 if we can't find any + return 0; +}; + +var saveInUnwindCache = callstack => { + for (var line of callstack) { + var pc = convertFrameToPC(line); + if (pc) { + UNWIND_CACHE[pc] = line; + } + } +}; + +var jsStackTrace = () => (new Error).stack.toString(); + +var _emscripten_stack_snapshot = () => { + var callstack = jsStackTrace().split("\n"); + if (callstack[0] == "Error") { + callstack.shift(); + } + saveInUnwindCache(callstack); + // Caches the stack snapshot so that emscripten_stack_unwind_buffer() can + // unwind from this spot. + UNWIND_CACHE.last_addr = convertFrameToPC(callstack[3]); + UNWIND_CACHE.last_stack = callstack; + return UNWIND_CACHE.last_addr; +}; + +var _emscripten_pc_get_function = pc => { + var frame = UNWIND_CACHE[pc]; + if (!frame) return 0; + var name; + var match; + // First try to match foo.wasm.sym files explcitly. e.g. + // at test_return_address.wasm.main (wasm://wasm/test_return_address.wasm-0012cc2a:wasm-function[26]:0x9f3 + // Then match JS symbols which don't include that module name: + // at invokeEntryPoint (.../test_return_address.js:1500:42) + // Finally match firefox format: + // Object._main@http://server.com:4324:12' + if (match = /^\s+at .*\.wasm\.(.*) \(.*\)$/.exec(frame)) { + name = match[1]; + } else if (match = /^\s+at (.*) \(.*\)$/.exec(frame)) { + name = match[1]; + } else if (match = /^(.+?)@/.exec(frame)) { + name = match[1]; + } else { + return 0; + } + _free(_emscripten_pc_get_function.ret ?? 0); + _emscripten_pc_get_function.ret = stringToNewUTF8(name); + return _emscripten_pc_get_function.ret; +}; + +var growMemory = size => { + var oldHeapSize = wasmMemory.buffer.byteLength; + var pages = ((size - oldHeapSize + 65535) / 65536) | 0; + try { + // round size grow request up to wasm page size (fixed 64KB per spec) + wasmMemory.grow(pages); + // .grow() takes a delta compared to the previous size + updateMemoryViews(); + return 1; + } catch (e) {} +}; + +var _emscripten_resize_heap = requestedSize => { + var oldSize = HEAPU8.length; + // With CAN_ADDRESS_2GB or MEMORY64, pointers are already unsigned. + requestedSize >>>= 0; + // With multithreaded builds, races can happen (another thread might increase the size + // in between), so return a failure, and let the caller retry. + // Memory resize rules: + // 1. Always increase heap size to at least the requested size, rounded up + // to next page multiple. + // 2a. If MEMORY_GROWTH_LINEAR_STEP == -1, excessively resize the heap + // geometrically: increase the heap size according to + // MEMORY_GROWTH_GEOMETRIC_STEP factor (default +20%), At most + // overreserve by MEMORY_GROWTH_GEOMETRIC_CAP bytes (default 96MB). + // 2b. If MEMORY_GROWTH_LINEAR_STEP != -1, excessively resize the heap + // linearly: increase the heap size by at least + // MEMORY_GROWTH_LINEAR_STEP bytes. + // 3. Max size for the heap is capped at 2048MB-WASM_PAGE_SIZE, or by + // MAXIMUM_MEMORY, or by ASAN limit, depending on which is smallest + // 4. If we were unable to allocate as much memory, it may be due to + // over-eager decision to excessively reserve due to (3) above. + // Hence if an allocation fails, cut down on the amount of excess + // growth, in an attempt to succeed to perform a smaller allocation. + // A limit is set for how much we can grow. We should not exceed that + // (the wasm binary specifies it, so if we tried, we'd fail anyhow). + var maxHeapSize = getHeapMax(); + if (requestedSize > maxHeapSize) { + return false; + } + // Loop through potential heap size increases. If we attempt a too eager + // reservation that fails, cut down on the attempted size and reserve a + // smaller bump instead. (max 3 times, chosen somewhat arbitrarily) + for (var cutDown = 1; cutDown <= 4; cutDown *= 2) { + var overGrownHeapSize = oldSize * (1 + .2 / cutDown); + // ensure geometric growth + // but limit overreserving (default to capping at +96MB overgrowth at most) + overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296); + var newSize = Math.min(maxHeapSize, alignMemory(Math.max(requestedSize, overGrownHeapSize), 65536)); + var replacement = growMemory(newSize); + if (replacement) { + return true; + } + } + return false; +}; + +var _emscripten_stack_unwind_buffer = (addr, buffer, count) => { + var stack; + if (UNWIND_CACHE.last_addr == addr) { + stack = UNWIND_CACHE.last_stack; + } else { + stack = jsStackTrace().split("\n"); + if (stack[0] == "Error") { + stack.shift(); + } + saveInUnwindCache(stack); + } + var offset = 3; + while (stack[offset] && convertFrameToPC(stack[offset]) != addr) { + ++offset; + } + for (var i = 0; i < count && stack[i + offset]; ++i) { + HEAP32[(((buffer) + (i * 4)) >> 2)] = convertFrameToPC(stack[i + offset]); + } + return i; +}; + +var GLctx; + +var webgl_enable_ANGLE_instanced_arrays = ctx => { + // Extension available in WebGL 1 from Firefox 26 and Google Chrome 30 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("ANGLE_instanced_arrays"); + // Because this extension is a core function in WebGL 2, assign the extension entry points in place of + // where the core functions will reside in WebGL 2. This way the calling code can call these without + // having to dynamically branch depending if running against WebGL 1 or WebGL 2. + if (ext) { + ctx["vertexAttribDivisor"] = (index, divisor) => ext["vertexAttribDivisorANGLE"](index, divisor); + ctx["drawArraysInstanced"] = (mode, first, count, primcount) => ext["drawArraysInstancedANGLE"](mode, first, count, primcount); + ctx["drawElementsInstanced"] = (mode, count, type, indices, primcount) => ext["drawElementsInstancedANGLE"](mode, count, type, indices, primcount); + return 1; + } +}; + +var webgl_enable_OES_vertex_array_object = ctx => { + // Extension available in WebGL 1 from Firefox 25 and WebKit 536.28/desktop Safari 6.0.3 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("OES_vertex_array_object"); + if (ext) { + ctx["createVertexArray"] = () => ext["createVertexArrayOES"](); + ctx["deleteVertexArray"] = vao => ext["deleteVertexArrayOES"](vao); + ctx["bindVertexArray"] = vao => ext["bindVertexArrayOES"](vao); + ctx["isVertexArray"] = vao => ext["isVertexArrayOES"](vao); + return 1; + } +}; + +var webgl_enable_WEBGL_draw_buffers = ctx => { + // Extension available in WebGL 1 from Firefox 28 onwards. Core feature in WebGL 2. + var ext = ctx.getExtension("WEBGL_draw_buffers"); + if (ext) { + ctx["drawBuffers"] = (n, bufs) => ext["drawBuffersWEBGL"](n, bufs); + return 1; + } +}; + +var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance = ctx => // Closure is expected to be allowed to minify the '.dibvbi' property, so not accessing it quoted. +!!(ctx.dibvbi = ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance")); + +var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance = ctx => !!(ctx.mdibvbi = ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance")); + +var webgl_enable_EXT_polygon_offset_clamp = ctx => !!(ctx.extPolygonOffsetClamp = ctx.getExtension("EXT_polygon_offset_clamp")); + +var webgl_enable_EXT_clip_control = ctx => !!(ctx.extClipControl = ctx.getExtension("EXT_clip_control")); + +var webgl_enable_WEBGL_polygon_mode = ctx => !!(ctx.webglPolygonMode = ctx.getExtension("WEBGL_polygon_mode")); + +var webgl_enable_WEBGL_multi_draw = ctx => // Closure is expected to be allowed to minify the '.multiDrawWebgl' property, so not accessing it quoted. +!!(ctx.multiDrawWebgl = ctx.getExtension("WEBGL_multi_draw")); + +var getEmscriptenSupportedExtensions = ctx => { + // Restrict the list of advertised extensions to those that we actually + // support. + var supportedExtensions = [ // WebGL 1 extensions + "ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_disjoint_timer_query", "EXT_frag_depth", "EXT_shader_texture_lod", "EXT_sRGB", "OES_element_index_uint", "OES_fbo_render_mipmap", "OES_standard_derivatives", "OES_texture_float", "OES_texture_half_float", "OES_texture_half_float_linear", "OES_vertex_array_object", "WEBGL_color_buffer_float", "WEBGL_depth_texture", "WEBGL_draw_buffers", // WebGL 2 extensions + "EXT_color_buffer_float", "EXT_conservative_depth", "EXT_disjoint_timer_query_webgl2", "EXT_texture_norm16", "NV_shader_noperspective_interpolation", "WEBGL_clip_cull_distance", // WebGL 1 and WebGL 2 extensions + "EXT_clip_control", "EXT_color_buffer_half_float", "EXT_depth_clamp", "EXT_float_blend", "EXT_polygon_offset_clamp", "EXT_texture_compression_bptc", "EXT_texture_compression_rgtc", "EXT_texture_filter_anisotropic", "KHR_parallel_shader_compile", "OES_texture_float_linear", "WEBGL_blend_func_extended", "WEBGL_compressed_texture_astc", "WEBGL_compressed_texture_etc", "WEBGL_compressed_texture_etc1", "WEBGL_compressed_texture_s3tc", "WEBGL_compressed_texture_s3tc_srgb", "WEBGL_debug_renderer_info", "WEBGL_debug_shaders", "WEBGL_lose_context", "WEBGL_multi_draw", "WEBGL_polygon_mode" ]; + // .getSupportedExtensions() can return null if context is lost, so coerce to empty array. + return (ctx.getSupportedExtensions() || []).filter(ext => supportedExtensions.includes(ext)); +}; + +var registerPreMainLoop = f => { + // Does nothing unless $MainLoop is included/used. + typeof MainLoop != "undefined" && MainLoop.preMainLoop.push(f); +}; + +var GL = { + counter: 1, + buffers: [], + mappedBuffers: {}, + programs: [], + framebuffers: [], + renderbuffers: [], + textures: [], + shaders: [], + vaos: [], + contexts: [], + offscreenCanvases: {}, + queries: [], + samplers: [], + transformFeedbacks: [], + syncs: [], + byteSizeByTypeRoot: 5120, + byteSizeByType: [ 1, 1, 2, 2, 4, 4, 4, 2, 3, 4, 8 ], + stringCache: {}, + stringiCache: {}, + unpackAlignment: 4, + unpackRowLength: 0, + recordError: errorCode => { + if (!GL.lastError) { + GL.lastError = errorCode; + } + }, + getNewId: table => { + var ret = GL.counter++; + for (var i = table.length; i < ret; i++) { + table[i] = null; + } + // Skip over any non-null elements that might have been created by + // glBindBuffer. + while (table[ret]) { + ret = GL.counter++; + } + return ret; + }, + genObject: (n, buffers, createFunction, objectTable) => { + for (var i = 0; i < n; i++) { + var buffer = GLctx[createFunction](); + var id = buffer && GL.getNewId(objectTable); + if (buffer) { + buffer.name = id; + objectTable[id] = buffer; + } else { + GL.recordError(1282); + } + HEAP32[(((buffers) + (i * 4)) >> 2)] = id; + } + }, + MAX_TEMP_BUFFER_SIZE: 2097152, + numTempVertexBuffersPerSize: 64, + log2ceilLookup: i => 32 - Math.clz32(i === 0 ? 0 : i - 1), + generateTempBuffers: (quads, context) => { + var largestIndex = GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE); + context.tempVertexBufferCounters1 = []; + context.tempVertexBufferCounters2 = []; + context.tempVertexBufferCounters1.length = context.tempVertexBufferCounters2.length = largestIndex + 1; + context.tempVertexBuffers1 = []; + context.tempVertexBuffers2 = []; + context.tempVertexBuffers1.length = context.tempVertexBuffers2.length = largestIndex + 1; + context.tempIndexBuffers = []; + context.tempIndexBuffers.length = largestIndex + 1; + for (var i = 0; i <= largestIndex; ++i) { + context.tempIndexBuffers[i] = null; + // Created on-demand + context.tempVertexBufferCounters1[i] = context.tempVertexBufferCounters2[i] = 0; + var ringbufferLength = GL.numTempVertexBuffersPerSize; + context.tempVertexBuffers1[i] = []; + context.tempVertexBuffers2[i] = []; + var ringbuffer1 = context.tempVertexBuffers1[i]; + var ringbuffer2 = context.tempVertexBuffers2[i]; + ringbuffer1.length = ringbuffer2.length = ringbufferLength; + for (var j = 0; j < ringbufferLength; ++j) { + ringbuffer1[j] = ringbuffer2[j] = null; + } + } + if (quads) { + // GL_QUAD indexes can be precalculated + context.tempQuadIndexBuffer = GLctx.createBuffer(); + context.GLctx.bindBuffer(34963, context.tempQuadIndexBuffer); + var numIndexes = GL.MAX_TEMP_BUFFER_SIZE >> 1; + var quadIndexes = new Uint16Array(numIndexes); + var i = 0, v = 0; + while (1) { + quadIndexes[i++] = v; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 1; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 2; + if (i >= numIndexes) break; + quadIndexes[i++] = v; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 2; + if (i >= numIndexes) break; + quadIndexes[i++] = v + 3; + if (i >= numIndexes) break; + v += 4; + } + context.GLctx.bufferData(34963, quadIndexes, 35044); + context.GLctx.bindBuffer(34963, null); + } + }, + getTempVertexBuffer: sizeBytes => { + var idx = GL.log2ceilLookup(sizeBytes); + var ringbuffer = GL.currentContext.tempVertexBuffers1[idx]; + var nextFreeBufferIndex = GL.currentContext.tempVertexBufferCounters1[idx]; + GL.currentContext.tempVertexBufferCounters1[idx] = (GL.currentContext.tempVertexBufferCounters1[idx] + 1) & (GL.numTempVertexBuffersPerSize - 1); + var vbo = ringbuffer[nextFreeBufferIndex]; + if (vbo) { + return vbo; + } + var prevVBO = GLctx.getParameter(34964); + ringbuffer[nextFreeBufferIndex] = GLctx.createBuffer(); + GLctx.bindBuffer(34962, ringbuffer[nextFreeBufferIndex]); + GLctx.bufferData(34962, 1 << idx, 35048); + GLctx.bindBuffer(34962, prevVBO); + return ringbuffer[nextFreeBufferIndex]; + }, + getTempIndexBuffer: sizeBytes => { + var idx = GL.log2ceilLookup(sizeBytes); + var ibo = GL.currentContext.tempIndexBuffers[idx]; + if (ibo) { + return ibo; + } + var prevIBO = GLctx.getParameter(34965); + GL.currentContext.tempIndexBuffers[idx] = GLctx.createBuffer(); + GLctx.bindBuffer(34963, GL.currentContext.tempIndexBuffers[idx]); + GLctx.bufferData(34963, 1 << idx, 35048); + GLctx.bindBuffer(34963, prevIBO); + return GL.currentContext.tempIndexBuffers[idx]; + }, + newRenderingFrameStarted: () => { + if (!GL.currentContext) { + return; + } + var vb = GL.currentContext.tempVertexBuffers1; + GL.currentContext.tempVertexBuffers1 = GL.currentContext.tempVertexBuffers2; + GL.currentContext.tempVertexBuffers2 = vb; + vb = GL.currentContext.tempVertexBufferCounters1; + GL.currentContext.tempVertexBufferCounters1 = GL.currentContext.tempVertexBufferCounters2; + GL.currentContext.tempVertexBufferCounters2 = vb; + var largestIndex = GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE); + for (var i = 0; i <= largestIndex; ++i) { + GL.currentContext.tempVertexBufferCounters1[i] = 0; + } + }, + getSource: (shader, count, string, length) => { + var source = ""; + for (var i = 0; i < count; ++i) { + var len = length ? HEAPU32[(((length) + (i * 4)) >> 2)] : undefined; + source += UTF8ToString(HEAPU32[(((string) + (i * 4)) >> 2)], len); + } + return source; + }, + calcBufLength: (size, type, stride, count) => { + if (stride > 0) { + return count * stride; + } + var typeSize = GL.byteSizeByType[type - GL.byteSizeByTypeRoot]; + return size * typeSize * count; + }, + usedTempBuffers: [], + preDrawHandleClientVertexAttribBindings: count => { + GL.resetBufferBinding = false; + // TODO: initial pass to detect ranges we need to upload, might not need + // an upload per attrib + for (var i = 0; i < GL.currentContext.maxVertexAttribs; ++i) { + var cb = GL.currentContext.clientBuffers[i]; + if (!cb.clientside || !cb.enabled) continue; + GL.resetBufferBinding = true; + var size = GL.calcBufLength(cb.size, cb.type, cb.stride, count); + var buf = GL.getTempVertexBuffer(size); + GLctx.bindBuffer(34962, buf); + GLctx.bufferSubData(34962, 0, HEAPU8.subarray(cb.ptr, cb.ptr + size)); + cb.vertexAttribPointerAdaptor.call(GLctx, i, cb.size, cb.type, cb.normalized, cb.stride, 0); + } + }, + postDrawHandleClientVertexAttribBindings: () => { + if (GL.resetBufferBinding) { + GLctx.bindBuffer(34962, GL.buffers[GLctx.currentArrayBufferBinding]); + } + }, + createContext: (/** @type {HTMLCanvasElement} */ canvas, webGLContextAttributes) => { + // BUG: Workaround Safari WebGL issue: After successfully acquiring WebGL + // context on a canvas, calling .getContext() will always return that + // context independent of which 'webgl' or 'webgl2' + // context version was passed. See: + // https://webkit.org/b/222758 + // and: + // https://github.com/emscripten-core/emscripten/issues/13295. + // TODO: Once the bug is fixed and shipped in Safari, adjust the Safari + // version field in above check. + if (!canvas.getContextSafariWebGL2Fixed) { + canvas.getContextSafariWebGL2Fixed = canvas.getContext; + /** @type {function(this:HTMLCanvasElement, string, (Object|null)=): (Object|null)} */ function fixedGetContext(ver, attrs) { + var gl = canvas.getContextSafariWebGL2Fixed(ver, attrs); + return ((ver == "webgl") == (gl instanceof WebGLRenderingContext)) ? gl : null; + } + canvas.getContext = fixedGetContext; + } + var ctx = (webGLContextAttributes.majorVersion > 1) ? canvas.getContext("webgl2", webGLContextAttributes) : canvas.getContext("webgl", webGLContextAttributes); + if (!ctx) return 0; + var handle = GL.registerContext(ctx, webGLContextAttributes); + return handle; + }, + registerContext: (ctx, webGLContextAttributes) => { + // without pthreads a context is just an integer ID + var handle = GL.getNewId(GL.contexts); + var context = { + handle, + attributes: webGLContextAttributes, + version: webGLContextAttributes.majorVersion, + GLctx: ctx + }; + // Store the created context object so that we can access the context + // given a canvas without having to pass the parameters again. + if (ctx.canvas) ctx.canvas.GLctxObject = context; + GL.contexts[handle] = context; + if (typeof webGLContextAttributes.enableExtensionsByDefault == "undefined" || webGLContextAttributes.enableExtensionsByDefault) { + GL.initExtensions(context); + } + context.maxVertexAttribs = context.GLctx.getParameter(34921); + context.clientBuffers = []; + for (var i = 0; i < context.maxVertexAttribs; i++) { + context.clientBuffers[i] = { + enabled: false, + clientside: false, + size: 0, + type: 0, + normalized: 0, + stride: 0, + ptr: 0, + vertexAttribPointerAdaptor: null + }; + } + GL.generateTempBuffers(false, context); + return handle; + }, + makeContextCurrent: contextHandle => { + // Active Emscripten GL layer context object. + GL.currentContext = GL.contexts[contextHandle]; + // Active WebGL context object. + Module["ctx"] = GLctx = GL.currentContext?.GLctx; + return !(contextHandle && !GLctx); + }, + getContext: contextHandle => GL.contexts[contextHandle], + deleteContext: contextHandle => { + if (GL.currentContext === GL.contexts[contextHandle]) { + GL.currentContext = null; + } + if (typeof JSEvents == "object") { + // Release all JS event handlers on the DOM element that the GL context is + // associated with since the context is now deleted. + JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas); + } + // Make sure the canvas object no longer refers to the context object so + // there are no GC surprises. + if (GL.contexts[contextHandle]?.GLctx.canvas) { + GL.contexts[contextHandle].GLctx.canvas.GLctxObject = undefined; + } + GL.contexts[contextHandle] = null; + }, + initExtensions: context => { + // If this function is called without a specific context object, init the + // extensions of the currently active context. + context ||= GL.currentContext; + if (context.initExtensionsDone) return; + context.initExtensionsDone = true; + var GLctx = context.GLctx; + // Detect the presence of a few extensions manually, since the GL interop + // layer itself will need to know if they exist. + // Extensions that are available in both WebGL 1 and WebGL 2 + webgl_enable_WEBGL_multi_draw(GLctx); + webgl_enable_EXT_polygon_offset_clamp(GLctx); + webgl_enable_EXT_clip_control(GLctx); + webgl_enable_WEBGL_polygon_mode(GLctx); + // Extensions that are only available in WebGL 1 (the calls will be no-ops + // if called on a WebGL 2 context active) + webgl_enable_ANGLE_instanced_arrays(GLctx); + webgl_enable_OES_vertex_array_object(GLctx); + webgl_enable_WEBGL_draw_buffers(GLctx); + // Extensions that are available from WebGL >= 2 (no-op if called on a WebGL 1 context active) + webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx); + webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx); + // On WebGL 2, EXT_disjoint_timer_query is replaced with an alternative + // that's based on core APIs, and exposes only the queryCounterEXT() + // entrypoint. + if (context.version >= 2) { + GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query_webgl2"); + } + // However, Firefox exposes the WebGL 1 version on WebGL 2 as well and + // thus we look for the WebGL 1 version again if the WebGL 2 version + // isn't present. https://bugzil.la/1328882 + if (context.version < 2 || !GLctx.disjointTimerQueryExt) { + GLctx.disjointTimerQueryExt = GLctx.getExtension("EXT_disjoint_timer_query"); + } + for (var ext of getEmscriptenSupportedExtensions(GLctx)) { + // WEBGL_lose_context, WEBGL_debug_renderer_info and WEBGL_debug_shaders + // are not enabled by default. + if (!ext.includes("lose_context") && !ext.includes("debug")) { + // Call .getExtension() to enable that extension permanently. + GLctx.getExtension(ext); + } + } + } +}; + +var webglPowerPreferences = [ "default", "low-power", "high-performance" ]; + +/** @type {Object} */ var specialHTMLTargets = [ 0, globalThis.document ?? 0, globalThis.window ?? 0 ]; + +var findEventTarget = target => { + // The sensible "default" target varies between events, but use window as the default + // since DOM events mostly can default to that. Specific callback registrations + // override their own defaults. + if (!target) return window; + if (typeof target == "number") target = specialHTMLTargets[target] || UTF8ToString(target); + if (target === "#window") return window; else if (target === "#document") return document; else if (target === "#screen") return screen; else if (target === "#canvas") return Module["canvas"]; else if (typeof target == "string") return globalThis.document?.getElementById(target); + return target; +}; + +var findCanvasEventTarget = target => { + if (typeof target == "number") target = UTF8ToString(target); + if (!target || target === "#canvas") { + if (typeof GL != "undefined" && GL.offscreenCanvases["canvas"]) return GL.offscreenCanvases["canvas"]; + // TODO: Remove this line, target '#canvas' should refer only to Module['canvas'], not to GL.offscreenCanvases['canvas'] - but need stricter tests to be able to remove this line. + return Module["canvas"]; + } + if (typeof GL != "undefined" && GL.offscreenCanvases[target]) return GL.offscreenCanvases[target]; + return findEventTarget(target); +}; + +var _emscripten_webgl_do_create_context = (target, attributes) => { + var attr32 = ((attributes) >> 2); + var powerPreference = HEAP32[attr32 + (8 >> 2)]; + var contextAttributes = { + "alpha": !!HEAP8[attributes + 0], + "depth": !!HEAP8[attributes + 1], + "stencil": !!HEAP8[attributes + 2], + "antialias": !!HEAP8[attributes + 3], + "premultipliedAlpha": !!HEAP8[attributes + 4], + "preserveDrawingBuffer": !!HEAP8[attributes + 5], + "powerPreference": webglPowerPreferences[powerPreference], + "failIfMajorPerformanceCaveat": !!HEAP8[attributes + 12], + // The following are not predefined WebGL context attributes in the WebGL specification, so the property names can be minified by Closure. + majorVersion: HEAP32[attr32 + (16 >> 2)], + minorVersion: HEAP32[attr32 + (20 >> 2)], + enableExtensionsByDefault: HEAP8[attributes + 24], + explicitSwapControl: HEAP8[attributes + 25], + proxyContextToMainThread: HEAP32[attr32 + (28 >> 2)], + renderViaOffscreenBackBuffer: HEAP8[attributes + 32] + }; + var canvas = findCanvasEventTarget(target); + if (!canvas) { + return 0; + } + if (contextAttributes.explicitSwapControl) { + return 0; + } + var contextHandle = GL.createContext(canvas, contextAttributes); + return contextHandle; +}; + +var _emscripten_webgl_create_context = _emscripten_webgl_do_create_context; + +var _emscripten_webgl_destroy_context = contextHandle => { + if (GL.currentContext == contextHandle) GL.currentContext = 0; + GL.deleteContext(contextHandle); +}; + +var _emscripten_webgl_get_context_attributes = (c, a) => { + if (!a) return -5; + c = GL.contexts[c]; + if (!c) return -3; + var t = c.GLctx?.getContextAttributes(); + if (!t) return -3; + HEAP8[a] = t.alpha; + HEAP8[(a) + (1)] = t.depth; + HEAP8[(a) + (2)] = t.stencil; + HEAP8[(a) + (3)] = t.antialias; + HEAP8[(a) + (4)] = t.premultipliedAlpha; + HEAP8[(a) + (5)] = t.preserveDrawingBuffer; + var power = t["powerPreference"] && webglPowerPreferences.indexOf(t["powerPreference"]); + HEAP32[(((a) + (8)) >> 2)] = power; + HEAP8[(a) + (12)] = t.failIfMajorPerformanceCaveat; + HEAP32[(((a) + (16)) >> 2)] = c.version; + HEAP32[(((a) + (20)) >> 2)] = 0; + HEAP8[(a) + (24)] = c.attributes.enableExtensionsByDefault; + return 0; +}; + +var _emscripten_webgl_do_get_current_context = () => GL.currentContext ? GL.currentContext.handle : 0; + +var _emscripten_webgl_get_current_context = _emscripten_webgl_do_get_current_context; + +var _emscripten_webgl_make_context_current = contextHandle => { + var success = GL.makeContextCurrent(contextHandle); + return success ? 0 : -5; +}; + +var stackAlloc = sz => __emscripten_stack_alloc(sz); + +var stringToUTF8OnStack = str => { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8(str, ret, size); + return ret; +}; + +var writeI53ToI64 = (ptr, num) => { + HEAPU32[((ptr) >> 2)] = num; + var lower = HEAPU32[((ptr) >> 2)]; + HEAPU32[(((ptr) + (4)) >> 2)] = (num - lower) / 4294967296; +}; + +var readI53FromI64 = ptr => HEAPU32[((ptr) >> 2)] + HEAP32[(((ptr) + (4)) >> 2)] * 4294967296; + +var wasmTableMirror = []; + +var getWasmTableEntry = funcPtr => { + var func = wasmTableMirror[funcPtr]; + if (!func) { + /** @suppress {checkTypes} */ wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr); + } + return func; +}; + +var WebGPU = { + Internals: { + jsObjects: [], + jsObjectInsert: (ptr, jsObject) => { + ptr >>>= 0; + WebGPU.Internals.jsObjects[ptr] = jsObject; + }, + bufferOnUnmaps: [], + futures: [], + futureInsert: (futureId, promise) => {} + }, + getJsObject: ptr => { + if (!ptr) return undefined; + ptr >>>= 0; + return WebGPU.Internals.jsObjects[ptr]; + }, + importJsAdapter: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateAdapter(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBindGroup: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateBindGroup(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBindGroupLayout: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateBindGroupLayout(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsBuffer: (buffer, parentPtr = 0) => { + // At the moment, we do not allow importing pending buffers. + assert(buffer.mapState === "unmapped"); + var bufferPtr = _emwgpuImportBuffer(parentPtr); + WebGPU.Internals.jsObjectInsert(bufferPtr, buffer); + return bufferPtr; + }, + importJsCommandBuffer: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateCommandBuffer(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsCommandEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateCommandEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsComputePassEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateComputePassEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsComputePipeline: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateComputePipeline(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsDevice: (device, parentPtr = 0) => { + var queuePtr = _emwgpuCreateQueue(parentPtr); + var devicePtr = _emwgpuCreateDevice(parentPtr, queuePtr); + WebGPU.Internals.jsObjectInsert(queuePtr, device.queue); + WebGPU.Internals.jsObjectInsert(devicePtr, device); + return devicePtr; + }, + importJsExternalTexture: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateExternalTexture(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsPipelineLayout: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreatePipelineLayout(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsQuerySet: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateQuerySet(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsQueue: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateQueue(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderBundle: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderBundle(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderBundleEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderBundleEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderPassEncoder: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderPassEncoder(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsRenderPipeline: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateRenderPipeline(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsSampler: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateSampler(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsShaderModule: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateShaderModule(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsSurface: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateSurface(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsTexture: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateTexture(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + importJsTextureView: (obj, parentPtr = 0) => { + var ptr = _emwgpuCreateTextureView(parentPtr); + WebGPU.Internals.jsObjects[ptr] = obj; + return ptr; + }, + errorCallback: (callback, type, message, userdata) => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(message); + getWasmTableEntry(callback)(type, messagePtr, userdata); + stackRestore(sp); + }, + iterateExtensions: (root, handlers) => { + for (var ptr = HEAPU32[((root) >> 2)]; ptr; ptr = HEAPU32[((ptr) >> 2)]) { + var sType = HEAP32[(((ptr) + (4)) >> 2)]; + // This will crash if there's no handler indicating either a bogus + // sType, or one we haven't implemented yet. + var handler = handlers[sType](ptr); + } + }, + setStringView: (ptr, data, length) => { + HEAPU32[((ptr) >> 2)] = data; + HEAPU32[(((ptr) + (4)) >> 2)] = length; + }, + makeStringFromStringView: stringViewPtr => { + var ptr = HEAPU32[((stringViewPtr) >> 2)]; + var length = HEAPU32[(((stringViewPtr) + (4)) >> 2)]; + // UTF8ToString stops at the first null terminator character in the + // string regardless of the length. + return UTF8ToString(ptr, length); + }, + makeStringFromOptionalStringView: stringViewPtr => { + var ptr = HEAPU32[((stringViewPtr) >> 2)]; + var length = HEAPU32[(((stringViewPtr) + (4)) >> 2)]; + // If we don't have a valid string pointer, just return undefined when + // optional. + if (!ptr) { + if (length === 0) { + return ""; + } + return undefined; + } + // UTF8ToString stops at the first null terminator character in the + // string regardless of the length. + return UTF8ToString(ptr, length); + }, + makeColor: ptr => ({ + "r": HEAPF64[((ptr) >> 3)], + "g": HEAPF64[(((ptr) + (8)) >> 3)], + "b": HEAPF64[(((ptr) + (16)) >> 3)], + "a": HEAPF64[(((ptr) + (24)) >> 3)] + }), + makeExtent3D: ptr => ({ + "width": HEAPU32[((ptr) >> 2)], + "height": HEAPU32[(((ptr) + (4)) >> 2)], + "depthOrArrayLayers": HEAPU32[(((ptr) + (8)) >> 2)] + }), + makeOrigin3D: ptr => ({ + "x": HEAPU32[((ptr) >> 2)], + "y": HEAPU32[(((ptr) + (4)) >> 2)], + "z": HEAPU32[(((ptr) + (8)) >> 2)] + }), + makeTexelCopyTextureInfo: ptr => ({ + "texture": WebGPU.getJsObject(HEAPU32[((ptr) >> 2)]), + "mipLevel": HEAPU32[(((ptr) + (4)) >> 2)], + "origin": WebGPU.makeOrigin3D(ptr + 8), + "aspect": WebGPU.TextureAspect[HEAP32[(((ptr) + (20)) >> 2)]] + }), + makeTexelCopyBufferLayout: ptr => { + var bytesPerRow = HEAPU32[(((ptr) + (8)) >> 2)]; + var rowsPerImage = HEAPU32[(((ptr) + (12)) >> 2)]; + return { + "offset": readI53FromI64(ptr), + "bytesPerRow": bytesPerRow === 4294967295 ? undefined : bytesPerRow, + "rowsPerImage": rowsPerImage === 4294967295 ? undefined : rowsPerImage + }; + }, + makeTexelCopyBufferInfo: ptr => { + var layoutPtr = ptr + 0; + var bufferCopyView = WebGPU.makeTexelCopyBufferLayout(layoutPtr); + bufferCopyView["buffer"] = WebGPU.getJsObject(HEAPU32[(((ptr) + (16)) >> 2)]); + return bufferCopyView; + }, + makePassTimestampWrites: ptr => { + if (ptr === 0) return undefined; + return { + "querySet": WebGPU.getJsObject(HEAPU32[(((ptr) + (4)) >> 2)]), + "beginningOfPassWriteIndex": HEAPU32[(((ptr) + (8)) >> 2)], + "endOfPassWriteIndex": HEAPU32[(((ptr) + (12)) >> 2)] + }; + }, + makePipelineConstants: (constantCount, constantsPtr) => { + if (!constantCount) return; + var constants = {}; + for (var i = 0; i < constantCount; ++i) { + var entryPtr = constantsPtr + 24 * i; + var key = WebGPU.makeStringFromStringView(entryPtr + 4); + constants[key] = HEAPF64[(((entryPtr) + (16)) >> 3)]; + } + return constants; + }, + makePipelineLayout: layoutPtr => { + if (!layoutPtr) return "auto"; + return WebGPU.getJsObject(layoutPtr); + }, + makeComputeState: ptr => { + if (!ptr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((ptr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((ptr) + (16)) >> 2)], HEAPU32[(((ptr) + (20)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(ptr + 8) + }; + return desc; + }, + makeComputePipelineDesc: descriptor => { + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.makePipelineLayout(HEAPU32[(((descriptor) + (12)) >> 2)]), + "compute": WebGPU.makeComputeState(descriptor + 16) + }; + return desc; + }, + makeRenderPipelineDesc: descriptor => { + function makePrimitiveState(psPtr) { + if (!psPtr) return undefined; + return { + "topology": WebGPU.PrimitiveTopology[HEAP32[(((psPtr) + (4)) >> 2)]], + "stripIndexFormat": WebGPU.IndexFormat[HEAP32[(((psPtr) + (8)) >> 2)]], + "frontFace": WebGPU.FrontFace[HEAP32[(((psPtr) + (12)) >> 2)]], + "cullMode": WebGPU.CullMode[HEAP32[(((psPtr) + (16)) >> 2)]], + "unclippedDepth": !!(HEAPU32[(((psPtr) + (20)) >> 2)]) + }; + } + function makeBlendComponent(bdPtr) { + if (!bdPtr) return undefined; + return { + "operation": WebGPU.BlendOperation[HEAP32[((bdPtr) >> 2)]], + "srcFactor": WebGPU.BlendFactor[HEAP32[(((bdPtr) + (4)) >> 2)]], + "dstFactor": WebGPU.BlendFactor[HEAP32[(((bdPtr) + (8)) >> 2)]] + }; + } + function makeBlendState(bsPtr) { + if (!bsPtr) return undefined; + return { + "alpha": makeBlendComponent(bsPtr + 12), + "color": makeBlendComponent(bsPtr + 0) + }; + } + function makeColorState(csPtr) { + var format = WebGPU.TextureFormat[HEAP32[(((csPtr) + (4)) >> 2)]]; + return format ? { + "format": format, + "blend": makeBlendState(HEAPU32[(((csPtr) + (8)) >> 2)]), + "writeMask": HEAPU32[(((csPtr) + (16)) >> 2)] + } : undefined; + } + function makeColorStates(count, csArrayPtr) { + var states = []; + for (var i = 0; i < count; ++i) { + states.push(makeColorState(csArrayPtr + 24 * i)); + } + return states; + } + function makeStencilStateFace(ssfPtr) { + return { + "compare": WebGPU.CompareFunction[HEAP32[((ssfPtr) >> 2)]], + "failOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (4)) >> 2)]], + "depthFailOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (8)) >> 2)]], + "passOp": WebGPU.StencilOperation[HEAP32[(((ssfPtr) + (12)) >> 2)]] + }; + } + function makeDepthStencilState(dssPtr) { + if (!dssPtr) return undefined; + return { + "format": WebGPU.TextureFormat[HEAP32[(((dssPtr) + (4)) >> 2)]], + "depthWriteEnabled": !!(HEAPU32[(((dssPtr) + (8)) >> 2)]), + "depthCompare": WebGPU.CompareFunction[HEAP32[(((dssPtr) + (12)) >> 2)]], + "stencilFront": makeStencilStateFace(dssPtr + 16), + "stencilBack": makeStencilStateFace(dssPtr + 32), + "stencilReadMask": HEAPU32[(((dssPtr) + (48)) >> 2)], + "stencilWriteMask": HEAPU32[(((dssPtr) + (52)) >> 2)], + "depthBias": HEAP32[(((dssPtr) + (56)) >> 2)], + "depthBiasSlopeScale": HEAPF32[(((dssPtr) + (60)) >> 2)], + "depthBiasClamp": HEAPF32[(((dssPtr) + (64)) >> 2)] + }; + } + function makeVertexAttribute(vaPtr) { + return { + "format": WebGPU.VertexFormat[HEAP32[(((vaPtr) + (4)) >> 2)]], + "offset": readI53FromI64((vaPtr) + (8)), + "shaderLocation": HEAPU32[(((vaPtr) + (16)) >> 2)] + }; + } + function makeVertexAttributes(count, vaArrayPtr) { + var vas = []; + for (var i = 0; i < count; ++i) { + vas.push(makeVertexAttribute(vaArrayPtr + i * 24)); + } + return vas; + } + function makeVertexBuffer(vbPtr) { + if (!vbPtr) return undefined; + var stepMode = WebGPU.VertexStepMode[HEAP32[(((vbPtr) + (4)) >> 2)]]; + var attributeCount = HEAPU32[(((vbPtr) + (16)) >> 2)]; + if (!stepMode && !attributeCount) { + return null; + } + return { + "arrayStride": readI53FromI64((vbPtr) + (8)), + "stepMode": stepMode, + "attributes": makeVertexAttributes(attributeCount, HEAPU32[(((vbPtr) + (20)) >> 2)]) + }; + } + function makeVertexBuffers(count, vbArrayPtr) { + if (!count) return undefined; + var vbs = []; + for (var i = 0; i < count; ++i) { + vbs.push(makeVertexBuffer(vbArrayPtr + i * 24)); + } + return vbs; + } + function makeVertexState(viPtr) { + if (!viPtr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((viPtr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((viPtr) + (16)) >> 2)], HEAPU32[(((viPtr) + (20)) >> 2)]), + "buffers": makeVertexBuffers(HEAPU32[(((viPtr) + (24)) >> 2)], HEAPU32[(((viPtr) + (28)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(viPtr + 8) + }; + return desc; + } + function makeMultisampleState(msPtr) { + if (!msPtr) return undefined; + return { + "count": HEAPU32[(((msPtr) + (4)) >> 2)], + "mask": HEAPU32[(((msPtr) + (8)) >> 2)], + "alphaToCoverageEnabled": !!(HEAPU32[(((msPtr) + (12)) >> 2)]) + }; + } + function makeFragmentState(fsPtr) { + if (!fsPtr) return undefined; + var desc = { + "module": WebGPU.getJsObject(HEAPU32[(((fsPtr) + (4)) >> 2)]), + "constants": WebGPU.makePipelineConstants(HEAPU32[(((fsPtr) + (16)) >> 2)], HEAPU32[(((fsPtr) + (20)) >> 2)]), + "targets": makeColorStates(HEAPU32[(((fsPtr) + (24)) >> 2)], HEAPU32[(((fsPtr) + (28)) >> 2)]), + "entryPoint": WebGPU.makeStringFromOptionalStringView(fsPtr + 8) + }; + return desc; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.makePipelineLayout(HEAPU32[(((descriptor) + (12)) >> 2)]), + "vertex": makeVertexState(descriptor + 16), + "primitive": makePrimitiveState(descriptor + 48), + "depthStencil": makeDepthStencilState(HEAPU32[(((descriptor) + (72)) >> 2)]), + "multisample": makeMultisampleState(descriptor + 76), + "fragment": makeFragmentState(HEAPU32[(((descriptor) + (92)) >> 2)]) + }; + return desc; + }, + fillLimitStruct: (limits, limitsOutPtr) => { + var nextInChainPtr = HEAPU32[((limitsOutPtr) >> 2)]; + function setLimitValueU32(name, basePtr, limitOffset, fallbackValue = 0) { + var limitValue = limits[name] ?? fallbackValue; + HEAPU32[(((basePtr) + (limitOffset)) >> 2)] = limitValue; + } + function setLimitValueU64(name, basePtr, limitOffset, fallbackValue = 0) { + var limitValue = limits[name] ?? fallbackValue; + // Limits are integer-valued JS `Number`s, so they fit in 'i53'. + writeI53ToI64((basePtr) + (limitOffset), limitValue); + } + setLimitValueU32("maxTextureDimension1D", limitsOutPtr, 4); + setLimitValueU32("maxTextureDimension2D", limitsOutPtr, 8); + setLimitValueU32("maxTextureDimension3D", limitsOutPtr, 12); + setLimitValueU32("maxTextureArrayLayers", limitsOutPtr, 16); + setLimitValueU32("maxBindGroups", limitsOutPtr, 20); + setLimitValueU32("maxBindGroupsPlusVertexBuffers", limitsOutPtr, 24); + setLimitValueU32("maxBindingsPerBindGroup", limitsOutPtr, 28); + setLimitValueU32("maxDynamicUniformBuffersPerPipelineLayout", limitsOutPtr, 32); + setLimitValueU32("maxDynamicStorageBuffersPerPipelineLayout", limitsOutPtr, 36); + setLimitValueU32("maxSampledTexturesPerShaderStage", limitsOutPtr, 40); + setLimitValueU32("maxSamplersPerShaderStage", limitsOutPtr, 44); + setLimitValueU32("maxStorageBuffersPerShaderStage", limitsOutPtr, 48); + setLimitValueU32("maxStorageTexturesPerShaderStage", limitsOutPtr, 52); + setLimitValueU32("maxUniformBuffersPerShaderStage", limitsOutPtr, 56); + setLimitValueU32("minUniformBufferOffsetAlignment", limitsOutPtr, 80); + setLimitValueU32("minStorageBufferOffsetAlignment", limitsOutPtr, 84); + setLimitValueU64("maxUniformBufferBindingSize", limitsOutPtr, 64); + setLimitValueU64("maxStorageBufferBindingSize", limitsOutPtr, 72); + setLimitValueU32("maxVertexBuffers", limitsOutPtr, 88); + setLimitValueU64("maxBufferSize", limitsOutPtr, 96); + setLimitValueU32("maxVertexAttributes", limitsOutPtr, 104); + setLimitValueU32("maxVertexBufferArrayStride", limitsOutPtr, 108); + setLimitValueU32("maxInterStageShaderVariables", limitsOutPtr, 112); + setLimitValueU32("maxColorAttachments", limitsOutPtr, 116); + setLimitValueU32("maxColorAttachmentBytesPerSample", limitsOutPtr, 120); + setLimitValueU32("maxComputeWorkgroupStorageSize", limitsOutPtr, 124); + setLimitValueU32("maxComputeInvocationsPerWorkgroup", limitsOutPtr, 128); + setLimitValueU32("maxComputeWorkgroupSizeX", limitsOutPtr, 132); + setLimitValueU32("maxComputeWorkgroupSizeY", limitsOutPtr, 136); + setLimitValueU32("maxComputeWorkgroupSizeZ", limitsOutPtr, 140); + setLimitValueU32("maxComputeWorkgroupsPerDimension", limitsOutPtr, 144); + // Note this limit is new and won't be present in all browsers for a while. Fall back to 0. + setLimitValueU32("maxImmediateSize", limitsOutPtr, 148); + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var compatibilityModeLimitsPtr = nextInChainPtr; + // Note these limits are new and won't be present in all browsers for a while. Fall back to exposing the PerShaderStage limit. + setLimitValueU32("maxStorageBuffersInVertexStage", compatibilityModeLimitsPtr, 8, limits.maxStorageBuffersPerShaderStage); + setLimitValueU32("maxStorageBuffersInFragmentStage", compatibilityModeLimitsPtr, 16, limits.maxStorageBuffersPerShaderStage); + setLimitValueU32("maxStorageTexturesInVertexStage", compatibilityModeLimitsPtr, 12, limits.maxStorageTexturesPerShaderStage); + setLimitValueU32("maxStorageTexturesInFragmentStage", compatibilityModeLimitsPtr, 20, limits.maxStorageTexturesPerShaderStage); + } + }, + fillAdapterInfoStruct: (info, infoStruct) => { + // Populate subgroup limits. + HEAPU32[(((infoStruct) + (52)) >> 2)] = info.subgroupMinSize; + HEAPU32[(((infoStruct) + (56)) >> 2)] = info.subgroupMaxSize; + // Append all the strings together to condense into a single malloc. + var strs = info.vendor + info.architecture + info.device + info.description; + var strPtr = stringToNewUTF8(strs); + var vendorLen = lengthBytesUTF8(info.vendor); + WebGPU.setStringView(infoStruct + 4, strPtr, vendorLen); + strPtr += vendorLen; + var architectureLen = lengthBytesUTF8(info.architecture); + WebGPU.setStringView(infoStruct + 12, strPtr, architectureLen); + strPtr += architectureLen; + var deviceLen = lengthBytesUTF8(info.device); + WebGPU.setStringView(infoStruct + 20, strPtr, deviceLen); + strPtr += deviceLen; + var descriptionLen = lengthBytesUTF8(info.description); + WebGPU.setStringView(infoStruct + 28, strPtr, descriptionLen); + strPtr += descriptionLen; + HEAP32[(((infoStruct) + (36)) >> 2)] = 2; + var adapterType = info.isFallbackAdapter ? 3 : 4; + HEAP32[(((infoStruct) + (40)) >> 2)] = adapterType; + HEAPU32[(((infoStruct) + (44)) >> 2)] = 0; + HEAPU32[(((infoStruct) + (48)) >> 2)] = 0; + }, + AddressMode: [ , "clamp-to-edge", "repeat", "mirror-repeat" ], + BlendFactor: [ , "zero", "one", "src", "one-minus-src", "src-alpha", "one-minus-src-alpha", "dst", "one-minus-dst", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated", "constant", "one-minus-constant", "src1", "one-minus-src1", "src1-alpha", "one-minus-src1-alpha" ], + BlendOperation: [ , "add", "subtract", "reverse-subtract", "min", "max" ], + BufferBindingType: [ , , "uniform", "storage", "read-only-storage" ], + BufferMapState: [ , "unmapped", "pending", "mapped" ], + CompareFunction: [ , "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always" ], + CompilationInfoRequestStatus: [ , "success", "callback-cancelled" ], + ComponentSwizzle: [ , "0", "1", "r", "g", "b", "a" ], + CompositeAlphaMode: [ , "opaque", "premultiplied", "unpremultiplied", "inherit" ], + CullMode: [ , "none", "front", "back" ], + ErrorFilter: [ , "validation", "out-of-memory", "internal" ], + FeatureLevel: [ , "compatibility", "core" ], + FeatureName: { + 1: "core-features-and-limits", + 2: "depth-clip-control", + 3: "depth32float-stencil8", + 4: "texture-compression-bc", + 5: "texture-compression-bc-sliced-3d", + 6: "texture-compression-etc2", + 7: "texture-compression-astc", + 8: "texture-compression-astc-sliced-3d", + 9: "timestamp-query", + 10: "indirect-first-instance", + 11: "shader-f16", + 12: "rg11b10ufloat-renderable", + 13: "bgra8unorm-storage", + 14: "float32-filterable", + 15: "float32-blendable", + 16: "clip-distances", + 17: "dual-source-blending", + 18: "subgroups", + 19: "texture-formats-tier1", + 20: "texture-formats-tier2", + 21: "primitive-index", + 22: "texture-component-swizzle", + 327692: "chromium-experimental-unorm16-texture-formats", + 327729: "chromium-experimental-multi-draw-indirect" + }, + FilterMode: [ , "nearest", "linear" ], + FrontFace: [ , "ccw", "cw" ], + IndexFormat: [ , "uint16", "uint32" ], + InstanceFeatureName: [ , "timed-wait-any", "shader-source-spirv", "multiple-devices-per-adapter" ], + LoadOp: [ , "load", "clear" ], + MipmapFilterMode: [ , "nearest", "linear" ], + OptionalBool: [ "false", "true" ], + PowerPreference: [ , "low-power", "high-performance" ], + PredefinedColorSpace: [ , "srgb", "display-p3" ], + PrimitiveTopology: [ , "point-list", "line-list", "line-strip", "triangle-list", "triangle-strip" ], + QueryType: [ , "occlusion", "timestamp" ], + SamplerBindingType: [ , , "filtering", "non-filtering", "comparison" ], + Status: [ , "success", "error" ], + StencilOperation: [ , "keep", "zero", "replace", "invert", "increment-clamp", "decrement-clamp", "increment-wrap", "decrement-wrap" ], + StorageTextureAccess: [ , , "write-only", "read-only", "read-write" ], + StoreOp: [ , "store", "discard" ], + SurfaceGetCurrentTextureStatus: [ , "success-optimal", "success-suboptimal", "timeout", "outdated", "lost", "error" ], + TextureAspect: [ , "all", "stencil-only", "depth-only" ], + TextureDimension: [ , "1d", "2d", "3d" ], + TextureFormat: [ , "r8unorm", "r8snorm", "r8uint", "r8sint", "r16unorm", "r16snorm", "r16uint", "r16sint", "r16float", "rg8unorm", "rg8snorm", "rg8uint", "rg8sint", "r32float", "r32uint", "r32sint", "rg16unorm", "rg16snorm", "rg16uint", "rg16sint", "rg16float", "rgba8unorm", "rgba8unorm-srgb", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rgb9e5ufloat", "rg32float", "rg32uint", "rg32sint", "rgba16unorm", "rgba16snorm", "rgba16uint", "rgba16sint", "rgba16float", "rgba32float", "rgba32uint", "rgba32sint", "stencil8", "depth16unorm", "depth24plus", "depth24plus-stencil8", "depth32float", "depth32float-stencil8", "bc1-rgba-unorm", "bc1-rgba-unorm-srgb", "bc2-rgba-unorm", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm", "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb", "etc2-rgb8unorm", "etc2-rgb8unorm-srgb", "etc2-rgb8a1unorm", "etc2-rgb8a1unorm-srgb", "etc2-rgba8unorm", "etc2-rgba8unorm-srgb", "eac-r11unorm", "eac-r11snorm", "eac-rg11unorm", "eac-rg11snorm", "astc-4x4-unorm", "astc-4x4-unorm-srgb", "astc-5x4-unorm", "astc-5x4-unorm-srgb", "astc-5x5-unorm", "astc-5x5-unorm-srgb", "astc-6x5-unorm", "astc-6x5-unorm-srgb", "astc-6x6-unorm", "astc-6x6-unorm-srgb", "astc-8x5-unorm", "astc-8x5-unorm-srgb", "astc-8x6-unorm", "astc-8x6-unorm-srgb", "astc-8x8-unorm", "astc-8x8-unorm-srgb", "astc-10x5-unorm", "astc-10x5-unorm-srgb", "astc-10x6-unorm", "astc-10x6-unorm-srgb", "astc-10x8-unorm", "astc-10x8-unorm-srgb", "astc-10x10-unorm", "astc-10x10-unorm-srgb", "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", "astc-12x12-unorm-srgb" ], + TextureSampleType: [ , , "float", "unfilterable-float", "depth", "sint", "uint" ], + TextureViewDimension: [ , "1d", "2d", "2d-array", "cube", "cube-array", "3d" ], + ToneMappingMode: [ , "standard", "extended" ], + VertexFormat: [ , "uint8", "uint8x2", "uint8x4", "sint8", "sint8x2", "sint8x4", "unorm8", "unorm8x2", "unorm8x4", "snorm8", "snorm8x2", "snorm8x4", "uint16", "uint16x2", "uint16x4", "sint16", "sint16x2", "sint16x4", "unorm16", "unorm16x2", "unorm16x4", "snorm16", "snorm16x2", "snorm16x4", "float16", "float16x2", "float16x4", "float32", "float32x2", "float32x3", "float32x4", "uint32", "uint32x2", "uint32x3", "uint32x4", "sint32", "sint32x2", "sint32x3", "sint32x4", "unorm10-10-10-2", "unorm8x4-bgra" ], + VertexStepMode: [ , "vertex", "instance" ], + WGSLLanguageFeatureName: [ , "readonly_and_readwrite_storage_textures", "packed_4x8_integer_dot_product", "unrestricted_pointer_parameters", "pointer_composite_access", "uniform_buffer_standard_layout", "subgroup_id", "texture_and_sampler_let", "subgroup_uniformity", "texture_formats_tier1", "linear_indexing" ] +}; + +var _emscripten_webgpu_get_device = () => { + if (WebGPU.preinitializedDeviceId === undefined) { + WebGPU.preinitializedDeviceId = WebGPU.importJsDevice(Module["preinitializedWebGPUDevice"]); + // Some users depend on this keeping the device alive, so we add an + // additional reference when we first initialize it. + _wgpuDeviceAddRef(WebGPU.preinitializedDeviceId); + } + _wgpuDeviceAddRef(WebGPU.preinitializedDeviceId); + return WebGPU.preinitializedDeviceId; +}; + +var _emwgpuBufferDestroy = bufferPtr => { + var buffer = WebGPU.getJsObject(bufferPtr); + var onUnmap = WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + if (onUnmap) { + for (var i = 0; i < onUnmap.length; ++i) { + onUnmap[i](); + } + delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + } + buffer.destroy(); +}; + +var _emwgpuBufferGetMappedRange = (bufferPtr, offset, size) => { + var buffer = WebGPU.getJsObject(bufferPtr); + if (size == -1) size = undefined; + var mapped; + try { + mapped = buffer.getMappedRange(offset, size); + } catch (ex) { + return 0; + } + var data = _memalign(16, mapped.byteLength); + HEAPU8.fill(0, data, mapped.byteLength); + WebGPU.Internals.bufferOnUnmaps[bufferPtr].push(() => { + new Uint8Array(mapped).set(HEAPU8.subarray(data, data + mapped.byteLength)); + _free(data); + }); + return data; +}; + +var _emwgpuBufferUnmap = bufferPtr => { + var buffer = WebGPU.getJsObject(bufferPtr); + var onUnmap = WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + if (!onUnmap) { + // Already unmapped + return; + } + for (var i = 0; i < onUnmap.length; ++i) { + onUnmap[i](); + } + delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]; + buffer.unmap(); +}; + +var _emwgpuDelete = ptr => { + delete WebGPU.Internals.jsObjects[ptr]; +}; + +var _emwgpuDeviceCreateBuffer = (devicePtr, descriptor, bufferPtr) => { + var mappedAtCreation = !!(HEAPU32[(((descriptor) + (32)) >> 2)]); + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "usage": HEAPU32[(((descriptor) + (16)) >> 2)], + "size": readI53FromI64((descriptor) + (24)), + "mappedAtCreation": mappedAtCreation + }; + var device = WebGPU.getJsObject(devicePtr); + var buffer; + try { + buffer = device.createBuffer(desc); + } catch (ex) { + // The only exception should be RangeError if mapping at creation ran out of memory. + return false; + } + WebGPU.Internals.jsObjectInsert(bufferPtr, buffer); + if (mappedAtCreation) { + WebGPU.Internals.bufferOnUnmaps[bufferPtr] = []; + } + return true; +}; + +var _emwgpuDeviceCreateComputePipelineAsync = function(devicePtr, futureId_low, futureId_high, descriptor, pipelinePtr) { + var futureId = convertI32PairToI53Checked(futureId_low, futureId_high); + var desc = WebGPU.makeComputePipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + // createComputePipelineAsync + WebGPU.Internals.futureInsert(futureId, device.createComputePipelineAsync(desc).then(pipeline => { + // createComputePipelineAsync fulfilled + callUserCallback(() => { + WebGPU.Internals.jsObjectInsert(pipelinePtr, pipeline); + _emwgpuOnCreateComputePipelineCompleted(futureId, 1, pipelinePtr, 0); + }); + }, pipelineError => { + // createComputePipelineAsync rejected + callUserCallback(() => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(pipelineError.message); + var status = pipelineError.reason === "validation" ? 3 : pipelineError.reason === "internal" ? 4 : 0; + _emwgpuOnCreateComputePipelineCompleted(futureId, status, pipelinePtr, messagePtr); + stackRestore(sp); + }); + })); +}; + +var _emwgpuDeviceCreateRenderPipelineAsync = function(devicePtr, futureId_low, futureId_high, descriptor, pipelinePtr) { + var futureId = convertI32PairToI53Checked(futureId_low, futureId_high); + var desc = WebGPU.makeRenderPipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + // createRenderPipelineAsync + WebGPU.Internals.futureInsert(futureId, device.createRenderPipelineAsync(desc).then(pipeline => { + // createRenderPipelineAsync fulfilled + callUserCallback(() => { + WebGPU.Internals.jsObjectInsert(pipelinePtr, pipeline); + _emwgpuOnCreateRenderPipelineCompleted(futureId, 1, pipelinePtr, 0); + }); + }, pipelineError => { + // createRenderPipelineAsync rejected + callUserCallback(() => { + var sp = stackSave(); + var messagePtr = stringToUTF8OnStack(pipelineError.message); + var status = pipelineError.reason === "validation" ? 3 : pipelineError.reason === "internal" ? 4 : 0; + _emwgpuOnCreateRenderPipelineCompleted(futureId, status, pipelinePtr, messagePtr); + stackRestore(sp); + }); + })); +}; + +var _emwgpuDeviceCreateShaderModule = (devicePtr, descriptor, shaderModulePtr) => { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "code": "" + }; + switch (sType) { + case 2: + { + desc["code"] = WebGPU.makeStringFromStringView(nextInChainPtr + 8); + break; + } + } + var device = WebGPU.getJsObject(devicePtr); + WebGPU.Internals.jsObjectInsert(shaderModulePtr, device.createShaderModule(desc)); +}; + +var _emwgpuDeviceDestroy = devicePtr => { + const device = WebGPU.getJsObject(devicePtr); + // Remove the onuncapturederror handler which holds a pointer to the WGPUDevice. + device.onuncapturederror = null; + device.destroy(); +}; + +var _emwgpuWaitAny = (futurePtr, futureCount, timeoutMSPtr) => { + abort("TODO: Implement asyncify-free WaitAny for timeout=0"); +}; + +var ENV = {}; + +var getExecutableName = () => thisProgram || "./this.program"; + +var getEnvStrings = () => { + if (!getEnvStrings.strings) { + // Default values. + // Browser language detection #8751 + var lang = (globalThis.navigator?.language ?? "C").replace("-", "_") + ".UTF-8"; + var env = { + "USER": "web_user", + "LOGNAME": "web_user", + "PATH": "/", + "PWD": "/", + "HOME": "/home/web_user", + "LANG": lang, + "_": getExecutableName() + }; + // Apply the user-provided values, if any. + for (var x in ENV) { + // x is a key in ENV; if ENV[x] is undefined, that means it was + // explicitly set to be so. We allow user code to do that to + // force variables with default values to remain unset. + if (ENV[x] === undefined) delete env[x]; else env[x] = ENV[x]; + } + var strings = []; + for (var x in env) { + strings.push(`${x}=${env[x]}`); + } + getEnvStrings.strings = strings; + } + return getEnvStrings.strings; +}; + +var _environ_get = (__environ, environ_buf) => { + var bufSize = 0; + var envp = 0; + for (var string of getEnvStrings()) { + var ptr = environ_buf + bufSize; + HEAPU32[(((__environ) + (envp)) >> 2)] = ptr; + bufSize += stringToUTF8(string, ptr, Infinity) + 1; + envp += 4; + } + return 0; +}; + +var _environ_sizes_get = (penviron_count, penviron_buf_size) => { + var strings = getEnvStrings(); + HEAPU32[((penviron_count) >> 2)] = strings.length; + var bufSize = 0; + for (var string of strings) { + bufSize += lengthBytesUTF8(string) + 1; + } + HEAPU32[((penviron_buf_size) >> 2)] = bufSize; + return 0; +}; + +function _fd_close(fd) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + FS.close(stream); + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +/** @param {number=} offset */ var doReadv = (stream, iov, iovcnt, offset) => { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[((iov) >> 2)]; + var len = HEAPU32[(((iov) + (4)) >> 2)]; + iov += 8; + var curr = FS.read(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) break; + // nothing more to read + if (typeof offset != "undefined") { + offset += curr; + } + } + return ret; +}; + +function _fd_read(fd, iov, iovcnt, pnum) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doReadv(stream, iov, iovcnt); + HEAPU32[((pnum) >> 2)] = num; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +function _fd_seek(fd, offset_low, offset_high, whence, newOffset) { + var offset = convertI32PairToI53Checked(offset_low, offset_high); + try { + if (isNaN(offset)) return 61; + var stream = SYSCALLS.getStreamFromFD(fd); + FS.llseek(stream, offset, whence); + (tempI64 = [ stream.position >>> 0, (tempDouble = stream.position, (+(Math.abs(tempDouble))) >= 1 ? (tempDouble > 0 ? (+(Math.floor((tempDouble) / 4294967296))) >>> 0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / 4294967296))))) >>> 0) : 0) ], + HEAP32[((newOffset) >> 2)] = tempI64[0], HEAP32[(((newOffset) + (4)) >> 2)] = tempI64[1]); + if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; + // reset readdir state + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +/** @param {number=} offset */ var doWritev = (stream, iov, iovcnt, offset) => { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[((iov) >> 2)]; + var len = HEAPU32[(((iov) + (4)) >> 2)]; + iov += 8; + var curr = FS.write(stream, HEAP8, ptr, len, offset); + if (curr < 0) return -1; + ret += curr; + if (curr < len) { + // No more space to write. + break; + } + if (typeof offset != "undefined") { + offset += curr; + } + } + return ret; +}; + +function _fd_write(fd, iov, iovcnt, pnum) { + try { + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doWritev(stream, iov, iovcnt); + HEAPU32[((pnum) >> 2)] = num; + return 0; + } catch (e) { + if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; + return e.errno; + } +} + +var _emscripten_glActiveTexture = x0 => GLctx.activeTexture(x0); + +var _glActiveTexture = _emscripten_glActiveTexture; + +var _emscripten_glAttachShader = (program, shader) => { + GLctx.attachShader(GL.programs[program], GL.shaders[shader]); +}; + +var _glAttachShader = _emscripten_glAttachShader; + +var _emscripten_glBindAttribLocation = (program, index, name) => { + GLctx.bindAttribLocation(GL.programs[program], index, UTF8ToString(name)); +}; + +var _glBindAttribLocation = _emscripten_glBindAttribLocation; + +var _emscripten_glBindBuffer = (target, buffer) => { + // Calling glBindBuffer with an unknown buffer will implicitly create a + // new one. Here we bypass `GL.counter` and directly using the ID passed + // in. + if (buffer && !GL.buffers[buffer]) { + var b = GLctx.createBuffer(); + b.name = buffer; + GL.buffers[buffer] = b; + } + if (target == 34962) { + GLctx.currentArrayBufferBinding = buffer; + } else if (target == 34963) { + GLctx.currentElementArrayBufferBinding = buffer; + } + if (target == 35051) { + // In WebGL 2 glReadPixels entry point, we need to use a different WebGL 2 + // API function call when a buffer is bound to + // GL_PIXEL_PACK_BUFFER_BINDING point, so must keep track whether that + // binding point is non-null to know what is the proper API function to + // call. + GLctx.currentPixelPackBufferBinding = buffer; + } else if (target == 35052) { + // In WebGL 2 gl(Compressed)Tex(Sub)Image[23]D entry points, we need to + // use a different WebGL 2 API function call when a buffer is bound to + // GL_PIXEL_UNPACK_BUFFER_BINDING point, so must keep track whether that + // binding point is non-null to know what is the proper API function to + // call. + GLctx.currentPixelUnpackBufferBinding = buffer; + } + GLctx.bindBuffer(target, GL.buffers[buffer]); +}; + +var _glBindBuffer = _emscripten_glBindBuffer; + +var _emscripten_glBindBufferBase = (target, index, buffer) => { + GLctx.bindBufferBase(target, index, GL.buffers[buffer]); +}; + +var _glBindBufferBase = _emscripten_glBindBufferBase; + +var _emscripten_glBindFramebuffer = (target, framebuffer) => { + GLctx.bindFramebuffer(target, GL.framebuffers[framebuffer]); +}; + +var _glBindFramebuffer = _emscripten_glBindFramebuffer; + +var _emscripten_glBindTexture = (target, texture) => { + GLctx.bindTexture(target, GL.textures[texture]); +}; + +var _glBindTexture = _emscripten_glBindTexture; + +var _emscripten_glBindVertexArray = vao => { + GLctx.bindVertexArray(GL.vaos[vao]); + var ibo = GLctx.getParameter(34965); + GLctx.currentElementArrayBufferBinding = ibo ? (ibo.name | 0) : 0; +}; + +var _glBindVertexArray = _emscripten_glBindVertexArray; + +var _emscripten_glBlendEquation = x0 => GLctx.blendEquation(x0); + +var _glBlendEquation = _emscripten_glBlendEquation; + +var _emscripten_glBlendFunc = (x0, x1) => GLctx.blendFunc(x0, x1); + +var _glBlendFunc = _emscripten_glBlendFunc; + +var _emscripten_glBufferData = (target, size, data, usage) => { + if (GL.currentContext.version >= 2) { + // If size is zero, WebGL would interpret uploading the whole input + // arraybuffer (starting from given offset), which would not make sense in + // WebAssembly, so avoid uploading if size is zero. However we must still + // call bufferData to establish a backing storage of zero bytes. + if (data && size) { + GLctx.bufferData(target, HEAPU8, usage, data, size); + } else { + GLctx.bufferData(target, size, usage); + } + return; + } + // N.b. here first form specifies a heap subarray, second form an integer + // size, so the ?: code here is polymorphic. It is advised to avoid + // randomly mixing both uses in calling code, to avoid any potential JS + // engine JIT issues. + GLctx.bufferData(target, data ? HEAPU8.subarray(data, data + size) : size, usage); +}; + +var _glBufferData = _emscripten_glBufferData; + +var _emscripten_glClear = x0 => GLctx.clear(x0); + +var _glClear = _emscripten_glClear; + +var _emscripten_glClearColor = (x0, x1, x2, x3) => GLctx.clearColor(x0, x1, x2, x3); + +var _glClearColor = _emscripten_glClearColor; + +var convertI32PairToI53 = (lo, hi) => (lo >>> 0) + hi * 4294967296; + +var _emscripten_glClientWaitSync = (sync, flags, timeout_low, timeout_high) => { + // WebGL2 vs GLES3 differences: in GLES3, the timeout parameter is a uint64, where 0xFFFFFFFFFFFFFFFFULL means GL_TIMEOUT_IGNORED. + // In JS, there's no 64-bit value types, so instead timeout is taken to be signed, and GL_TIMEOUT_IGNORED is given value -1. + // Inherently the value accepted in the timeout is lossy, and can't take in arbitrary u64 bit pattern (but most likely doesn't matter) + // See https://www.khronos.org/registry/webgl/specs/latest/2.0/#5.15 + var timeout = convertI32PairToI53(timeout_low, timeout_high); + return GLctx.clientWaitSync(GL.syncs[sync], flags, timeout); +}; + +var _glClientWaitSync = _emscripten_glClientWaitSync; + +var _emscripten_glColorMask = (red, green, blue, alpha) => { + GLctx.colorMask(!!red, !!green, !!blue, !!alpha); +}; + +var _glColorMask = _emscripten_glColorMask; + +var _emscripten_glCompileShader = shader => { + GLctx.compileShader(GL.shaders[shader]); +}; + +var _glCompileShader = _emscripten_glCompileShader; + +var _emscripten_glCreateProgram = () => { + var id = GL.getNewId(GL.programs); + var program = GLctx.createProgram(); + // Store additional information needed for each shader program: + program.name = id; + // Lazy cache results of + // glGetProgramiv(GL_ACTIVE_UNIFORM_MAX_LENGTH/GL_ACTIVE_ATTRIBUTE_MAX_LENGTH/GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH) + program.maxUniformLength = program.maxAttributeLength = program.maxUniformBlockNameLength = 0; + program.uniformIdCounter = 1; + GL.programs[id] = program; + return id; +}; + +var _glCreateProgram = _emscripten_glCreateProgram; + +var _emscripten_glCreateShader = shaderType => { + var id = GL.getNewId(GL.shaders); + GL.shaders[id] = GLctx.createShader(shaderType); + return id; +}; + +var _glCreateShader = _emscripten_glCreateShader; + +var _emscripten_glDeleteBuffers = (n, buffers) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((buffers) + (i * 4)) >> 2)]; + var buffer = GL.buffers[id]; + // From spec: "glDeleteBuffers silently ignores 0's and names that do not + // correspond to existing buffer objects." + if (!buffer) continue; + GLctx.deleteBuffer(buffer); + buffer.name = 0; + GL.buffers[id] = null; + if (id == GLctx.currentArrayBufferBinding) GLctx.currentArrayBufferBinding = 0; + if (id == GLctx.currentElementArrayBufferBinding) GLctx.currentElementArrayBufferBinding = 0; + if (id == GLctx.currentPixelPackBufferBinding) GLctx.currentPixelPackBufferBinding = 0; + if (id == GLctx.currentPixelUnpackBufferBinding) GLctx.currentPixelUnpackBufferBinding = 0; + } +}; + +var _glDeleteBuffers = _emscripten_glDeleteBuffers; + +var _emscripten_glDeleteFramebuffers = (n, framebuffers) => { + for (var i = 0; i < n; ++i) { + var id = HEAP32[(((framebuffers) + (i * 4)) >> 2)]; + var framebuffer = GL.framebuffers[id]; + if (!framebuffer) continue; + // GL spec: "glDeleteFramebuffers silently ignores 0s and names that do not correspond to existing framebuffer objects". + GLctx.deleteFramebuffer(framebuffer); + framebuffer.name = 0; + GL.framebuffers[id] = null; + } +}; + +var _glDeleteFramebuffers = _emscripten_glDeleteFramebuffers; + +var _emscripten_glDeleteProgram = id => { + if (!id) return; + var program = GL.programs[id]; + if (!program) { + // glDeleteProgram actually signals an error when deleting a nonexisting + // object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteProgram(program); + program.name = 0; + GL.programs[id] = null; +}; + +var _glDeleteProgram = _emscripten_glDeleteProgram; + +var _emscripten_glDeleteShader = id => { + if (!id) return; + var shader = GL.shaders[id]; + if (!shader) { + // glDeleteShader actually signals an error when deleting a nonexisting + // object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteShader(shader); + GL.shaders[id] = null; +}; + +var _glDeleteShader = _emscripten_glDeleteShader; + +var _emscripten_glDeleteSync = id => { + if (!id) return; + var sync = GL.syncs[id]; + if (!sync) { + // glDeleteSync signals an error when deleting a nonexisting object, unlike some other GL delete functions. + GL.recordError(1281); + return; + } + GLctx.deleteSync(sync); + sync.name = 0; + GL.syncs[id] = null; +}; + +var _glDeleteSync = _emscripten_glDeleteSync; + +var _emscripten_glDeleteTextures = (n, textures) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((textures) + (i * 4)) >> 2)]; + var texture = GL.textures[id]; + // GL spec: "glDeleteTextures silently ignores 0s and names that do not + // correspond to existing textures". + if (!texture) continue; + GLctx.deleteTexture(texture); + texture.name = 0; + GL.textures[id] = null; + } +}; + +var _glDeleteTextures = _emscripten_glDeleteTextures; + +var _emscripten_glDeleteVertexArrays = (n, vaos) => { + for (var i = 0; i < n; i++) { + var id = HEAP32[(((vaos) + (i * 4)) >> 2)]; + GLctx.deleteVertexArray(GL.vaos[id]); + GL.vaos[id] = null; + } +}; + +var _glDeleteVertexArrays = _emscripten_glDeleteVertexArrays; + +var _emscripten_glDetachShader = (program, shader) => { + GLctx.detachShader(GL.programs[program], GL.shaders[shader]); +}; + +var _glDetachShader = _emscripten_glDetachShader; + +var _emscripten_glDisable = x0 => GLctx.disable(x0); + +var _glDisable = _emscripten_glDisable; + +var _emscripten_glDisableVertexAttribArray = index => { + var cb = GL.currentContext.clientBuffers[index]; + cb.enabled = false; + GLctx.disableVertexAttribArray(index); +}; + +var _glDisableVertexAttribArray = _emscripten_glDisableVertexAttribArray; + +var _emscripten_glDrawArrays = (mode, first, count) => { + // bind any client-side buffers + GL.preDrawHandleClientVertexAttribBindings(first + count); + GLctx.drawArrays(mode, first, count); + GL.postDrawHandleClientVertexAttribBindings(); +}; + +var _glDrawArrays = _emscripten_glDrawArrays; + +var tempFixedLengthArray = []; + +var _emscripten_glDrawBuffers = (n, bufs) => { + var bufArray = tempFixedLengthArray[n]; + for (var i = 0; i < n; i++) { + bufArray[i] = HEAP32[(((bufs) + (i * 4)) >> 2)]; + } + GLctx.drawBuffers(bufArray); +}; + +var _glDrawBuffers = _emscripten_glDrawBuffers; + +var _emscripten_glEnable = x0 => GLctx.enable(x0); + +var _glEnable = _emscripten_glEnable; + +var _emscripten_glEnableVertexAttribArray = index => { + var cb = GL.currentContext.clientBuffers[index]; + cb.enabled = true; + GLctx.enableVertexAttribArray(index); +}; + +var _glEnableVertexAttribArray = _emscripten_glEnableVertexAttribArray; + +var _emscripten_glFenceSync = (condition, flags) => { + var sync = GLctx.fenceSync(condition, flags); + if (sync) { + var id = GL.getNewId(GL.syncs); + sync.name = id; + GL.syncs[id] = sync; + return id; + } + return 0; +}; + +var _glFenceSync = _emscripten_glFenceSync; + +var _emscripten_glFinish = () => GLctx.finish(); + +var _glFinish = _emscripten_glFinish; + +var _emscripten_glFlush = () => GLctx.flush(); + +var _glFlush = _emscripten_glFlush; + +var _emscripten_glFramebufferTexture2D = (target, attachment, textarget, texture, level) => { + GLctx.framebufferTexture2D(target, attachment, textarget, GL.textures[texture], level); +}; + +var _glFramebufferTexture2D = _emscripten_glFramebufferTexture2D; + +var _emscripten_glFramebufferTextureLayer = (target, attachment, texture, level, layer) => { + GLctx.framebufferTextureLayer(target, attachment, GL.textures[texture], level, layer); +}; + +var _glFramebufferTextureLayer = _emscripten_glFramebufferTextureLayer; + +var _emscripten_glGenBuffers = (n, buffers) => { + GL.genObject(n, buffers, "createBuffer", GL.buffers); +}; + +var _glGenBuffers = _emscripten_glGenBuffers; + +var _emscripten_glGenFramebuffers = (n, ids) => { + GL.genObject(n, ids, "createFramebuffer", GL.framebuffers); +}; + +var _glGenFramebuffers = _emscripten_glGenFramebuffers; + +var _emscripten_glGenTextures = (n, textures) => { + GL.genObject(n, textures, "createTexture", GL.textures); +}; + +var _glGenTextures = _emscripten_glGenTextures; + +var _emscripten_glGenVertexArrays = (n, arrays) => { + GL.genObject(n, arrays, "createVertexArray", GL.vaos); +}; + +var _glGenVertexArrays = _emscripten_glGenVertexArrays; + +var _emscripten_glGetAttribLocation = (program, name) => GLctx.getAttribLocation(GL.programs[program], UTF8ToString(name)); + +var _glGetAttribLocation = _emscripten_glGetAttribLocation; + +var _emscripten_glGetError = () => { + var error = GLctx.getError() || GL.lastError; + GL.lastError = 0; + return error; +}; + +var _glGetError = _emscripten_glGetError; + +var webglGetExtensions = () => { + var exts = getEmscriptenSupportedExtensions(GLctx); + exts = exts.concat(exts.map(e => "GL_" + e)); + return exts; +}; + +var emscriptenWebGLGet = (name_, p, type) => { + // Guard against user passing a null pointer. + // Note that GLES2 spec does not say anything about how passing a null + // pointer should be treated. Testing on desktop core GL 3, the application + // crashes on glGetIntegerv to a null pointer, but better to report an error + // instead of doing anything random. + if (!p) { + GL.recordError(1281); + return; + } + var ret = undefined; + switch (name_) { + // Handle a few trivial GLES values + case 36346: + // GL_SHADER_COMPILER + ret = 1; + break; + + case 36344: + // GL_SHADER_BINARY_FORMATS + if (type != 0 && type != 1) { + GL.recordError(1280); + } + // Do not write anything to the out pointer, since no binary formats are + // supported. + return; + + case 34814: + // GL_NUM_PROGRAM_BINARY_FORMATS + case 36345: + // GL_NUM_SHADER_BINARY_FORMATS + ret = 0; + break; + + case 34466: + // GL_NUM_COMPRESSED_TEXTURE_FORMATS + // WebGL doesn't have GL_NUM_COMPRESSED_TEXTURE_FORMATS (it's obsolete + // since GL_COMPRESSED_TEXTURE_FORMATS returns a JS array that can be + // queried for length), so implement it ourselves to allow C++ GLES2 + // code to get the length. + var formats = GLctx.getParameter(34467); + ret = formats ? formats.length : 0; + break; + + case 33309: + // GL_NUM_EXTENSIONS + if (GL.currentContext.version < 2) { + // Calling GLES3/WebGL2 function with a GLES2/WebGL1 context + GL.recordError(1282); + return; + } + ret = webglGetExtensions().length; + break; + + case 33307: + // GL_MAJOR_VERSION + case 33308: + // GL_MINOR_VERSION + if (GL.currentContext.version < 2) { + GL.recordError(1280); + // GL_INVALID_ENUM + return; + } + ret = name_ == 33307 ? 3 : 0; + // return version 3.0 + break; + } + if (ret === undefined) { + var result = GLctx.getParameter(name_); + switch (typeof result) { + case "number": + ret = result; + break; + + case "boolean": + ret = result ? 1 : 0; + break; + + case "string": + GL.recordError(1280); + // GL_INVALID_ENUM + return; + + case "object": + if (result === null) { + // null is a valid result for some (e.g., which buffer is bound - + // perhaps nothing is bound), but otherwise can mean an invalid + // name_, which we need to report as an error + switch (name_) { + case 34964: + // ARRAY_BUFFER_BINDING + case 35725: + // CURRENT_PROGRAM + case 34965: + // ELEMENT_ARRAY_BUFFER_BINDING + case 36006: + // FRAMEBUFFER_BINDING or DRAW_FRAMEBUFFER_BINDING + case 36007: + // RENDERBUFFER_BINDING + case 32873: + // TEXTURE_BINDING_2D + case 34229: + // WebGL 2 GL_VERTEX_ARRAY_BINDING, or WebGL 1 extension OES_vertex_array_object GL_VERTEX_ARRAY_BINDING_OES + case 36662: + // COPY_READ_BUFFER_BINDING or COPY_READ_BUFFER + case 36663: + // COPY_WRITE_BUFFER_BINDING or COPY_WRITE_BUFFER + case 35053: + // PIXEL_PACK_BUFFER_BINDING + case 35055: + // PIXEL_UNPACK_BUFFER_BINDING + case 36010: + // READ_FRAMEBUFFER_BINDING + case 35097: + // SAMPLER_BINDING + case 35869: + // TEXTURE_BINDING_2D_ARRAY + case 32874: + // TEXTURE_BINDING_3D + case 36389: + // TRANSFORM_FEEDBACK_BINDING + case 35983: + // TRANSFORM_FEEDBACK_BUFFER_BINDING + case 35368: + // UNIFORM_BUFFER_BINDING + case 34068: + { + // TEXTURE_BINDING_CUBE_MAP + ret = 0; + break; + } + + default: + { + GL.recordError(1280); + // GL_INVALID_ENUM + return; + } + } + } else if (result instanceof Float32Array || result instanceof Uint32Array || result instanceof Int32Array || result instanceof Array) { + for (var i = 0; i < result.length; ++i) { + switch (type) { + case 0: + HEAP32[(((p) + (i * 4)) >> 2)] = result[i]; + break; + + case 2: + HEAPF32[(((p) + (i * 4)) >> 2)] = result[i]; + break; + + case 4: + HEAP8[(p) + (i)] = result[i] ? 1 : 0; + break; + } + } + return; + } else { + try { + ret = result.name | 0; + } catch (e) { + GL.recordError(1280); + // GL_INVALID_ENUM + err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`); + return; + } + } + break; + + default: + GL.recordError(1280); + // GL_INVALID_ENUM + err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof (result)}!`); + return; + } + } + switch (type) { + case 1: + writeI53ToI64(p, ret); + break; + + case 0: + HEAP32[((p) >> 2)] = ret; + break; + + case 2: + HEAPF32[((p) >> 2)] = ret; + break; + + case 4: + HEAP8[p] = ret ? 1 : 0; + break; + } +}; + +var _emscripten_glGetFloatv = (name_, p) => emscriptenWebGLGet(name_, p, 2); + +var _glGetFloatv = _emscripten_glGetFloatv; + +var _emscripten_glGetIntegerv = (name_, p) => emscriptenWebGLGet(name_, p, 0); + +var _glGetIntegerv = _emscripten_glGetIntegerv; + +var _emscripten_glGetProgramiv = (program, pname, p) => { + if (!p) { + // GLES2 specification does not specify how to behave if p is a null + // pointer. Since calling this function does not make sense if p == null, + // issue a GL error to notify user about it. + GL.recordError(1281); + return; + } + if (program >= GL.counter) { + GL.recordError(1281); + return; + } + program = GL.programs[program]; + if (pname == 35716) { + // GL_INFO_LOG_LENGTH + var log = GLctx.getProgramInfoLog(program); + if (log === null) log = "(unknown error)"; + HEAP32[((p) >> 2)] = log.length + 1; + } else if (pname == 35719) { + if (!program.maxUniformLength) { + var numActiveUniforms = GLctx.getProgramParameter(program, 35718); + for (var i = 0; i < numActiveUniforms; ++i) { + program.maxUniformLength = Math.max(program.maxUniformLength, GLctx.getActiveUniform(program, i).name.length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxUniformLength; + } else if (pname == 35722) { + if (!program.maxAttributeLength) { + var numActiveAttributes = GLctx.getProgramParameter(program, 35721); + for (var i = 0; i < numActiveAttributes; ++i) { + program.maxAttributeLength = Math.max(program.maxAttributeLength, GLctx.getActiveAttrib(program, i).name.length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxAttributeLength; + } else if (pname == 35381) { + if (!program.maxUniformBlockNameLength) { + var numActiveUniformBlocks = GLctx.getProgramParameter(program, 35382); + for (var i = 0; i < numActiveUniformBlocks; ++i) { + program.maxUniformBlockNameLength = Math.max(program.maxUniformBlockNameLength, GLctx.getActiveUniformBlockName(program, i).length + 1); + } + } + HEAP32[((p) >> 2)] = program.maxUniformBlockNameLength; + } else { + HEAP32[((p) >> 2)] = GLctx.getProgramParameter(program, pname); + } +}; + +var _glGetProgramiv = _emscripten_glGetProgramiv; + +var _emscripten_glGetShaderInfoLog = (shader, maxLength, length, infoLog) => { + var log = GLctx.getShaderInfoLog(GL.shaders[shader]); + if (log === null) log = "(unknown error)"; + var numBytesWrittenExclNull = (maxLength > 0 && infoLog) ? stringToUTF8(log, infoLog, maxLength) : 0; + if (length) HEAP32[((length) >> 2)] = numBytesWrittenExclNull; +}; + +var _glGetShaderInfoLog = _emscripten_glGetShaderInfoLog; + +var _emscripten_glGetShaderiv = (shader, pname, p) => { + if (!p) { + // GLES2 specification does not specify how to behave if p is a null + // pointer. Since calling this function does not make sense if p == null, + // issue a GL error to notify user about it. + GL.recordError(1281); + return; + } + if (pname == 35716) { + // GL_INFO_LOG_LENGTH + var log = GLctx.getShaderInfoLog(GL.shaders[shader]); + if (log === null) log = "(unknown error)"; + // The GLES2 specification says that if the shader has an empty info log, + // a value of 0 is returned. Otherwise the log has a null char appended. + // (An empty string is falsey, so we can just check that instead of + // looking at log.length.) + var logLength = log ? log.length + 1 : 0; + HEAP32[((p) >> 2)] = logLength; + } else if (pname == 35720) { + // GL_SHADER_SOURCE_LENGTH + var source = GLctx.getShaderSource(GL.shaders[shader]); + // source may be a null, or the empty string, both of which are falsey + // values that we report a 0 length for. + var sourceLength = source ? source.length + 1 : 0; + HEAP32[((p) >> 2)] = sourceLength; + } else { + HEAP32[((p) >> 2)] = GLctx.getShaderParameter(GL.shaders[shader], pname); + } +}; + +var _glGetShaderiv = _emscripten_glGetShaderiv; + +var _emscripten_glGetString = name_ => { + var ret = GL.stringCache[name_]; + if (!ret) { + switch (name_) { + case 7939: + ret = stringToNewUTF8(webglGetExtensions().join(" ")); + break; + + case 7936: + case 7937: + case 37445: + case 37446: + var s = GLctx.getParameter(name_); + if (!s) { + GL.recordError(1280); + } + ret = s ? stringToNewUTF8(s) : 0; + break; + + case 7938: + var webGLVersion = GLctx.getParameter(7938); + // return GLES version string corresponding to the version of the WebGL context + var glVersion = `OpenGL ES 2.0 (${webGLVersion})`; + if (GL.currentContext.version >= 2) glVersion = `OpenGL ES 3.0 (${webGLVersion})`; + ret = stringToNewUTF8(glVersion); + break; + + case 35724: + var glslVersion = GLctx.getParameter(35724); + // extract the version number 'N.M' from the string 'WebGL GLSL ES N.M ...' + var ver_re = /^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/; + var ver_num = glslVersion.match(ver_re); + if (ver_num !== null) { + if (ver_num[1].length == 3) ver_num[1] = ver_num[1] + "0"; + // ensure minor version has 2 digits + glslVersion = `OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`; + } + ret = stringToNewUTF8(glslVersion); + break; + + default: + GL.recordError(1280); + } + GL.stringCache[name_] = ret; + } + return ret; +}; + +var _glGetString = _emscripten_glGetString; + +var _emscripten_glGetUniformBlockIndex = (program, uniformBlockName) => GLctx.getUniformBlockIndex(GL.programs[program], UTF8ToString(uniformBlockName)); + +var _glGetUniformBlockIndex = _emscripten_glGetUniformBlockIndex; + +/** @suppress {checkTypes} */ var jstoi_q = str => parseInt(str); + +/** @noinline */ var webglGetLeftBracePos = name => name.slice(-1) == "]" && name.lastIndexOf("["); + +var webglPrepareUniformLocationsBeforeFirstUse = program => { + var uniformLocsById = program.uniformLocsById, // Maps GLuint -> WebGLUniformLocation + uniformSizeAndIdsByName = program.uniformSizeAndIdsByName, // Maps name -> [uniform array length, GLuint] + i, j; + // On the first time invocation of glGetUniformLocation on this shader program: + // initialize cache data structures and discover which uniforms are arrays. + if (!uniformLocsById) { + // maps GLint integer locations to WebGLUniformLocations + program.uniformLocsById = uniformLocsById = {}; + // maps integer locations back to uniform name strings, so that we can lazily fetch uniform array locations + program.uniformArrayNamesById = {}; + var numActiveUniforms = GLctx.getProgramParameter(program, 35718); + for (i = 0; i < numActiveUniforms; ++i) { + var u = GLctx.getActiveUniform(program, i); + var nm = u.name; + var sz = u.size; + var lb = webglGetLeftBracePos(nm); + var arrayName = lb > 0 ? nm.slice(0, lb) : nm; + // Assign a new location. + var id = program.uniformIdCounter; + program.uniformIdCounter += sz; + // Eagerly get the location of the uniformArray[0] base element. + // The remaining indices >0 will be left for lazy evaluation to + // improve performance. Those may never be needed to fetch, if the + // application fills arrays always in full starting from the first + // element of the array. + uniformSizeAndIdsByName[arrayName] = [ sz, id ]; + // Store placeholder integers in place that highlight that these + // >0 index locations are array indices pending population. + for (j = 0; j < sz; ++j) { + uniformLocsById[id] = j; + program.uniformArrayNamesById[id++] = arrayName; + } + } + } +}; + +var _emscripten_glGetUniformLocation = (program, name) => { + name = UTF8ToString(name); + if (program = GL.programs[program]) { + webglPrepareUniformLocationsBeforeFirstUse(program); + var uniformLocsById = program.uniformLocsById; + // Maps GLuint -> WebGLUniformLocation + var arrayIndex = 0; + var uniformBaseName = name; + // Invariant: when populating integer IDs for uniform locations, we must + // maintain the precondition that arrays reside in contiguous addresses, + // i.e. for a 'vec4 colors[10];', colors[4] must be at location + // colors[0]+4. However, user might call glGetUniformLocation(program, + // "colors") for an array, so we cannot discover based on the user input + // arguments whether the uniform we are dealing with is an array. The only + // way to discover which uniforms are arrays is to enumerate over all the + // active uniforms in the program. + var leftBrace = webglGetLeftBracePos(name); + // If user passed an array accessor "[index]", parse the array index off the accessor. + if (leftBrace > 0) { + arrayIndex = jstoi_q(name.slice(leftBrace + 1)) >>> 0; + // "index]", coerce parseInt(']') with >>>0 to treat "foo[]" as "foo[0]" and foo[-1] as unsigned out-of-bounds. + uniformBaseName = name.slice(0, leftBrace); + } + // Have we cached the location of this uniform before? + // A pair [array length, GLint of the uniform location] + var sizeAndId = program.uniformSizeAndIdsByName[uniformBaseName]; + // If a uniform with this name exists, and if its index is within the + // array limits (if it's even an array), query the WebGLlocation, or + // return an existing cached location. + if (sizeAndId && arrayIndex < sizeAndId[0]) { + arrayIndex += sizeAndId[1]; + // Add the base location of the uniform to the array index offset. + if ((uniformLocsById[arrayIndex] = uniformLocsById[arrayIndex] || GLctx.getUniformLocation(program, name))) { + return arrayIndex; + } + } + } else { + // N.b. we are currently unable to distinguish between GL program IDs that + // never existed vs GL program IDs that have been deleted, so report + // GL_INVALID_VALUE in both cases. + GL.recordError(1281); + } + return -1; +}; + +var _glGetUniformLocation = _emscripten_glGetUniformLocation; + +var _emscripten_glLineWidth = x0 => GLctx.lineWidth(x0); + +var _glLineWidth = _emscripten_glLineWidth; + +var _emscripten_glLinkProgram = program => { + program = GL.programs[program]; + GLctx.linkProgram(program); + // Invalidate earlier computed uniform->ID mappings, those have now become stale + program.uniformLocsById = 0; + // Mark as null-like so that glGetUniformLocation() knows to populate this again. + program.uniformSizeAndIdsByName = {}; +}; + +var _glLinkProgram = _emscripten_glLinkProgram; + +var _emscripten_glPixelStorei = (pname, param) => { + if (pname == 3317) { + GL.unpackAlignment = param; + } else if (pname == 3314) { + GL.unpackRowLength = param; + } + GLctx.pixelStorei(pname, param); +}; + +var _glPixelStorei = _emscripten_glPixelStorei; + +var computeUnpackAlignedImageSize = (width, height, sizePerPixel) => { + function roundedToNextMultipleOf(x, y) { + return (x + y - 1) & -y; + } + var plainRowSize = (GL.unpackRowLength || width) * sizePerPixel; + var alignedRowSize = roundedToNextMultipleOf(plainRowSize, GL.unpackAlignment); + return height * alignedRowSize; +}; + +var colorChannelsInGlTextureFormat = format => { + // Micro-optimizations for size: map format to size by subtracting smallest + // enum value (0x1902) from all values first. Also omit the most common + // size value (1) from the list, which is assumed by formats not on the + // list. + var colorChannels = { + // 0x1902 /* GL_DEPTH_COMPONENT */ - 0x1902: 1, + // 0x1906 /* GL_ALPHA */ - 0x1902: 1, + 5: 3, + 6: 4, + // 0x1909 /* GL_LUMINANCE */ - 0x1902: 1, + 8: 2, + 29502: 3, + 29504: 4, + // 0x1903 /* GL_RED */ - 0x1902: 1, + 26917: 2, + 26918: 2, + // 0x8D94 /* GL_RED_INTEGER */ - 0x1902: 1, + 29846: 3, + 29847: 4 + }; + return colorChannels[format - 6402] || 1; +}; + +var heapObjectForWebGLType = type => { + // Micro-optimization for size: Subtract lowest GL enum number (0x1400/* GL_BYTE */) from type to compare + // smaller values for the heap, for shorter generated code size. + // Also the type HEAPU16 is not tested for explicitly, but any unrecognized type will return out HEAPU16. + // (since most types are HEAPU16) + type -= 5120; + if (type == 0) return HEAP8; + if (type == 1) return HEAPU8; + if (type == 2) return HEAP16; + if (type == 4) return HEAP32; + if (type == 6) return HEAPF32; + if (type == 5 || type == 28922 || type == 28520 || type == 30779 || type == 30782) return HEAPU32; + return HEAPU16; +}; + +var toTypedArrayIndex = (pointer, heap) => pointer >>> (31 - Math.clz32(heap.BYTES_PER_ELEMENT)); + +var emscriptenWebGLGetTexPixelData = (type, format, width, height, pixels, internalFormat) => { + var heap = heapObjectForWebGLType(type); + var sizePerPixel = colorChannelsInGlTextureFormat(format) * heap.BYTES_PER_ELEMENT; + var bytes = computeUnpackAlignedImageSize(width, height, sizePerPixel); + return heap.subarray(toTypedArrayIndex(pixels, heap), toTypedArrayIndex(pixels + bytes, heap)); +}; + +var _emscripten_glReadPixels = (x, y, width, height, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelPackBufferBinding) { + GLctx.readPixels(x, y, width, height, format, type, pixels); + return; + } + var heap = heapObjectForWebGLType(type); + var target = toTypedArrayIndex(pixels, heap); + GLctx.readPixels(x, y, width, height, format, type, heap, target); + return; + } + var pixelData = emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, format); + if (!pixelData) { + GL.recordError(1280); + return; + } + GLctx.readPixels(x, y, width, height, format, type, pixelData); +}; + +var _glReadPixels = _emscripten_glReadPixels; + +var _emscripten_glShaderSource = (shader, count, string, length) => { + var source = GL.getSource(shader, count, string, length); + GLctx.shaderSource(GL.shaders[shader], source); +}; + +var _glShaderSource = _emscripten_glShaderSource; + +var _emscripten_glTexImage2D = (target, level, internalFormat, width, height, border, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixels); + return; + } + if (pixels) { + var heap = heapObjectForWebGLType(type); + var index = toTypedArrayIndex(pixels, heap); + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, heap, index); + return; + } + } + var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, internalFormat) : null; + GLctx.texImage2D(target, level, internalFormat, width, height, border, format, type, pixelData); +}; + +var _glTexImage2D = _emscripten_glTexImage2D; + +var _emscripten_glTexParameterf = (x0, x1, x2) => GLctx.texParameterf(x0, x1, x2); + +var _glTexParameterf = _emscripten_glTexParameterf; + +var _emscripten_glTexParameterfv = (target, pname, params) => { + var param = HEAPF32[((params) >> 2)]; + GLctx.texParameterf(target, pname, param); +}; + +var _glTexParameterfv = _emscripten_glTexParameterfv; + +var _emscripten_glTexParameteri = (x0, x1, x2) => GLctx.texParameteri(x0, x1, x2); + +var _glTexParameteri = _emscripten_glTexParameteri; + +var _emscripten_glTexStorage2D = (x0, x1, x2, x3, x4) => GLctx.texStorage2D(x0, x1, x2, x3, x4); + +var _glTexStorage2D = _emscripten_glTexStorage2D; + +var _emscripten_glTexStorage3D = (x0, x1, x2, x3, x4, x5) => GLctx.texStorage3D(x0, x1, x2, x3, x4, x5); + +var _glTexStorage3D = _emscripten_glTexStorage3D; + +var _emscripten_glTexSubImage2D = (target, level, xoffset, yoffset, width, height, format, type, pixels) => { + if (GL.currentContext.version >= 2) { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + return; + } + if (pixels) { + var heap = heapObjectForWebGLType(type); + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, heap, toTypedArrayIndex(pixels, heap)); + return; + } + } + var pixelData = pixels ? emscriptenWebGLGetTexPixelData(type, format, width, height, pixels, 0) : null; + GLctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixelData); +}; + +var _glTexSubImage2D = _emscripten_glTexSubImage2D; + +var _emscripten_glTexSubImage3D = (target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels) => { + if (GLctx.currentPixelUnpackBufferBinding) { + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels); + } else if (pixels) { + var heap = heapObjectForWebGLType(type); + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, heap, toTypedArrayIndex(pixels, heap)); + } else { + GLctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, null); + } +}; + +var _glTexSubImage3D = _emscripten_glTexSubImage3D; + +var webglGetUniformLocation = location => { + var p = GLctx.currentProgram; + if (p) { + var webglLoc = p.uniformLocsById[location]; + // p.uniformLocsById[location] stores either an integer, or a + // WebGLUniformLocation. + // If an integer, we have not yet bound the location, so do it now. The + // integer value specifies the array index we should bind to. + if (typeof webglLoc == "number") { + p.uniformLocsById[location] = webglLoc = GLctx.getUniformLocation(p, p.uniformArrayNamesById[location] + (webglLoc > 0 ? `[${webglLoc}]` : "")); + } + // Else an already cached WebGLUniformLocation, return it. + return webglLoc; + } else { + GL.recordError(1282); + } +}; + +var _emscripten_glUniform1f = (location, v0) => { + GLctx.uniform1f(webglGetUniformLocation(location), v0); +}; + +var _glUniform1f = _emscripten_glUniform1f; + +var miniTempWebGLFloatBuffers = []; + +var _emscripten_glUniform1fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform1fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count); + return; + } + if (count <= 288) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; ++i) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 4) >> 2)); + } + GLctx.uniform1fv(webglGetUniformLocation(location), view); +}; + +var _glUniform1fv = _emscripten_glUniform1fv; + +var _emscripten_glUniform1i = (location, v0) => { + GLctx.uniform1i(webglGetUniformLocation(location), v0); +}; + +var _glUniform1i = _emscripten_glUniform1i; + +var _emscripten_glUniform2f = (location, v0, v1) => { + GLctx.uniform2f(webglGetUniformLocation(location), v0, v1); +}; + +var _glUniform2f = _emscripten_glUniform2f; + +var _emscripten_glUniform2fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform2fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count * 2); + return; + } + if (count <= 144) { + // avoid allocation when uploading few enough uniforms + count *= 2; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 2) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 8) >> 2)); + } + GLctx.uniform2fv(webglGetUniformLocation(location), view); +}; + +var _glUniform2fv = _emscripten_glUniform2fv; + +var _emscripten_glUniform3f = (location, v0, v1, v2) => { + GLctx.uniform3f(webglGetUniformLocation(location), v0, v1, v2); +}; + +var _glUniform3f = _emscripten_glUniform3f; + +var _emscripten_glUniform4f = (location, v0, v1, v2, v3) => { + GLctx.uniform4f(webglGetUniformLocation(location), v0, v1, v2, v3); +}; + +var _glUniform4f = _emscripten_glUniform4f; + +var _emscripten_glUniform4fv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform4fv(webglGetUniformLocation(location), HEAPF32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[4 * count]; + // hoist the heap out of the loop for size and for pthreads+growth. + var heap = HEAPF32; + value = ((value) >> 2); + count *= 4; + for (var i = 0; i < count; i += 4) { + var dst = value + i; + view[i] = heap[dst]; + view[i + 1] = heap[dst + 1]; + view[i + 2] = heap[dst + 2]; + view[i + 3] = heap[dst + 3]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniform4fv(webglGetUniformLocation(location), view); +}; + +var _glUniform4fv = _emscripten_glUniform4fv; + +var miniTempWebGLIntBuffers = []; + +var _emscripten_glUniform4iv = (location, count, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniform4iv(webglGetUniformLocation(location), HEAP32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + count *= 4; + var view = miniTempWebGLIntBuffers[count]; + for (var i = 0; i < count; i += 4) { + view[i] = HEAP32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAP32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAP32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAP32[(((value) + (4 * i + 12)) >> 2)]; + } + } else { + var view = HEAP32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniform4iv(webglGetUniformLocation(location), view); +}; + +var _glUniform4iv = _emscripten_glUniform4iv; + +var _emscripten_glUniformBlockBinding = (program, uniformBlockIndex, uniformBlockBinding) => { + program = GL.programs[program]; + GLctx.uniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); +}; + +var _glUniformBlockBinding = _emscripten_glUniformBlockBinding; + +var _emscripten_glUniformMatrix2fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 4); + return; + } + if (count <= 72) { + // avoid allocation when uploading few enough uniforms + count *= 4; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 4) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAPF32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAPF32[(((value) + (4 * i + 12)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 16) >> 2)); + } + GLctx.uniformMatrix2fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix2fv = _emscripten_glUniformMatrix2fv; + +var _emscripten_glUniformMatrix3fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 9); + return; + } + if (count <= 32) { + // avoid allocation when uploading few enough uniforms + count *= 9; + var view = miniTempWebGLFloatBuffers[count]; + for (var i = 0; i < count; i += 9) { + view[i] = HEAPF32[(((value) + (4 * i)) >> 2)]; + view[i + 1] = HEAPF32[(((value) + (4 * i + 4)) >> 2)]; + view[i + 2] = HEAPF32[(((value) + (4 * i + 8)) >> 2)]; + view[i + 3] = HEAPF32[(((value) + (4 * i + 12)) >> 2)]; + view[i + 4] = HEAPF32[(((value) + (4 * i + 16)) >> 2)]; + view[i + 5] = HEAPF32[(((value) + (4 * i + 20)) >> 2)]; + view[i + 6] = HEAPF32[(((value) + (4 * i + 24)) >> 2)]; + view[i + 7] = HEAPF32[(((value) + (4 * i + 28)) >> 2)]; + view[i + 8] = HEAPF32[(((value) + (4 * i + 32)) >> 2)]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 36) >> 2)); + } + GLctx.uniformMatrix3fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix3fv = _emscripten_glUniformMatrix3fv; + +var _emscripten_glUniformMatrix4fv = (location, count, transpose, value) => { + if (GL.currentContext.version >= 2) { + count && GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, HEAPF32, ((value) >> 2), count * 16); + return; + } + if (count <= 18) { + // avoid allocation when uploading few enough uniforms + var view = miniTempWebGLFloatBuffers[16 * count]; + // hoist the heap out of the loop for size and for pthreads+growth. + var heap = HEAPF32; + value = ((value) >> 2); + count *= 16; + for (var i = 0; i < count; i += 16) { + var dst = value + i; + view[i] = heap[dst]; + view[i + 1] = heap[dst + 1]; + view[i + 2] = heap[dst + 2]; + view[i + 3] = heap[dst + 3]; + view[i + 4] = heap[dst + 4]; + view[i + 5] = heap[dst + 5]; + view[i + 6] = heap[dst + 6]; + view[i + 7] = heap[dst + 7]; + view[i + 8] = heap[dst + 8]; + view[i + 9] = heap[dst + 9]; + view[i + 10] = heap[dst + 10]; + view[i + 11] = heap[dst + 11]; + view[i + 12] = heap[dst + 12]; + view[i + 13] = heap[dst + 13]; + view[i + 14] = heap[dst + 14]; + view[i + 15] = heap[dst + 15]; + } + } else { + var view = HEAPF32.subarray((((value) >> 2)), ((value + count * 64) >> 2)); + } + GLctx.uniformMatrix4fv(webglGetUniformLocation(location), !!transpose, view); +}; + +var _glUniformMatrix4fv = _emscripten_glUniformMatrix4fv; + +var _emscripten_glUseProgram = program => { + program = GL.programs[program]; + GLctx.useProgram(program); + // Record the currently active program so that we can access the uniform + // mapping table of that program. + GLctx.currentProgram = program; +}; + +var _glUseProgram = _emscripten_glUseProgram; + +var _emscripten_glVertexAttribPointer = (index, size, type, normalized, stride, ptr) => { + var cb = GL.currentContext.clientBuffers[index]; + if (!GLctx.currentArrayBufferBinding) { + cb.size = size; + cb.type = type; + cb.normalized = normalized; + cb.stride = stride; + cb.ptr = ptr; + cb.clientside = true; + cb.vertexAttribPointerAdaptor = function(index, size, type, normalized, stride, ptr) { + this.vertexAttribPointer(index, size, type, normalized, stride, ptr); + }; + return; + } + cb.clientside = false; + GLctx.vertexAttribPointer(index, size, type, !!normalized, stride, ptr); +}; + +var _glVertexAttribPointer = _emscripten_glVertexAttribPointer; + +var _emscripten_glViewport = (x0, x1, x2, x3) => GLctx.viewport(x0, x1, x2, x3); + +var _glViewport = _emscripten_glViewport; + +function _mediapipe_find_canvas_event_target(canvasSelector) { + let target = findCanvasEventTarget(canvasSelector); + // WebGPU-on-worker uses this function to try to grab the canvas, but + // doesn't have a DOM element to find. So as a quick patch, if the default + // behavior is unsuccessful here then we try a webgpu canvas property + // which is set by the user directly on the Module, much like how our old + // pipeline used the Module.canvas property. See b/265271517 for details. + if (Module && !target) { + target = Module.canvasWebGpu; + } + return Emval.toHandle(target); +} + +function _mediapipe_webgl_tex_image_drawable(drawableHandle) { + const drawable = Emval.toValue(drawableHandle); + GLctx.texImage2D(GLctx.TEXTURE_2D, 0, GLctx.RGBA, GLctx.RGBA, GLctx.UNSIGNED_BYTE, drawable); +} + +var _random_get = (buffer, size) => randomFill(HEAPU8.subarray(buffer, buffer + size)); + +var _wgpuCommandEncoderBeginComputePass = (encoderPtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "timestampWrites": WebGPU.makePassTimestampWrites(HEAPU32[(((descriptor) + (12)) >> 2)]) + }; + } + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateComputePassEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.beginComputePass(desc)); + return ptr; +}; + +var _wgpuCommandEncoderBeginRenderPass = (encoderPtr, descriptor) => { + function makeColorAttachment(caPtr) { + var viewPtr = HEAPU32[(((caPtr) + (4)) >> 2)]; + if (viewPtr === 0) { + // Null `view` means no attachment in this slot. + return undefined; + } + var depthSlice = HEAPU32[(((caPtr) + (8)) >> 2)]; + if (depthSlice == 4294967295) depthSlice = undefined; + return { + "view": WebGPU.getJsObject(viewPtr), + "depthSlice": depthSlice, + "resolveTarget": WebGPU.getJsObject(HEAPU32[(((caPtr) + (12)) >> 2)]), + "clearValue": WebGPU.makeColor(caPtr + 24), + "loadOp": WebGPU.LoadOp[HEAP32[(((caPtr) + (16)) >> 2)]], + "storeOp": WebGPU.StoreOp[HEAP32[(((caPtr) + (20)) >> 2)]] + }; + } + function makeColorAttachments(count, caPtr) { + var attachments = []; + for (var i = 0; i < count; ++i) { + attachments.push(makeColorAttachment(caPtr + 56 * i)); + } + return attachments; + } + function makeDepthStencilAttachment(dsaPtr) { + if (dsaPtr === 0) return undefined; + return { + "view": WebGPU.getJsObject(HEAPU32[(((dsaPtr) + (4)) >> 2)]), + "depthClearValue": HEAPF32[(((dsaPtr) + (16)) >> 2)], + "depthLoadOp": WebGPU.LoadOp[HEAP32[(((dsaPtr) + (8)) >> 2)]], + "depthStoreOp": WebGPU.StoreOp[HEAP32[(((dsaPtr) + (12)) >> 2)]], + "depthReadOnly": !!(HEAPU32[(((dsaPtr) + (20)) >> 2)]), + "stencilClearValue": HEAPU32[(((dsaPtr) + (32)) >> 2)], + "stencilLoadOp": WebGPU.LoadOp[HEAP32[(((dsaPtr) + (24)) >> 2)]], + "stencilStoreOp": WebGPU.StoreOp[HEAP32[(((dsaPtr) + (28)) >> 2)]], + "stencilReadOnly": !!(HEAPU32[(((dsaPtr) + (36)) >> 2)]) + }; + } + function makeRenderPassDescriptor(descriptor) { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var maxDrawCount = undefined; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var renderPassMaxDrawCount = nextInChainPtr; + // Note: The user could have passed a really huge value here, which is technically valid in + // C but will not be allowed by WebGPU in JS because of [EnforceRange]. We intentionally + // ignore that case because it's not useful - apps can just pick a smaller maxDrawCount. + maxDrawCount = readI53FromI64((renderPassMaxDrawCount) + (8)); + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "colorAttachments": makeColorAttachments(HEAPU32[(((descriptor) + (12)) >> 2)], HEAPU32[(((descriptor) + (16)) >> 2)]), + "depthStencilAttachment": makeDepthStencilAttachment(HEAPU32[(((descriptor) + (20)) >> 2)]), + "occlusionQuerySet": WebGPU.getJsObject(HEAPU32[(((descriptor) + (24)) >> 2)]), + "timestampWrites": WebGPU.makePassTimestampWrites(HEAPU32[(((descriptor) + (28)) >> 2)]), + "maxDrawCount": maxDrawCount + }; + return desc; + } + var desc = makeRenderPassDescriptor(descriptor); + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateRenderPassEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.beginRenderPass(desc)); + return ptr; +}; + +var _wgpuCommandEncoderCopyBufferToTexture = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyBufferToTexture(WebGPU.makeTexelCopyBufferInfo(srcPtr), WebGPU.makeTexelCopyTextureInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderCopyTextureToBuffer = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyTextureToBuffer(WebGPU.makeTexelCopyTextureInfo(srcPtr), WebGPU.makeTexelCopyBufferInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderCopyTextureToTexture = (encoderPtr, srcPtr, dstPtr, copySizePtr) => { + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var copySize = WebGPU.makeExtent3D(copySizePtr); + commandEncoder.copyTextureToTexture(WebGPU.makeTexelCopyTextureInfo(srcPtr), WebGPU.makeTexelCopyTextureInfo(dstPtr), copySize); +}; + +var _wgpuCommandEncoderFinish = (encoderPtr, descriptor) => { + // TODO: Use the descriptor. + var commandEncoder = WebGPU.getJsObject(encoderPtr); + var ptr = _emwgpuCreateCommandBuffer(0); + WebGPU.Internals.jsObjectInsert(ptr, commandEncoder.finish()); + return ptr; +}; + +var _wgpuComputePassEncoderDispatchWorkgroups = (passPtr, x, y, z) => { + var pass = WebGPU.getJsObject(passPtr); + pass.dispatchWorkgroups(x, y, z); +}; + +var _wgpuComputePassEncoderEnd = passPtr => { + var pass = WebGPU.getJsObject(passPtr); + pass.end(); +}; + +var _wgpuComputePassEncoderSetBindGroup = (passPtr, groupIndex, groupPtr, dynamicOffsetCount, dynamicOffsetsPtr) => { + var pass = WebGPU.getJsObject(passPtr); + var group = WebGPU.getJsObject(groupPtr); + if (dynamicOffsetCount == 0) { + pass.setBindGroup(groupIndex, group); + } else { + pass.setBindGroup(groupIndex, group, HEAPU32, ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount); + } +}; + +var _wgpuComputePassEncoderSetPipeline = (passPtr, pipelinePtr) => { + var pass = WebGPU.getJsObject(passPtr); + var pipeline = WebGPU.getJsObject(pipelinePtr); + pass.setPipeline(pipeline); +}; + +var _wgpuComputePipelineGetBindGroupLayout = (pipelinePtr, groupIndex) => { + var pipeline = WebGPU.getJsObject(pipelinePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, pipeline.getBindGroupLayout(groupIndex)); + return ptr; +}; + +var _wgpuDeviceCreateBindGroup = (devicePtr, descriptor) => { + function makeEntry(entryPtr) { + var bufferPtr = HEAPU32[(((entryPtr) + (8)) >> 2)]; + var samplerPtr = HEAPU32[(((entryPtr) + (32)) >> 2)]; + var textureViewPtr = HEAPU32[(((entryPtr) + (36)) >> 2)]; + var externalTexturePtr = 0; + WebGPU.iterateExtensions(entryPtr, { + 14: ptr => { + externalTexturePtr = HEAPU32[(((ptr) + (8)) >> 2)]; + } + }); + var resource; + if (bufferPtr) { + // Note the sentinel UINT64_MAX will be read as -1. + var size = readI53FromI64((entryPtr) + (24)); + if (size == -1) size = undefined; + resource = { + "buffer": WebGPU.getJsObject(bufferPtr), + "offset": readI53FromI64((entryPtr) + (16)), + "size": size + }; + } else { + resource = WebGPU.getJsObject(samplerPtr || textureViewPtr || externalTexturePtr); + } + return { + "binding": HEAPU32[(((entryPtr) + (4)) >> 2)], + "resource": resource + }; + } + function makeEntries(count, entriesPtrs) { + var entries = []; + for (var i = 0; i < count; ++i) { + entries.push(makeEntry(entriesPtrs + 40 * i)); + } + return entries; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "layout": WebGPU.getJsObject(HEAPU32[(((descriptor) + (12)) >> 2)]), + "entries": makeEntries(HEAPU32[(((descriptor) + (16)) >> 2)], HEAPU32[(((descriptor) + (20)) >> 2)]) + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateBindGroup(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createBindGroup(desc)); + return ptr; +}; + +var _wgpuDeviceCreateBindGroupLayout = (devicePtr, descriptor) => { + function makeBufferEntry(substructPtr) { + var typeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!typeInt) return undefined; + return { + "type": WebGPU.BufferBindingType[typeInt], + "hasDynamicOffset": !!(HEAPU32[(((substructPtr) + (8)) >> 2)]), + "minBindingSize": readI53FromI64((substructPtr) + (16)) + }; + } + function makeSamplerEntry(substructPtr) { + var typeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!typeInt) return undefined; + return { + "type": WebGPU.SamplerBindingType[typeInt] + }; + } + function makeTextureEntry(substructPtr) { + var sampleTypeInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!sampleTypeInt) return undefined; + return { + "sampleType": WebGPU.TextureSampleType[sampleTypeInt], + "viewDimension": WebGPU.TextureViewDimension[HEAP32[(((substructPtr) + (8)) >> 2)]], + "multisampled": !!(HEAPU32[(((substructPtr) + (12)) >> 2)]) + }; + } + function makeStorageTextureEntry(substructPtr) { + var accessInt = HEAPU32[(((substructPtr) + (4)) >> 2)]; + if (!accessInt) return undefined; + return { + "access": WebGPU.StorageTextureAccess[accessInt], + "format": WebGPU.TextureFormat[HEAP32[(((substructPtr) + (8)) >> 2)]], + "viewDimension": WebGPU.TextureViewDimension[HEAP32[(((substructPtr) + (12)) >> 2)]] + }; + } + function makeEntry(entryPtr) { + var entry = { + "binding": HEAPU32[(((entryPtr) + (4)) >> 2)], + "visibility": HEAPU32[(((entryPtr) + (8)) >> 2)], + "buffer": makeBufferEntry(entryPtr + 24), + "sampler": makeSamplerEntry(entryPtr + 48), + "texture": makeTextureEntry(entryPtr + 56), + "storageTexture": makeStorageTextureEntry(entryPtr + 72) + }; + WebGPU.iterateExtensions(entryPtr, { + 13: ptr => { + entry["externalTexture"] = {}; + } + }); + return entry; + } + function makeEntries(count, entriesPtrs) { + var entries = []; + for (var i = 0; i < count; ++i) { + entries.push(makeEntry(entriesPtrs + 88 * i)); + } + return entries; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "entries": makeEntries(HEAPU32[(((descriptor) + (12)) >> 2)], HEAPU32[(((descriptor) + (16)) >> 2)]) + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createBindGroupLayout(desc)); + return ptr; +}; + +var _wgpuDeviceCreateCommandEncoder = (devicePtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4) + }; + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateCommandEncoder(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createCommandEncoder(desc)); + return ptr; +}; + +var _wgpuDeviceCreateComputePipeline = (devicePtr, descriptor) => { + var desc = WebGPU.makeComputePipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateComputePipeline(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createComputePipeline(desc)); + return ptr; +}; + +var _wgpuDeviceCreatePipelineLayout = (devicePtr, descriptor) => { + var bglCount = HEAPU32[(((descriptor) + (12)) >> 2)]; + var bglPtr = HEAPU32[(((descriptor) + (16)) >> 2)]; + var bgls = []; + for (var i = 0; i < bglCount; ++i) { + bgls.push(WebGPU.getJsObject(HEAPU32[(((bglPtr) + (4 * i)) >> 2)])); + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "bindGroupLayouts": bgls + }; + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreatePipelineLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createPipelineLayout(desc)); + return ptr; +}; + +var _wgpuDeviceCreateRenderPipeline = (devicePtr, descriptor) => { + var desc = WebGPU.makeRenderPipelineDesc(descriptor); + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateRenderPipeline(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createRenderPipeline(desc)); + return ptr; +}; + +var _wgpuDeviceCreateSampler = (devicePtr, descriptor) => { + var desc; + if (descriptor) { + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "addressModeU": WebGPU.AddressMode[HEAP32[(((descriptor) + (12)) >> 2)]], + "addressModeV": WebGPU.AddressMode[HEAP32[(((descriptor) + (16)) >> 2)]], + "addressModeW": WebGPU.AddressMode[HEAP32[(((descriptor) + (20)) >> 2)]], + "magFilter": WebGPU.FilterMode[HEAP32[(((descriptor) + (24)) >> 2)]], + "minFilter": WebGPU.FilterMode[HEAP32[(((descriptor) + (28)) >> 2)]], + "mipmapFilter": WebGPU.MipmapFilterMode[HEAP32[(((descriptor) + (32)) >> 2)]], + "lodMinClamp": HEAPF32[(((descriptor) + (36)) >> 2)], + "lodMaxClamp": HEAPF32[(((descriptor) + (40)) >> 2)], + "compare": WebGPU.CompareFunction[HEAP32[(((descriptor) + (44)) >> 2)]], + "maxAnisotropy": HEAPU16[(((descriptor) + (48)) >> 1)] + }; + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateSampler(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createSampler(desc)); + return ptr; +}; + +var _wgpuDeviceCreateTexture = (devicePtr, descriptor) => { + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + var textureBindingViewDimension; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var textureBindingViewDimensionDescriptor = nextInChainPtr; + textureBindingViewDimension = WebGPU.TextureViewDimension[HEAP32[(((textureBindingViewDimensionDescriptor) + (8)) >> 2)]]; + } + var desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "size": WebGPU.makeExtent3D(descriptor + 28), + "mipLevelCount": HEAPU32[(((descriptor) + (44)) >> 2)], + "sampleCount": HEAPU32[(((descriptor) + (48)) >> 2)], + "dimension": WebGPU.TextureDimension[HEAP32[(((descriptor) + (24)) >> 2)]], + "format": WebGPU.TextureFormat[HEAP32[(((descriptor) + (40)) >> 2)]], + "usage": HEAPU32[(((descriptor) + (16)) >> 2)], + "textureBindingViewDimension": textureBindingViewDimension + }; + var viewFormatCount = HEAPU32[(((descriptor) + (52)) >> 2)]; + if (viewFormatCount) { + var viewFormatsPtr = HEAPU32[(((descriptor) + (56)) >> 2)]; + // viewFormatsPtr pointer to an array of TextureFormat which is an enum of size uint32_t + desc["viewFormats"] = Array.from(HEAP32.subarray((((viewFormatsPtr) >> 2)), ((viewFormatsPtr + viewFormatCount * 4) >> 2)), format => WebGPU.TextureFormat[format]); + } + var device = WebGPU.getJsObject(devicePtr); + var ptr = _emwgpuCreateTexture(0); + WebGPU.Internals.jsObjectInsert(ptr, device.createTexture(desc)); + return ptr; +}; + +var _wgpuQueueSubmit = (queuePtr, commandCount, commands) => { + var queue = WebGPU.getJsObject(queuePtr); + var cmds = Array.from(HEAP32.subarray((((commands) >> 2)), ((commands + commandCount * 4) >> 2)), id => WebGPU.getJsObject(id)); + queue.submit(cmds); +}; + +function _wgpuQueueWriteBuffer(queuePtr, bufferPtr, bufferOffset_low, bufferOffset_high, data, size) { + var bufferOffset = convertI32PairToI53Checked(bufferOffset_low, bufferOffset_high); + var queue = WebGPU.getJsObject(queuePtr); + var buffer = WebGPU.getJsObject(bufferPtr); + // There is a size limitation for ArrayBufferView. Work around by passing in a subarray + // instead of the whole heap. crbug.com/1201109 + var subarray = HEAPU8.subarray(data, data + size); + queue.writeBuffer(buffer, bufferOffset, subarray, 0, size); +} + +var _wgpuQueueWriteTexture = (queuePtr, destinationPtr, data, dataSize, dataLayoutPtr, writeSizePtr) => { + var queue = WebGPU.getJsObject(queuePtr); + var destination = WebGPU.makeTexelCopyTextureInfo(destinationPtr); + var dataLayout = WebGPU.makeTexelCopyBufferLayout(dataLayoutPtr); + var writeSize = WebGPU.makeExtent3D(writeSizePtr); + // This subarray isn't strictly necessary, but helps work around an issue + // where Chromium makes a copy of the entire heap. crbug.com/1134457 + var subarray = HEAPU8.subarray(data, data + dataSize); + queue.writeTexture(destination, subarray, dataLayout, writeSize); +}; + +var _wgpuRenderPassEncoderDraw = (passPtr, vertexCount, instanceCount, firstVertex, firstInstance) => { + firstVertex >>>= 0; + firstInstance >>>= 0; + var pass = WebGPU.getJsObject(passPtr); + pass.draw(vertexCount, instanceCount, firstVertex, firstInstance); +}; + +var _wgpuRenderPassEncoderEnd = encoderPtr => { + var encoder = WebGPU.getJsObject(encoderPtr); + encoder.end(); +}; + +var _wgpuRenderPassEncoderSetBindGroup = (passPtr, groupIndex, groupPtr, dynamicOffsetCount, dynamicOffsetsPtr) => { + var pass = WebGPU.getJsObject(passPtr); + var group = WebGPU.getJsObject(groupPtr); + if (dynamicOffsetCount == 0) { + pass.setBindGroup(groupIndex, group); + } else { + pass.setBindGroup(groupIndex, group, HEAPU32, ((dynamicOffsetsPtr) >> 2), dynamicOffsetCount); + } +}; + +var _wgpuRenderPassEncoderSetPipeline = (passPtr, pipelinePtr) => { + var pass = WebGPU.getJsObject(passPtr); + var pipeline = WebGPU.getJsObject(pipelinePtr); + pass.setPipeline(pipeline); +}; + +var _wgpuRenderPipelineGetBindGroupLayout = (pipelinePtr, groupIndex) => { + var pipeline = WebGPU.getJsObject(pipelinePtr); + var ptr = _emwgpuCreateBindGroupLayout(0); + WebGPU.Internals.jsObjectInsert(ptr, pipeline.getBindGroupLayout(groupIndex)); + return ptr; +}; + +var _wgpuTextureCreateView = (texturePtr, descriptor) => { + var desc; + if (descriptor) { + var swizzle; + var nextInChainPtr = HEAPU32[((descriptor) >> 2)]; + if (nextInChainPtr !== 0) { + var sType = HEAP32[(((nextInChainPtr) + (4)) >> 2)]; + var swizzleDescriptor = nextInChainPtr; + var swizzlePtr = swizzleDescriptor + 8; + var r = WebGPU.ComponentSwizzle[HEAP32[((swizzlePtr) >> 2)]] || "r"; + var g = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (4)) >> 2)]] || "g"; + var b = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (8)) >> 2)]] || "b"; + var a = WebGPU.ComponentSwizzle[HEAP32[(((swizzlePtr) + (12)) >> 2)]] || "a"; + swizzle = `${r}${g}${b}${a}`; + } + var mipLevelCount = HEAPU32[(((descriptor) + (24)) >> 2)]; + var arrayLayerCount = HEAPU32[(((descriptor) + (32)) >> 2)]; + desc = { + "label": WebGPU.makeStringFromOptionalStringView(descriptor + 4), + "format": WebGPU.TextureFormat[HEAP32[(((descriptor) + (12)) >> 2)]], + "dimension": WebGPU.TextureViewDimension[HEAP32[(((descriptor) + (16)) >> 2)]], + "baseMipLevel": HEAPU32[(((descriptor) + (20)) >> 2)], + "mipLevelCount": mipLevelCount === 4294967295 ? undefined : mipLevelCount, + "baseArrayLayer": HEAPU32[(((descriptor) + (28)) >> 2)], + "arrayLayerCount": arrayLayerCount === 4294967295 ? undefined : arrayLayerCount, + "aspect": WebGPU.TextureAspect[HEAP32[(((descriptor) + (36)) >> 2)]], + "usage": HEAPU32[(((descriptor) + (40)) >> 2)], + "swizzle": swizzle + }; + } + var texture = WebGPU.getJsObject(texturePtr); + var ptr = _emwgpuCreateTextureView(0); + WebGPU.Internals.jsObjectInsert(ptr, texture.createView(desc)); + return ptr; +}; + +var _wgpuTextureDestroy = texturePtr => { + WebGPU.getJsObject(texturePtr).destroy(); +}; + +var _wgpuTextureGetFormat = texturePtr => { + var texture = WebGPU.getJsObject(texturePtr); + // Should return the enum integer instead of string. + return WebGPU.TextureFormat.indexOf(texture.format); +}; + +var getCFunc = ident => { + var func = Module["_" + ident]; + // closure exported function + return func; +}; + +var writeArrayToMemory = (array, buffer) => { + HEAP8.set(array, buffer); +}; + +/** + * @param {string|null=} returnType + * @param {Array=} argTypes + * @param {Array=} args + * @param {Object=} opts + */ var ccall = (ident, returnType, argTypes, args, opts) => { + // For fast lookup of conversion functions + var toC = { + "string": str => { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { + // null string + ret = stringToUTF8OnStack(str); + } + return ret; + }, + "array": arr => { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret; + } + }; + function convertReturnValue(ret) { + if (returnType === "string") { + return UTF8ToString(ret); + } + if (returnType === "boolean") return Boolean(ret); + return ret; + } + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func(...cArgs); + function onDone(ret) { + if (stack !== 0) stackRestore(stack); + return convertReturnValue(ret); + } + ret = onDone(ret); + return ret; +}; + +var FS_createPath = (...args) => FS.createPath(...args); + +var FS_unlink = (...args) => FS.unlink(...args); + +var FS_createLazyFile = (...args) => FS.createLazyFile(...args); + +var FS_createDevice = (...args) => FS.createDevice(...args); + +FS.createPreloadedFile = FS_createPreloadedFile; + +FS.preloadFile = FS_preloadFile; + +FS.staticInit(); + +// Signal GL rendering layer that processing of a new frame is about to +// start. This helps it optimize VBO double-buffering and reduce GPU stalls. +registerPreMainLoop(() => GL.newRenderingFrameStarted()); + +for (let i = 0; i < 32; ++i) tempFixedLengthArray.push(new Array(i)); + +var miniTempWebGLFloatBuffersStorage = new Float32Array(288); + +// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive +for (/**@suppress{duplicate}*/ var i = 0; i <= 288; ++i) { + miniTempWebGLFloatBuffers[i] = miniTempWebGLFloatBuffersStorage.subarray(0, i); +} + +var miniTempWebGLIntBuffersStorage = new Int32Array(288); + +// Create GL_POOL_TEMP_BUFFERS_SIZE+1 temporary buffers, for uploads of size 0 through GL_POOL_TEMP_BUFFERS_SIZE inclusive +for (/**@suppress{duplicate}*/ var i = 0; i <= 288; ++i) { + miniTempWebGLIntBuffers[i] = miniTempWebGLIntBuffersStorage.subarray(0, i); +} + +// End JS library code +// include: postlibrary.js +// This file is included after the automatically-generated JS library code +// but before the wasm module is created. +{ + // Begin ATMODULES hooks + if (Module["preloadPlugins"]) preloadPlugins = Module["preloadPlugins"]; + if (Module["noExitRuntime"]) noExitRuntime = Module["noExitRuntime"]; + if (Module["print"]) out = Module["print"]; + if (Module["printErr"]) err = Module["printErr"]; + if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; + // End ATMODULES hooks + if (Module["arguments"]) arguments_ = Module["arguments"]; + if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; + if (Module["preInit"]) { + if (typeof Module["preInit"] == "function") Module["preInit"] = [ Module["preInit"] ]; + while (Module["preInit"].length > 0) { + Module["preInit"].shift()(); + } + } +} + +// Begin runtime exports +Module["addRunDependency"] = addRunDependency; + +Module["removeRunDependency"] = removeRunDependency; + +Module["ccall"] = ccall; + +Module["stringToNewUTF8"] = stringToNewUTF8; + +Module["FS_preloadFile"] = FS_preloadFile; + +Module["FS_unlink"] = FS_unlink; + +Module["FS_createPath"] = FS_createPath; + +Module["FS_createDevice"] = FS_createDevice; + +Module["FS_createDataFile"] = FS_createDataFile; + +Module["FS_createLazyFile"] = FS_createLazyFile; + +// End runtime exports +// Begin JS library exports +// End JS library exports +// end include: postlibrary.js +var ASM_CONSTS = { + 1461389: $0 => { + const canvas = Emval.toValue($0); + const context = canvas.getContext("webgpu"); + return WebGPU.importJsTexture(context.getCurrentTexture()); + }, + 1461532: ($0, $1, $2, $3, $4) => { + const drawable = Emval.toValue($0); + const device = WebGPU.getJsObject($1); + const texture = WebGPU.getJsObject($2); + const width = $3; + const height = $4; + device.queue.copyExternalImageToTexture({ + source: drawable + }, { + texture + }, [ width, height ]); + }, + 1461791: ($0, $1, $2, $3) => { + const sourceExtTex = Emval.toValue($0); + const device = WebGPU.getJsObject($1); + const sampler = WebGPU.getJsObject($2); + const bgLayout = WebGPU.getJsObject($3); + const bindGroup = device.createBindGroup({ + layout: bgLayout, + entries: [ { + binding: 0, + resource: sampler + }, { + binding: 1, + resource: sourceExtTex + } ] + }); + return WebGPU.importJsBindGroup(bindGroup); + }, + 1462161: ($0, $1) => { + const input = Emval.toValue($0); + const output = Emval.toValue($1); + const ctx = output.getContext("2d"); + ctx.drawImage(input, 0, 0, output.width, output.height); + }, + 1462326: ($0, $1) => { + const inputArray = Emval.toValue($0); + const output = Emval.toValue($1); + const ctx = output.getContext("2d"); + const image_data = new ImageData(inputArray, output.width, output.height); + ctx.putImageData(image_data, 0, 0); + }, + 1462550: ($0, $1) => { + const input = Emval.toValue($0); + const outputArray = Emval.toValue($1); + const ctx = input.getContext("2d"); + const data = ctx.getImageData(0, 0, input.width, input.height); + outputArray.set(data.data); + }, + 1462754: () => (typeof HTMLCanvasElement !== "undefined"), + 1462809: () => !!Module["preinitializedWebGPUDevice"], + 1462860: () => { + specialHTMLTargets["#canvas"] = Module.canvas; + } +}; + +function BeginGlQueryTiming(calc_name, num_repetitions) { + const gl = Module.canvas.getContext("webgl2"); + const query = gl.createQuery(); + Module.WEBGL_SHADER_CALC_METRICS = Module.WEBGL_SHADER_CALC_METRICS || {}; + Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)] = { + query, + repetitions: num_repetitions + }; + Module.WEBGL_QUERY_TIMER_EXT = Module.WEBGL_QUERY_TIMER_EXT || gl.getExtension("EXT_disjoint_timer_query_webgl2"); + gl.beginQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT, query); +} + +function EndGlQueryTiming(calc_name) { + const gl = Module.canvas.getContext("webgl2"); + gl.endQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT, Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)].query); +} + +function JsWrapImageConverter() { + if (!Module._imageConverter) { + Module._imageConverter = (binaryPtr, binarySize, width, height, numChannels, makeDeepCopy, outputType) => { + const imageData = new outputType(makeDeepCopy ? Module.HEAPU8.slice(binaryPtr, binaryPtr + binarySize).buffer : Module.HEAPU8.buffer, binaryPtr, width * height * numChannels); + return { + data: imageData, + width, + height + }; + }; + } +} + +function JsOnUint8ArrayImageListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Uint8Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, timestamp_ms); +} + +function JsOnFloat32ArrayImageListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Float32Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, timestamp_ms); +} + +function JsOnWebGLTextureListener(output_stream_name, name, width, height, timestamp_ms) { + Module._wrapSimpleListenerOutput(output_stream_name, { + data: GL.textures[name], + width, + height + }, timestamp_ms); +} + +function JsOnUint8ArrayImageVectorListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Uint8Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, false, timestamp_ms); +} + +function JsOnFloat32ArrayImageVectorListener(output_stream_name, binary_ptr, binary_size, width, height, num_channels, make_deep_copy, timestamp_ms) { + const image = Module._imageConverter(binary_ptr, binary_size, width, height, num_channels, make_deep_copy, Float32Array); + Module._wrapSimpleListenerOutput(output_stream_name, image, false, timestamp_ms); +} + +function JsOnWebGLTextureVectorListener(output_stream_name, name, width, height, timestamp_ms) { + Module._wrapSimpleListenerOutput(output_stream_name, { + data: GL.textures[name], + width, + height + }, false, timestamp_ms); +} + +function JsOnEmptyPacketListener(output_stream_name, timestamp) { + Module._wrapEmptyPacketListenerOutput(output_stream_name, timestamp); +} + +function JsOnVectorFinishedListener(output_stream_name, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, undefined, true, timestamp); +} + +function JsOnSimpleListenerBool(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerBool(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerInt(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerInt(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerUint(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerUint(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerDouble(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerDouble(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerFloat(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, timestamp); +} + +function JsOnVectorListenerFloat(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, out_data, false, timestamp); +} + +function JsOnSimpleListenerString(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, UTF8ToString(out_data), timestamp); +} + +function JsOnVectorListenerString(output_stream_name, out_data, timestamp) { + Module._wrapSimpleListenerOutput(output_stream_name, UTF8ToString(out_data), false, timestamp); +} + +function JsOnVectorListenerProto(output_stream_name, proto_ptr, proto_size, make_deep_copy, timestamp) { + const newProtoArray = make_deep_copy ? Module.HEAPU8.slice(proto_ptr, proto_ptr + proto_size) : new Uint8Array(Module.HEAPU8.buffer, proto_ptr, proto_size); + Module._wrapSimpleListenerOutput(output_stream_name, newProtoArray, false, timestamp); +} + +function JsWrapSimpleListeners() { + if (!Module._wrapSimpleListenerOutput) { + Module._wrapSimpleListenerOutput = (outputStreamName, ...args) => { + if (Module.simpleListeners) { + const streamName = UTF8ToString(outputStreamName); + if (Module.simpleListeners[streamName]) { + Module.simpleListeners[streamName](...args); + } + } + }; + } + if (!Module._wrapEmptyPacketListenerOutput) { + Module._wrapEmptyPacketListenerOutput = (outputStreamName, timestamp) => { + if (Module.emptyPacketListeners) { + const streamName = UTF8ToString(outputStreamName); + if (Module.emptyPacketListeners[streamName]) { + Module.emptyPacketListeners[streamName](timestamp); + } + } + }; + } +} + +function JsOnSimpleListenerBinaryArray(output_stream_name, binary_ptr, binary_size, make_deep_copy, timestamp) { + const newProtoArray = make_deep_copy ? Module.HEAPU8.slice(binary_ptr, binary_ptr + binary_size) : new Uint8Array(Module.HEAPU8.buffer, binary_ptr, binary_size); + Module._wrapSimpleListenerOutput(output_stream_name, newProtoArray, timestamp); +} + +function mediapipe_import_external_texture(device_handle, source_handle) { + const device = WebGPU.getJsObject(device_handle); + const source = Emval.toValue(source_handle); + const externalTexture = device.importExternalTexture({ + source + }); + return Emval.toHandle(externalTexture); +} + +function mediapipe_create_utility_canvas2d() { + let canvas; + if (typeof HTMLCanvasElement !== "undefined") { + canvas = document.createElement("canvas"); + canvas.style.display = "none"; + } else { + canvas = new OffscreenCanvas(0, 0); + } + return Emval.toHandle(canvas); +} + +function GetAdapterArchitecture() { + const device = Module["preinitializedWebGPUDevice"]; + const architecture = device.adapterInfo ? device.adapterInfo.architecture : "Unknown"; + return stringToNewUTF8(architecture); +} + +function GetAdapterDescription() { + const device = Module["preinitializedWebGPUDevice"]; + const description = device.adapterInfo ? device.adapterInfo.description : "Unknown"; + return stringToNewUTF8(description); +} + +function GetAdapterDeviceName() { + const device = Module["preinitializedWebGPUDevice"]; + const deviceName = device.adapterInfo ? device.adapterInfo.device : "Unknown"; + return stringToNewUTF8(deviceName); +} + +function GetAdapterVendor() { + const device = Module["preinitializedWebGPUDevice"]; + const vendor = device.adapterInfo ? device.adapterInfo.vendor : "Unknown"; + return stringToNewUTF8(vendor); +} + +function __asyncjs__mediapipe_map_buffer_jspi(buffer_handle, data) { + return Asyncify.handleAsync(async () => { + const buffer = WebGPU.getJsObject(buffer_handle); + if ("mapSync" in buffer) { + buffer.mapSync(GPUMapMode.READ); + } else { + await buffer.mapAsync(GPUMapMode.READ); + } + const mapped = buffer.getMappedRange(); + HEAPU8.set(new Uint8Array(mapped), data); + buffer.unmap(); + }); +} + +function JsWrapErrorListener(code, message) { + if (Module.errorListener) { + const stringMessage = UTF8ToString(message); + Module.errorListener(code, stringMessage); + } +} + +function UseBottomLeftGpuOrigin() { + return (Module && Module.gpuOriginForWebTexturesIsBottomLeft); +} + +function custom_emscripten_dbgn(str, len) { + if (typeof (dbg) !== "undefined") { + dbg(UTF8ToString(str, len)); + } else { + if (typeof (custom_dbg) === "undefined") { + function custom_dbg(text) { + console.warn.apply(console, arguments); + } + } + custom_dbg(UTF8ToString(str, len)); + } +} + +// Imports from the Wasm binary. +var _free, _malloc, _wgpuDeviceAddRef, _addBoundTextureAsImageToStream, _attachImageListener, _attachImageVectorListener, _registerModelResourcesGraphService, _bindTextureToStream, _addBoundTextureToStream, _addDoubleToInputStream, _addFloatToInputStream, _addBoolToInputStream, _addIntToInputStream, _addUintToInputStream, _addStringToInputStream, _addRawDataSpanToInputStream, _allocateBoolVector, _allocateFloatVector, _allocateDoubleVector, _allocateIntVector, _allocateUintVector, _allocateStringVector, _addBoolVectorEntry, _addFloatVectorEntry, _addDoubleVectorEntry, _addIntVectorEntry, _addUintVectorEntry, _addStringVectorEntry, _addBoolVectorToInputStream, _addFloatVectorToInputStream, _addDoubleVectorToInputStream, _addIntVectorToInputStream, _addUintVectorToInputStream, _addStringVectorToInputStream, _addFlatHashMapToInputStream, _addProtoToInputStream, _addEmptyPacketToInputStream, _addBoolToInputSidePacket, _addDoubleToInputSidePacket, _addFloatToInputSidePacket, _addIntToInputSidePacket, _addUintToInputSidePacket, _addStringToInputSidePacket, _addRawDataSpanToInputSidePacket, _addProtoToInputSidePacket, _addBoolVectorToInputSidePacket, _addDoubleVectorToInputSidePacket, _addFloatVectorToInputSidePacket, _addIntVectorToInputSidePacket, _addUintVectorToInputSidePacket, _addStringVectorToInputSidePacket, _attachBoolListener, _attachBoolVectorListener, _attachDoubleListener, _attachDoubleVectorListener, _attachFloatListener, _attachFloatVectorListener, _attachIntListener, _attachIntVectorListener, _attachUintListener, _attachUintVectorListener, _attachStringListener, _attachStringVectorListener, _attachProtoListener, _attachProtoVectorListener, _getGraphConfig, ___getTypeName, _emwgpuCreateBindGroup, _emwgpuCreateBindGroupLayout, _emwgpuCreateCommandBuffer, _emwgpuCreateCommandEncoder, _emwgpuCreateComputePassEncoder, _emwgpuCreateComputePipeline, _emwgpuCreateExternalTexture, _emwgpuCreatePipelineLayout, _emwgpuCreateQuerySet, _emwgpuCreateRenderBundle, _emwgpuCreateRenderBundleEncoder, _emwgpuCreateRenderPassEncoder, _emwgpuCreateRenderPipeline, _emwgpuCreateSampler, _emwgpuCreateSurface, _emwgpuCreateTexture, _emwgpuCreateTextureView, _emwgpuCreateAdapter, _emwgpuImportBuffer, _emwgpuCreateDevice, _emwgpuCreateQueue, _emwgpuCreateShaderModule, _emwgpuOnCreateComputePipelineCompleted, _emwgpuOnCreateRenderPipelineCompleted, _clearSubgraphs, _pushBinarySubgraph, _pushTextSubgraph, _changeBinaryGraph, _changeTextGraph, _processGl, _process, _bindTextureToCanvas, _requestShaderRefreshOnGraphChange, _waitUntilIdle, _closeGraph, _setAutoRenderToScreen, _emscripten_builtin_memalign, _memalign, __emscripten_tempret_set, __emscripten_stack_restore, __emscripten_stack_alloc, _emscripten_stack_get_current, dynCall_ji, dynCall_jii, dynCall_iiiijij, dynCall_viiji, dynCall_viji, dynCall_iiiji, dynCall_jjj, dynCall_iiiijj, dynCall_viijj, dynCall_viiijjj, dynCall_vij, dynCall_viiiji, dynCall_viijii, dynCall_vijjj, dynCall_vj, dynCall_viij, dynCall_jiji, dynCall_iiiiij, dynCall_iiiiijj, dynCall_iiiiiijj, memory, _kVersionStampBuildChangelistStr, _kVersionStampCitcSnapshotStr, _kVersionStampCitcWorkspaceIdStr, _kVersionStampSourceUriStr, _kVersionStampBuildClientStr, _kVersionStampBuildClientMintStatusStr, _kVersionStampBuildCompilerStr, _kVersionStampBuildDateTimePstStr, _kVersionStampBuildDepotPathStr, _kVersionStampBuildIdStr, _kVersionStampBuildInfoStr, _kVersionStampBuildLabelStr, _kVersionStampBuildTargetStr, _kVersionStampBuildTimestampStr, _kVersionStampBuildToolStr, _kVersionStampG3BuildTargetStr, _kVersionStampVerifiableStr, _kVersionStampBuildFdoTypeStr, _kVersionStampBuildBaselineChangelistStr, _kVersionStampBuildLtoTypeStr, _kVersionStampBuildPropellerTypeStr, _kVersionStampBuildPghoTypeStr, _kVersionStampBuildUsernameStr, _kVersionStampBuildHostnameStr, _kVersionStampBuildDirectoryStr, _kVersionStampBuildChangelistInt, _kVersionStampCitcSnapshotInt, _kVersionStampBuildClientMintStatusInt, _kVersionStampBuildTimestampInt, _kVersionStampVerifiableInt, _kVersionStampBuildCoverageEnabledInt, _kVersionStampBuildBaselineChangelistInt, _kVersionStampPrecookedTimestampStr, _kVersionStampPrecookedClientInfoStr, __indirect_function_table, wasmMemory, wasmTable; + +function assignWasmExports(wasmExports) { + _free = Module["_free"] = wasmExports["Sd"]; + _malloc = Module["_malloc"] = wasmExports["Td"]; + _wgpuDeviceAddRef = wasmExports["Ud"]; + _addBoundTextureAsImageToStream = Module["_addBoundTextureAsImageToStream"] = wasmExports["Vd"]; + _attachImageListener = Module["_attachImageListener"] = wasmExports["Wd"]; + _attachImageVectorListener = Module["_attachImageVectorListener"] = wasmExports["Xd"]; + _registerModelResourcesGraphService = Module["_registerModelResourcesGraphService"] = wasmExports["Yd"]; + _bindTextureToStream = Module["_bindTextureToStream"] = wasmExports["Zd"]; + _addBoundTextureToStream = Module["_addBoundTextureToStream"] = wasmExports["_d"]; + _addDoubleToInputStream = Module["_addDoubleToInputStream"] = wasmExports["$d"]; + _addFloatToInputStream = Module["_addFloatToInputStream"] = wasmExports["ae"]; + _addBoolToInputStream = Module["_addBoolToInputStream"] = wasmExports["be"]; + _addIntToInputStream = Module["_addIntToInputStream"] = wasmExports["ce"]; + _addUintToInputStream = Module["_addUintToInputStream"] = wasmExports["de"]; + _addStringToInputStream = Module["_addStringToInputStream"] = wasmExports["ee"]; + _addRawDataSpanToInputStream = Module["_addRawDataSpanToInputStream"] = wasmExports["fe"]; + _allocateBoolVector = Module["_allocateBoolVector"] = wasmExports["ge"]; + _allocateFloatVector = Module["_allocateFloatVector"] = wasmExports["he"]; + _allocateDoubleVector = Module["_allocateDoubleVector"] = wasmExports["ie"]; + _allocateIntVector = Module["_allocateIntVector"] = wasmExports["je"]; + _allocateUintVector = Module["_allocateUintVector"] = wasmExports["ke"]; + _allocateStringVector = Module["_allocateStringVector"] = wasmExports["le"]; + _addBoolVectorEntry = Module["_addBoolVectorEntry"] = wasmExports["me"]; + _addFloatVectorEntry = Module["_addFloatVectorEntry"] = wasmExports["ne"]; + _addDoubleVectorEntry = Module["_addDoubleVectorEntry"] = wasmExports["oe"]; + _addIntVectorEntry = Module["_addIntVectorEntry"] = wasmExports["pe"]; + _addUintVectorEntry = Module["_addUintVectorEntry"] = wasmExports["qe"]; + _addStringVectorEntry = Module["_addStringVectorEntry"] = wasmExports["re"]; + _addBoolVectorToInputStream = Module["_addBoolVectorToInputStream"] = wasmExports["se"]; + _addFloatVectorToInputStream = Module["_addFloatVectorToInputStream"] = wasmExports["te"]; + _addDoubleVectorToInputStream = Module["_addDoubleVectorToInputStream"] = wasmExports["ue"]; + _addIntVectorToInputStream = Module["_addIntVectorToInputStream"] = wasmExports["ve"]; + _addUintVectorToInputStream = Module["_addUintVectorToInputStream"] = wasmExports["we"]; + _addStringVectorToInputStream = Module["_addStringVectorToInputStream"] = wasmExports["xe"]; + _addFlatHashMapToInputStream = Module["_addFlatHashMapToInputStream"] = wasmExports["ye"]; + _addProtoToInputStream = Module["_addProtoToInputStream"] = wasmExports["ze"]; + _addEmptyPacketToInputStream = Module["_addEmptyPacketToInputStream"] = wasmExports["Ae"]; + _addBoolToInputSidePacket = Module["_addBoolToInputSidePacket"] = wasmExports["Be"]; + _addDoubleToInputSidePacket = Module["_addDoubleToInputSidePacket"] = wasmExports["Ce"]; + _addFloatToInputSidePacket = Module["_addFloatToInputSidePacket"] = wasmExports["De"]; + _addIntToInputSidePacket = Module["_addIntToInputSidePacket"] = wasmExports["Ee"]; + _addUintToInputSidePacket = Module["_addUintToInputSidePacket"] = wasmExports["Fe"]; + _addStringToInputSidePacket = Module["_addStringToInputSidePacket"] = wasmExports["Ge"]; + _addRawDataSpanToInputSidePacket = Module["_addRawDataSpanToInputSidePacket"] = wasmExports["He"]; + _addProtoToInputSidePacket = Module["_addProtoToInputSidePacket"] = wasmExports["Ie"]; + _addBoolVectorToInputSidePacket = Module["_addBoolVectorToInputSidePacket"] = wasmExports["Je"]; + _addDoubleVectorToInputSidePacket = Module["_addDoubleVectorToInputSidePacket"] = wasmExports["Ke"]; + _addFloatVectorToInputSidePacket = Module["_addFloatVectorToInputSidePacket"] = wasmExports["Le"]; + _addIntVectorToInputSidePacket = Module["_addIntVectorToInputSidePacket"] = wasmExports["Me"]; + _addUintVectorToInputSidePacket = Module["_addUintVectorToInputSidePacket"] = wasmExports["Ne"]; + _addStringVectorToInputSidePacket = Module["_addStringVectorToInputSidePacket"] = wasmExports["Oe"]; + _attachBoolListener = Module["_attachBoolListener"] = wasmExports["Pe"]; + _attachBoolVectorListener = Module["_attachBoolVectorListener"] = wasmExports["Qe"]; + _attachDoubleListener = Module["_attachDoubleListener"] = wasmExports["Re"]; + _attachDoubleVectorListener = Module["_attachDoubleVectorListener"] = wasmExports["Se"]; + _attachFloatListener = Module["_attachFloatListener"] = wasmExports["Te"]; + _attachFloatVectorListener = Module["_attachFloatVectorListener"] = wasmExports["Ue"]; + _attachIntListener = Module["_attachIntListener"] = wasmExports["Ve"]; + _attachIntVectorListener = Module["_attachIntVectorListener"] = wasmExports["We"]; + _attachUintListener = Module["_attachUintListener"] = wasmExports["Xe"]; + _attachUintVectorListener = Module["_attachUintVectorListener"] = wasmExports["Ye"]; + _attachStringListener = Module["_attachStringListener"] = wasmExports["Ze"]; + _attachStringVectorListener = Module["_attachStringVectorListener"] = wasmExports["_e"]; + _attachProtoListener = Module["_attachProtoListener"] = wasmExports["$e"]; + _attachProtoVectorListener = Module["_attachProtoVectorListener"] = wasmExports["af"]; + _getGraphConfig = Module["_getGraphConfig"] = wasmExports["bf"]; + ___getTypeName = wasmExports["cf"]; + _emwgpuCreateBindGroup = wasmExports["df"]; + _emwgpuCreateBindGroupLayout = wasmExports["ef"]; + _emwgpuCreateCommandBuffer = wasmExports["ff"]; + _emwgpuCreateCommandEncoder = wasmExports["gf"]; + _emwgpuCreateComputePassEncoder = wasmExports["hf"]; + _emwgpuCreateComputePipeline = wasmExports["jf"]; + _emwgpuCreateExternalTexture = wasmExports["kf"]; + _emwgpuCreatePipelineLayout = wasmExports["lf"]; + _emwgpuCreateQuerySet = wasmExports["mf"]; + _emwgpuCreateRenderBundle = wasmExports["nf"]; + _emwgpuCreateRenderBundleEncoder = wasmExports["of"]; + _emwgpuCreateRenderPassEncoder = wasmExports["pf"]; + _emwgpuCreateRenderPipeline = wasmExports["qf"]; + _emwgpuCreateSampler = wasmExports["rf"]; + _emwgpuCreateSurface = wasmExports["sf"]; + _emwgpuCreateTexture = wasmExports["tf"]; + _emwgpuCreateTextureView = wasmExports["uf"]; + _emwgpuCreateAdapter = wasmExports["vf"]; + _emwgpuImportBuffer = wasmExports["wf"]; + _emwgpuCreateDevice = wasmExports["xf"]; + _emwgpuCreateQueue = wasmExports["yf"]; + _emwgpuCreateShaderModule = wasmExports["zf"]; + _emwgpuOnCreateComputePipelineCompleted = wasmExports["Af"]; + _emwgpuOnCreateRenderPipelineCompleted = wasmExports["Bf"]; + _clearSubgraphs = Module["_clearSubgraphs"] = wasmExports["Cf"]; + _pushBinarySubgraph = Module["_pushBinarySubgraph"] = wasmExports["Df"]; + _pushTextSubgraph = Module["_pushTextSubgraph"] = wasmExports["Ef"]; + _changeBinaryGraph = Module["_changeBinaryGraph"] = wasmExports["Ff"]; + _changeTextGraph = Module["_changeTextGraph"] = wasmExports["Gf"]; + _processGl = Module["_processGl"] = wasmExports["Hf"]; + _process = Module["_process"] = wasmExports["If"]; + _bindTextureToCanvas = Module["_bindTextureToCanvas"] = wasmExports["Jf"]; + _requestShaderRefreshOnGraphChange = Module["_requestShaderRefreshOnGraphChange"] = wasmExports["Kf"]; + _waitUntilIdle = Module["_waitUntilIdle"] = wasmExports["Lf"]; + _closeGraph = Module["_closeGraph"] = wasmExports["Mf"]; + _setAutoRenderToScreen = Module["_setAutoRenderToScreen"] = wasmExports["Nf"]; + _emscripten_builtin_memalign = wasmExports["Of"]; + _memalign = wasmExports["Pf"]; + __emscripten_tempret_set = wasmExports["Qf"]; + __emscripten_stack_restore = wasmExports["Rf"]; + __emscripten_stack_alloc = wasmExports["Sf"]; + _emscripten_stack_get_current = wasmExports["Tf"]; + dynCall_ji = wasmExports["dynCall_ji"]; + dynCall_jii = wasmExports["dynCall_jii"]; + dynCall_iiiijij = wasmExports["dynCall_iiiijij"]; + dynCall_viiji = wasmExports["dynCall_viiji"]; + dynCall_viji = wasmExports["dynCall_viji"]; + dynCall_iiiji = wasmExports["dynCall_iiiji"]; + dynCall_jjj = wasmExports["dynCall_jjj"]; + dynCall_iiiijj = wasmExports["dynCall_iiiijj"]; + dynCall_viijj = wasmExports["dynCall_viijj"]; + dynCall_viiijjj = wasmExports["dynCall_viiijjj"]; + dynCall_vij = wasmExports["dynCall_vij"]; + dynCall_viiiji = wasmExports["dynCall_viiiji"]; + dynCall_viijii = wasmExports["dynCall_viijii"]; + dynCall_vijjj = wasmExports["dynCall_vijjj"]; + dynCall_vj = wasmExports["dynCall_vj"]; + dynCall_viij = wasmExports["dynCall_viij"]; + dynCall_jiji = wasmExports["dynCall_jiji"]; + dynCall_iiiiij = wasmExports["dynCall_iiiiij"]; + dynCall_iiiiijj = wasmExports["dynCall_iiiiijj"]; + dynCall_iiiiiijj = wasmExports["dynCall_iiiiiijj"]; + memory = wasmMemory = wasmExports["hd"]; + _kVersionStampBuildChangelistStr = Module["_kVersionStampBuildChangelistStr"] = wasmExports["jd"].value; + _kVersionStampCitcSnapshotStr = Module["_kVersionStampCitcSnapshotStr"] = wasmExports["kd"].value; + _kVersionStampCitcWorkspaceIdStr = Module["_kVersionStampCitcWorkspaceIdStr"] = wasmExports["ld"].value; + _kVersionStampSourceUriStr = Module["_kVersionStampSourceUriStr"] = wasmExports["md"].value; + _kVersionStampBuildClientStr = Module["_kVersionStampBuildClientStr"] = wasmExports["nd"].value; + _kVersionStampBuildClientMintStatusStr = Module["_kVersionStampBuildClientMintStatusStr"] = wasmExports["od"].value; + _kVersionStampBuildCompilerStr = Module["_kVersionStampBuildCompilerStr"] = wasmExports["pd"].value; + _kVersionStampBuildDateTimePstStr = Module["_kVersionStampBuildDateTimePstStr"] = wasmExports["qd"].value; + _kVersionStampBuildDepotPathStr = Module["_kVersionStampBuildDepotPathStr"] = wasmExports["rd"].value; + _kVersionStampBuildIdStr = Module["_kVersionStampBuildIdStr"] = wasmExports["sd"].value; + _kVersionStampBuildInfoStr = Module["_kVersionStampBuildInfoStr"] = wasmExports["td"].value; + _kVersionStampBuildLabelStr = Module["_kVersionStampBuildLabelStr"] = wasmExports["ud"].value; + _kVersionStampBuildTargetStr = Module["_kVersionStampBuildTargetStr"] = wasmExports["vd"].value; + _kVersionStampBuildTimestampStr = Module["_kVersionStampBuildTimestampStr"] = wasmExports["wd"].value; + _kVersionStampBuildToolStr = Module["_kVersionStampBuildToolStr"] = wasmExports["xd"].value; + _kVersionStampG3BuildTargetStr = Module["_kVersionStampG3BuildTargetStr"] = wasmExports["yd"].value; + _kVersionStampVerifiableStr = Module["_kVersionStampVerifiableStr"] = wasmExports["zd"].value; + _kVersionStampBuildFdoTypeStr = Module["_kVersionStampBuildFdoTypeStr"] = wasmExports["Ad"].value; + _kVersionStampBuildBaselineChangelistStr = Module["_kVersionStampBuildBaselineChangelistStr"] = wasmExports["Bd"].value; + _kVersionStampBuildLtoTypeStr = Module["_kVersionStampBuildLtoTypeStr"] = wasmExports["Cd"].value; + _kVersionStampBuildPropellerTypeStr = Module["_kVersionStampBuildPropellerTypeStr"] = wasmExports["Dd"].value; + _kVersionStampBuildPghoTypeStr = Module["_kVersionStampBuildPghoTypeStr"] = wasmExports["Ed"].value; + _kVersionStampBuildUsernameStr = Module["_kVersionStampBuildUsernameStr"] = wasmExports["Fd"].value; + _kVersionStampBuildHostnameStr = Module["_kVersionStampBuildHostnameStr"] = wasmExports["Gd"].value; + _kVersionStampBuildDirectoryStr = Module["_kVersionStampBuildDirectoryStr"] = wasmExports["Hd"].value; + _kVersionStampBuildChangelistInt = Module["_kVersionStampBuildChangelistInt"] = wasmExports["Id"].value; + _kVersionStampCitcSnapshotInt = Module["_kVersionStampCitcSnapshotInt"] = wasmExports["Jd"].value; + _kVersionStampBuildClientMintStatusInt = Module["_kVersionStampBuildClientMintStatusInt"] = wasmExports["Kd"].value; + _kVersionStampBuildTimestampInt = Module["_kVersionStampBuildTimestampInt"] = wasmExports["Ld"].value; + _kVersionStampVerifiableInt = Module["_kVersionStampVerifiableInt"] = wasmExports["Md"].value; + _kVersionStampBuildCoverageEnabledInt = Module["_kVersionStampBuildCoverageEnabledInt"] = wasmExports["Nd"].value; + _kVersionStampBuildBaselineChangelistInt = Module["_kVersionStampBuildBaselineChangelistInt"] = wasmExports["Od"].value; + _kVersionStampPrecookedTimestampStr = Module["_kVersionStampPrecookedTimestampStr"] = wasmExports["Pd"].value; + _kVersionStampPrecookedClientInfoStr = Module["_kVersionStampPrecookedClientInfoStr"] = wasmExports["Qd"].value; + __indirect_function_table = wasmTable = wasmExports["Rd"]; +} + +var wasmImports = { + /** @export */ gd: BeginGlQueryTiming, + /** @export */ fd: EndGlQueryTiming, + /** @export */ ed: GetAdapterArchitecture, + /** @export */ dd: GetAdapterDescription, + /** @export */ cd: GetAdapterDeviceName, + /** @export */ bd: GetAdapterVendor, + /** @export */ ad: JsOnEmptyPacketListener, + /** @export */ $c: JsOnFloat32ArrayImageListener, + /** @export */ _c: JsOnFloat32ArrayImageVectorListener, + /** @export */ ob: JsOnSimpleListenerBinaryArray, + /** @export */ Zc: JsOnSimpleListenerBool, + /** @export */ Yc: JsOnSimpleListenerDouble, + /** @export */ Xc: JsOnSimpleListenerFloat, + /** @export */ Wc: JsOnSimpleListenerInt, + /** @export */ Vc: JsOnSimpleListenerString, + /** @export */ Uc: JsOnSimpleListenerUint, + /** @export */ Tc: JsOnUint8ArrayImageListener, + /** @export */ Sc: JsOnUint8ArrayImageVectorListener, + /** @export */ P: JsOnVectorFinishedListener, + /** @export */ Rc: JsOnVectorListenerBool, + /** @export */ Qc: JsOnVectorListenerDouble, + /** @export */ Pc: JsOnVectorListenerFloat, + /** @export */ Oc: JsOnVectorListenerInt, + /** @export */ Nc: JsOnVectorListenerProto, + /** @export */ Mc: JsOnVectorListenerString, + /** @export */ Lc: JsOnVectorListenerUint, + /** @export */ Kc: JsOnWebGLTextureListener, + /** @export */ Jc: JsOnWebGLTextureVectorListener, + /** @export */ Oa: JsWrapErrorListener, + /** @export */ nb: JsWrapImageConverter, + /** @export */ u: JsWrapSimpleListeners, + /** @export */ mb: UseBottomLeftGpuOrigin, + /** @export */ xb: __asyncjs__mediapipe_map_buffer_jspi, + /** @export */ r: ___cxa_throw, + /** @export */ Ic: ___syscall_dup, + /** @export */ Hc: ___syscall_faccessat, + /** @export */ lb: ___syscall_fcntl64, + /** @export */ Gc: ___syscall_fstat64, + /** @export */ Lb: ___syscall_ftruncate64, + /** @export */ Fc: ___syscall_ioctl, + /** @export */ Ec: ___syscall_lstat64, + /** @export */ Dc: ___syscall_newfstatat, + /** @export */ kb: ___syscall_openat, + /** @export */ Cc: ___syscall_stat64, + /** @export */ xc: __abort_js, + /** @export */ Ib: __embind_register_bigint, + /** @export */ wc: __embind_register_bool, + /** @export */ vc: __embind_register_emval, + /** @export */ ib: __embind_register_float, + /** @export */ J: __embind_register_integer, + /** @export */ q: __embind_register_memory_view, + /** @export */ uc: __embind_register_std_string, + /** @export */ La: __embind_register_std_wstring, + /** @export */ tc: __embind_register_void, + /** @export */ $: __emval_create_invoker, + /** @export */ p: __emval_decref, + /** @export */ Ka: __emval_get_global, + /** @export */ hb: __emval_get_property, + /** @export */ ja: __emval_incref, + /** @export */ Ja: __emval_instanceof, + /** @export */ _: __emval_invoke, + /** @export */ ua: __emval_new_cstring, + /** @export */ Z: __emval_run_destructors, + /** @export */ gb: __emval_set_property, + /** @export */ sc: __emval_typeof, + /** @export */ Hb: __gmtime_js, + /** @export */ Gb: __localtime_js, + /** @export */ Fb: __mktime_js, + /** @export */ Eb: __mmap_js, + /** @export */ Db: __munmap_js, + /** @export */ rc: __tzset_js, + /** @export */ Kb: _clock_time_get, + /** @export */ qc: custom_emscripten_dbgn, + /** @export */ Y: _emscripten_asm_const_int, + /** @export */ fb: _emscripten_asm_const_ptr, + /** @export */ Ia: _emscripten_errn, + /** @export */ pc: _emscripten_get_heap_max, + /** @export */ y: _emscripten_get_now, + /** @export */ ia: _emscripten_has_asyncify, + /** @export */ oc: _emscripten_outn, + /** @export */ nc: _emscripten_pc_get_function, + /** @export */ mc: _emscripten_resize_heap, + /** @export */ eb: _emscripten_stack_snapshot, + /** @export */ lc: _emscripten_stack_unwind_buffer, + /** @export */ kc: _emscripten_webgl_create_context, + /** @export */ jc: _emscripten_webgl_destroy_context, + /** @export */ ic: _emscripten_webgl_get_context_attributes, + /** @export */ ha: _emscripten_webgl_get_current_context, + /** @export */ hc: _emscripten_webgl_make_context_current, + /** @export */ R: _emscripten_webgpu_get_device, + /** @export */ gc: _emwgpuBufferDestroy, + /** @export */ fc: _emwgpuBufferGetMappedRange, + /** @export */ ec: _emwgpuBufferUnmap, + /** @export */ x: _emwgpuDelete, + /** @export */ dc: _emwgpuDeviceCreateBuffer, + /** @export */ Cb: _emwgpuDeviceCreateComputePipelineAsync, + /** @export */ Bb: _emwgpuDeviceCreateRenderPipelineAsync, + /** @export */ cc: _emwgpuDeviceCreateShaderModule, + /** @export */ bc: _emwgpuDeviceDestroy, + /** @export */ ac: _emwgpuWaitAny, + /** @export */ Bc: _environ_get, + /** @export */ Ac: _environ_sizes_get, + /** @export */ db: _exit, + /** @export */ Na: _fd_close, + /** @export */ jb: _fd_read, + /** @export */ Jb: _fd_seek, + /** @export */ Ma: _fd_write, + /** @export */ b: _glActiveTexture, + /** @export */ ta: _glAttachShader, + /** @export */ $b: _glBindAttribLocation, + /** @export */ c: _glBindBuffer, + /** @export */ cb: _glBindBufferBase, + /** @export */ t: _glBindFramebuffer, + /** @export */ a: _glBindTexture, + /** @export */ m: _glBindVertexArray, + /** @export */ bb: _glBlendEquation, + /** @export */ _b: _glBlendFunc, + /** @export */ j: _glBufferData, + /** @export */ I: _glClear, + /** @export */ H: _glClearColor, + /** @export */ da: _glClientWaitSync, + /** @export */ ga: _glColorMask, + /** @export */ ab: _glCompileShader, + /** @export */ $a: _glCreateProgram, + /** @export */ _a: _glCreateShader, + /** @export */ o: _glDeleteBuffers, + /** @export */ Q: _glDeleteFramebuffers, + /** @export */ h: _glDeleteProgram, + /** @export */ sa: _glDeleteShader, + /** @export */ ra: _glDeleteSync, + /** @export */ D: _glDeleteTextures, + /** @export */ A: _glDeleteVertexArrays, + /** @export */ Za: _glDetachShader, + /** @export */ G: _glDisable, + /** @export */ n: _glDisableVertexAttribArray, + /** @export */ i: _glDrawArrays, + /** @export */ fa: _glDrawBuffers, + /** @export */ Zb: _glEnable, + /** @export */ l: _glEnableVertexAttribArray, + /** @export */ Ya: _glFenceSync, + /** @export */ qa: _glFinish, + /** @export */ v: _glFlush, + /** @export */ C: _glFramebufferTexture2D, + /** @export */ Xa: _glFramebufferTextureLayer, + /** @export */ s: _glGenBuffers, + /** @export */ X: _glGenFramebuffers, + /** @export */ F: _glGenTextures, + /** @export */ B: _glGenVertexArrays, + /** @export */ Wa: _glGetAttribLocation, + /** @export */ ea: _glGetError, + /** @export */ Yb: _glGetFloatv, + /** @export */ w: _glGetIntegerv, + /** @export */ Xb: _glGetProgramiv, + /** @export */ Wb: _glGetShaderInfoLog, + /** @export */ Vb: _glGetShaderiv, + /** @export */ O: _glGetString, + /** @export */ Ub: _glGetUniformBlockIndex, + /** @export */ d: _glGetUniformLocation, + /** @export */ Tb: _glLineWidth, + /** @export */ Va: _glLinkProgram, + /** @export */ pa: _glPixelStorei, + /** @export */ oa: _glReadPixels, + /** @export */ Ua: _glShaderSource, + /** @export */ E: _glTexImage2D, + /** @export */ na: _glTexParameterf, + /** @export */ Ta: _glTexParameterfv, + /** @export */ f: _glTexParameteri, + /** @export */ ma: _glTexStorage2D, + /** @export */ Sb: _glTexStorage3D, + /** @export */ W: _glTexSubImage2D, + /** @export */ Rb: _glTexSubImage3D, + /** @export */ N: _glUniform1f, + /** @export */ la: _glUniform1fv, + /** @export */ e: _glUniform1i, + /** @export */ V: _glUniform2f, + /** @export */ Qb: _glUniform2fv, + /** @export */ Ha: _glUniform3f, + /** @export */ Sa: _glUniform4f, + /** @export */ U: _glUniform4fv, + /** @export */ Pb: _glUniform4iv, + /** @export */ Ob: _glUniformBlockBinding, + /** @export */ Nb: _glUniformMatrix2fv, + /** @export */ Mb: _glUniformMatrix3fv, + /** @export */ Ga: _glUniformMatrix4fv, + /** @export */ g: _glUseProgram, + /** @export */ k: _glVertexAttribPointer, + /** @export */ T: _glViewport, + /** @export */ Ab: mediapipe_create_utility_canvas2d, + /** @export */ zb: _mediapipe_find_canvas_event_target, + /** @export */ yb: mediapipe_import_external_texture, + /** @export */ wb: _mediapipe_webgl_tex_image_drawable, + /** @export */ zc: _proc_exit, + /** @export */ yc: _random_get, + /** @export */ Fa: _wgpuCommandEncoderBeginComputePass, + /** @export */ Ea: _wgpuCommandEncoderBeginRenderPass, + /** @export */ vb: _wgpuCommandEncoderCopyBufferToTexture, + /** @export */ ub: _wgpuCommandEncoderCopyTextureToBuffer, + /** @export */ tb: _wgpuCommandEncoderCopyTextureToTexture, + /** @export */ M: _wgpuCommandEncoderFinish, + /** @export */ Da: _wgpuComputePassEncoderDispatchWorkgroups, + /** @export */ Ca: _wgpuComputePassEncoderEnd, + /** @export */ Ba: _wgpuComputePassEncoderSetBindGroup, + /** @export */ Aa: _wgpuComputePassEncoderSetPipeline, + /** @export */ za: _wgpuComputePipelineGetBindGroupLayout, + /** @export */ ca: _wgpuDeviceCreateBindGroup, + /** @export */ sb: _wgpuDeviceCreateBindGroupLayout, + /** @export */ L: _wgpuDeviceCreateCommandEncoder, + /** @export */ rb: _wgpuDeviceCreateComputePipeline, + /** @export */ qb: _wgpuDeviceCreatePipelineLayout, + /** @export */ Ra: _wgpuDeviceCreateRenderPipeline, + /** @export */ S: _wgpuDeviceCreateSampler, + /** @export */ ba: _wgpuDeviceCreateTexture, + /** @export */ K: _wgpuQueueSubmit, + /** @export */ ka: _wgpuQueueWriteBuffer, + /** @export */ pb: _wgpuQueueWriteTexture, + /** @export */ ya: _wgpuRenderPassEncoderDraw, + /** @export */ xa: _wgpuRenderPassEncoderEnd, + /** @export */ wa: _wgpuRenderPassEncoderSetBindGroup, + /** @export */ va: _wgpuRenderPassEncoderSetPipeline, + /** @export */ Qa: _wgpuRenderPipelineGetBindGroupLayout, + /** @export */ z: _wgpuTextureCreateView, + /** @export */ Pa: _wgpuTextureDestroy, + /** @export */ aa: _wgpuTextureGetFormat +}; + +// include: postamble.js +// === Auto-generated postamble setup entry stuff === +function run() { + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + preRun(); + // a preRun added a dependency, run will be called later + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + function doRun() { + // run may have just been called through dependencies being fulfilled just in this very frame, + // or while the async setStatus time below was happening + Module["calledRun"] = true; + if (ABORT) return; + initRuntime(); + readyPromiseResolve?.(Module); + Module["onRuntimeInitialized"]?.(); + postRun(); + } + if (Module["setStatus"]) { + Module["setStatus"]("Running..."); + setTimeout(() => { + setTimeout(() => Module["setStatus"](""), 1); + doRun(); + }, 1); + } else { + doRun(); + } +} + +var wasmExports; + +// In modularize mode the generated code is within a factory function so we +// can use await here (since it's not top-level-await). +wasmExports = await (createWasm()); + +run(); + +// end include: postamble.js +// include: postamble_modularize.js +// In MODULARIZE mode we wrap the generated code in a factory function +// and return either the Module itself, or a promise of the module. +// We assign to the `moduleRtn` global here and configure closure to see +// this as an extern so it won't get minified. +if (runtimeInitialized) { + moduleRtn = Module; +} else { + // Set up the promise that indicates the Module is initialized + moduleRtn = new Promise((resolve, reject) => { + readyPromiseResolve = resolve; + readyPromiseReject = reject; + }); +} + + + return moduleRtn; + }; +})(); + +// Export using a UMD style export, or ES6 exports if selected +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = ModuleFactory; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = ModuleFactory; +} else if (typeof define === 'function' && define['amd']) + define([], () => ModuleFactory); + diff --git a/public/models/mediapipe/wasm/vision_wasm_nosimd_internal.wasm b/public/models/mediapipe/wasm/vision_wasm_nosimd_internal.wasm new file mode 100644 index 0000000..82cd2e1 Binary files /dev/null and b/public/models/mediapipe/wasm/vision_wasm_nosimd_internal.wasm differ diff --git a/public/models/onnxruntime/ort-wasm-simd-threaded.mjs b/public/models/onnxruntime/ort-wasm-simd-threaded.mjs new file mode 100644 index 0000000..db216f9 --- /dev/null +++ b/public/models/onnxruntime/ort-wasm-simd-threaded.mjs @@ -0,0 +1,59 @@ +async function ortWasmThreaded(moduleArg={}){var moduleRtn;var h=moduleArg,aa=!!globalThis.window,k=!!globalThis.WorkerGlobalScope,m=globalThis.process?.versions?.node&&"renderer"!=globalThis.process?.type,n=k&&self.name?.startsWith("em-pthread");if(m){const {createRequire:a}=await import("module");var require=a(import.meta.url),ba=require("worker_threads");global.Worker=ba.Worker;n=(k=!ba.hc)&&"em-pthread"==ba.workerData}h.mountExternalData=(a,b)=>{a.startsWith("./")&&(a=a.substring(2));(h.Rb||(h.Rb=new Map)).set(a,b)}; +h.unmountExternalData=()=>{delete h.Rb};var SharedArrayBuffer=globalThis.SharedArrayBuffer??(new WebAssembly.Memory({initial:0,maximum:0,shared:!0})).buffer.constructor,ca="./this.program",da=(a,b)=>{throw b;},ea=import.meta.url,fa="",ha,ia; +if(m){var fs=require("fs");ea.startsWith("file:")&&(fa=require("path").dirname(require("url").fileURLToPath(ea))+"/");ia=a=>{a=ja(a)?new URL(a):a;return fs.readFileSync(a)};ha=async a=>{a=ja(a)?new URL(a):a;return fs.readFileSync(a,void 0)};1{process.exitCode=a;throw b;}}else if(aa||k){try{fa=(new URL(".",ea)).href}catch{}m||(k&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer"; +b.send(null);return new Uint8Array(b.response)}),ha=async a=>{if(ja(a))return new Promise((d,c)=>{var e=new XMLHttpRequest;e.open("GET",a,!0);e.responseType="arraybuffer";e.onload=()=>{200==e.status||0==e.status&&e.response?d(e.response):c(e.status)};e.onerror=c;e.send(null)});var b=await fetch(a,{credentials:"same-origin"});if(b.ok)return b.arrayBuffer();throw Error(b.status+" : "+b.url);})}var ka=console.log.bind(console),la=console.error.bind(console); +if(m){var ma=require("util"),na=a=>"object"==typeof a?ma.inspect(a):a;ka=(...a)=>fs.writeSync(1,a.map(na).join(" ")+"\n");la=(...a)=>fs.writeSync(2,a.map(na).join(" ")+"\n")}var oa=ka,p=la,q,r,pa=!1,t,ja=a=>a.startsWith("file://");function v(){x.buffer!=z.buffer&&qa()}var ra,sa; +if(m&&n){var ta=ba.parentPort;ta.on("message",a=>global.onmessage?.({data:a}));Object.assign(globalThis,{self:global,postMessage:a=>ta.postMessage(a)});process.on("uncaughtException",a=>{postMessage({Pb:"uncaughtException",error:a});process.exit(1)})}var ua; +if(n){var va=!1;self.onunhandledrejection=b=>{throw b.reason||b;};function a(b){try{var d=b.data,c=d.Pb;if("load"===c){let e=[];self.onmessage=f=>e.push(f);ua=()=>{postMessage({Pb:"loaded"});for(let f of e)a(f);self.onmessage=a};for(const f of d.Zb)if(!h[f]||h[f].proxy)h[f]=(...g)=>{postMessage({Pb:"callHandler",Yb:f,args:g})},"print"==f&&(oa=h[f]),"printErr"==f&&(p=h[f]);x=d.dc;qa();r=d.ec;wa();xa()}else if("run"===c){ya(d.Ob);za(d.Ob,0,0,1,0,0);Aa();Ba(d.Ob);va||=!0;try{Ca(d.bc,d.Tb)}catch(e){if("unwind"!= +e)throw e;}}else"setimmediate"!==d.target&&("checkMailbox"===c?va&&Da():c&&(p(`worker: received unknown command ${c}`),p(d)))}catch(e){throw Ea(),e;}}self.onmessage=a}var z,A,Fa,C,D,Ga,G,H,Ha=!1;function qa(){var a=x.buffer;h.HEAP8=z=new Int8Array(a);Fa=new Int16Array(a);h.HEAPU8=A=new Uint8Array(a);new Uint16Array(a);h.HEAP32=C=new Int32Array(a);h.HEAPU32=D=new Uint32Array(a);Ga=new Float32Array(a);G=new Float64Array(a);H=new BigInt64Array(a);new BigUint64Array(a)} +function Ia(){Ha=!0;n?ua():I.Ta()}function J(a){a="Aborted("+a+")";p(a);pa=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");sa?.(a);throw a;}var Ja;async function Ka(a){if(!q)try{var b=await ha(a);return new Uint8Array(b)}catch{}if(a==Ja&&q)a=new Uint8Array(q);else if(ia)a=ia(a);else throw"both async and sync fetching of the wasm failed";return a} +async function La(a,b){try{var d=await Ka(a);return await WebAssembly.instantiate(d,b)}catch(c){p(`failed to asynchronously prepare wasm: ${c}`),J(c)}}async function Na(a){var b=Ja;if(!q&&!ja(b)&&!m)try{var d=fetch(b,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(d,a)}catch(c){p(`wasm streaming compile failed: ${c}`),p("falling back to ArrayBuffer instantiation")}return La(b,a)} +function Oa(){Pa={R:Qa,f:Ra,w:Sa,e:Ta,k:Ua,g:Va,S:Wa,b:Xa,G:Ya,ta:Za,j:$a,L:ab,Ja:bb,pa:cb,ra:db,Ka:eb,Ha:fb,Aa:gb,Ga:hb,Y:ib,qa:jb,na:kb,Ia:lb,oa:mb,Pa:nb,Da:ob,la:pb,ua:qb,ia:rb,T:sb,Ca:Ba,Ma:tb,xa:ub,ya:vb,za:wb,va:xb,wa:yb,ja:zb,Ra:Ab,Oa:Bb,V:Cb,U:Db,Na:Eb,F:Fb,La:Gb,ma:Hb,u:Ib,H:Jb,Q:Kb,ka:Lb,aa:Mb,Sa:Nb,Ea:Ob,Fa:Pb,sa:Qb,I:Rb,X:Sb,Ba:Tb,W:Ub,_:Vb,M:Wb,$:Xb,N:Yb,v:Zb,c:$b,m:ac,n:bc,q:cc,ba:dc,x:ec,o:fc,O:gc,D:hc,J:ic,ca:jc,da:kc,A:lc,P:mc,ea:nc,z:oc,E:pc,d:qc,r:rc,i:sc,Z:tc,l:uc,p:vc,s:wc,t:xc, +y:yc,fa:zc,B:Ac,K:Bc,C:Cc,ga:Dc,ha:Ec,h:Fc,a:x,Qa:Gc};return{a:Pa}} +async function wa(){function a(c,e){I=c.exports;I=Hc();Ic.push(I.vb);c=I;h._OrtInit=c.Ua;h._OrtGetLastError=c.Va;h._OrtCreateSessionOptions=c.Wa;h._OrtAppendExecutionProvider=c.Xa;h._OrtAddFreeDimensionOverride=c.Ya;h._OrtAddSessionConfigEntry=c.Za;h._OrtReleaseSessionOptions=c._a;h._OrtCreateSession=c.$a;h._OrtReleaseSession=c.ab;h._OrtGetInputOutputCount=c.bb;h._OrtGetInputOutputMetadata=c.cb;h._OrtFree=c.db;h._OrtCreateTensor=c.eb;h._OrtGetTensorData=c.fb;h._OrtReleaseTensor=c.gb;h._OrtCreateRunOptions= +c.hb;h._OrtAddRunConfigEntry=c.ib;h._OrtReleaseRunOptions=c.jb;h._OrtCreateBinding=c.kb;h._OrtBindInput=c.lb;h._OrtBindOutput=c.mb;h._OrtClearBoundOutputs=c.nb;h._OrtReleaseBinding=c.ob;h._OrtRunWithBinding=c.pb;h._OrtRun=c.qb;h._OrtEndProfiling=c.rb;Jc=c.sb;Kc=h._free=c.tb;Lc=h._malloc=c.ub;za=c.xb;Ea=c.yb;Mc=c.zb;Nc=c.Ab;Oc=c.Bb;Pc=c.Cb;Qc=c.Db;K=c.Eb;L=c.Fb;Rc=c.Gb;M=c.Hb;Sc=c.Ib;N=c.Jb;Tc=c.Kb;Uc=c.Lb;Vc=c.Mb;Wc=c.Nb;Xc=c.wb;r=e;return I}var b=Oa();if(h.instantiateWasm)return new Promise(c=>{h.instantiateWasm(b, +(e,f)=>{c(a(e,f))})});if(n){var d=new WebAssembly.Instance(r,Oa());return a(d,r)}Ja??=h.locateFile?h.locateFile?h.locateFile("ort-wasm-simd-threaded.wasm",fa):fa+"ort-wasm-simd-threaded.wasm":(new URL("ort-wasm-simd-threaded.wasm",import.meta.url)).href;return function(c){return a(c.instance,c.module)}(await Na(b))}class Yc{name="ExitStatus";constructor(a){this.message=`Program terminated with exit(${a})`;this.status=a}} +var Zc=a=>{a.terminate();a.onmessage=()=>{}},$c=[],O=0,P=null,cd=a=>{0==Q.length&&(ad(),bd(Q[0]));var b=Q.pop();if(!b)return 6;R.push(b);S[a.Ob]=b;b.Ob=a.Ob;var d={Pb:"run",bc:a.ac,Tb:a.Tb,Ob:a.Ob};m&&b.unref();b.postMessage(d,a.Xb);return 0},T=0,U=(a,b,...d)=>{var c=16*d.length,e=N(),f=Sc(c),g=f>>>3,l;for(l of d)"bigint"==typeof l?((v(),H)[g++>>>0]=1n,(v(),H)[g++>>>0]=l):((v(),H)[g++>>>0]=0n,(v(),G)[g++>>>0]=l);a=Mc(a,0,c,f,b);M(e);return a}; +function Gc(a){if(n)return U(0,1,a);t=a;if(!(0{t=a;if(n)throw dd(a),"unwind";Gc(a)},Q=[],R=[],Ic=[],S={};function ed(){for(var a=h.numThreads-1;a--;)ad();$c.push(async()=>{var b=fd();O++;await b;O--;0==O&&P&&(b=P,P=null,b())})}var gd=a=>{var b=a.Ob;delete S[b];Q.push(a);R.splice(R.indexOf(a),1);a.Ob=0;Nc(b)};function Aa(){Ic.forEach(a=>a())} +var bd=a=>new Promise(b=>{a.onmessage=f=>{var g=f.data;f=g.Pb;if(g.Sb&&g.Sb!=Jc()){var l=S[g.Sb];l?l.postMessage(g,g.Xb):p(`Internal error! Worker sent a message "${f}" to target pthread ${g.Sb}, but that thread no longer exists!`)}else if("checkMailbox"===f)Da();else if("spawnThread"===f)cd(g);else if("cleanupThread"===f)hd(()=>{gd(S[g.cc])});else if("loaded"===f)a.loaded=!0,m&&!a.Ob&&a.unref(),b(a);else if("setimmediate"===g.target)a.postMessage(g);else if("uncaughtException"===f)a.onerror(g.error); +else if("callHandler"===f)h[g.Yb](...g.args);else f&&p(`worker sent an unknown command ${f}`)};a.onerror=f=>{p(`${"worker sent an error!"} ${f.filename}:${f.lineno}: ${f.message}`);throw f;};m&&(a.on("message",f=>a.onmessage({data:f})),a.on("error",f=>a.onerror(f)));var d=[],c=[],e;for(e of c)h.propertyIsEnumerable(e)&&d.push(e);a.postMessage({Pb:"load",Zb:d,dc:x,ec:r})});async function fd(){if(!n)return Promise.all(Q.map(bd))} +function ad(){var a=new Worker(new URL(import.meta.url),{type:"module",workerData:"em-pthread",name:"em-pthread"});Q.push(a)}function ya(a){var b=(v(),D)[a+52>>>2>>>0];a=(v(),D)[a+56>>>2>>>0];Rc(b,b-a);M(b)}var jd=[],V=a=>{var b=jd[a];b||(jd[a]=b=Xc.get(a));return b},Ca=(a,b)=>{T=0;a=V(a)(b);0>>=0;var b=new md(a);0==(v(),z)[b.Qb+12>>>0]&&(nd(b,!0),ld--);od(b,!1);kd.push(b);return Wc(a)} +var W=0,Sa=()=>{K(0,0);var a=kd.pop();Tc(a.Ub);W=0};function nd(a,b){b=b?1:0;(v(),z)[a.Qb+12>>>0]=b}function od(a,b){b=b?1:0;(v(),z)[a.Qb+13>>>0]=b}class md{constructor(a){this.Ub=a;this.Qb=a-24}}var pd=a=>{var b=W;if(!b)return L(0),0;var d=new md(b);(v(),D)[d.Qb+16>>>2>>>0]=b;var c=(v(),D)[d.Qb+4>>>2>>>0];if(!c)return L(0),b;for(var e of a){if(0===e||e===c)break;if(Vc(e,c,d.Qb+16))return L(e),b}L(c);return b};function Ta(){return pd([])}function Ua(a){return pd([a>>>0])} +function Va(a,b,d,c){return pd([a>>>0,b>>>0,d>>>0,c>>>0])}var Wa=()=>{var a=kd.pop();a||J("no exception to throw");var b=a.Ub;0==(v(),z)[a.Qb+13>>>0]&&(kd.push(a),od(a,!0),nd(a,!1),ld++);Uc(b);W=b;throw W;};function Xa(a,b,d){a>>>=0;var c=new md(a);b>>>=0;d>>>=0;(v(),D)[c.Qb+16>>>2>>>0]=0;(v(),D)[c.Qb+4>>>2>>>0]=b;(v(),D)[c.Qb+8>>>2>>>0]=d;Uc(a);W=a;ld++;throw W;}var Ya=()=>ld;function qd(a,b,d,c){return n?U(2,1,a,b,d,c):Za(a,b,d,c)} +function Za(a,b,d,c){a>>>=0;b>>>=0;d>>>=0;c>>>=0;if(!globalThis.SharedArrayBuffer)return 6;var e=[];if(n&&0===e.length)return qd(a,b,d,c);a={ac:d,Ob:a,Tb:c,Xb:e};return n?(a.Pb="spawnThread",postMessage(a,e),0):cd(a)}function $a(a){W||=a>>>0;throw W;} +var rd=globalThis.TextDecoder&&new TextDecoder,sd=(a,b=0,d,c)=>{b>>>=0;var e=b;d=e+d;if(c)c=d;else{for(;a[e]&&!(e>=d);)++e;c=e}if(16d?e+=String.fromCharCode(d):(d-=65536,e+=String.fromCharCode(55296|d>>10,56320| +d&1023))}}else e+=String.fromCharCode(d);return e},td=(a,b,d)=>(a>>>=0)?sd((v(),A),a,b,d):"";function ab(a,b,d){return n?U(3,1,a,b,d):0}function bb(a,b){if(n)return U(4,1,a,b)}function cb(a,b){if(n)return U(5,1,a,b)}function db(a,b,d){if(n)return U(6,1,a,b,d)}function eb(a,b,d){return n?U(7,1,a,b,d):0}function fb(a,b){if(n)return U(8,1,a,b)}function gb(a,b,d){if(n)return U(9,1,a,b,d)}function hb(a,b,d,c){if(n)return U(10,1,a,b,d,c)}function ib(a,b,d,c){if(n)return U(11,1,a,b,d,c)} +function jb(a,b,d,c){if(n)return U(12,1,a,b,d,c)}function kb(a){if(n)return U(13,1,a)}function lb(a,b){if(n)return U(14,1,a,b)}function mb(a,b,d){if(n)return U(15,1,a,b,d)}var nb=()=>J("");function ob(a){za(a>>>0,!k,1,!aa,131072,!1);Aa()} +var hd=a=>{if(!pa)try{if(a(),!(0Number((navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)||[])[2]);function Ba(a){a>>>=0;ud||(Atomics.waitAsync((v(),C),a>>>2,a).value.then(Da),a+=128,Atomics.store((v(),C),a>>>2,1))}var Da=()=>hd(()=>{var a=Jc();a&&(Ba(a),Qc())}); +function pb(a,b){a>>>=0;a==b>>>0?setTimeout(Da):n?postMessage({Sb:a,Pb:"checkMailbox"}):(a=S[a])&&a.postMessage({Pb:"checkMailbox"})}var vd=[];function qb(a,b,d,c,e){b>>>=0;e>>>=0;vd.length=0;d=e>>>3;for(c=e+c>>>3;d>>0]?f=(v(),H)[d++>>>0]:f=(v(),G)[d++>>>0];vd.push(f)}return(b?wd[b]:xd[a])(...vd)}var rb=()=>{T=0};function sb(a){a>>>=0;n?postMessage({Pb:"cleanupThread",cc:a}):gd(S[a])}function tb(a){m&&S[a>>>0].ref()} +function ub(a,b){a=-9007199254740992>a||9007199254740992>>=0;a=new Date(1E3*a);(v(),C)[b>>>2>>>0]=a.getUTCSeconds();(v(),C)[b+4>>>2>>>0]=a.getUTCMinutes();(v(),C)[b+8>>>2>>>0]=a.getUTCHours();(v(),C)[b+12>>>2>>>0]=a.getUTCDate();(v(),C)[b+16>>>2>>>0]=a.getUTCMonth();(v(),C)[b+20>>>2>>>0]=a.getUTCFullYear()-1900;(v(),C)[b+24>>>2>>>0]=a.getUTCDay();a=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;(v(),C)[b+28>>>2>>>0]=a} +var yd=a=>0===a%4&&(0!==a%100||0===a%400),zd=[0,31,60,91,121,152,182,213,244,274,305,335],Ad=[0,31,59,90,120,151,181,212,243,273,304,334]; +function vb(a,b){a=-9007199254740992>a||9007199254740992>>=0;a=new Date(1E3*a);(v(),C)[b>>>2>>>0]=a.getSeconds();(v(),C)[b+4>>>2>>>0]=a.getMinutes();(v(),C)[b+8>>>2>>>0]=a.getHours();(v(),C)[b+12>>>2>>>0]=a.getDate();(v(),C)[b+16>>>2>>>0]=a.getMonth();(v(),C)[b+20>>>2>>>0]=a.getFullYear()-1900;(v(),C)[b+24>>>2>>>0]=a.getDay();var d=(yd(a.getFullYear())?zd:Ad)[a.getMonth()]+a.getDate()-1|0;(v(),C)[b+28>>>2>>>0]=d;(v(),C)[b+36>>>2>>>0]=-(60*a.getTimezoneOffset());d=(new Date(a.getFullYear(), +6,1)).getTimezoneOffset();var c=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();a=(d!=c&&a.getTimezoneOffset()==Math.min(c,d))|0;(v(),C)[b+32>>>2>>>0]=a} +function wb(a){a>>>=0;var b=new Date((v(),C)[a+20>>>2>>>0]+1900,(v(),C)[a+16>>>2>>>0],(v(),C)[a+12>>>2>>>0],(v(),C)[a+8>>>2>>>0],(v(),C)[a+4>>>2>>>0],(v(),C)[a>>>2>>>0],0),d=(v(),C)[a+32>>>2>>>0],c=b.getTimezoneOffset(),e=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),f=(new Date(b.getFullYear(),0,1)).getTimezoneOffset(),g=Math.min(f,e);0>d?(v(),C)[a+32>>>2>>>0]=Number(e!=f&&g==c):0>>2>>>0]=b.getDay();d=(yd(b.getFullYear())? +zd:Ad)[b.getMonth()]+b.getDate()-1|0;(v(),C)[a+28>>>2>>>0]=d;(v(),C)[a>>>2>>>0]=b.getSeconds();(v(),C)[a+4>>>2>>>0]=b.getMinutes();(v(),C)[a+8>>>2>>>0]=b.getHours();(v(),C)[a+12>>>2>>>0]=b.getDate();(v(),C)[a+16>>>2>>>0]=b.getMonth();(v(),C)[a+20>>>2>>>0]=b.getYear();a=b.getTime();return BigInt(isNaN(a)?-1:a/1E3)}function xb(a,b,d,c,e,f,g){return n?U(16,1,a,b,d,c,e,f,g):-52}function yb(a,b,d,c,e,f){if(n)return U(17,1,a,b,d,c,e,f)}var X={},Ib=()=>performance.timeOrigin+performance.now(); +function zb(a,b){if(n)return U(18,1,a,b);X[a]&&(clearTimeout(X[a].id),delete X[a]);if(!b)return 0;var d=setTimeout(()=>{delete X[a];hd(()=>Pc(a,performance.timeOrigin+performance.now()))},b);X[a]={id:d,jc:b};return 0} +var Y=(a,b,d)=>{var c=(v(),A);b>>>=0;if(0=g){if(b>=d)break;c[b++>>>0]=g}else if(2047>=g){if(b+1>=d)break;c[b++>>>0]=192|g>>6;c[b++>>>0]=128|g&63}else if(65535>=g){if(b+2>=d)break;c[b++>>>0]=224|g>>12;c[b++>>>0]=128|g>>6&63;c[b++>>>0]=128|g&63}else{if(b+3>=d)break;c[b++>>>0]=240|g>>18;c[b++>>>0]=128|g>>12&63;c[b++>>>0]=128|g>>6&63;c[b++>>>0]=128|g&63;f++}}c[b>>>0]=0;a=b-e}else a=0;return a}; +function Ab(a,b,d,c){a>>>=0;b>>>=0;d>>>=0;c>>>=0;var e=(new Date).getFullYear(),f=(new Date(e,0,1)).getTimezoneOffset();e=(new Date(e,6,1)).getTimezoneOffset();var g=Math.max(f,e);(v(),D)[a>>>2>>>0]=60*g;(v(),C)[b>>>2>>>0]=Number(f!=e);b=l=>{var u=Math.abs(l);return`UTC${0<=l?"-":"+"}${String(Math.floor(u/60)).padStart(2,"0")}${String(u%60).padStart(2,"0")}`};a=b(f);b=b(e);eDate.now(),Bd=1; +function Bb(a,b,d){d>>>=0;if(!(0<=a&&3>=a))return 28;if(0===a)a=Date.now();else if(Bd)a=performance.timeOrigin+performance.now();else return 52;a=Math.round(1E6*a);(v(),H)[d>>>3>>>0]=BigInt(a);return 0}var Cd=[];function Cb(a,b,d){a>>>=0;b>>>=0;d>>>=0;Cd.length=0;for(var c;c=(v(),A)[b++>>>0];){var e=105!=c;e&=112!=c;d+=e&&d%8?4:0;Cd.push(112==c?(v(),D)[d>>>2>>>0]:106==c?(v(),H)[d>>>3>>>0]:105==c?(v(),C)[d>>>2>>>0]:(v(),G)[d>>>3>>>0]);d+=e?8:4}return wd[a](...Cd)}var Db=()=>{}; +function Fb(a,b){return p(td(a>>>0,b>>>0))}var Gb=()=>{T+=1;throw"unwind";};function Hb(){return 4294901760}var Jb=()=>m?require("os").cpus().length:navigator.hardwareConcurrency,Z={},Dd=a=>{for(var b=0,d=0;d=c?b++:2047>=c?b+=2:55296<=c&&57343>=c?(b+=4,++d):b+=3}return b},Ed=a=>{var b;return(b=/\bwasm-function\[\d+\]:(0x[0-9a-f]+)/.exec(a))?+b[1]:(b=/:(\d+):\d+(?:\)|$)/.exec(a))?2147483648|+b[1]:0},Fd=a=>{for(var b of a)(a=Ed(b))&&(Z[a]=b)}; +function Mb(){var a=Error().stack.toString().split("\n");"Error"==a[0]&&a.shift();Fd(a);Z.Vb=Ed(a[3]);Z.$b=a;return Z.Vb}function Kb(a){a=Z[a>>>0];if(!a)return 0;var b;if(b=/^\s+at .*\.wasm\.(.*) \(.*\)$/.exec(a))a=b[1];else if(b=/^\s+at (.*) \(.*\)$/.exec(a))a=b[1];else if(b=/^(.+?)@/.exec(a))a=b[1];else return 0;Kc(Kb.Wb??0);b=Dd(a)+1;var d=Lc(b);d&&Y(a,d,b);Kb.Wb=d;return Kb.Wb} +function Lb(a){a>>>=0;var b=(v(),A).length;if(a<=b||4294901760=d;d*=2){var c=b*(1+.2/d);c=Math.min(c,a+100663296);a:{c=(Math.min(4294901760,65536*Math.ceil(Math.max(a,c)/65536))-x.buffer.byteLength+65535)/65536|0;try{x.grow(c);qa();var e=1;break a}catch(f){}e=void 0}if(e)return!0}return!1} +function Nb(a,b,d){a>>>=0;b>>>=0;if(Z.Vb==a)var c=Z.$b;else c=Error().stack.toString().split("\n"),"Error"==c[0]&&c.shift(),Fd(c);for(var e=3;c[e]&&Ed(c[e])!=a;)++e;for(a=0;a>>2>>>0]=Ed(c[a+e]);return a} +var Gd={},Id=()=>{if(!Hd){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8",_:ca||"./this.program"},b;for(b in Gd)void 0===Gd[b]?delete a[b]:a[b]=Gd[b];var d=[];for(b in a)d.push(`${b}=${a[b]}`);Hd=d}return Hd},Hd;function Ob(a,b){if(n)return U(19,1,a,b);a>>>=0;b>>>=0;var d=0,c=0,e;for(e of Id()){var f=b+d;(v(),D)[a+c>>>2>>>0]=f;d+=Y(e,f,Infinity)+1;c+=4}return 0} +function Pb(a,b){if(n)return U(20,1,a,b);a>>>=0;b>>>=0;var d=Id();(v(),D)[a>>>2>>>0]=d.length;a=0;for(var c of d)a+=Dd(c)+1;(v(),D)[b>>>2>>>0]=a;return 0}function Rb(a){return n?U(21,1,a):52}function Sb(a,b,d,c){return n?U(22,1,a,b,d,c):52}function Tb(a,b,d,c){return n?U(23,1,a,b,d,c):70}var Jd=[null,[],[]]; +function Ub(a,b,d,c){if(n)return U(24,1,a,b,d,c);b>>>=0;d>>>=0;c>>>=0;for(var e=0,f=0;f>>2>>>0],l=(v(),D)[b+4>>>2>>>0];b+=8;for(var u=0;u>>0],B=Jd[w];0===y||10===y?((1===w?oa:p)(sd(B)),B.length=0):B.push(y)}e+=l}(v(),D)[c>>>2>>>0]=e;return 0}function Fc(a){return a>>>0}n||ed();n||(x=new WebAssembly.Memory({initial:256,maximum:65536,shared:!0}),qa());h.wasmBinary&&(q=h.wasmBinary);h.stackSave=()=>N();h.stackRestore=a=>M(a);h.stackAlloc=a=>Sc(a); +h.setValue=function(a,b,d="i8"){d.endsWith("*")&&(d="*");switch(d){case "i1":(v(),z)[a>>>0]=b;break;case "i8":(v(),z)[a>>>0]=b;break;case "i16":(v(),Fa)[a>>>1>>>0]=b;break;case "i32":(v(),C)[a>>>2>>>0]=b;break;case "i64":(v(),H)[a>>>3>>>0]=BigInt(b);break;case "float":(v(),Ga)[a>>>2>>>0]=b;break;case "double":(v(),G)[a>>>3>>>0]=b;break;case "*":(v(),D)[a>>>2>>>0]=b;break;default:J(`invalid type for setValue: ${d}`)}}; +h.getValue=function(a,b="i8"){b.endsWith("*")&&(b="*");switch(b){case "i1":return(v(),z)[a>>>0];case "i8":return(v(),z)[a>>>0];case "i16":return(v(),Fa)[a>>>1>>>0];case "i32":return(v(),C)[a>>>2>>>0];case "i64":return(v(),H)[a>>>3>>>0];case "float":return(v(),Ga)[a>>>2>>>0];case "double":return(v(),G)[a>>>3>>>0];case "*":return(v(),D)[a>>>2>>>0];default:J(`invalid type for getValue: ${b}`)}};h.UTF8ToString=td;h.stringToUTF8=Y;h.lengthBytesUTF8=Dd; +var xd=[Gc,dd,qd,ab,bb,cb,db,eb,fb,gb,hb,ib,jb,kb,lb,mb,xb,yb,zb,Ob,Pb,Rb,Sb,Tb,Ub],wd={930988:(a,b,d,c,e)=>{if("undefined"==typeof h||!h.Rb)return 1;a=td(Number(a>>>0));a.startsWith("./")&&(a=a.substring(2));a=h.Rb.get(a);if(!a)return 2;b=Number(b>>>0);d=Number(d>>>0);c=Number(c>>>0);if(b+d>a.byteLength)return 3;try{const f=a.subarray(b,b+d);switch(e){case 0:(v(),A).set(f,c>>>0);break;case 1:h.fc?h.fc(c,f):h.ic(c,f);break;default:return 4}return 0}catch{return 4}},931812:()=>"undefined"!==typeof wasmOffsetConverter}; +function Qa(){return"undefined"!==typeof wasmOffsetConverter}var Jc,Kc,Lc,za,Ea,Mc,Nc,Oc,Pc,Qc,K,L,Rc,M,Sc,N,Tc,Uc,Vc,Wc,Xc,Pa;function bc(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function ac(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function qc(a){var b=N();try{V(a)()}catch(d){M(b);if(d!==d+0)throw d;K(1,0)}}function $b(a,b){var d=N();try{return V(a)(b)}catch(c){M(d);if(c!==c+0)throw c;K(1,0)}} +function sc(a,b,d){var c=N();try{V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function rc(a,b){var d=N();try{V(a)(b)}catch(c){M(d);if(c!==c+0)throw c;K(1,0)}}function fc(a,b,d,c,e,f,g){var l=N();try{return V(a)(b,d,c,e,f,g)}catch(u){M(l);if(u!==u+0)throw u;K(1,0)}}function wc(a,b,d,c,e,f){var g=N();try{V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}}function uc(a,b,d,c){var e=N();try{V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}} +function vc(a,b,d,c,e){var f=N();try{V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0)}}function xc(a,b,d,c,e,f,g){var l=N();try{V(a)(b,d,c,e,f,g)}catch(u){M(l);if(u!==u+0)throw u;K(1,0)}}function Ec(a,b,d,c,e,f,g){var l=N();try{V(a)(b,d,c,e,f,g)}catch(u){M(l);if(u!==u+0)throw u;K(1,0)}}function Dc(a,b,d,c,e,f,g,l){var u=N();try{V(a)(b,d,c,e,f,g,l)}catch(w){M(u);if(w!==w+0)throw w;K(1,0)}}function cc(a,b,d,c,e){var f=N();try{return V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0)}} +function lc(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function yc(a,b,d,c,e,f,g,l){var u=N();try{V(a)(b,d,c,e,f,g,l)}catch(w){M(u);if(w!==w+0)throw w;K(1,0)}}function Bc(a,b,d,c,e,f,g,l,u,w,y,B){var E=N();try{V(a)(b,d,c,e,f,g,l,u,w,y,B)}catch(F){M(E);if(F!==F+0)throw F;K(1,0)}}function ec(a,b,d,c,e,f){var g=N();try{return V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}} +function oc(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0);return 0n}}function zc(a,b,d,c,e,f,g,l,u){var w=N();try{V(a)(b,d,c,e,f,g,l,u)}catch(y){M(w);if(y!==y+0)throw y;K(1,0)}}function Zb(a){var b=N();try{return V(a)()}catch(d){M(b);if(d!==d+0)throw d;K(1,0)}}function nc(a,b){var d=N();try{return V(a)(b)}catch(c){M(d);if(c!==c+0)throw c;K(1,0);return 0n}}function mc(a){var b=N();try{return V(a)()}catch(d){M(b);if(d!==d+0)throw d;K(1,0);return 0n}} +function kc(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function jc(a,b,d,c,e){var f=N();try{return V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0)}}function ic(a,b,d,c,e,f){var g=N();try{return V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}}function dc(a,b,d,c,e,f){var g=N();try{return V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}} +function gc(a,b,d,c,e,f,g,l){var u=N();try{return V(a)(b,d,c,e,f,g,l)}catch(w){M(u);if(w!==w+0)throw w;K(1,0)}}function pc(a,b,d,c,e){var f=N();try{return V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0);return 0n}}function Yb(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function Wb(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}} +function hc(a,b,d,c,e,f,g,l,u,w,y,B){var E=N();try{return V(a)(b,d,c,e,f,g,l,u,w,y,B)}catch(F){M(E);if(F!==F+0)throw F;K(1,0)}}function Ac(a,b,d,c,e,f,g,l,u,w,y){var B=N();try{V(a)(b,d,c,e,f,g,l,u,w,y)}catch(E){M(B);if(E!==E+0)throw E;K(1,0)}}function Cc(a,b,d,c,e,f,g,l,u,w,y,B,E,F,Kd,Ld){var Md=N();try{V(a)(b,d,c,e,f,g,l,u,w,y,B,E,F,Kd,Ld)}catch(Ma){M(Md);if(Ma!==Ma+0)throw Ma;K(1,0)}}function Xb(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}} +function Vb(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function tc(a,b,d,c){var e=N();try{V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function Hc(){var a=I;a=Object.assign({},a);var b=c=>()=>c()>>>0,d=c=>e=>c(e)>>>0;a.sb=b(a.sb);a.ub=d(a.ub);a.Ib=d(a.Ib);a.Jb=b(a.Jb);a.Nb=d(a.Nb);return a}function xa(){if(0{ra=a;sa=b}); +;return moduleRtn}export default ortWasmThreaded;var isPthread=globalThis.self?.name?.startsWith("em-pthread");var isNode=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(isNode)isPthread=(await import("worker_threads")).workerData==="em-pthread";isPthread&&ortWasmThreaded(); diff --git a/public/models/onnxruntime/ort-wasm-simd-threaded.wasm b/public/models/onnxruntime/ort-wasm-simd-threaded.wasm new file mode 100644 index 0000000..7ab49a2 Binary files /dev/null and b/public/models/onnxruntime/ort-wasm-simd-threaded.wasm differ diff --git a/public/models/opencv/sface/LICENSE b/public/models/opencv/sface/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/public/models/opencv/sface/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/public/models/opencv/sface/face_recognition_sface_2021dec.onnx b/public/models/opencv/sface/face_recognition_sface_2021dec.onnx new file mode 100644 index 0000000..6767be0 Binary files /dev/null and b/public/models/opencv/sface/face_recognition_sface_2021dec.onnx differ diff --git a/public/models/opencv/yunet/face_detection_yunet_2023mar.onnx b/public/models/opencv/yunet/face_detection_yunet_2023mar.onnx new file mode 100644 index 0000000..f9beb30 Binary files /dev/null and b/public/models/opencv/yunet/face_detection_yunet_2023mar.onnx differ diff --git a/public/tauri.svg b/public/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/research/arbor-framecull-lab/ARBOR_CONTRACT.md b/research/arbor-framecull-lab/ARBOR_CONTRACT.md new file mode 100644 index 0000000..22ff645 --- /dev/null +++ b/research/arbor-framecull-lab/ARBOR_CONTRACT.md @@ -0,0 +1,88 @@ +# Arbor Contract: FrameCull Pro Research Lab + +## Target + +- Target project: `C:\Users\29238\Documents\筛图app` +- Research workspace: `research/arbor-framecull-lab` +- Current branch at setup: `codex/apple-ui-redesign` +- Mode: skill-only Arbor reconstruction, smoke/setup mode. + +## Task + +把 FrameCull 的 AI / Pro 模型研发工作整理成 Arbor 风格的持久研究空间:目标、数据、指标、实验、证据和报告分层管理,同时不改动软件开发主体。 + +## Metric + +Primary research metrics: + +- low-ratio AI Pick recall at `38% / 45% / 50% / 60%`; +- false-face risk by scene, especially student-layer full-dataset `grounded - flat`; +- negative pick rate; +- duplicate pollution; +- model latency and package size. + +Direction: + +- maximize recall under hard gates; +- minimize false-face risk, negative picks, duplicate pollution, latency, and package size. + +## Baseline Anchor + +Known current evidence: + +- v11 semantic best recall at 45%: `56.85%`. +- current production recall at 45% in the same summary: `42.19%`. +- v11 recall trade-off: `+14.66% @45%`. +- false-face v11 student-layer original delta: `+0.2883`, target `< +0.05`, status `FAIL`. +- old false-face v1 delta: `+0.3032`. + +These are evidence anchors, not final product claims. + +## Evaluation Policy + +- B_dev: cached lab outputs and paper snapshots under `output/`. +- B_test: any future held-out shoot / new user dataset. Do not use B_test for routine iteration. +- Expensive eval, GPU jobs, downloads, package installs, or retraining require explicit user approval. +- Cached evidence may be used for planning, reporting, and smoke Arbor plumbing. + +## Protected Paths + +Do not modify as part of R&D workspace restructuring: + +- `src/` +- `src-tauri/src/` +- `src-tauri/tauri*.conf.json` +- `package.json` +- `pnpm-lock.yaml` +- product README files unless the user asks for product docs +- raw datasets on external drives +- remote server data except through explicit sync/archive tasks + +Allowed edit surface for this setup: + +- `research/arbor-framecull-lab/**` + +## Scope Preference + +Mixed: + +- effect-leaning for measurable recall / false-face / latency improvements; +- novelty-leaning for teacher-student research ideas that may support a paper. + +## Budget + +This setup run is smoke/setup only: + +- no training; +- no model inference; +- no package install; +- no source refactor; +- no branch merge. + +Future real runs should set explicit cycle/time/GPU budgets. + +## Human Gate + +Interaction mode: review. + +The agent may create research plans, indexes, and Arbor session artifacts. Product code changes, long jobs, and merge attempts require user review. diff --git a/research/arbor-framecull-lab/README.md b/research/arbor-framecull-lab/README.md new file mode 100644 index 0000000..f635ada --- /dev/null +++ b/research/arbor-framecull-lab/README.md @@ -0,0 +1,50 @@ +# FrameCull Arbor R&D Workspace + +这个目录是 FrameCull Pro / AI 研发的 Arbor 风格工作区。 + +它只做三件事: + +1. 记录研发合同、指标、数据、实验与结论。 +2. 给后续 teacher / student / pick-ranker 实验提供可追溯的 Idea Tree。 +3. 给论文写作保留稳定入口。 + +它不做三件事: + +1. 不移动 `src/`、`src-tauri/`、`tools/`、`output/` 里的现有文件。 +2. 不替代软件开发工作区。 +3. 不把大模型、训练缓存、原图数据复制进来。 + +## 目录 + +- `ARBOR_CONTRACT.md`: 本研发工作区的约束和目标。 +- `research_config.yaml`: Arbor 风格机器可读配置。 +- `.arbor/sessions/`: 每次研究 run 的 Idea Tree、报告和事件。 +- `registry/`: 数据、指标、产物、保护路径索引。 +- `experiments/`: 手工或后续 executor 写的实验卡片。 +- `benchmarks/`: benchmark 入口说明,不存放大数据。 +- `paper/`: 论文写作入口和快照索引。 + +## 当前主线 + +当前主线不是继续调产品 UI,而是研究: + +- heavy teacher 如何产生可解释语义标签; +- student 蒸馏后哪些信号能传导,哪些不能; +- Pro 模型能否提升低比例精选召回; +- false-face 风险如何从 teacher 层传导到 student 层; +- 哪些结果可以写成论文证据,哪些只能作为负结果。 + +## 快速入口 + +- 最新论文快照:`../../output/paper-artifacts/semantic-teacher-lab/latest-snapshot.json` +- 最新论文摘要:`../../output/paper-artifacts/semantic-teacher-lab/latest-paper-summary.md` +- false-face v11 闭环报告:`../../output/semantic-false-face-diagnosis/v11-final/false-face-closure-report.md` +- Pro Semantic Teacher 任务书:`../../docs/GOAL_pro_semantic_teacher_lab.md` +- false-face 修复任务:`../../docs/TASK_fix_semantic_false_face.md` +- false-face 闭环任务:`../../docs/TASK_close_false_face_evidence.md` + +## 工作规则 + +新增实验先写 `experiments/-.md` 或进入 `.arbor/sessions//`。 + +实验通过后再考虑是否进入 `tools/` 或产品代码。产品代码改动仍走正常开发流程,不在这个目录直接“顺手改”。 diff --git a/research/arbor-framecull-lab/benchmarks/README.md b/research/arbor-framecull-lab/benchmarks/README.md new file mode 100644 index 0000000..86aa3c1 --- /dev/null +++ b/research/arbor-framecull-lab/benchmarks/README.md @@ -0,0 +1,16 @@ +# Benchmark Entry Points + +这个目录只记录 benchmark 入口,不保存大数据。 + +## Cached / Cheap + +- false-face closure: `../../output/semantic-false-face-diagnosis/v11-final/false-face-delta-summary.json` +- paper summary: `../../output/paper-artifacts/semantic-teacher-lab/latest-paper-summary.md` + +## Potential Real Eval + +- Pro persona eval: `../../tools/ai-lab/bench-pro-persona.mjs` +- Pro semantic student eval: `../../tools/ai-lab/bench-pro-semantic-student.mjs` +- supervised pick tuning: `../../tools/ai-lab/tune-ai-picks-supervised.mjs` + +Real eval may run browser inference, model inference, or server jobs. Do not run it from Arbor setup without explicit approval. diff --git a/research/arbor-framecull-lab/experiments/README.md b/research/arbor-framecull-lab/experiments/README.md new file mode 100644 index 0000000..798341f --- /dev/null +++ b/research/arbor-framecull-lab/experiments/README.md @@ -0,0 +1,26 @@ +# Experiment Cards + +每个实验一个 Markdown 卡片,建议格式: + +```text +# YYYY-MM-DD experiment name + +## Hypothesis + +## Data + +## Metric + +## Command + +## Result + +## Decision +``` + +规则: + +- 没有明确指标的想法先进入 Idea Tree,不直接训练。 +- 用 B_dev 调参,不用 B_test 日常迭代。 +- 任何长训练、下载、包安装、GPU job 都先征得用户同意。 +- 产物放在 `output/` 或服务器 `/data/FrameCullModelLab/outputs/`,本目录只放卡片和索引。 diff --git a/research/arbor-framecull-lab/paper/README.md b/research/arbor-framecull-lab/paper/README.md new file mode 100644 index 0000000..f253bfc --- /dev/null +++ b/research/arbor-framecull-lab/paper/README.md @@ -0,0 +1,22 @@ +# Paper Evidence Entry + +论文相关证据优先从这里进入: + +1. `../../output/paper-artifacts/semantic-teacher-lab/latest-paper-summary.md` +2. `../../output/paper-artifacts/semantic-teacher-lab/latest-snapshot.json` +3. `../../output/paper-artifacts/semantic-teacher-lab/snapshot-history.jsonl` +4. `../../output/semantic-false-face-diagnosis/v11-final/false-face-closure-report.md` + +## 写作口径 + +- 正结果:semantic / persona 路线对低比例召回有提升迹象。 +- 负结果:false-face teacher prompt 子集有效,但 student 层没有充分传导。 +- 边界:不要把 teacher 子集 `-0.4602` 写成 full student 闭环。 +- 生产边界:Pro semantic 仍是实验候选,不是默认生产策略。 + +## 下一轮论文需要补的证据 + +- 更干净的 held-out test set。 +- product_object / documentary_moment / event 的 false-face 专项验证集。 +- student loss reweighting 前后对比。 +- 模型大小、耗时和召回三者 trade-off 曲线。 diff --git a/research/arbor-framecull-lab/registry/artifact-map.md b/research/arbor-framecull-lab/registry/artifact-map.md new file mode 100644 index 0000000..218aabf --- /dev/null +++ b/research/arbor-framecull-lab/registry/artifact-map.md @@ -0,0 +1,37 @@ +# Artifact Map + +这里记录“去哪里找证据”,不复制大文件。 + +## Paper Snapshots + +- Latest snapshot metadata: `../../output/paper-artifacts/semantic-teacher-lab/latest-snapshot.json` +- Latest paper summary: `../../output/paper-artifacts/semantic-teacher-lab/latest-paper-summary.md` +- Snapshot history: `../../output/paper-artifacts/semantic-teacher-lab/snapshot-history.jsonl` +- Latest collected snapshot folder: `../../output/paper-artifacts/semantic-teacher-lab/semantic-teacher-paper-20260625-170818-paper-log-results-20260626` +- Latest collected snapshot zip: `../../output/paper-artifacts/semantic-teacher-lab/semantic-teacher-paper-20260625-170818-paper-log-results-20260626.zip` + +## False-Face Diagnosis + +- v11 closure report: `../../output/semantic-false-face-diagnosis/v11-final/false-face-closure-report.md` +- v11 scene comparison: `../../output/semantic-false-face-diagnosis/v11-final/false-face-scene-comparison.csv` +- v11 delta summary: `../../output/semantic-false-face-diagnosis/v11-final/false-face-delta-summary.json` +- v11 samples: `../../output/semantic-false-face-diagnosis/v11-final/false-face-samples.csv` +- v11 metrics by scene: `../../output/semantic-false-face-diagnosis/v11-final/metrics-by-scene.csv` + +## Pro Model Artifacts + +- Local Pro models: `../../output/pro-models` +- ConvNeXt persona INT8 package: `../../output/pro-models/convnext_persona_v1_linear_int8_final` + +## AI Bench + +- AI bench root: `../../output/ai-bench` +- Flash / pick-head scripts: `../../tools/ai-lab` +- Pro train scripts: `../../tools/pro-train` + +## Planning Docs + +- Pro semantic teacher goal: `../../docs/GOAL_pro_semantic_teacher_lab.md` +- Pro model architecture: `../../docs/PRO_MODEL_ARCHITECTURE.md` +- Pro infer layer goal: `../../docs/GOAL_pro_infer_layer.md` +- Semantic paper artifacts: `../../docs/PRO_SEMANTIC_PAPER_ARTIFACTS.md` diff --git a/research/arbor-framecull-lab/registry/metric-map.md b/research/arbor-framecull-lab/registry/metric-map.md new file mode 100644 index 0000000..ac2b1e4 --- /dev/null +++ b/research/arbor-framecull-lab/registry/metric-map.md @@ -0,0 +1,37 @@ +# Metric Map + +## Pick Quality + +| Metric | Direction | Meaning | Current Evidence | +|---|---:|---|---| +| low-ratio recall | maximize | 人工可用片在 AI Pick 中被覆盖的比例 | `output/.../summary.md` and `metrics-by-ratio.csv` | +| negative pick rate | minimize | 0 星/无星/淘汰片混入率 | `metrics-by-ratio.csv` | +| duplicate pollution | minimize | 重复/连拍非代表混入 | `duplicate-pollution*.csv` | +| selected similar adjacent pairs | minimize | 相邻相似图被同时选中数量 | `metrics-by-ratio.csv` | + +## Semantic / False-Face + +| Metric | Direction | Meaning | Current Evidence | +|---|---:|---|---| +| false_face_risk_mean | minimize | 每个 scene 的假脸风险均值 | `metrics-by-scene.csv` | +| student-layer grounded-flat delta | minimize | 学生层全量 `grounded - flat` false-face risk | `false-face-delta-summary.json` | +| face verdict coverage | maximize | teacher 是否真正给出 face-region 判据 | `teacher-quality-report.json` | +| uncertain rate | minimize | teacher 输出不确定比例 | `teacher-quality-report.json` | + +## Performance / Packaging + +| Metric | Direction | Meaning | Current Evidence | +|---|---:|---|---| +| latency ms/image | minimize | Pro 推理单图耗时 | `pro-infer-latency.csv` | +| model bytes | minimize | fp32/int8 模型大小 | `export-report.json` | +| active EP | descriptive | CPU / DirectML / CUDA 实际后端 | `summary.md` | + +## Current Anchors + +- Semantic best recall at 45%: `56.85%`. +- Current production recall at 45%: `42.19%`. +- Semantic recall trade-off: `+14.66% @45%`. +- False-face v1 original delta: `+0.3032`. +- False-face v11 original delta: `+0.2883`. +- False-face target: `< +0.05`. +- False-face status: not closed. diff --git a/research/arbor-framecull-lab/registry/protected-paths.md b/research/arbor-framecull-lab/registry/protected-paths.md new file mode 100644 index 0000000..737bd2f --- /dev/null +++ b/research/arbor-framecull-lab/registry/protected-paths.md @@ -0,0 +1,29 @@ +# Protected Paths + +这个研发工作区的第一条规则是:不把研发整理动作混进软件开发主体。 + +## 不在本工作区任务里改 + +- `src/` +- `src-tauri/src/` +- `src-tauri/tauri*.conf.json` +- `package.json` +- `pnpm-lock.yaml` +- `README.md` +- `docs/README_CN.md` +- 外部原始数据目录,例如 `D:\FrameCullRawAudit\...` +- 服务器 `/data/FrameCullModelLab/...` 原始数据,除非用户明确要求同步或归档 + +## 可以改 + +- `research/arbor-framecull-lab/**` + +## 可以引用但不移动 + +- `docs/GOAL_pro_semantic_teacher_lab.md` +- `docs/TASK_fix_semantic_false_face.md` +- `docs/TASK_close_false_face_evidence.md` +- `output/paper-artifacts/semantic-teacher-lab/**` +- `output/semantic-false-face-diagnosis/**` +- `output/pro-models/**` +- `output/ai-bench/**` diff --git a/research/arbor-framecull-lab/research_config.yaml b/research/arbor-framecull-lab/research_config.yaml new file mode 100644 index 0000000..a4d25ab --- /dev/null +++ b/research/arbor-framecull-lab/research_config.yaml @@ -0,0 +1,57 @@ +task: > + Organize FrameCull Pro / AI research into an Arbor-style durable workspace + without changing product source code. Track goals, metrics, datasets, + evidence artifacts, Idea Tree state, and paper-facing reports for teacher, + student, pick-ranker, false-face, and package/performance experiments. + +coordinator: + max_cycles: 1 + ui: + interaction_mode: review + +run: + mode: smoke_setup + session_name: workspace-reorg-20260626 + trunk_branch: arbor/trunk/framecull-pro-research + +metric: + direction: mixed + primary: + - name: low_ratio_recall + direction: maximize + - name: false_face_risk_delta + direction: minimize + - name: negative_pick_rate + direction: minimize + - name: duplicate_pollution + direction: minimize + - name: latency_ms_per_image + direction: minimize + +protected_paths: + - src/** + - src-tauri/src/** + - src-tauri/tauri*.conf.json + - package.json + - pnpm-lock.yaml + - README.md + - docs/README_CN.md + +allowed_edit_surface: + - research/arbor-framecull-lab/** + +evidence_roots: + - output/paper-artifacts/semantic-teacher-lab + - output/semantic-false-face-diagnosis + - output/pro-models + - output/ai-bench + - docs + +expensive_actions_require_approval: + - training + - gpu_jobs + - full_inference + - package_install + - downloads + - product_source_edits + - merge_attempts diff --git a/research/framecull-pro-semantic-teacher-lab/fields.yaml b/research/framecull-pro-semantic-teacher-lab/fields.yaml new file mode 100644 index 0000000..a1ee627 --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/fields.yaml @@ -0,0 +1,26 @@ +fields: + overview: + description: What this item contributes to FrameCull Pro Semantic Teacher Lab. + detail_level: moderate + evidence: + description: Primary papers, docs, repositories, or local project specs supporting the item. + detail_level: detailed + applicability: + description: How the item can be used in FrameCull without overextending beyond evidence. + detail_level: detailed + implementation_tasks: + description: Concrete tasks or code paths needed to realize the item. + detail_level: detailed + validation: + description: Metrics, artifacts, or checks that decide whether the item passes. + detail_level: detailed + risks: + description: Technical, licensing, data leakage, performance, or product risks. + detail_level: moderate + go_no_go: + description: Decision threshold for keeping, changing, or dropping this item. + detail_level: moderate +uncertain: + - exact CVPR 2026 final paper list may change before proceedings are fully stable + - InternVL candidate license and memory footprint require live verification before official teacher use + - final Pro Student V2 performance depends on real full-run teacher labels and 6GB hardware validation diff --git a/research/framecull-pro-semantic-teacher-lab/outline.yaml b/research/framecull-pro-semantic-teacher-lab/outline.yaml new file mode 100644 index 0000000..fcf787c --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/outline.yaml @@ -0,0 +1,33 @@ +topic: FrameCull Pro Semantic Teacher Lab +created_at: 2026-06-22 +status: planned +primary_output: docs/PRO_SEMANTIC_TEACHER_LAB_RESEARCH_TASK.md +items: + - name: CVPR 2026 grounded visual reasoning + category: research_inspiration + description: Extract method-level ideas from G2VLM, GR3D, DeepScan, and related grounded reasoning work without binding the product to a single paper. + - name: Semantic teacher selection + category: teacher_model + description: Compare Qwen2.5-VL and InternVL-style candidates for local 5090 offline labeling, JSON reliability, grounding quality, license suitability, and memory footprint. + - name: Teacher schema and quality gates + category: experiment_design + description: Define grounded JSONL records with reasoningTrace, faceRegionVerdicts, semanticKeepScore, and QA-only fields that must not become student heads. + - name: Quality and embedding teachers + category: feature_teacher + description: Generate MUSIQ, CLIP, and DINOv2 teacher features, including mandatory dino[768], for non-VLM distillation targets. + - name: Pro Student V2 distillation + category: student_model + description: Train ConvNeXt-Tiny multi-head student first, keep small ViT as optional only after 6GB DirectML/CoreML feasibility. + - name: Evaluation and product gate + category: validation + description: Evaluate recall, false positives, duplicate pollution, false-face behavior, grounded-vs-flat ablation, and Pro latency before any product entry. +execution: + batch_size: 2 + items_per_agent: 1 + output_dir: research/framecull-pro-semantic-teacher-lab/results +constraints: + - Keep Flash wasm workers unchanged. + - Keep all heavy cache and model files under /data/FrameCullModelLab. + - Use rating>=3 for audit3groups and rating>=1 for camera/other datasets. + - Do not use rating, folder, path, or filename as ranking features. + - Teacher reads high-resolution originals or prepared high-resolution teacher JPEGs, never 384 previews. diff --git a/research/framecull-pro-semantic-teacher-lab/results/cvpr_2026_grounded_visual_reasoning.json b/research/framecull-pro-semantic-teacher-lab/results/cvpr_2026_grounded_visual_reasoning.json new file mode 100644 index 0000000..beaf52f --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/results/cvpr_2026_grounded_visual_reasoning.json @@ -0,0 +1,46 @@ +{ + "overview": "CVPR 2026 grounded and reasoning-oriented vision-language work is useful to FrameCull as methodology, not as a direct product dependency. The reusable idea is to require visual evidence before scoring: regions, observations, and links from evidence to keep/reject decisions.", + "evidence": [ + { + "name": "Mirage: Machine Mental Imagery for Multimodal Reasoning", + "source": "https://openaccess.thecvf.com/content/CVPR2026/html/Dai_Mirage_Machine_Mental_Imagery_for_Multimodal_Reasoning_CVPR_2026_paper.html", + "use": "Supports the idea that multimodal reasoning can benefit from internal visual representations instead of only text explanations." + }, + { + "name": "G2VLM: Geometry Grounded Vision-Language Model with Unified 3D Reconstruction", + "source": "https://openaccess.thecvf.com/content/CVPR2026/html/Hu_G2VLM_Geometry_Grounded_Vision_Language_Model_with_Unified_3D_Reconstruction_CVPR_2026_paper.html", + "use": "Supports grounding visual-language reasoning in spatial or geometric evidence." + }, + { + "name": "DeepScan: A Training-Free Framework for Visually Grounded Reasoning in Large Vision-Language Models", + "source": "https://openaccess.thecvf.com/content/CVPR2026/html/Li_DeepScan_A_Training-Free_Framework_for_Visually_Grounded_Reasoning_in_Large_CVPR_2026_paper.html", + "use": "Supports evidence-first scanning before final reasoning." + }, + { + "name": "Grounded Chain-of-Thought for Multimodal Large Language Models", + "source": "https://openaccess.thecvf.com/content/CVPR2026/html/Jiang_Grounded_Chain-of-Thought_for_Multimodal_Large_Language_Models_CVPR_2026_paper.html", + "use": "Supports explicit grounding during reasoning steps." + } + ], + "applicability": "FrameCull should not attempt to ship these large research models. It should use their method-level lesson: a semantic teacher must produce grounded records with region-level evidence and a flat-scalar ablation must prove whether grounding adds measurable value.", + "implementation_tasks": [ + "Keep reasoningTrace mandatory in grounded teacher mode.", + "Keep faceRegionVerdicts mandatory for suspicious face-like regions.", + "Normalize all region boxes to 0..1 and validate them before training.", + "Run a matched flat-scalar ablation using the same images and teacher model.", + "Write grounded-vs-flat-ablation.md before any product recommendation." + ], + "validation": [ + "Teacher JSONL passes schema validation.", + "Grounded records have non-empty reasoningTrace coverage.", + "False-face samples include region evidence rather than only scalar risk.", + "Grounded student beats flat-scalar student on low-ratio recall or false-face reduction." + ], + "risks": [ + "Research papers may not be directly reproducible in the local product pipeline.", + "Teacher reasoning text can look plausible while visual evidence is wrong.", + "A student model may not learn the grounded structure from scalar supervision alone." + ], + "go_no_go": "Keep the grounded semantic path only if it outperforms flat-scalar at 38%, 45%, or 50% recall, or reduces false-face errors, without increasing hard issue picks or duplicate pollution.", + "uncertain": [] +} diff --git a/research/framecull-pro-semantic-teacher-lab/results/evaluation_and_product_gate.json b/research/framecull-pro-semantic-teacher-lab/results/evaluation_and_product_gate.json new file mode 100644 index 0000000..99ece90 --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/results/evaluation_and_product_gate.json @@ -0,0 +1,42 @@ +{ + "overview": "The final decision must be metric-driven. The semantic model is useful only if it improves real culling outcomes under the existing hard-fault, duplicate, and rejected-photo constraints.", + "evidence": [ + { + "name": "Supervised AI pick tuner", + "source": "tools/ai-lab/tune-ai-picks-supervised.mjs", + "use": "Existing evaluation structure for ratio-aware AI pick metrics." + }, + { + "name": "Pro semantic bench", + "source": "tools/ai-lab/bench-pro-semantic-student.mjs", + "use": "Evaluation output path for semantic student experiments." + }, + { + "name": "Goal document", + "source": "docs/GOAL_pro_semantic_teacher_lab.md", + "use": "Defines required reports and product gates." + } + ], + "applicability": "Evaluate current rules, ratio-aware rules, Pro persona v1, semantic-only, fused semantic, face-guard, and flat-scalar at 38%, 45%, 50%, and 60%. Report per dataset and per scene rather than hiding failures in an average.", + "implementation_tasks": [ + "Run matched grounded and flat-scalar teacher/student arms.", + "Generate metrics-by-ratio.csv and metrics-by-scene.csv.", + "Generate false-negatives-by-ratio.csv, duplicate-pollution-by-ratio.csv, and false-face-samples.csv.", + "Generate pro-infer-latency.csv.", + "Write production-recommendation.md with go/no-go and exact next action." + ], + "validation": [ + "G-drive/audit3groups uses rating>=3.", + "Camera/other datasets use rating>=1.", + "Hard issue picked count is zero.", + "Duplicate non-representatives are not rescued by semantic score.", + "Grounded-vs-flat ablation is present and uses matched inputs." + ], + "risks": [ + "Averaging datasets with different label meanings can create false improvements.", + "Semantic scores can improve recall by simply admitting too many weak positives.", + "Speed may be unacceptable on DirectML or CPU fallback even if accuracy improves." + ], + "go_no_go": "Enter Pro gated ranking only if any low ratio improves by at least 5% recall or 4/5-star coverage improves by at least 8%, negative mixing worsens by no more than 2%, duplicate pollution does not worsen, hard issue picked remains zero, and latency is acceptable.", + "uncertain": [] +} diff --git a/research/framecull-pro-semantic-teacher-lab/results/pro_student_v2_distillation.json b/research/framecull-pro-semantic-teacher-lab/results/pro_student_v2_distillation.json new file mode 100644 index 0000000..dc146be --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/results/pro_student_v2_distillation.json @@ -0,0 +1,45 @@ +{ + "overview": "Pro Student V2 should be a shared-backbone multi-head model, with ConvNeXt-Tiny as the first practical baseline. It distills semantic teacher outputs, quality features, embeddings, and persona labels into a model that can run through the Pro native inference layer.", + "evidence": [ + { + "name": "Pro model architecture", + "source": "docs/PRO_MODEL_ARCHITECTURE.md", + "use": "Defines Pro native inference, 384 input, shared backbone, and Flash isolation." + }, + { + "name": "Semantic student trainer", + "source": "tools/pro-train/train_semantic_student.py", + "use": "Current implementation path for grounded teacher student training." + }, + { + "name": "Semantic ONNX exporter", + "source": "tools/pro-train/export_pro_semantic_onnx.py", + "use": "Exports FP32 and INT8 manifests compatible with Pro inference." + } + ], + "applicability": "The student should learn product-facing heads: aesthetic, scene, personaScore, semanticKeepScore, faceValidityScore, falseFaceRisk, composition, moment, and lightingMood. QA-only fields remain for auditing.", + "implementation_tasks": [ + "Train ConvNeXt-Tiny first with 384 student previews.", + "Use dataset-specific positive thresholds: audit3groups rating>=3, camera rating>=1.", + "Use grouped split by shoot/source to reduce burst leakage.", + "Export FP32 and INT8 ONNX packages.", + "Write quant-compare.json and selected-model-manifest.json.", + "Keep Flash isolated from ort, ndarray, ONNX model files, and Pro calls." + ], + "validation": [ + "student-best.pt and training-report.json are written.", + "FP32 and INT8 exports load through Pro native inference.", + "INT8 drift stays under the configured threshold.", + "Bad image returns per-image error without failing the batch.", + "Latency report covers DirectML and CPU fallback where available." + ], + "risks": [ + "A small student may not preserve grounded teacher behavior.", + "INT8 quantization can change ranking enough to hurt recall.", + "Scene imbalance can make the semantic head overfit outdoor/person-heavy data." + ], + "go_no_go": "Promote only if low-ratio recall or 4/5-star coverage improves while hard issue picks remain zero and duplicate pollution does not increase.", + "uncertain": [ + "Small ViT remains optional until 6GB DirectML/CoreML feasibility is proven." + ] +} diff --git a/research/framecull-pro-semantic-teacher-lab/results/quality_and_embedding_teachers.json b/research/framecull-pro-semantic-teacher-lab/results/quality_and_embedding_teachers.json new file mode 100644 index 0000000..9bcd655 --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/results/quality_and_embedding_teachers.json @@ -0,0 +1,46 @@ +{ + "overview": "MUSIQ, CLIP, and DINOv2 provide non-VLM teacher targets for quality and embedding distillation. DINOv2 dino[768] is mandatory because it gives a strong visual representation that does not depend on language prompts.", + "evidence": [ + { + "name": "DINOv2", + "source": "https://arxiv.org/abs/2304.07193", + "use": "Self-supervised visual embedding teacher, required dino[768]." + }, + { + "name": "DINOv2 repository", + "source": "https://github.com/facebookresearch/dinov2", + "use": "Implementation and license source." + }, + { + "name": "CLIP", + "source": "https://arxiv.org/abs/2103.00020", + "use": "Language-aligned embedding teacher." + }, + { + "name": "MUSIQ", + "source": "https://arxiv.org/abs/2108.05997", + "use": "Image quality and aesthetic score teacher." + } + ], + "applicability": "These teachers should train student heads and embedding alignment, not directly override hard-fault rules. Their outputs are useful as distillation targets and ablation features.", + "implementation_tasks": [ + "Run build_quality_teacher_features.py on camera and audit preview sets.", + "Generate teacher-camera.npz and teacher-audit3groups.npz.", + "Fail if DINOv2 output is not exactly 768 dimensions.", + "Record feature summary and model/cache path under /data/FrameCullModelLab.", + "Keep these features separate from high-resolution VLM teacher input." + ], + "validation": [ + "teacher-feature-summary.json exists.", + "Each dataset NPZ includes musiq_tech, musiq_aes, clip[512], and dino[768].", + "Training script verifies dinoDim=768.", + "Missing features are reported and not silently imputed for official runs." + ], + "risks": [ + "MUSIQ and CLIP are generic and may not reflect personal culling preference.", + "Feature extraction can be slow or fail due to dependency/model downloads.", + "Using low-resolution previews for all teacher signals would weaken semantic supervision." + ], + "go_no_go": "Do not run official semantic student training without real DINOv2 features. Fake features are allowed only for plumbing smoke.", + "uncertain": [] +} diff --git a/research/framecull-pro-semantic-teacher-lab/results/semantic_teacher_selection.json b/research/framecull-pro-semantic-teacher-lab/results/semantic_teacher_selection.json new file mode 100644 index 0000000..39ffc23 --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/results/semantic_teacher_selection.json @@ -0,0 +1,43 @@ +{ + "overview": "The first official semantic teacher should be Qwen2.5-VL-7B-Instruct because it is open, locally runnable on the 5090 server, supports strong visual understanding and structured output, and has a clearer commercial path than many research-only alternatives.", + "evidence": [ + { + "name": "Qwen2.5-VL Technical Report", + "source": "https://arxiv.org/abs/2502.13923", + "use": "Primary technical reference for the chosen VLM teacher family." + }, + { + "name": "Qwen2.5-VL-7B-Instruct model page", + "source": "https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct", + "use": "Model distribution and license check source." + }, + { + "name": "InternVL3 Technical Report", + "source": "https://arxiv.org/abs/2504.10479", + "use": "Comparison teacher candidate for robustness and cross-teacher agreement." + } + ], + "applicability": "Use Qwen2.5-VL as the cleared primary teacher if license clearance passes. Use InternVL3 only as a comparison or agreement-check teacher until memory footprint and license status are confirmed.", + "implementation_tasks": [ + "Run teacher_license_clearance.py before any full annotation.", + "Store Hugging Face cache and model files under /data/FrameCullModelLab.", + "Use local model directories for reproducibility after model download.", + "Record teacherModel, teacherVersion, model path, and license state in every report.", + "Do not let model-generated metadata override runner-owned provenance." + ], + "validation": [ + "teacher-license-clearance.md marks at least one teacher as cleared.", + "Qwen local model loads and produces schema-valid JSONL on smoke samples.", + "Failure CSV is written and resume works.", + "Flat-scalar mode also runs with the same teacher and image list." + ], + "risks": [ + "Large VLM outputs may hallucinate unsupported evidence.", + "JSON formatting can fail under long prompts.", + "InternVL variants may have license or VRAM limits that block official use." + ], + "go_no_go": "Proceed to full annotation only after license clearance, local load success, grounded smoke success, flat-scalar smoke success, and QA report pass.", + "uncertain": [ + "InternVL exact variant and license suitability require live confirmation before official labels." + ] +} diff --git a/research/framecull-pro-semantic-teacher-lab/results/teacher_schema_and_quality_gates.json b/research/framecull-pro-semantic-teacher-lab/results/teacher_schema_and_quality_gates.json new file mode 100644 index 0000000..cce078d --- /dev/null +++ b/research/framecull-pro-semantic-teacher-lab/results/teacher_schema_and_quality_gates.json @@ -0,0 +1,41 @@ +{ + "overview": "The teacher schema is the control surface that turns vague visual understanding into auditable training data. It must force region evidence, bounded numeric scores, scene enums, and explicit uncertainty before any student training.", + "evidence": [ + { + "name": "Local schema validator", + "source": "tools/pro-train/semantic_teacher_schema.py", + "use": "Defines framecull-semantic-teacher-v1 validation rules." + }, + { + "name": "Teacher runner", + "source": "tools/pro-train/run_semantic_teacher.py", + "use": "Generates grounded and flat-scalar teacher JSONL records." + }, + { + "name": "Teacher QA auditor", + "source": "tools/pro-train/audit_semantic_teacher.py", + "use": "Writes teacher quality reports and QA samples." + } + ], + "applicability": "Schema fields should map to Pro student heads only when they are product-facing and measurable. QA-only fields may remain in teacher records but must not become student heads unless a later product decision explicitly promotes them.", + "implementation_tasks": [ + "Validate schemaVersion, sceneType enum, all numeric scores in 0..1, and normalized boxes.", + "Map unknown scene strings to the closest allowed enum or other before validation.", + "Reject or quarantine records with empty grounded evidence in grounded mode.", + "Write teacher-quality-report.md and teacher-qa-samples.csv for every teacher run.", + "Keep QA-only fields out of export_pro_semantic_onnx.py heads." + ], + "validation": [ + "semantic_teacher_schema.py passes grounded JSONL.", + "semantic_teacher_schema.py --allow-flat-scalar passes flat ablation JSONL.", + "QA report includes coverage, scene distribution, uncertainty, and false-face samples.", + "Student manifest contains only approved ProHeadScores-compatible heads." + ], + "risks": [ + "Over-strict schema can waste otherwise useful records.", + "Over-loose normalization can hide teacher failures.", + "Scene label drift can break training if enum mapping is not deterministic." + ], + "go_no_go": "Do not start full student training until grounded teacher JSONL, flat-scalar JSONL, and teacher QA all pass with recorded failure handling.", + "uncertain": [] +} diff --git a/research_config.yaml b/research_config.yaml new file mode 100644 index 0000000..905e9e0 --- /dev/null +++ b/research_config.yaml @@ -0,0 +1,20 @@ +task: > + FrameCull Pro Native RAW Preview Lab P1. Build an isolated smoke experiment + for native RAW preview decoding, focused on Nikon NEF support and speed. Keep + Flash isolated and do not replace the production RAW monitor path in this + round. +coordinator: + max_cycles: 1 + ui: + interaction_mode: review +metric: + name: decodeSuccessRate + direction: maximize +eval: + dev_command: "cd {cwd} && cargo run --manifest-path tools/pro-raw-preview-lab/Cargo.toml --release -- --input G:\\DCIM\\110NZ6_3\\_DSC0552.NEF --output output\\pro-raw-preview-lab\\p1-smoke" + test_command: "" +protected_paths: + - src/workers/aiAnalyzer.worker.ts + - src/workers/rawDecoder.worker.ts + - src-tauri/tauri.flash.conf.json + - public/models diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..b21bd68 --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..6a7aa11 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5733 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.11+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "framecull-ai" +version = "0.1.6" +dependencies = [ + "base64 0.22.1", + "image", + "kamadak-exif", + "ndarray", + "ort", + "rayon", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-opener", + "tauri-plugin-os", + "tauri-plugin-window-state", + "trash", + "windows-sys 0.61.2", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.61.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.13.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lzma-rust2" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "mutate_once" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "ort" +version = "2.0.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5df903c0d2c07b56950f1058104ab0c8557159f2741782223704de9be73c3c" +dependencies = [ + "ndarray", + "ort-sys", + "smallvec", + "tracing", + "ureq", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06503bb33f294c5f1ba484011e053bfa6ae227074bdb841e9863492dc5960d4b" +dependencies = [ + "hmac-sha256", + "lzma-rust2", + "ureq", +] + +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.114", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "http-range", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.17", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.11+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.114", + "tauri-utils", + "thiserror 2.0.17", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.11+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.11+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.10.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.17", + "toml 0.9.11+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trash" +version = "5.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b93a14fcf658568eb11b3ac4cb406822e916e2c55cdebc421beeb0bd7c94d8" +dependencies = [ + "chrono", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "percent-encoding", + "scopeguard", + "urlencoding", + "windows 0.56.0", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement 0.60.2", + "windows-interface 0.59.3", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.17", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f79257df967b6779afa536788657777a0001f5b42524fcaf5038d4344df40b" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.14", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad23e2d2f91cae771c7af7a630a49e755f1eb74f8a46e9f6d5f7a146edf5a37" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.14", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "326aaed414f04fe839777b4c443d4e94c74e7b3621093bd9c5e649ac8aa96543" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.14", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba44e1f8f4da9e6e2d25d2a60b116ef8b9d0be174a7685e55bb12a99866279a7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.114", + "winnow 0.7.14", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..027d468 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,83 @@ +[package] +name = "framecull-ai" +version = "0.1.6" +description = "FrameCull AI - Local AI photo culling workstation" +authors = ["AGPEIM"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "framecull_ai_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[[bin]] +name = "framecull-ai" +path = "src/main.rs" + +[[bin]] +name = "pro-infer-bench" +path = "src/pro-infer-bench.rs" +required-features = ["pro-bench"] + +[features] +default = [] +pro = ["dep:ort", "dep:ndarray", "dep:rayon", "dep:windows-sys"] +pro-bench = ["pro"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["protocol-asset"] } +tauri-plugin-opener = "2" +tauri-plugin-dialog = "2.6" +tauri-plugin-fs = "2" +tauri-plugin-window-state = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +kamadak-exif = "0.5" +trash = "5.1" +tauri-plugin-os = "2.3.2" +base64 = "0.22" +image = { version = "0.25", default-features = false, features = ["jpeg"] } + +# Pro-only native ONNX Runtime inference layer (process-internal `ort` binding). +# These crates must never enter the Flash dependency tree; they are gated behind +# the optional `pro` feature above and pulled in only via `dep:` activation. +# `ort` 2.0.0-rc.11 wraps ONNX Runtime 1.23 and depends on ndarray 0.17. +[dependencies.ort] +version = "=2.0.0-rc.11" +optional = true +default-features = false +features = ["std", "ndarray", "download-binaries", "copy-dylibs", "tracing", "tls-rustls"] + +[dependencies.ndarray] +version = "0.17" +optional = true + +[dependencies.rayon] +version = "1.10" +optional = true + +[target.'cfg(windows)'.dependencies.windows-sys] +version = "0.61" +optional = true +features = ["Win32_System_LibraryLoader"] + +# Execution-provider features are platform-conditioned so a build only compiles +# the EP backends that exist on the target. CPU is always the final fallback. +[target.'cfg(windows)'.dependencies.ort] +version = "=2.0.0-rc.11" +optional = true +default-features = false +features = ["std", "ndarray", "download-binaries", "copy-dylibs", "tracing", "tls-rustls", "cuda", "directml"] + +[target.'cfg(target_os = "macos")'.dependencies.ort] +version = "=2.0.0-rc.11" +optional = true +default-features = false +features = ["std", "ndarray", "download-binaries", "copy-dylibs", "tracing", "tls-rustls", "coreml"] diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..7ea9992 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,21 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default", + "dialog:default", + "fs:default", + "os:default", + "core:path:default", + "core:event:default", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-close", + "core:window:allow-is-maximized", + "window-state:default" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..e4eace3 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..e82a5ae Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..6234ce8 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..0a2be01 Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..aa526af Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..cd6adf8 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..513e21b Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..10aefee Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..1b8c611 Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..5f57112 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..c68d156 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..284b3be Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..a62d9b7 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..2a8655c Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..9aadf73 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ad492bf Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..aeac099 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..2774db7 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4d01853 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..c340eae Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..0338fd3 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2724445 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..cfaa021 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d498e7c Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..441a78c Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..81925e5 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..960af0a Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8f00b85 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..ecd802f Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..acc3c86 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..1184210 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..cc42256 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..cd4b52d Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..6ca44ec Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..6ca44ec Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..73a2a36 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..41cc2ef Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..8f17279 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..8f17279 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..fd4fa3d Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..6ca44ec Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..0c051e7 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..0c051e7 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..6c509af Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..91683d8 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..6c509af Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..e3be1d1 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..a6c2184 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..7fa28c5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..d2113d7 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/pro-models/placeholder/manifest.json b/src-tauri/pro-models/placeholder/manifest.json new file mode 100644 index 0000000..b359751 --- /dev/null +++ b/src-tauri/pro-models/placeholder/manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "backboneVersion": "placeholder-v0", + "model": "model.onnx", + "sha256": "9422da95e3e6296759c16378fd3aac5003c265fb6095c9ae69173014bccf4298", + "inputName": "pixel_values", + "inputResolution": 384, + "channels": 3, + "normalize": { + "mean": [ + 0.485, + 0.456, + 0.406 + ], + "std": [ + 0.229, + 0.224, + 0.225 + ] + }, + "heads": [ + { + "name": "aesthetic", + "output": "aesthetic", + "kind": "scalar01" + }, + { + "name": "scene", + "output": "scene_logits", + "kind": "classifier", + "labels": [ + "outdoor_portrait", + "indoor_portrait", + "scenery", + "other" + ] + }, + { + "name": "persona", + "output": "persona", + "kind": "scalar01" + } + ] +} diff --git a/src-tauri/pro-models/placeholder/model.onnx b/src-tauri/pro-models/placeholder/model.onnx new file mode 100644 index 0000000..197661b Binary files /dev/null and b/src-tauri/pro-models/placeholder/model.onnx differ diff --git a/src-tauri/pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json b/src-tauri/pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json new file mode 100644 index 0000000..f9edcab --- /dev/null +++ b/src-tauri/pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json @@ -0,0 +1,177 @@ +{ + "schemaVersion": 1, + "backboneVersion": "framecull-pro-semantic-v2-grounded-convnext-v14-five-mountain-region-int8-linear", + "model": "model.int8.onnx", + "sha256": "1bde98ea9c21b163558e6379b2a755ba8429b182a19dc59d93ba5ea768b8b504", + "inputName": "pixel_values", + "inputResolution": 384, + "channels": 3, + "normalize": { + "mean": [ + 0.485, + 0.456, + 0.406 + ], + "std": [ + 0.229, + 0.224, + 0.225 + ] + }, + "heads": [ + { + "name": "aesthetic", + "output": "aesthetic", + "kind": "scalar01" + }, + { + "name": "scene", + "output": "scene_logits", + "kind": "classifier", + "labels": [ + "animal", + "documentary_moment", + "empty_scene", + "environmental_portrait", + "event", + "food", + "group", + "landscape", + "other", + "portrait", + "product_object" + ] + }, + { + "name": "persona", + "output": "persona", + "kind": "scalar01" + }, + { + "name": "semantic_keep", + "output": "semantic_keep", + "kind": "scalar01" + }, + { + "name": "face_validity", + "output": "face_validity", + "kind": "scalar01" + }, + { + "name": "composition", + "output": "composition", + "kind": "scalar01" + }, + { + "name": "moment", + "output": "moment", + "kind": "scalar01" + }, + { + "name": "lighting", + "output": "lighting", + "kind": "scalar01" + }, + { + "name": "false_face_risk", + "output": "false_face_risk", + "kind": "scalar01" + } + ], + "labNotes": { + "semanticSchema": "framecull-semantic-teacher-v1", + "studentSchema": "framecull-pro-semantic-student-v2", + "teacherFlatScalar": false, + "studentCheckpoint": "/data/FrameCullModelLab/outputs/semantic-student/grounded-convnext-v14-five-mountain-region/student-best.pt", + "scalarHeads": [ + "semanticKeepScore", + "faceValidityScore", + "falseFaceRisk", + "compositionScore", + "momentScore", + "lightingMoodScore" + ], + "faceHeadSupervision": "faceValidityScore and independent falseFaceRisk heads with asymmetric hard-negative emphasis and high-risk scene oversampling", + "hasIndependentFalseFaceRiskHead": true, + "derivedOutputs": {}, + "dinoDim": 768, + "clipDim": 512, + "metrics": { + "sceneAccuracy": 0.8531390428543091, + "clipCosMean": 0.9182356595993042, + "dinoCosMean": 0.8495274186134338, + "aestheticCorr": { + "srcc": 0.9437538366979613, + "plcc": 0.941941499710083 + }, + "semanticKeepScoreCorr": { + "srcc": 0.8048753649670064, + "plcc": 0.834986686706543 + }, + "faceValidityScoreCorr": { + "srcc": 0.8774391815700302, + "plcc": 0.9113354682922363 + }, + "falseFaceRiskCorr": { + "srcc": 0.7629302501146534, + "plcc": 0.6803898215293884 + }, + "compositionScoreCorr": { + "srcc": 0.7259436073427525, + "plcc": 0.7443082332611084 + }, + "momentScoreCorr": { + "srcc": 0.7005775740922389, + "plcc": 0.7016218900680542 + }, + "lightingMoodScoreCorr": { + "srcc": 0.7284193810490783, + "plcc": 0.7144650220870972 + } + }, + "qaOnlyFieldsNotExported": [ + "storytellingScore", + "emptyOrFillerScore", + "technicalVisibleIssueScore", + "scenicValueScore" + ], + "quantization": { + "strategy": "dynamic-matmul-gemm", + "errors": [] + }, + "personaHead": { + "schema": "framecull-pro-persona-head-v2", + "studentSchema": "framecull-pro-semantic-student-v2", + "studentType": "semantic-student-v2", + "ratingSrcc": 0.39020828756080644, + "valMetrics": { + "auc": 0.7528432904876323, + "ap": 0.5633225668322066, + "mean_pos_score": 0.6618244051933289, + "mean_neg_score": 0.482837051153183 + }, + "allMetrics": { + "auc": 0.7757031057392646, + "ap": 0.5997640160505309, + "mean_pos_score": 0.6715916991233826, + "mean_neg_score": 0.4783063232898712 + }, + "labelPolicy": { + "camera": { + "positive_threshold": 1, + "description": "rating>=1 positive; rating==0 or missing negative" + }, + "five_mountain": { + "positive_threshold": 1, + "description": "rating>=1 positive; rating==0 or missing negative" + }, + "audit3groups": { + "positive_threshold": 3, + "description": "rating>=3 positive; rating<3 or missing negative" + } + }, + "checkpoint": "/data/FrameCullModelLab/outputs/semantic-student/grounded-convnext-v14-five-mountain-region-persona/persona-head.pt", + "hidden": 256 + } + } +} \ No newline at end of file diff --git a/src-tauri/pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx b/src-tauri/pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx new file mode 100644 index 0000000..5b20d80 Binary files /dev/null and b/src-tauri/pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..43e45d7 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,6918 @@ +use base64::Engine; +use image::GenericImageView; +use image::ImageFormat; +#[cfg(feature = "pro")] +use rayon::{prelude::*, ThreadPoolBuilder}; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::fs::{self, File}; +use std::hash::{Hash, Hasher}; +use std::io::{BufReader, Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; +#[cfg(feature = "pro")] +use std::process::{Output, Stdio}; +#[cfg(feature = "pro")] +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +#[cfg(feature = "pro")] +use std::sync::{Arc, Mutex}; +#[cfg(feature = "pro")] +use std::time::Instant; +use std::time::{Duration, UNIX_EPOCH}; +use tauri::async_runtime::spawn_blocking; +use tauri::ipc::Channel; +use tauri::utils::config::Color; +use tauri::Emitter; +use tauri::Manager; + +#[cfg(all(feature = "pro", windows))] +use std::os::windows::process::CommandExt; + +// Pro-only native ONNX Runtime inference layer (§10). Entire module compiles +// only for the pro build; Flash never pulls in ort/ndarray or this code. +#[cfg(feature = "pro")] +pub mod pro_infer; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExifData { + pub shutter_speed: Option, + pub aperture: Option, + pub iso: Option, + pub focal_length: Option, + pub date_time: Option, + pub model: Option, + pub lens: Option, + pub orientation: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PhotoFileInfo { + pub name: String, + pub extension: String, + pub path: String, + pub size: u64, + pub modified_ms: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhotoGroupInfo { + pub id: String, + pub jpg: Option, + pub raw: Option, + pub status: String, + #[serde(default)] + pub rating: u8, + pub exif: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenderedExportFile { + pub file_name: String, + pub data_url: String, + #[serde(default)] + pub rating: Option, + #[serde(default)] + pub metadata_mode: Option, + #[serde(default)] + pub metadata_source_path: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportProgressPayload { + pub phase: String, + pub processed: usize, + pub total: usize, + pub current: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RawEmbeddedPreview { + pub cache_path: String, + pub byte_length: usize, + pub offset: usize, + pub orientation: Option, + pub from_cache: bool, + pub source: String, + pub width: Option, + pub height: Option, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CachedJpegThumbnail { + pub cache_path: String, + pub from_cache: bool, + pub width: u32, + pub height: u32, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppCacheUsage { + pub disk_bytes: u64, + pub raw_preview_bytes: u64, + pub jpeg_thumbnail_bytes: u64, + pub raw_monitor_bytes: u64, +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RawEngineValidationResult { + pub ok: bool, + pub engine_kind: String, + pub engine_path: Option, + pub version: Option, + pub engine_source: Option, + pub bundled_engine_version: Option, + pub message: Option, +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RawMonitorCacheEntry { + pub raw_path: String, + pub profile_id: Option, + pub cache_path: Option, + pub from_cache: bool, + pub fallback: Option, + pub cache_source: Option, + pub fallback_reason: Option, + pub recent_failure: Option, + pub missing_reason: Option, +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RawMonitorCacheEvent { + pub kind: String, + pub processed: Option, + pub total: Option, + pub current: Option, + pub raw_path: Option, + pub profile_id: Option, + pub cache_path: Option, + pub fallback: Option, + pub cache_source: Option, + pub fallback_reason: Option, + pub skipped_reason: Option, + pub engine_version: Option, + pub errors: Option>, + pub error: Option, +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawMonitorCacheMetadata { + cache_source: String, + fallback: bool, + #[serde(default)] + fallback_reason: Option, + #[serde(default)] + renderer_version: u32, + profile_id: String, + written_at_ms: u64, +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawMonitorFailureRecord { + failed_at_ms: u64, + error: String, +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone)] +struct RawMonitorCacheStatus { + source: String, + fallback: bool, + fallback_reason: Option, +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone)] +struct RawMonitorRenderResult { + cache_path: PathBuf, + status: RawMonitorCacheStatus, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportStreamEvent { + pub kind: String, + pub phase: Option, + pub processed: Option, + pub total: Option, + pub current: Option, + pub groups: Option>, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExportStreamEvent { + pub kind: String, + pub phase: Option, + pub processed: Option, + pub total: Option, + pub current: Option, + pub files: Option>, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LightroomImportResult { + pub files: Vec, + pub launched: bool, + pub executable_path: Option, + pub warnings: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LightroomSourceFolderResult { + pub source_folder: String, + pub files: Vec, + pub launched: bool, + pub executable_path: Option, + pub warnings: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeopleExportClusterInput { + pub id: String, + pub display_name: String, + pub photo_paths: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiIssuePayload { + pub code: String, + pub level: String, + pub confidence: f64, + pub score: f64, + pub threshold: f64, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AiMetricsPayload { + pub sharpness: Option, + pub mean_luma: Option, + pub subject_mean_luma: Option, + pub subject_reliable: Option, + pub dark_clip_ratio: Option, + pub highlight_clip_ratio: Option, + pub subject_dark_clip_ratio: Option, + pub subject_highlight_clip_ratio: Option, + pub face_count: Option, + pub eye_closed_score: Option, + pub tenengrad: Option, + pub edge_density: Option, + pub focus_texture_score: Option, + pub focus_peak_sharpness: Option, + pub focus_peak_tenengrad: Option, + pub focus_peak_texture_score: Option, + pub focus_tile_count: Option, + pub focus_reliable: Option, + pub focus_reliability_score: Option, + pub focus_mode: Option, + pub eye_closed_face_count: Option, + pub eye_review_face_count: Option, + pub eye_review_score: Option, + pub face_candidate_count: Option, + pub landmarked_face_count: Option, + pub enhanced_face_detection_passes: Option, + pub face_quality_score: Option, + pub eye_reliability: Option, + pub pose_reliability: Option, + pub subject_exposure_score: Option, + pub primary_subject_count: Option, + pub subject_confidence_score: Option, + pub subject_confidence: Option, + pub group_face_count: Option, + pub group_eye_closed_face_count: Option, + pub group_eye_review_face_count: Option, + pub group_portrait_score: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiRegionPayload { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, + pub source: String, + pub label: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiFaceDiagnosticPayload { + pub index: u32, + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, + pub left_blink: Option, + pub right_blink: Option, + pub left_ear: Option, + pub right_ear: Option, + pub eye_closed_score: Option, + pub detector_confidence: Option, + pub detector_source: Option, + pub detector_name: Option, + pub face_size_ratio: Option, + pub face_quality_score: Option, + pub eye_reliability: Option, + pub pose_reliability: Option, + pub subject_role: Option, + pub subject_score: Option, + pub subject_rank: Option, + pub look_at_camera_score: Option, + pub center_score: Option, + pub size_score: Option, + pub sharpness_score: Option, + pub crop_safety_score: Option, + pub eligible_as_primary: Option, + pub subject_reason: Option, + pub landmarker_status: Option, + pub closed: bool, + pub review_hint: Option, + pub skipped_reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AiDiagnosticsPayload { + pub focus_mode: Option, + pub focus_reliable: Option, + pub focus_skip_reason: Option, + pub eye_skip_reason: Option, + pub model_load_error: Option, + pub wasm_base: Option, + pub model_asset_path: Option, + pub face_detector_status: Option, + pub face_detector_asset_path: Option, + pub face_detector_error: Option, + pub face_detector_name: Option, + pub landmarker_success_count: Option, + pub face_diagnostics: Option>, + pub primary_face_indices: Option>, + pub primary_subject_count: Option, + pub subject_confidence: Option, + pub subject_decision: Option, + pub photo_kind: Option, + pub group_face_indices: Option>, + pub group_portrait_reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiDuplicateSignaturePayload { + pub version: String, + pub width: u32, + pub height: u32, + pub aspect_ratio: f64, + pub luma_hash: String, + pub structure_hash: String, + pub color_histogram: Vec, + pub luma_histogram: Vec, + pub mean_luma: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiAnalysisPayload { + pub status: String, + pub issues: Vec, + pub confidence: f64, + pub preset: String, + pub reviewed: bool, + pub model_version: String, + pub analyzed_at: Option, + pub error: Option, + pub face_model_status: Option, + pub metrics: Option, + pub regions: Option>, + pub diagnostics: Option, + pub duplicate_signature: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiEvaluationRequest { + pub settings: AiSettingsPayload, + pub metrics: AiMetricsPayload, + pub diagnostics: AiDiagnosticsPayload, + pub regions: Option>, + pub face_model_status: Option, + pub analyzed_at: Option, + pub worker_error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AiSettingsPayload { + pub enabled_checks: HashMap, + pub sensitivity: String, + pub sensitivity_by_check: HashMap, +} + +const RAW_EXTENSIONS: &[&str] = &[ + "ARW", "CR2", "CR3", "NEF", "NRW", "DNG", "ORF", "RAF", "RW2", "SRW", "SRF", "SR2", +]; +const XMP_IDENTIFIER: &[u8] = b"http://ns.adobe.com/xap/1.0/\0"; +const EXIF_IDENTIFIER: &[u8] = b"Exif\0\0"; +const IMPORT_PROGRESS_EVENT: &str = "framecull://import-progress"; +const MIN_EMBEDDED_JPEG_BYTES: usize = 16 * 1024; +const RAW_PREVIEW_CACHE_VERSION: &str = "v2"; +const JPEG_THUMBNAIL_CACHE_VERSION: &str = "v1"; +#[cfg(feature = "pro")] +const RAW_MONITOR_CACHE_VERSION: &str = "v5"; +#[cfg(feature = "pro")] +const RAW_MONITOR_RENDERER_VERSION: u32 = 1; +#[cfg(feature = "pro")] +const RAW_MONITOR_PROFILE_BALANCED: &str = "FrameCull_Monitor_Balanced_v1"; +#[cfg(feature = "pro")] +const RAW_MONITOR_PROFILE_AUTO_EXPOSURE: &str = "FrameCull_Monitor_AutoExposure_v1"; +#[cfg(feature = "pro")] +const RAW_MONITOR_MAX_EDGE: u32 = 2400; +#[cfg(feature = "pro")] +const RAW_MONITOR_JPEG_QUALITY: u8 = 85; +#[cfg(feature = "pro")] +const RAW_MONITOR_MAX_PARALLELISM: usize = 3; +#[cfg(feature = "pro")] +const RAW_MONITOR_MAX_LUT_BYTES: u64 = 16 * 1024 * 1024; +#[cfg(feature = "pro")] +const RAW_MONITOR_FAILURE_RETRY_COOLDOWN_SECS: u64 = 30 * 60; +#[cfg(feature = "pro")] +const RAW_MONITOR_FALLBACK_DECODE_FAILURE: &str = "decodeFailure"; +#[cfg(feature = "pro")] +const RAW_MONITOR_FALLBACK_ENGINE_ERROR: &str = "engineError"; +#[cfg(feature = "pro")] +const RAW_MONITOR_FALLBACK_MISSING_OUTPUT: &str = "missingOutput"; +#[cfg(feature = "pro")] +const RAW_MONITOR_FALLBACK_INVALID_OUTPUT: &str = "invalidOutput"; +#[cfg(feature = "pro")] +const RAWTHERAPEE_BUNDLED_VERSION: &str = "5.12"; +#[cfg(feature = "pro")] +const RAWTHERAPEE_BUNDLED_RESOURCE_CLI: &str = + "raw-engines/rawtherapee/windows-x64/RawTherapee_5.12_win64_release/rawtherapee-cli.exe"; +#[cfg(feature = "pro")] +const RAWTHERAPEE_DEV_VENDOR_CLI: &str = + "vendor/rawtherapee/windows-x64/RawTherapee_5.12_win64_release/rawtherapee-cli.exe"; +const IMPORT_BATCH_SIZE: usize = 75; +const AI_MODEL_VERSION: &str = "local-native-rules-v30-focus-eye-time"; + +#[cfg(feature = "pro")] +static RAW_MONITOR_CANCEL_REQUESTED: AtomicBool = AtomicBool::new(false); + +#[derive(Debug, Clone, Copy)] +struct AiThresholds { + sharpness: f64, + tenengrad: f64, + min_edge_density: f64, + highlight_clip_ratio: f64, + dark_clip_ratio: f64, + under_mean_luma: f64, + over_mean_luma: f64, + eye_closed_score: f64, +} + +const AI_THRESHOLDS_WEAK: AiThresholds = AiThresholds { + sharpness: 25.0, + tenengrad: 32.0, + min_edge_density: 0.012, + highlight_clip_ratio: 0.12, + dark_clip_ratio: 0.45, + under_mean_luma: 60.0, + over_mean_luma: 205.0, + eye_closed_score: 0.82, +}; + +const AI_THRESHOLDS_STANDARD: AiThresholds = AiThresholds { + sharpness: 35.0, + tenengrad: 45.0, + min_edge_density: 0.018, + highlight_clip_ratio: 0.08, + dark_clip_ratio: 0.35, + under_mean_luma: 70.0, + over_mean_luma: 190.0, + eye_closed_score: 0.7, +}; + +const AI_THRESHOLDS_STRONG: AiThresholds = AiThresholds { + sharpness: 55.0, + tenengrad: 62.0, + min_edge_density: 0.014, + highlight_clip_ratio: 0.05, + dark_clip_ratio: 0.25, + under_mean_luma: 85.0, + over_mean_luma: 175.0, + eye_closed_score: 0.55, +}; + +#[derive(Debug, Clone)] +struct GroupPortraitDetection { + photo_kind: String, + group_face_indices: Vec, + group_face_count: u32, + group_portrait_score: f64, + group_portrait_reason: String, +} + +fn ai_thresholds_for_issue(settings: &AiSettingsPayload, code: &str) -> AiThresholds { + let sensitivity = settings + .sensitivity_by_check + .get(code) + .map(String::as_str) + .unwrap_or(settings.sensitivity.as_str()); + match sensitivity { + "weak" => AI_THRESHOLDS_WEAK, + "strong" => AI_THRESHOLDS_STRONG, + _ => AI_THRESHOLDS_STANDARD, + } +} + +fn ai_check_enabled(settings: &AiSettingsPayload, code: &str) -> bool { + settings.enabled_checks.get(code).copied().unwrap_or(true) +} + +fn classify_native_face_eyes( + faces: &mut [AiFaceDiagnosticPayload], + settings: &AiSettingsPayload, + formal_face_indices: &HashSet, +) { + let threshold = ai_thresholds_for_issue(settings, "EYES_CLOSED").eye_closed_score; + + for face in faces { + let is_formal_face = formal_face_indices.contains(&face.index); + let score = face.eye_closed_score; + let reliable_for_review = face.eye_reliability.unwrap_or(0.0) >= 0.28; + let closed = is_formal_face && score.map(|value| value >= threshold).unwrap_or(false); + let review_hint = is_formal_face + && !closed + && score + .map(|value| value >= threshold * 0.72 && reliable_for_review) + .unwrap_or(false); + + face.closed = closed; + face.review_hint = Some(review_hint); + face.skipped_reason = if closed { + None + } else if review_hint { + Some("Eye metrics are close to the closed-eye threshold; review manually.".to_string()) + } else if score.is_some() { + Some("Eye metrics are below the closed-eye threshold.".to_string()) + } else { + face.skipped_reason.clone() + }; + } +} + +fn detect_group_portrait_native(faces: &[AiFaceDiagnosticPayload]) -> GroupPortraitDetection { + let standard = |score: f64, reason: &str| GroupPortraitDetection { + photo_kind: "STANDARD".to_string(), + group_face_indices: Vec::new(), + group_face_count: 0, + group_portrait_score: score, + group_portrait_reason: reason.to_string(), + }; + + let candidates: Vec = faces + .iter() + .filter(|face| is_group_portrait_candidate_native(face)) + .cloned() + .collect(); + + if candidates.len() < 5 { + return standard( + 0.0, + "Fewer than five reliable faces; treated as a standard photo.", + ); + } + + let heights: Vec = candidates.iter().map(face_height_ratio_native).collect(); + let largest_face = heights.iter().copied().fold(0.0, f64::max); + let median_face = median_native(heights); + if median_face <= 0.0 || largest_face / median_face > 1.85 { + return standard( + 0.28, + "One face is much larger than the others; treated as a standard multi-person photo.", + ); + } + + let mut centers_x: Vec = candidates.iter().map(face_center_x_native).collect(); + let mut centers_y: Vec = candidates.iter().map(face_center_y_native).collect(); + centers_x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + centers_y.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let horizontal_span = percentile_native(¢ers_x, 0.9) - percentile_native(¢ers_x, 0.1); + if horizontal_span < 0.22 { + return standard( + 0.34, + "Reliable faces are not spread across the frame like a posed group.", + ); + } + + let rows = cluster_face_rows_native(&candidates); + let y_spread = percentile_native(¢ers_y, 0.9) - percentile_native(¢ers_y, 0.1); + let has_single_group_row = rows + .iter() + .any(|row| row.len() >= 5 && row_center_y_spread_native(row) <= 0.16); + let compact_rows: Vec<&Vec> = rows + .iter() + .filter(|row| row.len() >= 2 && row_center_y_spread_native(row) <= 0.12) + .collect(); + let has_two_group_rows = candidates.len() >= 5 && compact_rows.len() >= 2 && y_spread <= 0.38; + if !has_single_group_row && !has_two_group_rows { + return standard( + 0.4, + "Faces are not aligned closely enough to classify this as a formal group portrait.", + ); + } + + let density_score = rows + .iter() + .filter(|row| row.len() >= 2) + .map(|row| row_density_score_native(row)) + .fold(0.0, f64::max); + if density_score < 0.45 { + return standard( + 0.46, + "Faces are too loosely spaced for the conservative group portrait rule.", + ); + } + + let size_spread = + coefficient_of_variation_native(candidates.iter().map(face_height_ratio_native).collect()); + if size_spread > 0.42 { + return standard( + 0.5, + "Face sizes vary too much for the conservative group portrait rule.", + ); + } + + let count_score = clamp01((candidates.len() as f64 - 4.0) / 4.0); + let alignment_score = clamp01(1.0 - y_spread / 0.38); + let size_score = clamp01(1.0 - size_spread / 0.42); + let spread_score = clamp01(horizontal_span / 0.46); + let score = clamp01( + count_score * 0.24 + + alignment_score * 0.26 + + size_score * 0.22 + + density_score * 0.18 + + spread_score * 0.1, + ); + + if score < 0.68 { + return standard( + score, + "Group portrait evidence is below the conservative threshold.", + ); + } + + GroupPortraitDetection { + photo_kind: "GROUP_PORTRAIT".to_string(), + group_face_indices: candidates.iter().map(|face| face.index).collect(), + group_face_count: candidates.len() as u32, + group_portrait_score: score, + group_portrait_reason: if has_two_group_rows { + "Detected a compact two-row group portrait; closed eyes are checked across all reliable group faces." + } else { + "Detected a compact row of reliable faces; closed eyes are checked across all reliable group faces." + } + .to_string(), + } +} + +fn is_group_portrait_candidate_native(face: &AiFaceDiagnosticPayload) -> bool { + let height_ratio = face_height_ratio_native(face); + face.landmarker_status.as_deref() == Some("OK") + && height_ratio >= 0.025 + && face.face_quality_score.unwrap_or(0.0) >= 0.32 + && face.eye_reliability.unwrap_or(0.0) >= 0.16 + && !is_strong_edge_face_native(face) +} + +fn is_strong_edge_face_native(face: &AiFaceDiagnosticPayload) -> bool { + let touches_x = face.x <= 0.012 || face.x + face.width >= 0.988; + let touches_y = face.y <= 0.012 || face.y + face.height >= 0.988; + (touches_x || touches_y) && face.pose_reliability.unwrap_or(0.0) < 0.24 +} + +fn cluster_face_rows_native( + faces: &[AiFaceDiagnosticPayload], +) -> Vec> { + let mut rows: Vec> = Vec::new(); + let mut sorted = faces.to_vec(); + sorted.sort_by(|a, b| { + face_center_y_native(a) + .partial_cmp(&face_center_y_native(b)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + for face in sorted { + let center = face_center_y_native(&face); + if let Some(row) = rows.iter_mut().find(|items| { + let row_centers: Vec = items.iter().map(face_center_y_native).collect(); + (center - median_native(row_centers)).abs() <= 0.105 + }) { + row.push(face); + } else { + rows.push(vec![face]); + } + } + + rows +} + +fn row_density_score_native(row: &[AiFaceDiagnosticPayload]) -> f64 { + let mut sorted = row.to_vec(); + sorted.sort_by(|a, b| { + face_center_x_native(a) + .partial_cmp(&face_center_x_native(b)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + if sorted.len() < 2 { + return 0.0; + } + + let gaps: Vec = sorted + .windows(2) + .map(|pair| face_center_x_native(&pair[1]) - face_center_x_native(&pair[0])) + .collect(); + let median_gap = median_native(gaps); + let median_width = median_native(sorted.iter().map(|face| face.width).collect()); + let allowed_gap = 0.16_f64.max(median_width * 3.6); + clamp01(1.0 - median_gap / (allowed_gap * 1.8)) +} + +fn row_center_y_spread_native(row: &[AiFaceDiagnosticPayload]) -> f64 { + let mut centers: Vec = row.iter().map(face_center_y_native).collect(); + centers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + match (centers.first(), centers.last()) { + (Some(first), Some(last)) => last - first, + _ => 0.0, + } +} + +fn face_center_x_native(face: &AiFaceDiagnosticPayload) -> f64 { + face.x + face.width / 2.0 +} + +fn face_center_y_native(face: &AiFaceDiagnosticPayload) -> f64 { + face.y + face.height / 2.0 +} + +fn face_height_ratio_native(face: &AiFaceDiagnosticPayload) -> f64 { + face.face_size_ratio.unwrap_or(face.height) +} + +fn median_native(mut values: Vec) -> f64 { + if values.is_empty() { + return 0.0; + } + values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let middle = values.len() / 2; + if values.len() % 2 == 0 { + (values[middle - 1] + values[middle]) / 2.0 + } else { + values[middle] + } +} + +fn percentile_native(values: &[f64], ratio: f64) -> f64 { + if values.is_empty() { + return 0.0; + } + let index = ((values.len() - 1) as f64 * ratio).round() as usize; + values[index.min(values.len() - 1)] +} + +fn coefficient_of_variation_native(values: Vec) -> f64 { + if values.is_empty() { + return 0.0; + } + let mean = values.iter().sum::() / values.len() as f64; + if mean <= 0.0 { + return 0.0; + } + let variance = values + .iter() + .map(|value| (value - mean).powi(2)) + .sum::() + / values.len() as f64; + variance.sqrt() / mean +} + +fn classify_ai_issues_native( + metrics: &AiMetricsPayload, + settings: &AiSettingsPayload, +) -> Vec { + let sharpness_thresholds = ai_thresholds_for_issue(settings, "OUT_OF_FOCUS"); + let under_thresholds = ai_thresholds_for_issue(settings, "UNDER_EXPOSED"); + let over_thresholds = ai_thresholds_for_issue(settings, "OVER_EXPOSED"); + let eye_thresholds = ai_thresholds_for_issue(settings, "EYES_CLOSED"); + let sharpness = metrics.sharpness.unwrap_or(f64::INFINITY); + let tenengrad = metrics.tenengrad.unwrap_or(sharpness); + let edge_density = metrics.edge_density.unwrap_or(1.0); + let focus_texture_score = metrics + .focus_texture_score + .unwrap_or(sharpness.min(tenengrad)); + let focus_peak_sharpness = metrics.focus_peak_sharpness.unwrap_or(sharpness); + let focus_peak_tenengrad = metrics.focus_peak_tenengrad.unwrap_or(tenengrad); + let focus_peak_texture_score = metrics + .focus_peak_texture_score + .unwrap_or(focus_peak_sharpness.min(focus_peak_tenengrad)); + let mean_luma = metrics.mean_luma.unwrap_or(128.0); + let subject_mean_luma = metrics.subject_mean_luma.unwrap_or(mean_luma); + let dark_clip_ratio = metrics.dark_clip_ratio.unwrap_or(0.0); + let highlight_clip_ratio = metrics.highlight_clip_ratio.unwrap_or(0.0); + let subject_reliable = metrics.subject_reliable == Some(true); + let primary_subject_count = metrics.primary_subject_count.unwrap_or(0); + let subject_unclear = matches!( + metrics.subject_confidence.as_deref(), + Some("LOW") | Some("NONE") + ); + let subject_dark_clip_ratio = metrics.subject_dark_clip_ratio.unwrap_or(dark_clip_ratio); + let subject_highlight_clip_ratio = metrics + .subject_highlight_clip_ratio + .unwrap_or(highlight_clip_ratio); + let group_face_count = metrics.group_face_count.unwrap_or(0); + let group_eye_closed_face_count = metrics.group_eye_closed_face_count.unwrap_or(0); + let group_eye_review_face_count = metrics.group_eye_review_face_count.unwrap_or(0); + let is_group_portrait = group_face_count >= 5; + let eye_closed_score = metrics.eye_closed_score; + let eye_review_score = metrics.eye_review_score; + let mut issues: Vec = Vec::new(); + + let focus_reliable = metrics.focus_reliable == Some(true); + let focus_mode = metrics.focus_mode.as_deref().unwrap_or(""); + let has_face_focus_candidate = focus_mode == "FACE_ROI" && primary_subject_count > 0; + let has_enough_detail_to_judge = focus_mode == "FACE_ROI" + || (focus_mode == "NO_FACE_TEXTURED" + && edge_density >= sharpness_thresholds.min_edge_density); + let low_laplacian = sharpness < sharpness_thresholds.sharpness; + let low_tenengrad = tenengrad < sharpness_thresholds.tenengrad; + let low_composite = focus_texture_score < sharpness_thresholds.sharpness; + let face_has_structured_edges = focus_mode == "FACE_ROI" && edge_density >= 0.18; + let face_edge_density_is_low = focus_mode != "FACE_ROI" || edge_density < 0.12; + let local_detail_is_low = focus_peak_sharpness < sharpness_thresholds.sharpness * 1.05 + && focus_peak_tenengrad < sharpness_thresholds.tenengrad * 1.05 + && focus_peak_texture_score < sharpness_thresholds.sharpness + && !face_has_structured_edges + && face_edge_density_is_low; + let focus_evidence_count = + (low_laplacian as u8) + (low_tenengrad as u8) + (low_composite as u8); + let severe_focus_evidence = focus_texture_score < sharpness_thresholds.sharpness * 0.62 + && tenengrad < sharpness_thresholds.tenengrad * 0.72 + && focus_peak_texture_score < sharpness_thresholds.sharpness * 0.88 + && !face_has_structured_edges; + + let formal_focus_issue = ai_check_enabled(settings, "OUT_OF_FOCUS") + && (focus_mode != "FACE_ROI" || (primary_subject_count > 0 && !subject_unclear)) + && focus_reliable + && has_enough_detail_to_judge + && ((focus_evidence_count >= 2 && local_detail_is_low) || severe_focus_evidence); + + if formal_focus_issue { + issues.push(make_ai_issue( + "OUT_OF_FOCUS", + "ISSUE", + focus_confidence_native( + sharpness, + tenengrad, + focus_texture_score, + sharpness_thresholds.sharpness, + sharpness_thresholds.tenengrad, + ), + sharpness.min(tenengrad).min(focus_texture_score), + sharpness_thresholds.sharpness, + if focus_mode == "FACE_ROI" { + "Face or eye ROI focus metrics are consistently below threshold." + } else { + "Textured non-face region has consistently low focus metrics." + }, + )); + } else if ai_check_enabled(settings, "OUT_OF_FOCUS") + && has_face_focus_candidate + && low_laplacian + && low_tenengrad + && low_composite + && focus_peak_sharpness < sharpness_thresholds.sharpness * 1.65 + && focus_peak_tenengrad < sharpness_thresholds.tenengrad * 1.65 + { + issues.push(make_ai_issue( + "OUT_OF_FOCUS", + "REVIEW_HINT", + 0.78_f64.min(focus_confidence_native( + sharpness, + tenengrad, + focus_texture_score, + sharpness_thresholds.sharpness, + sharpness_thresholds.tenengrad, + )), + sharpness.min(tenengrad).min(focus_texture_score), + sharpness_thresholds.sharpness, + if focus_reliable { + "Face ROI focus is low but not consistent enough for a hard reject." + } else { + "Small, angled, or partly occluded face has low focus metrics; review manually." + }, + )); + } + + let subject_over_exposed = subject_reliable + && subject_highlight_clip_ratio > over_thresholds.highlight_clip_ratio + && subject_mean_luma > over_thresholds.over_mean_luma; + let has_detected_faces = metrics.face_count.unwrap_or(0) > 0; + let full_over_exposed = highlight_clip_ratio > over_thresholds.highlight_clip_ratio + && mean_luma > over_thresholds.over_mean_luma + && ((!subject_reliable && !has_detected_faces) || subject_over_exposed); + let disaster_full_over_exposed = !subject_reliable + && !has_detected_faces + && highlight_clip_ratio > over_thresholds.highlight_clip_ratio * 1.45 + && mean_luma > over_thresholds.over_mean_luma * 1.08; + + if ai_check_enabled(settings, "OVER_EXPOSED") + && (disaster_full_over_exposed || subject_over_exposed) + { + let mut confidence_scores = vec![confidence_above_native( + highlight_clip_ratio, + over_thresholds.highlight_clip_ratio, + 0.25, + )]; + if subject_reliable { + confidence_scores.push(confidence_above_native( + subject_highlight_clip_ratio, + over_thresholds.highlight_clip_ratio, + 0.25, + )); + } + issues.push(make_ai_issue( + "OVER_EXPOSED", + "ISSUE", + confidence_scores.into_iter().fold(0.0, f64::max), + if subject_reliable { + highlight_clip_ratio.max(subject_highlight_clip_ratio) + } else { + highlight_clip_ratio + }, + over_thresholds.highlight_clip_ratio, + "Highlight clipping or subject brightness is above the local threshold.", + )); + } else if ai_check_enabled(settings, "OVER_EXPOSED") + && (full_over_exposed + || (subject_reliable + && subject_highlight_clip_ratio > over_thresholds.highlight_clip_ratio * 0.72 + && subject_mean_luma > over_thresholds.over_mean_luma * 0.92) + || (has_detected_faces + && !subject_reliable + && highlight_clip_ratio > over_thresholds.highlight_clip_ratio * 0.9 + && mean_luma > over_thresholds.over_mean_luma * 0.96)) + { + issues.push(make_ai_issue( + "OVER_EXPOSED", + "REVIEW_HINT", + 0.76_f64.min(confidence_above_native( + if subject_reliable { + subject_highlight_clip_ratio.max(highlight_clip_ratio * 0.5) + } else { + highlight_clip_ratio + }, + over_thresholds.highlight_clip_ratio, + 0.28, + )), + if subject_reliable { + subject_highlight_clip_ratio.max(highlight_clip_ratio * 0.5) + } else { + highlight_clip_ratio + }, + over_thresholds.highlight_clip_ratio, + if subject_reliable { + "Subject highlights are close to the overexposure threshold; review manually." + } else { + "Full-image highlights are high, but no reliable subject ROI was found." + }, + )); + } + + let subject_under_exposed = subject_reliable + && subject_dark_clip_ratio > under_thresholds.dark_clip_ratio + && subject_mean_luma < under_thresholds.under_mean_luma; + let full_under_exposed = dark_clip_ratio > under_thresholds.dark_clip_ratio + && mean_luma < under_thresholds.under_mean_luma + && ((!subject_reliable && !has_detected_faces) || subject_under_exposed); + let disaster_full_under_exposed = !subject_reliable + && !has_detected_faces + && dark_clip_ratio > under_thresholds.dark_clip_ratio * 1.25 + && mean_luma < under_thresholds.under_mean_luma * 0.88; + + if ai_check_enabled(settings, "UNDER_EXPOSED") + && (disaster_full_under_exposed || subject_under_exposed) + { + let mut confidence_scores = vec![ + confidence_above_native(dark_clip_ratio, under_thresholds.dark_clip_ratio, 0.6), + confidence_below_native(mean_luma, under_thresholds.under_mean_luma), + ]; + if subject_reliable { + confidence_scores.push(confidence_above_native( + subject_dark_clip_ratio, + under_thresholds.dark_clip_ratio, + 0.6, + )); + confidence_scores.push(confidence_below_native( + subject_mean_luma, + under_thresholds.under_mean_luma, + )); + } + issues.push(make_ai_issue( + "UNDER_EXPOSED", + "ISSUE", + confidence_scores.into_iter().fold(0.0, f64::max), + if subject_reliable { + dark_clip_ratio.max(subject_dark_clip_ratio) + } else { + dark_clip_ratio + }, + under_thresholds.dark_clip_ratio, + "Shadow clipping and subject brightness indicate severe underexposure.", + )); + } else if ai_check_enabled(settings, "UNDER_EXPOSED") + && (full_under_exposed + || (subject_reliable + && subject_dark_clip_ratio > under_thresholds.dark_clip_ratio * 0.72 + && subject_mean_luma < under_thresholds.under_mean_luma * 1.18) + || (has_detected_faces + && !subject_reliable + && dark_clip_ratio > under_thresholds.dark_clip_ratio * 0.9 + && mean_luma < under_thresholds.under_mean_luma * 1.04)) + { + issues.push(make_ai_issue( + "UNDER_EXPOSED", + "REVIEW_HINT", + 0.76_f64.min( + confidence_above_native( + if subject_reliable { + subject_dark_clip_ratio + } else { + dark_clip_ratio + }, + under_thresholds.dark_clip_ratio, + 0.72, + ) + .max(confidence_below_native( + if subject_reliable { + subject_mean_luma + } else { + mean_luma + }, + under_thresholds.under_mean_luma, + )), + ), + if subject_reliable { + subject_dark_clip_ratio.max(dark_clip_ratio * 0.5) + } else { + dark_clip_ratio + }, + under_thresholds.dark_clip_ratio, + if subject_reliable { + "Subject shadows are close to the underexposure threshold; review manually." + } else { + "Full-image shadows are high, but no reliable subject ROI was found." + }, + )); + } + + let standard_closed_eye_issue = ai_check_enabled(settings, "EYES_CLOSED") + && primary_subject_count > 0 + && !subject_unclear + && metrics.eye_closed_face_count.unwrap_or(0) > 0 + && eye_closed_score + .map(|score| score >= eye_thresholds.eye_closed_score) + .unwrap_or(false); + let group_closed_eye_issue = ai_check_enabled(settings, "EYES_CLOSED") + && is_group_portrait + && group_face_count > 0 + && group_eye_closed_face_count > 0 + && eye_closed_score + .map(|score| score >= eye_thresholds.eye_closed_score) + .unwrap_or(false); + + if standard_closed_eye_issue || group_closed_eye_issue { + let score = eye_closed_score.unwrap_or(0.0); + issues.push(make_ai_issue( + "EYES_CLOSED", + "ISSUE", + score, + score, + eye_thresholds.eye_closed_score, + if is_group_portrait { + "At least one member in the group portrait appears to have both eyes closed." + } else { + "At least one detected face appears to have both eyes closed." + }, + )); + } else { + let standard_closed_eye_review = ai_check_enabled(settings, "EYES_CLOSED") + && primary_subject_count > 0 + && metrics.eye_review_face_count.unwrap_or(0) > 0 + && eye_review_score + .map(|score| score >= eye_thresholds.eye_closed_score * 0.72) + .unwrap_or(false); + let group_closed_eye_review = ai_check_enabled(settings, "EYES_CLOSED") + && is_group_portrait + && group_face_count > 0 + && group_eye_review_face_count > 0 + && eye_review_score + .map(|score| score >= eye_thresholds.eye_closed_score * 0.72) + .unwrap_or(false); + + if standard_closed_eye_review || group_closed_eye_review { + let score = eye_review_score.unwrap_or(0.0); + issues.push(make_ai_issue( + "EYES_CLOSED", + "REVIEW_HINT", + 0.78_f64.min(score), + score, + eye_thresholds.eye_closed_score, + if is_group_portrait { + "Eye metrics suggest a possible blink in the group portrait, but the evidence is not strong enough for a hard closed-eyes label." + } else { + "Eye metrics suggest a possible blink, but the evidence is not strong enough for a hard closed-eyes label." + }, + )); + } + } + + issues +} + +fn make_ai_issue( + code: &str, + level: &str, + confidence: f64, + score: f64, + threshold: f64, + message: &str, +) -> AiIssuePayload { + AiIssuePayload { + code: code.to_string(), + level: level.to_string(), + confidence: clamp01(confidence), + score, + threshold, + message: message.to_string(), + } +} + +fn highest_issue_confidence_native(issues: &[AiIssuePayload]) -> f64 { + issues + .iter() + .map(|issue| issue.confidence) + .fold(0.0, f64::max) +} + +fn confidence_below_native(score: f64, threshold: f64) -> f64 { + if threshold <= 0.0 { + return 0.0; + } + clamp01(0.55 + (threshold - score) / threshold) +} + +fn confidence_above_native(score: f64, threshold: f64, max_spread: f64) -> f64 { + if max_spread <= 0.0 { + return 0.0; + } + clamp01(0.55 + (score - threshold) / max_spread) +} + +fn focus_confidence_native( + sharpness: f64, + tenengrad: f64, + focus_texture_score: f64, + sharpness_threshold: f64, + tenengrad_threshold: f64, +) -> f64 { + let deficits = [ + deficit_ratio_native(sharpness, sharpness_threshold), + deficit_ratio_native(tenengrad, tenengrad_threshold), + deficit_ratio_native(focus_texture_score, sharpness_threshold), + ]; + let average_deficit = deficits.iter().sum::() / deficits.len() as f64; + let base = 0.58 + average_deficit * 0.34; + let all_extremely_low = deficits.iter().all(|value| *value > 0.82); + if all_extremely_low { + 0.98_f64.min(base + 0.05) + } else { + 0.92_f64.min(base) + } +} + +fn deficit_ratio_native(score: f64, threshold: f64) -> f64 { + if threshold <= 0.0 { + return 0.0; + } + clamp01((threshold - score) / threshold) +} + +fn clamp01(value: f64) -> f64 { + value.max(0.0).min(1.0) +} + +#[tauri::command] +fn evaluate_ai_analysis(request: AiEvaluationRequest) -> Result { + let mut diagnostics = request.diagnostics.clone(); + let mut face_diagnostics = diagnostics.face_diagnostics.clone().unwrap_or_default(); + let group_portrait = detect_group_portrait_native(&face_diagnostics); + + diagnostics.photo_kind = Some(group_portrait.photo_kind.clone()); + diagnostics.group_face_indices = Some(group_portrait.group_face_indices.clone()); + diagnostics.group_portrait_reason = Some(group_portrait.group_portrait_reason.clone()); + + let formal_face_indices: HashSet = if group_portrait.photo_kind == "GROUP_PORTRAIT" { + group_portrait.group_face_indices.iter().copied().collect() + } else { + diagnostics + .primary_face_indices + .clone() + .unwrap_or_default() + .into_iter() + .collect() + }; + + classify_native_face_eyes( + &mut face_diagnostics, + &request.settings, + &formal_face_indices, + ); + let closed_eye_faces: Vec<&AiFaceDiagnosticPayload> = + face_diagnostics.iter().filter(|face| face.closed).collect(); + let review_eye_faces: Vec<&AiFaceDiagnosticPayload> = face_diagnostics + .iter() + .filter(|face| face.review_hint == Some(true)) + .collect(); + let formal_faces: Vec<&AiFaceDiagnosticPayload> = face_diagnostics + .iter() + .filter(|face| formal_face_indices.contains(&face.index)) + .collect(); + + let eye_closed_score = if !closed_eye_faces.is_empty() { + closed_eye_faces + .iter() + .filter_map(|face| face.eye_closed_score) + .fold(0.0, f64::max) + } else { + formal_faces + .iter() + .filter_map(|face| face.eye_closed_score) + .fold(0.0, f64::max) + }; + let eye_review_score = if !review_eye_faces.is_empty() { + review_eye_faces + .iter() + .filter_map(|face| face.eye_closed_score) + .fold(0.0, f64::max) + } else { + formal_faces + .iter() + .filter_map(|face| face.eye_closed_score) + .fold(0.0, f64::max) + }; + + let mut metrics = request.metrics.clone(); + metrics.eye_closed_score = if eye_closed_score > 0.0 { + Some(eye_closed_score) + } else { + None + }; + metrics.eye_review_score = if eye_review_score > 0.0 { + Some(eye_review_score) + } else { + None + }; + metrics.eye_closed_face_count = Some(closed_eye_faces.len() as u32); + metrics.eye_review_face_count = Some(review_eye_faces.len() as u32); + metrics.group_face_count = if group_portrait.photo_kind == "GROUP_PORTRAIT" { + Some(group_portrait.group_face_count) + } else { + None + }; + metrics.group_eye_closed_face_count = if group_portrait.photo_kind == "GROUP_PORTRAIT" { + Some( + closed_eye_faces + .iter() + .filter(|face| formal_face_indices.contains(&face.index)) + .count() as u32, + ) + } else { + None + }; + metrics.group_eye_review_face_count = if group_portrait.photo_kind == "GROUP_PORTRAIT" { + Some( + review_eye_faces + .iter() + .filter(|face| formal_face_indices.contains(&face.index)) + .count() as u32, + ) + } else { + None + }; + metrics.group_portrait_score = Some(group_portrait.group_portrait_score); + + diagnostics.eye_skip_reason = if formal_faces.is_empty() { + Some(if group_portrait.photo_kind == "GROUP_PORTRAIT" { + group_portrait.group_portrait_reason.clone() + } else { + diagnostics + .subject_decision + .clone() + .unwrap_or_else(|| "Subject unclear; only review hints are allowed.".to_string()) + }) + } else if closed_eye_faces + .iter() + .any(|face| formal_face_indices.contains(&face.index)) + { + None + } else if review_eye_faces + .iter() + .any(|face| formal_face_indices.contains(&face.index)) + { + Some("At least one face is close to the closed-eye threshold; review manually.".to_string()) + } else { + Some("All formal faces are below the closed-eye threshold.".to_string()) + }; + diagnostics.face_diagnostics = Some(face_diagnostics.clone()); + + let mut regions = request.regions.unwrap_or_default(); + let group_face_index_set: HashSet = + group_portrait.group_face_indices.iter().copied().collect(); + for region in &mut regions { + if region.source != "detector" { + continue; + } + let parsed_index = region + .label + .split_whitespace() + .nth(1) + .and_then(|value| value.parse::().ok()) + .map(|value| value.saturating_sub(1)); + let Some(index) = parsed_index else { + continue; + }; + if let Some(face) = face_diagnostics.iter().find(|item| item.index == index) { + region.label = + face_region_label_native(face, group_face_index_set.contains(&face.index)); + } + } + + let issues = classify_ai_issues_native(&metrics, &request.settings); + Ok(AiAnalysisPayload { + status: "DONE".to_string(), + confidence: highest_issue_confidence_native(&issues), + issues, + preset: request.settings.sensitivity.clone(), + reviewed: false, + model_version: AI_MODEL_VERSION.to_string(), + analyzed_at: request.analyzed_at.or_else(|| { + Some( + std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64, + ) + }), + error: request.worker_error.clone(), + face_model_status: request.face_model_status.clone(), + metrics: Some(metrics), + regions: Some(regions), + diagnostics: Some(diagnostics), + duplicate_signature: None, + }) +} + +fn face_region_label_native(face: &AiFaceDiagnosticPayload, is_group_face: bool) -> String { + let role = face.subject_role.as_deref().unwrap_or("BACKGROUND"); + let score = face + .subject_score + .map(|value| format!(" {}%", (value * 100.0).round() as i32)) + .unwrap_or_default(); + let eye_state = if face.closed { + " EYE_CLOSED" + } else if face.review_hint == Some(true) { + " EYE_REVIEW" + } else { + "" + }; + let group_state = if is_group_face { " GROUP_FACE" } else { "" }; + format!( + "{} {}{}{}{}", + role, + face.index + 1, + score, + eye_state, + group_state + ) +} + +fn file_modified_ms(path: &Path) -> Option { + fs::metadata(path) + .ok() + .and_then(|metadata| metadata.modified().ok()) + .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_millis() as u64) +} + +fn unique_destination_path(dest_path: &Path, file_name: &str) -> PathBuf { + let safe_file_name = Path::new(file_name) + .file_name() + .and_then(|value| value.to_str()) + .map(sanitize_path_segment) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| "export".to_string()); + let original = Path::new(&safe_file_name); + let stem = original + .file_stem() + .and_then(|value| value.to_str()) + .unwrap_or("export"); + let extension = original.extension().and_then(|value| value.to_str()); + + let mut candidate = dest_path.join(&safe_file_name); + let mut counter = 1; + + while candidate.exists() { + let next_name = match extension { + Some(ext) if !ext.is_empty() => format!("{} ({counter}).{}", stem, ext), + _ => format!("{} ({counter})", stem), + }; + candidate = dest_path.join(next_name); + counter += 1; + } + + candidate +} + +fn sanitize_path_segment(value: &str) -> String { + let sanitized = value + .chars() + .map(|ch| { + if ch.is_control() || matches!(ch, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*') + { + '_' + } else { + ch + } + }) + .collect::() + .trim() + .trim_matches('.') + .to_string(); + if sanitized.is_empty() { + "person".to_string() + } else { + sanitized + } +} + +fn unique_cluster_destination(dest_path: &Path, name: &str) -> PathBuf { + let base = sanitize_path_segment(name); + let mut candidate = dest_path.join(&base); + let mut counter = 1; + while candidate.exists() { + candidate = dest_path.join(format!("{} ({counter})", base)); + counter += 1; + } + candidate +} + +fn clamp_rating(rating: u8) -> u8 { + rating.min(5) +} + +fn xmp_sidecar_path(path: &Path) -> PathBuf { + path.with_extension("xmp") +} + +fn extension_upper(path: &Path) -> String { + path.extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_uppercase()) + .unwrap_or_default() +} + +fn is_jpeg_path(path: &Path) -> bool { + matches!(extension_upper(path).as_str(), "JPG" | "JPEG") +} + +fn is_raw_path(path: &Path) -> bool { + RAW_EXTENSIONS.contains(&extension_upper(path).as_str()) +} + +fn is_supported_photo_path(path: &Path) -> bool { + is_jpeg_path(path) || is_raw_path(path) +} + +fn validate_photo_file_path(path: &Path) -> Result<(), String> { + if !is_supported_photo_path(path) { + return Err(format!("Unsupported photo file type: {}", path.display())); + } + if !path.is_file() { + return Err(format!("Photo file not found: {}", path.display())); + } + Ok(()) +} + +fn emit_import_progress( + app: &tauri::AppHandle, + phase: &str, + processed: usize, + total: usize, + current: Option, +) { + let payload = ImportProgressPayload { + phase: phase.to_string(), + processed, + total, + current, + }; + let _ = app.emit(IMPORT_PROGRESS_EVENT, payload); +} + +fn should_emit_progress(processed: usize, total: usize) -> bool { + processed == 0 || processed == total || processed % 25 == 0 +} + +fn collect_files_recursive(path: &Path) -> Result, String> { + let mut files = Vec::new(); + let mut stack = vec![path.to_path_buf()]; + + while let Some(current) = stack.pop() { + let entries = fs::read_dir(¤t) + .map_err(|e| format!("Failed to read directory {}: {}", current.display(), e))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let entry_path = entry.path(); + if entry_path.is_dir() { + stack.push(entry_path); + } else { + files.push(entry_path); + } + } + } + + files.sort(); + Ok(files) +} + +fn read_orientation(path: &Path) -> Option { + let file = File::open(path).ok()?; + let mut bufreader = BufReader::new(file); + let exif = exif::Reader::new() + .read_from_container(&mut bufreader) + .ok()?; + orientation_from_exif(&exif) +} + +fn raw_preview_cache_path( + app: &tauri::AppHandle, + path: &Path, + byte_length: usize, + orientation: Option, +) -> Result { + let metadata = fs::metadata(path).map_err(|e| format!("Failed to stat RAW file: {}", e))?; + let modified_ms = metadata + .modified() + .ok() + .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0); + let orientation = orientation.unwrap_or(1); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + RAW_PREVIEW_CACHE_VERSION.hash(&mut hasher); + path.to_string_lossy().hash(&mut hasher); + metadata.len().hash(&mut hasher); + modified_ms.hash(&mut hasher); + byte_length.hash(&mut hasher); + orientation.hash(&mut hasher); + let file_name = format!("{:016x}.jpg", hasher.finish()); + + let cache_root = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("raw-previews"); + + Ok(cache_root.join(file_name)) +} + +fn jpeg_thumbnail_cache_path( + app: &tauri::AppHandle, + path: &Path, + max_edge: u32, + orientation: Option, +) -> Result { + let metadata = fs::metadata(path).map_err(|e| format!("Failed to stat JPEG file: {}", e))?; + let modified_ms = metadata + .modified() + .ok() + .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0); + let orientation = orientation.unwrap_or(1); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + JPEG_THUMBNAIL_CACHE_VERSION.hash(&mut hasher); + path.to_string_lossy().hash(&mut hasher); + metadata.len().hash(&mut hasher); + modified_ms.hash(&mut hasher); + max_edge.hash(&mut hasher); + orientation.hash(&mut hasher); + let file_name = format!("{:016x}.jpg", hasher.finish()); + + let cache_root = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("jpg-thumbnails"); + + Ok(cache_root.join(file_name)) +} + +fn app_preview_cache_dir(app: &tauri::AppHandle, name: &str) -> Result { + Ok(app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join(name)) +} + +#[tauri::command] +fn get_app_cache_usage(app: tauri::AppHandle) -> Result { + let raw_preview_bytes = directory_size_if_exists(&app_preview_cache_dir(&app, "raw-previews")?) + .map_err(|e| format!("Failed to calculate RAW preview cache size: {}", e))?; + let jpeg_thumbnail_bytes = directory_size_if_exists(&app_preview_cache_dir(&app, "jpg-thumbnails")?) + .map_err(|e| format!("Failed to calculate JPEG thumbnail cache size: {}", e))?; + let raw_monitor_bytes = directory_size_if_exists(&app_preview_cache_dir(&app, "raw-monitor-previews")?) + .map_err(|e| format!("Failed to calculate RAW monitor cache size: {}", e))?; + + Ok(AppCacheUsage { + disk_bytes: raw_preview_bytes + jpeg_thumbnail_bytes + raw_monitor_bytes, + raw_preview_bytes, + jpeg_thumbnail_bytes, + raw_monitor_bytes, + }) +} + +#[tauri::command] +fn clear_app_preview_caches(app: tauri::AppHandle) -> Result { + let cache_dirs = [ + "raw-previews", + "jpg-thumbnails", + "raw-monitor-previews", + ]; + let mut cleared_bytes = 0u64; + + for dir_name in cache_dirs { + let cache_dir = app_preview_cache_dir(&app, dir_name)?; + if !cache_dir.exists() { + continue; + } + cleared_bytes += calculate_directory_size(&cache_dir) + .map_err(|e| format!("Failed to calculate cache size for {}: {}", dir_name, e))?; + fs::remove_dir_all(&cache_dir) + .map_err(|e| format!("Failed to clear cache directory {}: {}", dir_name, e))?; + } + + Ok(cleared_bytes) +} + +fn directory_size_if_exists(path: &Path) -> std::io::Result { + if path.exists() { + calculate_directory_size(path) + } else { + Ok(0) + } +} + +#[cfg(feature = "pro")] +fn normalize_raw_monitor_profile_id(profile_id: &str) -> &'static str { + if profile_id == RAW_MONITOR_PROFILE_AUTO_EXPOSURE { + RAW_MONITOR_PROFILE_AUTO_EXPOSURE + } else { + RAW_MONITOR_PROFILE_BALANCED + } +} + +#[cfg(feature = "pro")] +fn raw_monitor_cache_file_name( + path: &Path, + engine_version: &str, + profile_id: &str, +) -> Result { + let metadata = fs::metadata(path).map_err(|e| format!("Failed to stat RAW file: {}", e))?; + let modified_ms = metadata + .modified() + .ok() + .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + let profile_id = normalize_raw_monitor_profile_id(profile_id); + RAW_MONITOR_CACHE_VERSION.hash(&mut hasher); + profile_id.hash(&mut hasher); + RAW_MONITOR_MAX_EDGE.hash(&mut hasher); + RAW_MONITOR_JPEG_QUALITY.hash(&mut hasher); + engine_version.hash(&mut hasher); + path.to_string_lossy().hash(&mut hasher); + metadata.len().hash(&mut hasher); + modified_ms.hash(&mut hasher); + Ok(format!("{:016x}.jpg", hasher.finish())) +} + +#[cfg(feature = "pro")] +fn raw_monitor_cache_path( + app: &tauri::AppHandle, + path: &Path, + engine_version: &str, + profile_id: &str, +) -> Result { + let file_name = raw_monitor_cache_file_name(path, engine_version, profile_id)?; + let cache_root = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("raw-monitor-previews"); + Ok(cache_root.join(file_name)) +} + +#[cfg(feature = "pro")] +fn raw_monitor_cache_root(app: &tauri::AppHandle) -> Result { + Ok(app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("raw-monitor-previews")) +} + +#[cfg(feature = "pro")] +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct MonitorLutFile { + path: String, + name: String, + content: String, +} + +#[cfg(feature = "pro")] +fn monitor_lut_root(app: &tauri::AppHandle) -> Result { + let root = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to resolve app data directory: {}", e))? + .join("monitor-luts"); + fs::create_dir_all(&root) + .map_err(|e| format!("Failed to create LUT import directory: {}", e))?; + Ok(root) +} + +#[cfg(feature = "pro")] +fn read_monitor_lut_file(path: &Path) -> Result<(String, String), String> { + let extension = path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + if !extension.eq_ignore_ascii_case("cube") { + return Err("Only .cube LUT files are supported".to_string()); + } + + let metadata = + fs::metadata(path).map_err(|e| format!("Failed to inspect LUT file: {}", e))?; + if !metadata.is_file() { + return Err("Selected LUT path is not a file".to_string()); + } + if metadata.len() > RAW_MONITOR_MAX_LUT_BYTES { + return Err(format!( + "LUT file is too large. Maximum supported size is {} MB.", + RAW_MONITOR_MAX_LUT_BYTES / 1024 / 1024 + )); + } + + let content = + fs::read_to_string(path).map_err(|e| format!("Failed to read LUT file: {}", e))?; + if content.trim().is_empty() { + return Err("LUT file is empty".to_string()); + } + + Ok((readable_file_name(path), content)) +} + +#[cfg(feature = "pro")] +#[tauri::command] +fn import_monitor_lut( + app: tauri::AppHandle, + source_path: String, +) -> Result { + let source_path = PathBuf::from(source_path); + let (name, content) = read_monitor_lut_file(&source_path)?; + let root = monitor_lut_root(&app)?; + + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + source_path.hash(&mut hasher); + content.hash(&mut hasher); + let mut safe_name = sanitize_path_segment(&name); + if !safe_name.to_ascii_lowercase().ends_with(".cube") { + safe_name.push_str(".cube"); + } + let imported_path = root.join(format!("{:016x}-{}", hasher.finish(), safe_name)); + fs::write(&imported_path, content.as_bytes()) + .map_err(|e| format!("Failed to import LUT file: {}", e))?; + + Ok(MonitorLutFile { + path: imported_path.to_string_lossy().to_string(), + name, + content, + }) +} + +#[cfg(feature = "pro")] +#[tauri::command] +fn read_monitor_lut(path: String) -> Result { + let path = PathBuf::from(path); + let (name, content) = read_monitor_lut_file(&path)?; + Ok(MonitorLutFile { + path: path.to_string_lossy().to_string(), + name, + content, + }) +} + +#[cfg(feature = "pro")] +fn raw_monitor_cache_metadata_path(cache_path: &Path) -> PathBuf { + cache_path.with_extension("json") +} + +#[cfg(feature = "pro")] +fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0) +} + +#[cfg(feature = "pro")] +fn read_raw_monitor_cache_metadata(cache_path: &Path) -> Option { + let metadata_path = raw_monitor_cache_metadata_path(cache_path); + let bytes = fs::read(metadata_path).ok()?; + serde_json::from_slice::(&bytes).ok() +} + +#[cfg(feature = "pro")] +fn write_raw_monitor_cache_metadata( + cache_path: &Path, + profile_id: &str, + source: &str, + fallback: bool, + fallback_reason: Option<&str>, +) -> Result<(), String> { + let metadata = RawMonitorCacheMetadata { + cache_source: source.to_string(), + fallback, + fallback_reason: fallback_reason.map(str::to_string), + renderer_version: RAW_MONITOR_RENDERER_VERSION, + profile_id: normalize_raw_monitor_profile_id(profile_id).to_string(), + written_at_ms: now_ms(), + }; + let metadata_path = raw_monitor_cache_metadata_path(cache_path); + if let Some(parent) = metadata_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create RAW monitor cache metadata directory: {}", e))?; + } + let bytes = serde_json::to_vec_pretty(&metadata) + .map_err(|e| format!("Failed to serialize RAW monitor cache metadata: {}", e))?; + fs::write(metadata_path, bytes) + .map_err(|e| format!("Failed to write RAW monitor cache metadata: {}", e)) +} + +#[cfg(feature = "pro")] +fn raw_monitor_cache_status(cache_path: &Path) -> RawMonitorCacheStatus { + if let Some(metadata) = read_raw_monitor_cache_metadata(cache_path) { + return RawMonitorCacheStatus { + source: metadata.cache_source, + fallback: metadata.fallback, + fallback_reason: metadata.fallback_reason, + }; + } + RawMonitorCacheStatus { + source: "rawtherapee".to_string(), + fallback: false, + fallback_reason: None, + } +} + +#[cfg(feature = "pro")] +fn raw_monitor_cache_is_reusable(raw_path: &Path, cache_path: &Path) -> bool { + if validate_raw_monitor_cache_jpeg(cache_path).is_err() { + return false; + } + if !is_nikon_raw_path(raw_path) { + return true; + } + let Some(metadata) = read_raw_monitor_cache_metadata(cache_path) else { + return false; + }; + metadata.fallback + || metadata.cache_source != "rawtherapee" + || metadata.renderer_version >= RAW_MONITOR_RENDERER_VERSION +} + +#[cfg(feature = "pro")] +fn remove_raw_monitor_cache_artifacts(cache_path: &Path) { + let _ = fs::remove_file(cache_path); + let _ = fs::remove_file(raw_monitor_cache_metadata_path(cache_path)); +} + +#[cfg(feature = "pro")] +fn raw_monitor_failure_table_path(app: &tauri::AppHandle) -> Result { + Ok(raw_monitor_cache_root(app)?.join("failures.json")) +} + +#[cfg(feature = "pro")] +fn read_raw_monitor_failure_table( + app: &tauri::AppHandle, +) -> HashMap { + let Ok(path) = raw_monitor_failure_table_path(app) else { + return HashMap::new(); + }; + let Ok(bytes) = fs::read(path) else { + return HashMap::new(); + }; + serde_json::from_slice::>(&bytes) + .unwrap_or_default() +} + +#[cfg(feature = "pro")] +fn write_raw_monitor_failure_table( + app: &tauri::AppHandle, + table: &HashMap, +) -> Result<(), String> { + let path = raw_monitor_failure_table_path(app)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create RAW monitor failure table directory: {}", e))?; + } + let bytes = serde_json::to_vec_pretty(table) + .map_err(|e| format!("Failed to serialize RAW monitor failure table: {}", e))?; + fs::write(path, bytes) + .map_err(|e| format!("Failed to write RAW monitor failure table: {}", e)) +} + +#[cfg(feature = "pro")] +fn raw_monitor_failure_key(cache_path: &Path) -> String { + let cache_key = cache_path + .file_stem() + .and_then(|value| value.to_str()) + .map(|value| value.to_string()) + .unwrap_or_else(|| cache_path.display().to_string()); + format!("r{}-{}", RAW_MONITOR_RENDERER_VERSION, cache_key) +} + +#[cfg(feature = "pro")] +fn raw_monitor_recent_failure<'a>( + table: &'a HashMap, + key: &str, + now: u64, +) -> Option<&'a RawMonitorFailureRecord> { + let record = table.get(key)?; + let cooldown_ms = RAW_MONITOR_FAILURE_RETRY_COOLDOWN_SECS * 1000; + if now.saturating_sub(record.failed_at_ms) < cooldown_ms { + Some(record) + } else { + None + } +} + +#[cfg(feature = "pro")] +fn is_nikon_raw_path(path: &Path) -> bool { + matches!( + path.extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) + .as_deref(), + Some("nef") | Some("nrw") + ) +} + +#[cfg(feature = "pro")] +fn raw_monitor_pp3_path(app: &tauri::AppHandle, profile_id: &str) -> Result { + let profile_id = normalize_raw_monitor_profile_id(profile_id); + let cache_root = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("raw-monitor-previews"); + Ok(cache_root.join(format!("{}.pp3", profile_id))) +} + +#[cfg(feature = "pro")] +fn ensure_raw_monitor_pp3(app: &tauri::AppHandle, profile_id: &str) -> Result { + let path = raw_monitor_pp3_path(app, profile_id)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create RAW monitor cache directory: {}", e))?; + } + fs::write(&path, raw_monitor_pp3_content(profile_id)) + .map_err(|e| format!("Failed to write RAW monitor pp3: {}", e))?; + Ok(path) +} + +#[cfg(feature = "pro")] +fn raw_monitor_pp3_content(profile_id: &str) -> String { + if normalize_raw_monitor_profile_id(profile_id) == RAW_MONITOR_PROFILE_AUTO_EXPOSURE { + return raw_monitor_auto_exposure_pp3_content(); + } + raw_monitor_balanced_pp3_content() +} + +#[cfg(feature = "pro")] +fn raw_monitor_balanced_pp3_content() -> String { + format!( + r#"[Version] +AppVersion=5.12 +Version=349 + +[General] +Rank=0 +ColorLabel=0 + +[Exposure] +Auto=false +Clip=0.0005 +Compensation=0 +Brightness=0 +Contrast=0 +Saturation=0 +Black=0 +HighlightCompr=25 +HighlightComprThreshold=0 +ShadowCompr=0 + +[HLRecovery] +Enabled=true +Method=Coloropp + +[ToneCurve] +CurveMode=Standard +CurveMode2=Standard + +[White Balance] +Setting=Camera + +[Color Management] +InputProfile=(camera) +ToneCurve=true +ApplyLookTable=true +ApplyBaselineExposureOffset=true +ApplyHueSatMap=true +DCPIlluminant=0 +WorkingProfile=ProPhoto +OutputProfile=RTv4_sRGB +OutputProfileIntent=Relative Colorimetric +OutputBPC=true + +[Resize] +Enabled=true +AppliesTo=Cropped area +Method=Lanczos +DataSpecified=3 +Width={max_edge} +Height={max_edge} +Scale=1 +AllowUpscaling=false +"#, + max_edge = RAW_MONITOR_MAX_EDGE + ) +} + +#[cfg(feature = "pro")] +fn raw_monitor_auto_exposure_pp3_content() -> String { + format!( + r#"[Version] +AppVersion=5.12 +Version=349 + +[General] +Rank=0 +ColorLabel=0 + +[Exposure] +Auto=true +Clip=0.0005 +Compensation=0 +Brightness=8 +Contrast=0 +Saturation=0 +Black=0 +HighlightCompr=90 +HighlightComprThreshold=0 +ShadowCompr=55 + +[HLRecovery] +Enabled=true +Method=Coloropp + +[ToneCurve] +CurveMode=Standard +CurveMode2=Standard + +[White Balance] +Setting=Camera + +[Color Management] +InputProfile=(camera) +ToneCurve=true +ApplyLookTable=true +ApplyBaselineExposureOffset=true +ApplyHueSatMap=true +DCPIlluminant=0 +WorkingProfile=ProPhoto +OutputProfile=RTv4_sRGB +OutputProfileIntent=Relative Colorimetric +OutputBPC=true + +[Resize] +Enabled=true +AppliesTo=Cropped area +Method=Lanczos +DataSpecified=3 +Width={max_edge} +Height={max_edge} +Scale=1 +AllowUpscaling=false +"#, + max_edge = RAW_MONITOR_MAX_EDGE + ) +} + +fn normalize_orientation( + image: image::DynamicImage, + orientation: Option, +) -> image::DynamicImage { + match orientation.unwrap_or(1) { + 2 => image.fliph(), + 3 => image.rotate180(), + 4 => image.flipv(), + 5 => image.fliph().rotate90(), + 6 => image.rotate90(), + 7 => image.fliph().rotate270(), + 8 => image.rotate270(), + _ => image, + } +} + +fn resize_to_max_edge(image: image::DynamicImage, max_edge: u32) -> image::DynamicImage { + let max_edge = max_edge.max(64); + let (width, height) = image.dimensions(); + let long_edge = width.max(height); + if long_edge <= max_edge { + return image; + } + + let scale = max_edge as f32 / long_edge as f32; + let next_width = ((width as f32 * scale).round() as u32).max(1); + let next_height = ((height as f32 * scale).round() as u32).max(1); + image.resize( + next_width, + next_height, + image::imageops::FilterType::Triangle, + ) +} + +fn normalize_preview_jpeg(bytes: &[u8], orientation: Option) -> Result, String> { + let orientation = orientation.unwrap_or(1); + if orientation == 1 { + return Ok(bytes.to_vec()); + } + + let image = image::load_from_memory_with_format(bytes, ImageFormat::Jpeg) + .map_err(|e| format!("Failed to decode embedded JPEG preview: {}", e))?; + let normalized = normalize_orientation(image, Some(orientation)); + + let mut output = Vec::new(); + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut output, 92); + encoder + .encode_image(&normalized) + .map_err(|e| format!("Failed to normalize embedded JPEG preview: {}", e))?; + Ok(output) +} + +fn is_displayable_jpeg(bytes: &[u8]) -> bool { + image::load_from_memory_with_format(bytes, ImageFormat::Jpeg).is_ok() +} + +fn orientation_from_exif(exif: &exif::Exif) -> Option { + exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|field| match &field.value { + exif::Value::Short(values) => values.first().copied(), + _ => field + .display_value() + .with_unit(exif) + .to_string() + .split_whitespace() + .next() + .and_then(|value| value.parse::().ok()), + }) + .filter(|value| (1..=8).contains(value)) +} + +#[derive(Clone, Copy)] +struct TiffEntry { + tag: u16, + value_type: u16, + count: u32, + value_or_offset: u32, +} + +#[derive(Clone, Copy)] +struct TiffReader { + little_endian: bool, +} + +impl TiffReader { + fn new(bytes: &[u8]) -> Option { + if bytes.len() < 8 { + return None; + } + let little_endian = match &bytes[0..2] { + b"II" => true, + b"MM" => false, + _ => return None, + }; + let reader = Self { little_endian }; + (reader.u16(bytes, 2)? == 42).then_some(reader) + } + + fn u16(self, bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 2)?; + Some(if self.little_endian { + u16::from_le_bytes([chunk[0], chunk[1]]) + } else { + u16::from_be_bytes([chunk[0], chunk[1]]) + }) + } + + fn u32(self, bytes: &[u8], offset: usize) -> Option { + let chunk = bytes.get(offset..offset + 4)?; + Some(if self.little_endian { + u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) + } else { + u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) + }) + } + + fn first_ifd_offset(self, bytes: &[u8]) -> Option { + self.u32(bytes, 4) + } +} + +fn tiff_type_size(value_type: u16) -> Option { + match value_type { + 1 | 2 | 6 | 7 => Some(1), + 3 | 8 => Some(2), + 4 | 9 | 11 => Some(4), + 5 | 10 | 12 => Some(8), + _ => None, + } +} + +fn read_tiff_entry(reader: TiffReader, bytes: &[u8], offset: usize) -> Option { + Some(TiffEntry { + tag: reader.u16(bytes, offset)?, + value_type: reader.u16(bytes, offset + 2)?, + count: reader.u32(bytes, offset + 4)?, + value_or_offset: reader.u32(bytes, offset + 8)?, + }) +} + +fn tiff_entry_u32_values(reader: TiffReader, bytes: &[u8], entry: TiffEntry) -> Vec { + let Some(type_size) = tiff_type_size(entry.value_type) else { + return Vec::new(); + }; + let Ok(count) = usize::try_from(entry.count) else { + return Vec::new(); + }; + let total_size = type_size.saturating_mul(count); + if count == 0 || total_size == 0 { + return Vec::new(); + } + + let inline = total_size <= 4; + let base = if inline { + None + } else { + usize::try_from(entry.value_or_offset).ok() + }; + + let read_offset = |index: usize| -> Option { + if inline { + Some(8 + index * type_size) + } else { + base.map(|offset| offset + index * type_size) + } + }; + + let mut values = Vec::with_capacity(count); + for index in 0..count { + let Some(offset) = read_offset(index) else { + break; + }; + let value = match entry.value_type { + 1 | 6 | 7 => { + if inline { + entry + .value_or_offset + .to_ne_bytes() + .get(index) + .copied() + .map(u32::from) + } else { + bytes.get(offset).copied().map(u32::from) + } + } + 3 | 8 => { + if inline { + let raw = if reader.little_endian { + (entry.value_or_offset >> (index * 16)) as u16 + } else { + (entry.value_or_offset >> ((1usize.saturating_sub(index)) * 16)) as u16 + }; + Some(u32::from(raw)) + } else { + reader.u16(bytes, offset).map(u32::from) + } + } + 4 | 9 => { + if inline { + Some(entry.value_or_offset) + } else { + reader.u32(bytes, offset) + } + } + _ => None, + }; + + if let Some(value) = value { + values.push(value); + } + } + + values +} + +fn valid_jpeg_range(bytes: &[u8], offset: u32, length: u32) -> Option<(usize, usize)> { + let offset = usize::try_from(offset).ok()?; + let length = usize::try_from(length).ok()?; + if length < MIN_EMBEDDED_JPEG_BYTES { + return None; + } + let end = offset.checked_add(length)?; + let slice = bytes.get(offset..end)?; + let starts_like_jpeg = + slice.len() >= 4 && slice[0] == 0xFF && slice[1] == 0xD8 && slice[2] == 0xFF; + let ends_like_jpeg = + slice.len() >= 2 && slice[slice.len() - 2] == 0xFF && slice[slice.len() - 1] == 0xD9; + if starts_like_jpeg && ends_like_jpeg && is_displayable_jpeg(slice) { + Some((offset, length)) + } else { + None + } +} + +fn collect_tiff_preview_candidates( + bytes: &[u8], + reader: TiffReader, + ifd_offset: u32, + depth: usize, + candidates: &mut Vec<(usize, usize)>, +) { + if depth > 12 { + return; + } + let Ok(ifd_offset) = usize::try_from(ifd_offset) else { + return; + }; + let Some(entry_count) = reader.u16(bytes, ifd_offset).map(usize::from) else { + return; + }; + let entries_start = ifd_offset + 2; + let next_ifd_offset = entries_start + entry_count.saturating_mul(12); + if next_ifd_offset + 4 > bytes.len() { + return; + } + + let mut jpeg_offset_entries = Vec::new(); + let mut jpeg_length_entries = Vec::new(); + let mut strip_offset_entries = Vec::new(); + let mut strip_byte_count_entries = Vec::new(); + let mut nested_ifd_offsets = Vec::new(); + + for index in 0..entry_count { + let entry_offset = entries_start + index * 12; + let Some(entry) = read_tiff_entry(reader, bytes, entry_offset) else { + continue; + }; + match entry.tag { + 0x0201 => jpeg_offset_entries.push(entry), + 0x0202 => jpeg_length_entries.push(entry), + 0x0111 => strip_offset_entries.push(entry), + 0x0117 => strip_byte_count_entries.push(entry), + 0x8769 | 0x8825 | 0x014A => { + nested_ifd_offsets.extend(tiff_entry_u32_values(reader, bytes, entry)) + } + _ => {} + } + } + + let jpeg_offsets = jpeg_offset_entries + .iter() + .flat_map(|entry| tiff_entry_u32_values(reader, bytes, *entry)) + .collect::>(); + let jpeg_lengths = jpeg_length_entries + .iter() + .flat_map(|entry| tiff_entry_u32_values(reader, bytes, *entry)) + .collect::>(); + for (offset, length) in jpeg_offsets.iter().zip(jpeg_lengths.iter()) { + if let Some(candidate) = valid_jpeg_range(bytes, *offset, *length) { + candidates.push(candidate); + } + } + + let strip_offsets = strip_offset_entries + .iter() + .flat_map(|entry| tiff_entry_u32_values(reader, bytes, *entry)) + .collect::>(); + let strip_lengths = strip_byte_count_entries + .iter() + .flat_map(|entry| tiff_entry_u32_values(reader, bytes, *entry)) + .collect::>(); + for (offset, length) in strip_offsets.iter().zip(strip_lengths.iter()) { + if let Some(candidate) = valid_jpeg_range(bytes, *offset, *length) { + candidates.push(candidate); + } + } + + for nested_offset in nested_ifd_offsets { + collect_tiff_preview_candidates(bytes, reader, nested_offset, depth + 1, candidates); + } + + if let Some(next_offset) = reader + .u32(bytes, next_ifd_offset) + .filter(|offset| *offset > 0) + { + collect_tiff_preview_candidates(bytes, reader, next_offset, depth + 1, candidates); + } +} + +fn find_tiff_embedded_preview(bytes: &[u8]) -> Option<(usize, usize)> { + let reader = TiffReader::new(bytes)?; + let first_ifd_offset = reader.first_ifd_offset(bytes)?; + let mut candidates = Vec::new(); + collect_tiff_preview_candidates(bytes, reader, first_ifd_offset, 0, &mut candidates); + candidates.into_iter().max_by_key(|(_, length)| *length) +} + +fn find_largest_embedded_jpeg(bytes: &[u8]) -> Option<(usize, usize)> { + if let Some(candidate) = find_tiff_embedded_preview(bytes) { + return Some(candidate); + } + + let mut best: Option<(usize, usize)> = None; + let mut pos = 0usize; + + while pos + 4 < bytes.len() { + let Some(relative_start) = bytes[pos..] + .windows(3) + .position(|window| window[0] == 0xFF && window[1] == 0xD8 && window[2] == 0xFF) + else { + break; + }; + + let start = pos + relative_start; + let mut cursor = start + 3; + let mut end = None; + + while cursor + 1 < bytes.len() { + if bytes[cursor] == 0xFF && bytes[cursor + 1] == 0xD9 { + end = Some(cursor + 2); + break; + } + cursor += 1; + } + + if let Some(end_offset) = end { + let length = end_offset.saturating_sub(start); + if length >= MIN_EMBEDDED_JPEG_BYTES { + let slice = &bytes[start..end_offset]; + if is_displayable_jpeg(slice) { + match best { + Some((_, best_length)) if best_length >= length => {} + _ => best = Some((start, length)), + } + } + } + pos = end_offset; + } else { + break; + } + } + + best +} + +fn photo_file_from_path(file_path: &Path) -> Option<(String, PhotoFileInfo)> { + if !file_path.is_file() { + return None; + } + + let file_name = file_path.file_name()?.to_str()?.to_string(); + let extension = file_path + .extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_uppercase()) + .unwrap_or_default(); + + if extension != "JPG" && extension != "JPEG" && !RAW_EXTENSIONS.contains(&extension.as_str()) { + return None; + } + + let base_name = file_path.file_stem()?.to_str()?.to_string(); + let file_size = fs::metadata(file_path) + .map(|metadata| metadata.len()) + .unwrap_or(0); + let modified_ms = file_modified_ms(file_path); + + Some(( + base_name, + PhotoFileInfo { + name: file_name, + extension, + path: file_path.to_string_lossy().to_string(), + size: file_size, + modified_ms, + }, + )) +} + +fn collect_photo_groups_from_paths( + paths: Vec, + mut on_progress: F, +) -> Vec +where + F: FnMut(usize, usize, Option), +{ + let total = paths.len(); + on_progress(0, total, None); + + let mut groups: HashMap = HashMap::new(); + + for (index, file_path) in paths.into_iter().enumerate() { + let processed = index + 1; + if should_emit_progress(processed, total) { + on_progress( + processed, + total, + file_path + .file_name() + .and_then(|value| value.to_str()) + .map(|value| value.to_string()), + ); + } + + let Some((base_name, photo_file)) = photo_file_from_path(&file_path) else { + continue; + }; + + let group = groups + .entry(base_name.clone()) + .or_insert_with(|| PhotoGroupInfo { + id: base_name, + jpg: None, + raw: None, + status: String::from("UNMARKED"), + rating: 0, + exif: None, + }); + + if photo_file.extension == "JPG" || photo_file.extension == "JPEG" { + group.jpg = Some(photo_file); + } else if RAW_EXTENSIONS.contains(&photo_file.extension.as_str()) { + group.raw = Some(photo_file); + } + } + + on_progress(total, total, None); + finalize_photo_groups(groups) +} + +fn send_import_stream_event(channel: &Channel, event: ImportStreamEvent) { + let _ = channel.send(event); +} + +fn import_event( + kind: &str, + phase: Option<&str>, + processed: Option, + total: Option, + current: Option, + groups: Option>, + error: Option, +) -> ImportStreamEvent { + ImportStreamEvent { + kind: kind.to_string(), + phase: phase.map(|value| value.to_string()), + processed, + total, + current, + groups, + error, + } +} + +fn export_event( + kind: &str, + phase: Option<&str>, + processed: Option, + total: Option, + current: Option, + files: Option>, + error: Option, +) -> ExportStreamEvent { + ExportStreamEvent { + kind: kind.to_string(), + phase: phase.map(|value| value.to_string()), + processed, + total, + current, + files, + error, + } +} + +fn send_export_stream_event(channel: &Channel, event: ExportStreamEvent) { + let _ = channel.send(event); +} + +fn send_import_groups_in_chunks( + channel: &Channel, + kind: &str, + phase: &str, + groups: &[PhotoGroupInfo], +) { + for chunk in groups.chunks(IMPORT_BATCH_SIZE) { + send_import_stream_event( + channel, + import_event( + kind, + Some(phase), + None, + Some(groups.len()), + None, + Some(chunk.to_vec()), + None, + ), + ); + } +} + +fn finalize_photo_groups(groups: HashMap) -> Vec { + let mut result: Vec = Vec::new(); + + for (_, mut group) in groups { + group.status = match (&group.jpg, &group.raw) { + (Some(_), Some(_)) => "COMPLETE".to_string(), + (Some(_), None) => "JPG_ONLY".to_string(), + (None, Some(_)) => "RAW_ONLY".to_string(), + _ => continue, + }; + result.push(group); + } + + result.sort_by(|a, b| a.id.cmp(&b.id)); + result +} + +fn enrich_group_metadata(group: &mut PhotoGroupInfo) { + if let Some(jpg) = &group.jpg { + group.exif = read_exif(jpg.path.clone()).ok(); + } else if let Some(raw) = &group.raw { + group.exif = read_exif(raw.path.clone()).ok(); + } + group.rating = read_group_rating(group); +} + +fn parse_xmp_rating(text: &str) -> Option { + for marker in ["xmp:Rating=\"", "Rating=\"", "xmp:Rating='", "Rating='"] { + if let Some(start) = text.find(marker) { + let value_start = start + marker.len(); + let quote = if marker.ends_with('\'') { '\'' } else { '"' }; + if let Some(end) = text[value_start..].find(quote) { + return text[value_start..value_start + end] + .trim() + .parse::() + .ok() + .map(clamp_rating); + } + } + } + + if let Some(start) = text.find("") { + let value_start = start + "".len(); + if let Some(end) = text[value_start..].find("") { + return text[value_start..value_start + end] + .trim() + .parse::() + .ok() + .map(clamp_rating); + } + } + + None +} + +fn build_minimal_xmp(rating: u8) -> String { + format!( + r#" + + + + + +"#, + clamp_rating(rating) + ) +} + +fn replace_quoted_rating(text: &str, marker: &str, rating: u8) -> Option { + let start = text.find(marker)?; + let value_start = start + marker.len(); + let quote = if marker.ends_with('\'') { '\'' } else { '"' }; + let end = text[value_start..].find(quote)? + value_start; + + let mut updated = String::with_capacity(text.len() + 2); + updated.push_str(&text[..value_start]); + updated.push_str(&clamp_rating(rating).to_string()); + updated.push_str(&text[end..]); + Some(updated) +} + +fn upsert_xmp_rating(text: &str, rating: u8) -> String { + for marker in ["xmp:Rating=\"", "Rating=\"", "xmp:Rating='", "Rating='"] { + if let Some(updated) = replace_quoted_rating(text, marker, rating) { + return updated; + } + } + + if let Some(start) = text.find("") { + let value_start = start + "".len(); + if let Some(end_rel) = text[value_start..].find("") { + let end = value_start + end_rel; + let mut updated = String::with_capacity(text.len() + 2); + updated.push_str(&text[..value_start]); + updated.push_str(&clamp_rating(rating).to_string()); + updated.push_str(&text[end..]); + return updated; + } + } + + if let Some(desc_start) = text.find("') { + let tag_end = desc_start + tag_end_rel; + let insert_at = if tag_end > 0 && text.as_bytes()[tag_end - 1] == b'/' { + tag_end - 1 + } else { + tag_end + }; + let namespace = if text.contains("xmlns:xmp=") { + String::new() + } else { + " xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\"".to_string() + }; + let attr = format!("{} xmp:Rating=\"{}\"", namespace, clamp_rating(rating)); + let mut updated = String::with_capacity(text.len() + attr.len()); + updated.push_str(&text[..insert_at]); + updated.push_str(&attr); + updated.push_str(&text[insert_at..]); + return updated; + } + } + + build_minimal_xmp(rating) +} + +fn read_xmp_sidecar_rating(path: &Path) -> Option { + let sidecar = xmp_sidecar_path(path); + let text = fs::read_to_string(sidecar).ok()?; + parse_xmp_rating(&text) +} + +fn write_xmp_sidecar_rating(path: &Path, rating: u8) -> Result { + let sidecar = xmp_sidecar_path(path); + let next = match fs::read_to_string(&sidecar) { + Ok(existing) => upsert_xmp_rating(&existing, rating), + Err(_) => build_minimal_xmp(rating), + }; + + let mut file = File::create(&sidecar) + .map_err(|e| format!("Failed to create {}: {}", sidecar.display(), e))?; + file.write_all(next.as_bytes()) + .map_err(|e| format!("Failed to write {}: {}", sidecar.display(), e))?; + Ok(sidecar) +} + +fn read_jpeg_xmp_rating(path: &Path) -> Option { + let mut file = File::open(path).ok()?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes).ok()?; + + find_jpeg_xmp_packet(&bytes) + .and_then(|packet| std::str::from_utf8(packet).ok()) + .and_then(parse_xmp_rating) +} + +fn find_jpeg_xmp_packet(bytes: &[u8]) -> Option<&[u8]> { + if bytes.len() < 4 || bytes[0] != 0xFF || bytes[1] != 0xD8 { + return None; + } + + let mut pos = 2; + while pos + 4 <= bytes.len() { + if bytes[pos] != 0xFF { + return None; + } + + let marker = bytes[pos + 1]; + if marker == 0xDA || marker == 0xD9 { + return None; + } + + let len = u16::from_be_bytes([bytes[pos + 2], bytes[pos + 3]]) as usize; + if len < 2 || pos + 2 + len > bytes.len() { + return None; + } + + let payload_start = pos + 4; + let payload_end = pos + 2 + len; + if marker == 0xE1 && bytes[payload_start..payload_end].starts_with(XMP_IDENTIFIER) { + return Some(&bytes[payload_start + XMP_IDENTIFIER.len()..payload_end]); + } + + pos = payload_end; + } + + None +} + +fn build_jpeg_xmp_segment(rating: u8) -> Result, String> { + let packet = build_minimal_xmp(rating); + let mut payload = Vec::with_capacity(XMP_IDENTIFIER.len() + packet.len()); + payload.extend_from_slice(XMP_IDENTIFIER); + payload.extend_from_slice(packet.as_bytes()); + + let len = payload.len() + 2; + if len > u16::MAX as usize { + return Err("XMP packet is too large for a JPEG APP1 segment".to_string()); + } + + let mut segment = Vec::with_capacity(len + 2); + segment.push(0xFF); + segment.push(0xE1); + segment.extend_from_slice(&(len as u16).to_be_bytes()); + segment.extend_from_slice(&payload); + Ok(segment) +} + +fn collect_jpeg_app_metadata_segments(path: &Path) -> Result>, String> { + let bytes = fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + if bytes.len() < 2 || bytes[0] != 0xFF || bytes[1] != 0xD8 { + return Err(format!("{} is not a valid JPEG file", path.display())); + } + + let mut segments = Vec::new(); + let mut pos = 2usize; + while pos + 4 <= bytes.len() { + if bytes[pos] != 0xFF { + break; + } + + let marker = bytes[pos + 1]; + if marker == 0xDA || marker == 0xD9 { + break; + } + + let len = u16::from_be_bytes([bytes[pos + 2], bytes[pos + 3]]) as usize; + if len < 2 || pos + 2 + len > bytes.len() { + break; + } + + let payload_start = pos + 4; + let payload_end = pos + 2 + len; + let payload = &bytes[payload_start..payload_end]; + let is_exif_segment = marker == 0xE1 && payload.starts_with(EXIF_IDENTIFIER); + if is_exif_segment { + segments.push(bytes[pos..payload_end].to_vec()); + } + pos = payload_end; + } + + Ok(segments) +} + +fn jpeg_without_app_metadata_segments(path: &Path) -> Result, String> { + let bytes = fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + if bytes.len() < 2 || bytes[0] != 0xFF || bytes[1] != 0xD8 { + return Err(format!("{} is not a valid JPEG file", path.display())); + } + + let mut output = Vec::with_capacity(bytes.len()); + output.extend_from_slice(&bytes[..2]); + + let mut pos = 2usize; + while pos + 4 <= bytes.len() { + if bytes[pos] != 0xFF { + output.extend_from_slice(&bytes[pos..]); + return Ok(output); + } + + let marker = bytes[pos + 1]; + if marker == 0xDA || marker == 0xD9 { + output.extend_from_slice(&bytes[pos..]); + return Ok(output); + } + + let len = u16::from_be_bytes([bytes[pos + 2], bytes[pos + 3]]) as usize; + if len < 2 || pos + 2 + len > bytes.len() { + output.extend_from_slice(&bytes[pos..]); + return Ok(output); + } + + let payload_end = pos + 2 + len; + let is_app_segment = (0xE0..=0xEF).contains(&marker); + if !is_app_segment { + output.extend_from_slice(&bytes[pos..payload_end]); + } + pos = payload_end; + } + + output.extend_from_slice(&bytes[pos..]); + Ok(output) +} + +fn preserve_jpeg_capture_segments(destination: &Path, source: &Path) -> Result<(), String> { + let source_segments = collect_jpeg_app_metadata_segments(source)?; + let destination_without_metadata = jpeg_without_app_metadata_segments(destination)?; + + let mut output = Vec::new(); + output.extend_from_slice(&destination_without_metadata[..2]); + for segment in source_segments { + output.extend_from_slice(&segment); + } + output.extend_from_slice(&destination_without_metadata[2..]); + fs::write(destination, output).map_err(|e| { + format!( + "Failed to preserve metadata for {}: {}", + destination.display(), + e + ) + }) +} + +fn write_jpeg_xmp_rating(path: &Path, rating: u8) -> Result { + let bytes = fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + if bytes.len() < 2 || bytes[0] != 0xFF || bytes[1] != 0xD8 { + return Err(format!("{} is not a valid JPEG file", path.display())); + } + + let xmp_segment = build_jpeg_xmp_segment(rating)?; + let mut output = Vec::with_capacity(bytes.len() + xmp_segment.len()); + output.extend_from_slice(&bytes[..2]); + output.extend_from_slice(&xmp_segment); + + let mut pos = 2; + while pos < bytes.len() { + if pos + 4 > bytes.len() || bytes[pos] != 0xFF { + output.extend_from_slice(&bytes[pos..]); + break; + } + + let marker = bytes[pos + 1]; + if marker == 0xDA || marker == 0xD9 { + output.extend_from_slice(&bytes[pos..]); + break; + } + + let len = u16::from_be_bytes([bytes[pos + 2], bytes[pos + 3]]) as usize; + if len < 2 || pos + 2 + len > bytes.len() { + output.extend_from_slice(&bytes[pos..]); + break; + } + + let payload_start = pos + 4; + let payload_end = pos + 2 + len; + let is_xmp = + marker == 0xE1 && bytes[payload_start..payload_end].starts_with(XMP_IDENTIFIER); + if !is_xmp { + output.extend_from_slice(&bytes[pos..payload_end]); + } + pos = payload_end; + } + + fs::write(path, output).map_err(|e| format!("Failed to write {}: {}", path.display(), e))?; + Ok(path.to_path_buf()) +} + +fn read_rating_from_path(path: &Path) -> Option { + if is_jpeg_path(path) { + return read_jpeg_xmp_rating(path).or_else(|| read_xmp_sidecar_rating(path)); + } + + read_xmp_sidecar_rating(path) +} + +fn write_rating_to_path(path: &Path, rating: u8) -> Result { + if is_jpeg_path(path) { + return write_jpeg_xmp_rating(path, rating); + } + + if is_raw_path(path) { + return write_xmp_sidecar_rating(path, rating); + } + + write_xmp_sidecar_rating(path, rating) +} + +fn rendered_export_metadata_mode(value: Option<&str>) -> &'static str { + match value { + Some("RATING_ONLY") => "RATING_ONLY", + Some("CAPTURE_INFO_AND_RATING") => "CAPTURE_INFO_AND_RATING", + Some("ALL") => "ALL", + _ => "NONE", + } +} + +fn write_rendered_export_metadata( + path: &Path, + rating: Option, + metadata_mode: Option<&str>, + metadata_source_path: Option<&str>, +) -> Result, String> { + let mode = rendered_export_metadata_mode(metadata_mode); + if mode == "NONE" { + return Ok(Vec::new()); + } + + if mode == "CAPTURE_INFO_AND_RATING" && is_jpeg_path(path) { + let source_path = metadata_source_path + .map(Path::new) + .ok_or_else(|| "No JPEG source available for capture metadata copy".to_string())?; + preserve_jpeg_capture_segments(path, source_path)?; + } + + let Some(rating) = rating else { + return Ok(Vec::new()); + }; + + let target = write_rating_to_path(path, clamp_rating(rating))?; + Ok(vec![target.display().to_string()]) +} + +fn read_group_rating(group: &PhotoGroupInfo) -> u8 { + let mut rating = 0u8; + if let Some(jpg) = &group.jpg { + if let Some(value) = read_rating_from_path(Path::new(&jpg.path)) { + rating = rating.max(value); + } + } + if let Some(raw) = &group.raw { + if let Some(value) = read_rating_from_path(Path::new(&raw.path)) { + rating = rating.max(value); + } + } + clamp_rating(rating) +} + +// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +#[tauri::command] +fn greet(name: &str) -> String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +#[tauri::command] +fn read_exif(file_path: String) -> Result { + let path = Path::new(&file_path); + + // Open the file + let file = File::open(path).map_err(|e| format!("Failed to open file: {}", e))?; + let mut bufreader = BufReader::new(file); + + // Read EXIF data + let exifreader = exif::Reader::new(); + let exif = exifreader + .read_from_container(&mut bufreader) + .map_err(|e| format!("Failed to read EXIF: {}", e))?; + + // Extract commonly used EXIF fields + let mut exif_data = ExifData { + shutter_speed: None, + aperture: None, + iso: None, + focal_length: None, + date_time: None, + model: None, + lens: None, + orientation: orientation_from_exif(&exif), + }; + + // Extract exposure time (shutter speed) + if let Some(field) = exif.get_field(exif::Tag::ExposureTime, exif::In::PRIMARY) { + match &field.value { + exif::Value::Rational(ref vals) => { + if let Some(val) = vals.first() { + if val.denom != 0 { + let speed = val.num as f64 / val.denom as f64; + if speed >= 1.0 { + exif_data.shutter_speed = Some(format!("{:.1}s", speed)); + } else { + // For speeds less than 1 second, display as fraction + let reciprocal = (val.denom as f64 / val.num as f64).round() as u32; + exif_data.shutter_speed = Some(format!("1/{}", reciprocal)); + } + } + } + } + _ => { + // Fallback to display value if not a rational + let display = field.display_value().with_unit(&exif).to_string(); + exif_data.shutter_speed = Some(display.trim_matches('"').to_string()); + } + } + } + + // Extract aperture (F-number) + if let Some(field) = exif.get_field(exif::Tag::FNumber, exif::In::PRIMARY) { + if let exif::Value::Rational(ref vals) = field.value { + if let Some(val) = vals.first() { + if val.denom != 0 { + let aperture = val.num as f64 / val.denom as f64; + exif_data.aperture = Some(format!("f/{:.1}", aperture)); + } + } + } + } + + // Extract ISO + if let Some(field) = exif.get_field(exif::Tag::PhotographicSensitivity, exif::In::PRIMARY) { + let iso_str = field.display_value().with_unit(&exif).to_string(); + exif_data.iso = Some(iso_str.trim_matches('"').to_string()); + } + + // Extract focal length + if let Some(field) = exif.get_field(exif::Tag::FocalLength, exif::In::PRIMARY) { + if let exif::Value::Rational(ref vals) = field.value { + if let Some(val) = vals.first() { + if val.denom != 0 { + let focal = val.num as f64 / val.denom as f64; + exif_data.focal_length = Some(format!("{:.0}mm", focal)); + } + } + } + } + + // Extract date time + if let Some(field) = exif.get_field(exif::Tag::DateTime, exif::In::PRIMARY) { + let datetime_str = field.display_value().with_unit(&exif).to_string(); + exif_data.date_time = Some(datetime_str.trim_matches('"').to_string()); + } + + // Extract camera model + if let Some(field) = exif.get_field(exif::Tag::Model, exif::In::PRIMARY) { + let model_str = field.display_value().with_unit(&exif).to_string(); + exif_data.model = Some(model_str.trim_matches('"').to_string()); + } + + // Extract lens model + if let Some(field) = exif.get_field(exif::Tag::LensModel, exif::In::PRIMARY) { + match &field.value { + exif::Value::Ascii(ref vec) => { + // Handle ASCII value - join all non-empty strings + let lens_parts: Vec = vec + .iter() + .filter_map(|bytes| { + let s = std::str::from_utf8(bytes) + .ok()? + .trim_matches('\0') + .trim() + .trim_matches('"'); + if !s.is_empty() { + Some(s.to_string()) + } else { + None + } + }) + .collect(); + + if !lens_parts.is_empty() { + exif_data.lens = Some(lens_parts[0].clone()); + } + } + _ => { + // Fallback to display value + let lens_str = field.display_value().with_unit(&exif).to_string(); + let cleaned = lens_str.trim_matches('"').trim().to_string(); + // Remove any trailing empty quoted strings like ","","" + let final_lens = cleaned + .split(',') + .next() + .unwrap_or(&cleaned) + .trim() + .trim_matches('"') + .to_string(); + + if !final_lens.is_empty() { + exif_data.lens = Some(final_lens); + } + } + } + } + + Ok(exif_data) +} + +#[tauri::command] +fn scan_folder(app: tauri::AppHandle, folder_path: String) -> Result, String> { + let path = Path::new(&folder_path); + + if !path.is_dir() { + return Err("Path is not a directory".to_string()); + } + + let entries = collect_files_recursive(path)?; + + Ok(collect_photo_groups_from_paths( + entries, + |processed, total, current| { + emit_import_progress(&app, "scan", processed, total, current); + }, + )) +} + +#[tauri::command] +fn scan_files( + app: tauri::AppHandle, + file_paths: Vec, +) -> Result, String> { + let paths: Vec = file_paths.into_iter().map(PathBuf::from).collect(); + Ok(collect_photo_groups_from_paths( + paths, + |processed, total, current| { + emit_import_progress(&app, "scan", processed, total, current); + }, + )) +} + +#[tauri::command] +fn enrich_photo_metadata( + app: tauri::AppHandle, + groups: Vec, +) -> Result, String> { + let total = groups.len(); + let mut result = Vec::with_capacity(total); + emit_import_progress(&app, "metadata", 0, total, None); + + for (index, mut group) in groups.into_iter().enumerate() { + enrich_group_metadata(&mut group); + let processed = index + 1; + if should_emit_progress(processed, total) { + emit_import_progress(&app, "metadata", processed, total, Some(group.id.clone())); + } + result.push(group); + } + + emit_import_progress(&app, "done", total, total, None); + Ok(result) +} + +fn stream_import_groups( + channel: &Channel, + phase: &str, + groups: &[PhotoGroupInfo], +) { + let total = groups.len(); + send_import_stream_event( + channel, + import_event( + "progress", + Some(phase), + Some(0), + Some(total), + None, + None, + None, + ), + ); + send_import_groups_in_chunks(channel, "groups", phase, groups); + send_import_stream_event( + channel, + import_event( + "progress", + Some(phase), + Some(total), + Some(total), + None, + None, + None, + ), + ); +} + +fn stream_import_metadata( + channel: &Channel, + groups: &[PhotoGroupInfo], +) -> Vec { + let total = groups.len(); + let mut enriched = Vec::with_capacity(total); + let mut batch = Vec::with_capacity(IMPORT_BATCH_SIZE); + + send_import_stream_event( + channel, + import_event( + "progress", + Some("metadata"), + Some(0), + Some(total), + None, + None, + None, + ), + ); + + for (index, mut group) in groups.iter().cloned().enumerate() { + enrich_group_metadata(&mut group); + batch.push(group.clone()); + let processed = index + 1; + if batch.len() >= IMPORT_BATCH_SIZE { + send_import_stream_event( + channel, + import_event( + "metadata", + Some("metadata"), + Some(processed), + Some(total), + None, + Some(std::mem::take(&mut batch)), + None, + ), + ); + std::thread::sleep(Duration::from_millis(8)); + } + enriched.push(group); + } + + if !batch.is_empty() { + send_import_stream_event( + channel, + import_event( + "metadata", + Some("metadata"), + Some(total), + Some(total), + None, + Some(batch), + None, + ), + ); + } + + send_import_stream_event( + channel, + import_event( + "done", + Some("done"), + Some(total), + Some(total), + None, + None, + None, + ), + ); + + enriched +} + +fn run_import_folder_stream( + app: tauri::AppHandle, + folder_path: String, + channel: Channel, +) -> Result<(), String> { + let path = Path::new(&folder_path); + if !path.is_dir() { + let error = "Path is not a directory".to_string(); + send_import_stream_event( + &channel, + import_event( + "error", + Some("error"), + None, + None, + None, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + let entries = collect_files_recursive(path)?; + + let groups = collect_photo_groups_from_paths(entries, |processed, total, current| { + send_import_stream_event( + &channel, + import_event( + "progress", + Some("scan"), + Some(processed), + Some(total), + current, + None, + None, + ), + ); + }); + + stream_import_groups(&channel, "pair", &groups); + stream_import_metadata(&channel, &groups); + + let _ = app; + Ok(()) +} + +fn run_import_files_stream( + app: tauri::AppHandle, + file_paths: Vec, + channel: Channel, +) -> Result<(), String> { + let paths: Vec = file_paths.into_iter().map(PathBuf::from).collect(); + let groups = collect_photo_groups_from_paths(paths, |processed, total, current| { + send_import_stream_event( + &channel, + import_event( + "progress", + Some("scan"), + Some(processed), + Some(total), + current, + None, + None, + ), + ); + }); + + stream_import_groups(&channel, "pair", &groups); + stream_import_metadata(&channel, &groups); + + let _ = app; + Ok(()) +} + +#[tauri::command] +async fn import_folder_stream( + app: tauri::AppHandle, + folder_path: String, + on_event: Channel, +) -> Result<(), String> { + spawn_blocking(move || run_import_folder_stream(app, folder_path, on_event)) + .await + .map_err(|e| format!("Import task failed: {}", e))? +} + +#[tauri::command] +async fn import_files_stream( + app: tauri::AppHandle, + file_paths: Vec, + on_event: Channel, +) -> Result<(), String> { + spawn_blocking(move || run_import_files_stream(app, file_paths, on_event)) + .await + .map_err(|e| format!("Import task failed: {}", e))? +} + +fn extract_raw_embedded_preview_blocking( + app: tauri::AppHandle, + file_path: String, +) -> Result, String> { + let path = Path::new(&file_path); + + if !path.is_file() || !is_raw_path(path) { + return Ok(None); + } + + let orientation = read_orientation(path); + let bytes = fs::read(path).map_err(|e| format!("Failed to read RAW file: {}", e))?; + let Some((offset, byte_length)) = find_largest_embedded_jpeg(&bytes) else { + return Ok(None); + }; + + let cache_path = raw_preview_cache_path(&app, path, byte_length, orientation)?; + if cache_path.exists() { + return Ok(Some(RawEmbeddedPreview { + cache_path: cache_path.to_string_lossy().to_string(), + byte_length, + offset, + orientation, + from_cache: true, + source: "embedded-jpeg".to_string(), + width: None, + height: None, + error: None, + })); + } + + let end = offset + byte_length; + let normalized = normalize_preview_jpeg(&bytes[offset..end], orientation)?; + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create preview cache directory: {}", e))?; + } + let mut file = File::create(&cache_path) + .map_err(|e| format!("Failed to create preview cache file: {}", e))?; + file.write_all(&normalized) + .map_err(|e| format!("Failed to write preview cache file: {}", e))?; + + Ok(Some(RawEmbeddedPreview { + cache_path: cache_path.to_string_lossy().to_string(), + byte_length, + offset, + orientation, + from_cache: false, + source: "embedded-jpeg".to_string(), + width: None, + height: None, + error: None, + })) +} + +fn get_jpeg_thumbnail_blocking( + app: tauri::AppHandle, + file_path: String, + max_edge: Option, +) -> Result { + let path = Path::new(&file_path); + + if !path.is_file() || !is_jpeg_path(path) { + return Err("Path is not a JPEG file".to_string()); + } + + let max_edge = max_edge.unwrap_or(360).clamp(160, 720); + let orientation = read_orientation(path); + let cache_path = jpeg_thumbnail_cache_path(&app, path, max_edge, orientation)?; + if cache_path.exists() { + let image = image::open(&cache_path) + .map_err(|e| format!("Failed to read cached JPEG thumbnail: {}", e))?; + let (width, height) = image.dimensions(); + return Ok(CachedJpegThumbnail { + cache_path: cache_path.to_string_lossy().to_string(), + from_cache: true, + width, + height, + }); + } + + let image = image::open(path).map_err(|e| format!("Failed to decode JPEG thumbnail: {}", e))?; + let normalized = normalize_orientation(image, orientation); + let thumbnail = resize_to_max_edge(normalized, max_edge); + let (width, height) = thumbnail.dimensions(); + + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create JPEG thumbnail cache directory: {}", e))?; + } + + let mut file = File::create(&cache_path) + .map_err(|e| format!("Failed to create JPEG thumbnail cache file: {}", e))?; + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut file, 78); + encoder + .encode_image(&thumbnail) + .map_err(|e| format!("Failed to write JPEG thumbnail cache: {}", e))?; + + Ok(CachedJpegThumbnail { + cache_path: cache_path.to_string_lossy().to_string(), + from_cache: false, + width, + height, + }) +} + +#[cfg(feature = "pro")] +fn raw_monitor_event( + kind: &str, + processed: Option, + total: Option, + current: Option, + raw_path: Option, + cache_path: Option, + engine_version: Option, + errors: Option>, + error: Option, +) -> RawMonitorCacheEvent { + RawMonitorCacheEvent { + kind: kind.to_string(), + processed, + total, + current, + raw_path, + profile_id: None, + cache_path, + fallback: None, + cache_source: None, + fallback_reason: None, + skipped_reason: None, + engine_version, + errors, + error, + } +} + +#[cfg(feature = "pro")] +fn send_raw_monitor_event(channel: &Channel, event: RawMonitorCacheEvent) { + let _ = channel.send(event); +} + +#[cfg(feature = "pro")] +fn rawtherapee_command(engine_path: &Path) -> Command { + let mut command = Command::new(engine_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if let Some(parent) = engine_path.parent() { + command.current_dir(parent); + } + #[cfg(windows)] + { + command.creation_flags(0x08000000); + } + command +} + +#[cfg(feature = "pro")] +fn run_command_with_timeout(mut command: Command, timeout: Duration) -> Result { + let mut child = command + .spawn() + .map_err(|e| format!("Failed to launch process: {}", e))?; + let started = Instant::now(); + + loop { + if let Some(_status) = child + .try_wait() + .map_err(|e| format!("Failed to wait for process: {}", e))? + { + return child + .wait_with_output() + .map_err(|e| format!("Failed to read process output: {}", e)); + } + + if started.elapsed() >= timeout { + let _ = child.kill(); + let _ = child.wait(); + return Err("Process timed out".to_string()); + } + + std::thread::sleep(Duration::from_millis(80)); + } +} + +#[cfg(feature = "pro")] +fn raw_engine_result( + ok: bool, + engine_path: Option, + version: Option, + engine_source: Option<&str>, + message: Option, +) -> RawEngineValidationResult { + RawEngineValidationResult { + ok, + engine_kind: "RAWTHERAPEE".to_string(), + engine_path, + version, + engine_source: engine_source.map(|value| value.to_string()), + bundled_engine_version: Some(RAWTHERAPEE_BUNDLED_VERSION.to_string()), + message, + } +} + +#[cfg(feature = "pro")] +fn validate_raw_engine_path(engine_path: &Path, engine_source: &str) -> RawEngineValidationResult { + if !engine_path.is_file() { + return raw_engine_result( + false, + Some(engine_path.display().to_string()), + None, + Some(engine_source), + Some("RawTherapee CLI was not found at this path".to_string()), + ); + } + + let mut command = rawtherapee_command(engine_path); + command.arg("-v"); + match run_command_with_timeout(command, Duration::from_secs(5)) { + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let version = if !stdout.is_empty() { + stdout + } else if !stderr.is_empty() { + stderr + } else { + "RawTherapee CLI".to_string() + }; + let ok = output.status.success() || version.to_lowercase().contains("rawtherapee"); + raw_engine_result( + ok, + Some(engine_path.display().to_string()), + Some(version), + Some(engine_source), + if ok { + Some(match engine_source { + "BUNDLED" => "Bundled RawTherapee CLI is available".to_string(), + "MANUAL" => "RawTherapee CLI is available".to_string(), + _ => "System RawTherapee CLI is available".to_string(), + }) + } else { + Some(format!("RawTherapee CLI returned status {}", output.status)) + }, + ) + } + Err(error) => raw_engine_result( + false, + Some(engine_path.display().to_string()), + None, + Some(engine_source), + Some(error), + ), + } +} + +#[cfg(feature = "pro")] +fn validate_raw_engine_blocking(engine_path: String) -> RawEngineValidationResult { + let path = PathBuf::from(engine_path.trim()); + if path.as_os_str().is_empty() { + return raw_engine_result( + false, + None, + None, + Some("MANUAL"), + Some("RawTherapee CLI path is empty".to_string()), + ); + } + + validate_raw_engine_path(&path, "MANUAL") +} + +#[cfg(feature = "pro")] +fn bundled_rawtherapee_candidates(app: &tauri::AppHandle) -> Vec { + let mut candidates = Vec::new(); + + if let Ok(resource_dir) = app.path().resource_dir() { + candidates.push(resource_dir.join(RAWTHERAPEE_BUNDLED_RESOURCE_CLI)); + } + + if let Ok(current_exe) = std::env::current_exe() { + if let Some(dir) = current_exe.parent() { + candidates.push(dir.join(RAWTHERAPEE_BUNDLED_RESOURCE_CLI)); + candidates.push( + dir.join("../Resources") + .join(RAWTHERAPEE_BUNDLED_RESOURCE_CLI), + ); + } + } + + candidates.push(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(RAWTHERAPEE_DEV_VENDOR_CLI)); + candidates +} + +#[cfg(any(test, all(feature = "pro", target_os = "macos")))] +fn macos_rawtherapee_candidates(home_dir: Option<&Path>) -> Vec<(PathBuf, &'static str)> { + let mut candidates = Vec::new(); + + if let Some(home_dir) = home_dir { + candidates.push(( + home_dir.join("Library/Application Support/com.framecull.ai.pro/tools/rawtherapee-cli"), + "SYSTEM", + )); + } + + candidates.push((PathBuf::from("/usr/local/bin/rawtherapee-cli"), "SYSTEM")); + candidates.push((PathBuf::from("/opt/homebrew/bin/rawtherapee-cli"), "SYSTEM")); + candidates +} + +#[cfg(feature = "pro")] +fn rawtherapee_candidates(app: Option<&tauri::AppHandle>) -> Vec<(PathBuf, &'static str)> { + let mut candidates = Vec::new(); + + if let Some(app) = app { + for path in bundled_rawtherapee_candidates(app) { + candidates.push((path, "BUNDLED")); + } + } + + #[cfg(target_os = "macos")] + { + let home_dir = std::env::var_os("HOME").map(PathBuf::from); + candidates.extend(macos_rawtherapee_candidates(home_dir.as_deref())); + } + + if let Some(paths) = std::env::var_os("PATH") { + for path in std::env::split_paths(&paths) { + candidates.push((path.join("rawtherapee-cli.exe"), "SYSTEM")); + candidates.push((path.join("rawtherapee-cli"), "SYSTEM")); + } + } + + #[cfg(target_os = "windows")] + { + for root in ["C:\\Program Files", "C:\\Program Files (x86)"] { + candidates.push(( + PathBuf::from(root) + .join("RawTherapee") + .join("rawtherapee-cli.exe"), + "SYSTEM", + )); + if let Ok(entries) = fs::read_dir(PathBuf::from(root).join("RawTherapee")) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + candidates.push((path.join("rawtherapee-cli.exe"), "SYSTEM")); + } + } + } + } + } + + candidates +} + +#[cfg(feature = "pro")] +fn detect_rawtherapee_cli_blocking(app: Option) -> RawEngineValidationResult { + let mut seen = HashSet::new(); + for (candidate, source) in rawtherapee_candidates(app.as_ref()) { + let key = candidate.to_string_lossy().to_string(); + if !seen.insert(key) || !candidate.is_file() { + continue; + } + let result = validate_raw_engine_path(&candidate, source); + if result.ok { + return result; + } + } + + raw_engine_result( + false, + None, + None, + None, + Some( + "RawTherapee CLI was not found in the Pro bundle, PATH, or common install locations" + .to_string(), + ), + ) +} + +#[cfg(feature = "pro")] +fn readable_file_name(path: &Path) -> String { + path.file_name() + .and_then(|value| value.to_str()) + .map(|value| value.to_string()) + .unwrap_or_else(|| path.display().to_string()) +} + +#[cfg(feature = "pro")] +fn render_raw_monitor_cache_file( + engine_path: &Path, + pp3_path: &Path, + raw_path: &Path, + cache_path: &Path, + profile_id: &str, +) -> Result { + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create RAW monitor cache directory: {}", e))?; + } + + if cache_path.exists() && raw_monitor_cache_is_reusable(raw_path, cache_path) { + return Ok(RawMonitorRenderResult { + cache_path: cache_path.to_path_buf(), + status: raw_monitor_cache_status(cache_path), + }); + } + if cache_path.exists() { + remove_raw_monitor_cache_artifacts(cache_path); + } + + let mut command = rawtherapee_command(engine_path); + command + .arg("-o") + .arg(cache_path) + .arg("-p") + .arg(pp3_path) + .arg(format!("-j{}", RAW_MONITOR_JPEG_QUALITY)) + .arg("-Y") + .arg("-c") + .arg(raw_path); + + let output = run_command_with_timeout(command, Duration::from_secs(180))?; + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !output.status.success() { + let detail = if !stderr.is_empty() { stderr } else { stdout }; + return write_raw_monitor_embedded_preview_cache( + raw_path, + cache_path, + profile_id, + RAW_MONITOR_FALLBACK_ENGINE_ERROR, + ).map( + |status| RawMonitorRenderResult { + cache_path: cache_path.to_path_buf(), + status, + }, + ).map_err( + |fallback_error| { + let rawtherapee_error = if detail.is_empty() { + format!("RawTherapee CLI exited with {}", output.status) + } else { + detail + }; + format!( + "{}; embedded preview fallback also failed: {}", + rawtherapee_error, fallback_error + ) + }, + ); + } + + if rawtherapee_reported_decode_failure(&stderr) { + remove_raw_monitor_cache_artifacts(cache_path); + let detail = if stderr.is_empty() { + "RawTherapee reported a RAW decode failure".to_string() + } else { + stderr + }; + return write_raw_monitor_embedded_preview_cache( + raw_path, + cache_path, + profile_id, + RAW_MONITOR_FALLBACK_DECODE_FAILURE, + ).map( + |status| RawMonitorRenderResult { + cache_path: cache_path.to_path_buf(), + status, + }, + ).map_err( + |fallback_error| { + format!( + "{}; embedded preview fallback also failed: {}", + detail, fallback_error + ) + }, + ); + } + + if !cache_path.is_file() { + return write_raw_monitor_embedded_preview_cache( + raw_path, + cache_path, + profile_id, + RAW_MONITOR_FALLBACK_MISSING_OUTPUT, + ).map( + |status| RawMonitorRenderResult { + cache_path: cache_path.to_path_buf(), + status, + }, + ).map_err( + |fallback_error| { + format!( + "RawTherapee did not produce a cache JPEG; embedded preview fallback also failed: {}", + fallback_error + ) + }, + ); + } + validate_raw_monitor_cache_jpeg(cache_path).map(|_| { + let _ = write_raw_monitor_cache_metadata( + cache_path, + profile_id, + "rawtherapee", + false, + None, + ); + RawMonitorRenderResult { + cache_path: cache_path.to_path_buf(), + status: RawMonitorCacheStatus { + source: "rawtherapee".to_string(), + fallback: false, + fallback_reason: None, + }, + } + }).or_else(|error| { + remove_raw_monitor_cache_artifacts(cache_path); + write_raw_monitor_embedded_preview_cache( + raw_path, + cache_path, + profile_id, + RAW_MONITOR_FALLBACK_INVALID_OUTPUT, + ).map( + |status| RawMonitorRenderResult { + cache_path: cache_path.to_path_buf(), + status, + }, + ).map_err(|fallback_error| { + format!( + "{}; embedded preview fallback also failed: {}", + error, fallback_error + ) + }) + }) +} + +#[cfg(feature = "pro")] +fn write_raw_monitor_embedded_preview_cache( + raw_path: &Path, + cache_path: &Path, + profile_id: &str, + fallback_reason: &str, +) -> Result { + let orientation = read_orientation(raw_path); + let bytes = fs::read(raw_path) + .map_err(|e| format!("Failed to read RAW file for embedded preview fallback: {}", e))?; + let Some((offset, byte_length)) = find_largest_embedded_jpeg(&bytes) else { + return Err("RAW file does not contain a displayable embedded JPEG preview".to_string()); + }; + let end = offset + .checked_add(byte_length) + .filter(|end| *end <= bytes.len()) + .ok_or_else(|| "Embedded JPEG preview points outside RAW file".to_string())?; + let image = image::load_from_memory_with_format(&bytes[offset..end], ImageFormat::Jpeg) + .map_err(|e| format!("Failed to decode embedded JPEG preview fallback: {}", e))?; + let normalized = normalize_orientation(image, orientation); + let resized = resize_to_max_edge(normalized, RAW_MONITOR_MAX_EDGE); + + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create RAW monitor cache directory: {}", e))?; + } + let file = File::create(cache_path) + .map_err(|e| format!("Failed to create embedded preview cache JPEG: {}", e))?; + let mut encoder = + image::codecs::jpeg::JpegEncoder::new_with_quality(file, RAW_MONITOR_JPEG_QUALITY); + encoder + .encode_image(&resized) + .map_err(|e| format!("Failed to write embedded preview cache JPEG: {}", e))?; + validate_raw_monitor_cache_jpeg(cache_path)?; + write_raw_monitor_cache_metadata( + cache_path, + profile_id, + "embeddedFallback", + true, + Some(fallback_reason), + )?; + Ok(RawMonitorCacheStatus { + source: "embeddedFallback".to_string(), + fallback: true, + fallback_reason: Some(fallback_reason.to_string()), + }) +} + +#[cfg(feature = "pro")] +fn validate_raw_monitor_cache_jpeg(cache_path: &Path) -> Result<(), String> { + image::open(cache_path) + .map(|_| ()) + .map_err(|e| format!("RAW monitor cache is not a readable JPEG: {}", e)) +} + +#[cfg(feature = "pro")] +fn rawtherapee_reported_decode_failure(stderr: &str) -> bool { + let text = stderr.to_ascii_lowercase(); + [ + "unsupported file", + "cannot decode", + "failed to decode", + "decode failed", + "decoder error", + "unknown file: data corrupted", + "data corrupted at", + ] + .iter() + .any(|needle| text.contains(needle)) +} + +#[cfg(feature = "pro")] +fn raw_monitor_parallelism() -> usize { + let logical = std::thread::available_parallelism() + .map(|value| value.get()) + .unwrap_or(4); + (logical / 4).clamp(1, RAW_MONITOR_MAX_PARALLELISM) +} + +#[cfg(feature = "pro")] +fn run_raw_monitor_cache_stream( + app: tauri::AppHandle, + engine_path: String, + raw_paths: Vec, + profile_id: String, + priority_count: usize, + on_event: Channel, +) -> Result<(), String> { + RAW_MONITOR_CANCEL_REQUESTED.store(false, Ordering::SeqCst); + let profile_id = normalize_raw_monitor_profile_id(&profile_id).to_string(); + + let validation = validate_raw_engine_blocking(engine_path.clone()); + if !validation.ok { + let error = validation + .message + .clone() + .unwrap_or_else(|| "RawTherapee CLI is not available".to_string()); + send_raw_monitor_event( + &on_event, + raw_monitor_event( + "error", + Some(0), + Some(raw_paths.len()), + None, + None, + None, + validation.version, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + let engine_version = validation + .version + .clone() + .unwrap_or_else(|| "RawTherapee CLI".to_string()); + let pp3_path = ensure_raw_monitor_pp3(&app, &profile_id)?; + let engine_path = PathBuf::from(engine_path); + let total = raw_paths.len(); + let processed = Arc::new(AtomicUsize::new(0)); + let errors = Arc::new(Mutex::new(Vec::new())); + let failure_table = Arc::new(Mutex::new(read_raw_monitor_failure_table(&app))); + let failure_table_changed = Arc::new(AtomicBool::new(false)); + + send_raw_monitor_event( + &on_event, + raw_monitor_event( + "started", + Some(0), + Some(total), + None, + None, + None, + Some(engine_version.clone()), + None, + None, + ), + ); + + // RawTherapee is a heavy external process, so keep concurrency deliberately + // below CPU thread count. Unbounded rayon parallelism can make preview + // generation slower and freeze the UI on laptop-class machines. + let pool = ThreadPoolBuilder::new() + .num_threads(raw_monitor_parallelism()) + .build() + .map_err(|e| format!("Failed to create RAW monitor worker pool: {}", e))?; + + let priority_count = priority_count.min(total); + pool.install(|| { + let process_raw_path = |raw_path_string: &String| { + if RAW_MONITOR_CANCEL_REQUESTED.load(Ordering::SeqCst) { + return; + } + + let raw_path = PathBuf::from(raw_path_string); + let current = readable_file_name(&raw_path); + let current_processed = processed.load(Ordering::SeqCst); + + send_raw_monitor_event( + &on_event, + raw_monitor_event( + "progress", + Some(current_processed), + Some(total), + Some(current.clone()), + Some(raw_path_string.clone()), + None, + Some(engine_version.clone()), + None, + None, + ), + ); + + if !raw_path.is_file() || !is_raw_path(&raw_path) { + let mut errors_lock = errors.lock().unwrap(); + errors_lock.push(format!( + "Unsupported or missing RAW source: {}", + raw_path_string + )); + processed.fetch_add(1, Ordering::SeqCst); + return; + } + + match raw_monitor_cache_path(&app, &raw_path, &engine_version, &profile_id) { + Ok(cache_path) => { + let failure_key = raw_monitor_failure_key(&cache_path); + if cache_path.is_file() && raw_monitor_cache_is_reusable(&raw_path, &cache_path) { + let status = raw_monitor_cache_status(&cache_path); + if is_nikon_raw_path(&raw_path) { + let mut table = failure_table.lock().unwrap(); + if table.remove(&failure_key).is_some() { + failure_table_changed.store(true, Ordering::SeqCst); + } + } + let new_processed = processed.fetch_add(1, Ordering::SeqCst) + 1; + let mut event = raw_monitor_event( + "cached", + Some(new_processed), + Some(total), + Some(current), + Some(raw_path_string.clone()), + Some(cache_path.display().to_string()), + Some(engine_version.clone()), + None, + None, + ); + event.cache_source = Some(status.source); + event.fallback = Some(status.fallback); + event.fallback_reason = status.fallback_reason; + send_raw_monitor_event(&on_event, event); + return; + } + + if is_nikon_raw_path(&raw_path) { + let recent_failure = { + let mut table = failure_table.lock().unwrap(); + let now = now_ms(); + if let Some(record) = + raw_monitor_recent_failure(&table, &failure_key, now).cloned() + { + Some(record) + } else { + if table.remove(&failure_key).is_some() { + failure_table_changed.store(true, Ordering::SeqCst); + } + None + } + }; + + if let Some(record) = recent_failure { + let new_processed = processed.fetch_add(1, Ordering::SeqCst) + 1; + let mut event = raw_monitor_event( + "skipped", + Some(new_processed), + Some(total), + Some(current), + Some(raw_path_string.clone()), + None, + Some(engine_version.clone()), + None, + None, + ); + event.skipped_reason = Some(format!( + "Recent NEF RAW monitor failure; retry is cooled down. Last error: {}", + record.error + )); + send_raw_monitor_event(&on_event, event); + return; + } + } + + match render_raw_monitor_cache_file( + &engine_path, + &pp3_path, + &raw_path, + &cache_path, + &profile_id, + ) { + Ok(result) => { + if is_nikon_raw_path(&raw_path) { + let mut table = failure_table.lock().unwrap(); + if table.remove(&failure_key).is_some() { + failure_table_changed.store(true, Ordering::SeqCst); + } + } + let new_processed = processed.fetch_add(1, Ordering::SeqCst) + 1; + let mut event = raw_monitor_event( + "cached", + Some(new_processed), + Some(total), + Some(current), + Some(raw_path_string.clone()), + Some(result.cache_path.display().to_string()), + Some(engine_version.clone()), + None, + None, + ); + event.cache_source = Some(result.status.source); + event.fallback = Some(result.status.fallback); + event.fallback_reason = result.status.fallback_reason; + send_raw_monitor_event(&on_event, event); + } + Err(error) => { + let new_processed = processed.fetch_add(1, Ordering::SeqCst) + 1; + let message = format!("{}: {}", current, error); + if is_nikon_raw_path(&raw_path) { + let mut table = failure_table.lock().unwrap(); + table.insert( + failure_key, + RawMonitorFailureRecord { + failed_at_ms: now_ms(), + error: error.clone(), + }, + ); + failure_table_changed.store(true, Ordering::SeqCst); + } + let mut errors_lock = errors.lock().unwrap(); + errors_lock.push(message.clone()); + let current_errors = errors_lock.clone(); + drop(errors_lock); + send_raw_monitor_event( + &on_event, + raw_monitor_event( + "error", + Some(new_processed), + Some(total), + Some(current), + Some(raw_path_string.clone()), + None, + Some(engine_version.clone()), + Some(current_errors), + Some(message), + ), + ); + } + } + } + Err(error) => { + let new_processed = processed.fetch_add(1, Ordering::SeqCst) + 1; + let message = format!("{}: {}", current, error); + let mut errors_lock = errors.lock().unwrap(); + errors_lock.push(message.clone()); + let current_errors = errors_lock.clone(); + drop(errors_lock); + send_raw_monitor_event( + &on_event, + raw_monitor_event( + "error", + Some(new_processed), + Some(total), + Some(current), + Some(raw_path_string.clone()), + None, + Some(engine_version.clone()), + Some(current_errors), + Some(message), + ), + ); + } + } + }; + + let (priority_paths, background_paths) = raw_paths.split_at(priority_count); + priority_paths.par_iter().for_each(&process_raw_path); + background_paths.par_iter().for_each(&process_raw_path); + }); + + if failure_table_changed.load(Ordering::SeqCst) { + let table = failure_table.lock().unwrap().clone(); + if let Err(error) = write_raw_monitor_failure_table(&app, &table) { + eprintln!("Failed to write RAW monitor failure table: {}", error); + } + } + + if RAW_MONITOR_CANCEL_REQUESTED.load(Ordering::SeqCst) { + let final_processed = processed.load(Ordering::SeqCst); + let final_errors = errors.lock().unwrap().clone(); + send_raw_monitor_event( + &on_event, + raw_monitor_event( + "cancelled", + Some(final_processed), + Some(total), + None, + None, + None, + Some(engine_version.clone()), + Some(final_errors), + None, + ), + ); + return Ok(()); + } + + let final_processed = processed.load(Ordering::SeqCst); + let final_errors = errors.lock().unwrap().clone(); + send_raw_monitor_event( + &on_event, + raw_monitor_event( + "done", + Some(final_processed), + Some(total), + None, + None, + None, + Some(engine_version), + Some(final_errors), + None, + ), + ); + Ok(()) +} + +#[tauri::command] +async fn extract_raw_embedded_preview( + app: tauri::AppHandle, + file_path: String, +) -> Result, String> { + spawn_blocking(move || extract_raw_embedded_preview_blocking(app, file_path)) + .await + .map_err(|e| format!("RAW preview task failed: {}", e))? +} + +#[tauri::command] +async fn get_jpeg_thumbnail( + app: tauri::AppHandle, + file_path: String, + max_edge: Option, +) -> Result { + spawn_blocking(move || get_jpeg_thumbnail_blocking(app, file_path, max_edge)) + .await + .map_err(|e| format!("JPEG thumbnail task failed: {}", e))? +} + +#[cfg(feature = "pro")] +#[tauri::command] +async fn detect_rawtherapee_cli( + app: tauri::AppHandle, +) -> Result { + spawn_blocking(move || detect_rawtherapee_cli_blocking(Some(app))) + .await + .map_err(|e| format!("RawTherapee detection task failed: {}", e)) +} + +#[cfg(feature = "pro")] +#[tauri::command] +async fn validate_raw_engine(engine_path: String) -> Result { + spawn_blocking(move || validate_raw_engine_blocking(engine_path)) + .await + .map_err(|e| format!("RAW engine validation task failed: {}", e)) +} + +#[cfg(feature = "pro")] +#[tauri::command] +async fn get_raw_monitor_cache_entry( + app: tauri::AppHandle, + raw_path: String, + engine_version: String, + profile_id: String, +) -> Result { + spawn_blocking(move || { + let path = PathBuf::from(&raw_path); + let normalized_profile_id = normalize_raw_monitor_profile_id(&profile_id).to_string(); + if !path.is_file() || !is_raw_path(&path) { + return Ok(RawMonitorCacheEntry { + raw_path, + profile_id: Some(normalized_profile_id), + cache_path: None, + from_cache: false, + fallback: None, + cache_source: None, + fallback_reason: None, + recent_failure: None, + missing_reason: Some("Path is not a supported RAW file".to_string()), + }); + } + let cache_path = + raw_monitor_cache_path(&app, &path, &engine_version, &normalized_profile_id)?; + if cache_path.is_file() && raw_monitor_cache_is_reusable(&path, &cache_path) { + let status = raw_monitor_cache_status(&cache_path); + Ok(RawMonitorCacheEntry { + raw_path, + profile_id: Some(normalized_profile_id), + cache_path: Some(cache_path.display().to_string()), + from_cache: true, + fallback: Some(status.fallback), + cache_source: Some(status.source), + fallback_reason: status.fallback_reason, + recent_failure: None, + missing_reason: None, + }) + } else { + let failure_key = raw_monitor_failure_key(&cache_path); + let recent_failure = is_nikon_raw_path(&path) + && raw_monitor_recent_failure( + &read_raw_monitor_failure_table(&app), + &failure_key, + now_ms(), + ) + .is_some(); + Ok(RawMonitorCacheEntry { + raw_path, + profile_id: Some(normalized_profile_id), + cache_path: None, + from_cache: false, + fallback: None, + cache_source: None, + fallback_reason: None, + recent_failure: Some(recent_failure), + missing_reason: Some(if recent_failure { + "Recent NEF RAW monitor failure is cooling down".to_string() + } else { + "RAW monitor cache has not been generated".to_string() + }), + }) + } + }) + .await + .map_err(|e| format!("RAW monitor cache lookup task failed: {}", e))? +} + +#[cfg(feature = "pro")] +#[tauri::command] +async fn render_raw_monitor_cache_stream( + app: tauri::AppHandle, + engine_path: String, + raw_paths: Vec, + profile_id: String, + priority_count: Option, + on_event: Channel, +) -> Result<(), String> { + spawn_blocking(move || { + run_raw_monitor_cache_stream( + app, + engine_path, + raw_paths, + profile_id, + priority_count.unwrap_or(0), + on_event, + ) + }) + .await + .map_err(|e| format!("RAW monitor cache task failed: {}", e))? +} + +#[cfg(feature = "pro")] +#[tauri::command] +fn cancel_raw_monitor_cache_render() -> Result<(), String> { + RAW_MONITOR_CANCEL_REQUESTED.store(true, Ordering::SeqCst); + Ok(()) +} + +#[cfg(feature = "pro")] +const RAW_MONITOR_MAX_CACHE_SIZE_GB: u64 = 10; + +#[cfg(feature = "pro")] +#[tauri::command] +fn clear_raw_monitor_cache(app: tauri::AppHandle) -> Result<(), String> { + let cache_root = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("raw-monitor-previews"); + if cache_root.exists() { + fs::remove_dir_all(&cache_root) + .map_err(|e| format!("Failed to clear RAW monitor cache: {}", e))?; + } + Ok(()) +} + +#[cfg(feature = "pro")] +#[tauri::command] +fn get_raw_monitor_cache_size(app: tauri::AppHandle) -> Result { + let cache_root = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("raw-monitor-previews"); + + if !cache_root.exists() { + return Ok(0); + } + + let total_size = calculate_directory_size(&cache_root) + .map_err(|e| format!("Failed to calculate cache size: {}", e))?; + + Ok(total_size) +} + +#[cfg(feature = "pro")] +#[tauri::command] +fn cleanup_raw_monitor_cache_lru(app: tauri::AppHandle) -> Result { + let cache_root = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve cache directory: {}", e))? + .join("raw-monitor-previews"); + + if !cache_root.exists() { + return Ok(0); + } + + let total_size = calculate_directory_size(&cache_root) + .map_err(|e| format!("Failed to calculate cache size: {}", e))?; + + let max_size_bytes = RAW_MONITOR_MAX_CACHE_SIZE_GB * 1024 * 1024 * 1024; + + if total_size <= max_size_bytes { + return Ok(0); + } + + // 需要清理:删除最旧的缓存文件直到低于限制 + let target_size = (max_size_bytes as f64 * 0.8) as u64; // 清理到80% + let mut files_with_time: Vec<(PathBuf, std::time::SystemTime, u64)> = Vec::new(); + + // 收集所有缓存文件及其修改时间和大小 + collect_cache_files(&cache_root, &mut files_with_time) + .map_err(|e| format!("Failed to collect cache files: {}", e))?; + + files_with_time.sort_by_key(|(_, time, _)| *time); + + let mut current_size = total_size; + let mut deleted_size = 0u64; + + for (file_path, _, file_size) in files_with_time { + if current_size <= target_size { + break; + } + + if let Err(e) = fs::remove_file(&file_path) { + eprintln!("Failed to delete cache file {:?}: {}", file_path, e); + } else { + current_size = current_size.saturating_sub(file_size); + deleted_size += file_size; + } + } + + Ok(deleted_size) +} + +fn calculate_directory_size(path: &Path) -> std::io::Result { + let mut total_size = 0u64; + + if path.is_file() { + return Ok(path.metadata()?.len()); + } + + for entry in fs::read_dir(path)? { + let entry = entry?; + let metadata = entry.metadata()?; + + if metadata.is_file() { + total_size += metadata.len(); + } else if metadata.is_dir() { + total_size += calculate_directory_size(&entry.path())?; + } + } + + Ok(total_size) +} + +#[cfg(feature = "pro")] +fn collect_cache_files( + path: &Path, + files: &mut Vec<(PathBuf, std::time::SystemTime, u64)>, +) -> std::io::Result<()> { + if path.is_file() { + let metadata = path.metadata()?; + if let Ok(modified) = metadata.modified() { + files.push((path.to_path_buf(), modified, metadata.len())); + } + return Ok(()); + } + + for entry in fs::read_dir(path)? { + let entry = entry?; + let file_path = entry.path(); + let metadata = entry.metadata()?; + + if metadata.is_file() && file_path.extension().and_then(|s| s.to_str()) == Some("jpg") { + if let Ok(modified) = metadata.modified() { + files.push((file_path, modified, metadata.len())); + } + } else if metadata.is_dir() { + collect_cache_files(&file_path, files)?; + } + } + + Ok(()) +} + +#[tauri::command] +fn move_to_trash(groups: Vec) -> Result, String> { + let mut moved_files = Vec::new(); + let mut failed_files = Vec::new(); + + for group in groups { + // Move JPG file to trash if it exists + if let Some(jpg) = &group.jpg { + let path = Path::new(&jpg.path); + if let Err(error) = validate_photo_file_path(path) { + failed_files.push(error); + } else { + match trash::delete(path) { + Ok(_) => { + moved_files.push(jpg.path.clone()); + } + Err(e) => { + failed_files.push(format!("Failed to move {} to trash: {}", jpg.path, e)); + } + } + } + } + + // Move RAW file to trash if it exists + if let Some(raw) = &group.raw { + let path = Path::new(&raw.path); + if let Err(error) = validate_photo_file_path(path) { + failed_files.push(error); + } else { + match trash::delete(path) { + Ok(_) => { + moved_files.push(raw.path.clone()); + } + Err(e) => { + failed_files.push(format!("Failed to move {} to trash: {}", raw.path, e)); + } + } + } + } + } + + if !failed_files.is_empty() { + Err(format!( + "Some files failed to move to trash:\n{}", + failed_files.join("\n") + )) + } else { + Ok(moved_files) + } +} + +#[tauri::command] +fn delete_files_permanently(groups: Vec) -> Result, String> { + let mut deleted_files = Vec::new(); + let mut failed_files = Vec::new(); + + for group in groups { + // Delete JPG file if it exists + if let Some(jpg) = &group.jpg { + let path = Path::new(&jpg.path); + if let Err(error) = validate_photo_file_path(path) { + failed_files.push(error); + } else { + match fs::remove_file(path) { + Ok(_) => { + deleted_files.push(jpg.path.clone()); + } + Err(e) => { + failed_files.push(format!("Failed to delete {}: {}", jpg.path, e)); + } + } + } + } + + // Delete RAW file if it exists + if let Some(raw) = &group.raw { + let path = Path::new(&raw.path); + if let Err(error) = validate_photo_file_path(path) { + failed_files.push(error); + } else { + match fs::remove_file(path) { + Ok(_) => { + deleted_files.push(raw.path.clone()); + } + Err(e) => { + failed_files.push(format!("Failed to delete {}: {}", raw.path, e)); + } + } + } + } + } + + if !failed_files.is_empty() { + Err(format!( + "Some files failed to delete permanently:\n{}", + failed_files.join("\n") + )) + } else { + Ok(deleted_files) + } +} + +#[tauri::command] +fn export_files( + groups: Vec, + export_mode: String, + operation: String, + destination_folder: String, +) -> Result, String> { + let dest_path = Path::new(&destination_folder); + + if !dest_path.exists() { + return Err("Destination folder does not exist".to_string()); + } + + if !dest_path.is_dir() { + return Err("Destination path is not a directory".to_string()); + } + + let mut processed_files = Vec::new(); + let mut failed_files = Vec::new(); + + for group in groups { + // Determine which files to export based on export_mode + let files_to_export: Vec<&PhotoFileInfo> = match export_mode.as_str() { + "JPG" => { + if let Some(ref jpg) = group.jpg { + vec![jpg] + } else { + continue; + } + } + "RAW" => { + if let Some(ref raw) = group.raw { + vec![raw] + } else { + continue; + } + } + "BOTH" => { + let mut files = Vec::new(); + if let Some(ref jpg) = group.jpg { + files.push(jpg); + } + if let Some(ref raw) = group.raw { + files.push(raw); + } + if files.is_empty() { + continue; + } + files + } + _ => { + failed_files.push(format!("Unknown export mode: {}", export_mode)); + continue; + } + }; + + // Process each file + for file_info in files_to_export { + let source_path = Path::new(&file_info.path); + + if let Err(error) = validate_photo_file_path(source_path) { + failed_files.push(error); + continue; + } + + let file_name = source_path + .file_name() + .and_then(|n| n.to_str()) + .ok_or_else(|| format!("Invalid file name: {}", file_info.path))?; + + let dest_file_path = unique_destination_path(dest_path, file_name); + + // Perform the operation (copy or move) + let result = match operation.as_str() { + "COPY" => fs::copy(source_path, &dest_file_path) + .map(|_| ()) + .map_err(|e| format!("Failed to copy {}: {}", file_name, e)), + "MOVE" => fs::rename(source_path, &dest_file_path) + .map_err(|e| format!("Failed to move {}: {}", file_name, e)), + _ => Err(format!("Unknown operation: {}", operation)), + }; + + match result { + Ok(_) => { + processed_files.push(format!( + "{} {} to {}", + if operation == "COPY" { + "Copied" + } else { + "Moved" + }, + file_name, + dest_file_path.display() + )); + } + Err(e) => { + failed_files.push(e); + } + } + } + } + + if !failed_files.is_empty() { + Err(format!( + "Export completed with errors:\n{}\n\nSuccessfully processed {} files", + failed_files.join("\n"), + processed_files.len() + )) + } else if processed_files.is_empty() { + Err("No files were exported".to_string()) + } else { + Ok(processed_files) + } +} + +fn files_for_export<'a>( + group: &'a PhotoGroupInfo, + export_mode: &str, +) -> Result, String> { + match export_mode { + "JPG" => Ok(group.jpg.iter().collect()), + "RAW" => Ok(group.raw.iter().collect()), + "BOTH" => { + let mut files = Vec::new(); + if let Some(ref jpg) = group.jpg { + files.push(jpg); + } + if let Some(ref raw) = group.raw { + files.push(raw); + } + Ok(files) + } + _ => Err(format!("Unknown export mode: {}", export_mode)), + } +} + +fn raw_sidecar_should_export(source_path: &Path, include_raw_sidecars: bool, rating: u8) -> bool { + if !include_raw_sidecars || !is_raw_path(source_path) { + return false; + } + + xmp_sidecar_path(source_path).exists() || clamp_rating(rating) > 0 +} + +fn export_raw_sidecar_to_target( + source_path: &Path, + target_path: &Path, + operation: &str, + include_raw_sidecars: bool, + rating: u8, +) -> Result, String> { + if !raw_sidecar_should_export(source_path, include_raw_sidecars, rating) { + return Ok(None); + } + + let source_sidecar = xmp_sidecar_path(source_path); + let target_sidecar = xmp_sidecar_path(target_path); + let copied_existing = source_sidecar.exists(); + + if copied_existing && source_sidecar != target_sidecar { + match operation { + "MOVE" => fs::rename(&source_sidecar, &target_sidecar) + .map_err(|e| format!("Failed to move {}: {}", source_sidecar.display(), e))?, + "COPY" => { + fs::copy(&source_sidecar, &target_sidecar) + .map_err(|e| format!("Failed to copy {}: {}", source_sidecar.display(), e))?; + } + _ => return Err(format!("Unknown operation: {}", operation)), + } + } + + if clamp_rating(rating) > 0 || !copied_existing { + write_xmp_sidecar_rating(target_path, rating)?; + } + + Ok(Some(target_sidecar)) +} + +fn write_exported_target_rating_metadata( + source_path: &Path, + target_path: &Path, + rating: u8, + operation: &str, + include_raw_sidecars: bool, +) -> Result, String> { + let rating = clamp_rating(rating); + let mut written = Vec::new(); + + if is_raw_path(target_path) { + if let Some(sidecar) = export_raw_sidecar_to_target( + source_path, + target_path, + operation, + include_raw_sidecars, + rating, + )? { + written.push(sidecar); + } + } else if rating > 0 { + written.push(write_rating_to_path(target_path, rating)?); + } + + Ok(written) +} + +#[tauri::command] +fn export_files_stream( + groups: Vec, + export_mode: String, + operation: String, + destination_folder: String, + on_event: Channel, + include_raw_sidecars: Option, +) -> Result, String> { + let dest_path = Path::new(&destination_folder); + let include_raw_sidecars = include_raw_sidecars.unwrap_or(true); + + if !dest_path.exists() { + let error = "Destination folder does not exist".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("copying"), + None, + None, + None, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + if !dest_path.is_dir() { + let error = "Destination path is not a directory".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("copying"), + None, + None, + None, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + let phase = if operation == "MOVE" { + "moving" + } else { + "copying" + }; + let total = groups + .iter() + .filter_map(|group| { + let files = files_for_export(group, &export_mode).ok()?; + Some((files, group.rating)) + }) + .map(|(files, rating)| { + files + .iter() + .map(|file| { + let source_path = Path::new(&file.path); + 1 + usize::from(raw_sidecar_should_export( + source_path, + include_raw_sidecars, + rating, + )) + }) + .sum::() + }) + .sum::(); + let mut processed = 0usize; + let mut processed_files = Vec::new(); + let mut failed_files = Vec::new(); + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some(phase), + Some(0), + Some(total), + None, + None, + None, + ), + ); + + for group in groups { + let files_to_export = match files_for_export(&group, &export_mode) { + Ok(files) => files, + Err(error) => { + failed_files.push(error); + continue; + } + }; + + for file_info in files_to_export { + let source_path = Path::new(&file_info.path); + let current = source_path + .file_name() + .and_then(|n| n.to_str()) + .map(|value| value.to_string()) + .unwrap_or_else(|| file_info.name.clone()); + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some(phase), + Some(processed), + Some(total), + Some(current.clone()), + None, + None, + ), + ); + + if let Err(error) = validate_photo_file_path(source_path) { + failed_files.push(error); + processed += 1; + continue; + } + + let Some(file_name) = source_path.file_name().and_then(|n| n.to_str()) else { + failed_files.push(format!("Invalid file name: {}", file_info.path)); + processed += 1; + continue; + }; + + let dest_file_path = unique_destination_path(dest_path, file_name); + let result = match operation.as_str() { + "COPY" => fs::copy(source_path, &dest_file_path) + .map(|_| ()) + .map_err(|e| format!("Failed to copy {}: {}", file_name, e)), + "MOVE" => fs::rename(source_path, &dest_file_path) + .map_err(|e| format!("Failed to move {}: {}", file_name, e)), + _ => Err(format!("Unknown operation: {}", operation)), + }; + + let mut file_exported = false; + match result { + Ok(_) => { + if let Err(error) = write_exported_target_rating_metadata( + source_path, + &dest_file_path, + group.rating, + &operation, + include_raw_sidecars, + ) { + failed_files.push(format!( + "Failed to write rating metadata to {}: {}", + dest_file_path.display(), + error + )); + } + processed_files.push(dest_file_path.display().to_string()); + file_exported = true; + } + Err(error) => failed_files.push(error), + } + + processed += 1; + if file_exported + && raw_sidecar_should_export(source_path, include_raw_sidecars, group.rating) + && is_raw_path(&dest_file_path) + { + let target_sidecar_path = xmp_sidecar_path(&dest_file_path); + let sidecar_name = target_sidecar_path + .file_name() + .and_then(|n| n.to_str()) + .map(|value| value.to_string()); + + if let Some(sidecar_name) = sidecar_name { + let sidecar_current = sidecar_name; + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some(phase), + Some(processed), + Some(total), + Some(sidecar_current.clone()), + None, + None, + ), + ); + + processed_files.push(target_sidecar_path.display().to_string()); + + processed += 1; + send_export_stream_event( + &on_event, + export_event( + "progress", + Some(phase), + Some(processed), + Some(total), + Some(sidecar_current), + None, + None, + ), + ); + } + } + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some(phase), + Some(processed), + Some(total), + Some(current), + None, + None, + ), + ); + } + } + + if !failed_files.is_empty() { + let error = format!( + "Export completed with errors:\n{}\n\nSuccessfully processed {} files", + failed_files.join("\n"), + processed_files.len() + ); + send_export_stream_event( + &on_event, + export_event( + "error", + Some(phase), + Some(processed), + Some(total), + None, + Some(processed_files.clone()), + Some(error.clone()), + ), + ); + Err(error) + } else if processed_files.is_empty() { + let error = "No files were exported".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some(phase), + Some(processed), + Some(total), + None, + None, + Some(error.clone()), + ), + ); + Err(error) + } else { + send_export_stream_event( + &on_event, + export_event( + "done", + Some(phase), + Some(processed), + Some(total), + None, + Some(processed_files.clone()), + None, + ), + ); + Ok(processed_files) + } +} + +#[tauri::command] +fn write_rating_metadata(groups: Vec, rating: u8) -> Result, String> { + let rating = clamp_rating(rating); + let mut written = Vec::new(); + let mut failed = Vec::new(); + let mut seen_paths = std::collections::HashSet::new(); + + for group in groups { + let mut paths = Vec::new(); + if let Some(jpg) = group.jpg { + paths.push(jpg.path); + } + if let Some(raw) = group.raw { + paths.push(raw.path); + } + + for path_string in paths { + if !seen_paths.insert(path_string.clone()) { + continue; + } + + let path = Path::new(&path_string); + if !path.exists() { + failed.push(format!("Source file not found: {}", path.display())); + continue; + } + + match write_rating_to_path(path, rating) { + Ok(target) => written.push(target.display().to_string()), + Err(error) => failed.push(error), + } + } + } + + if !failed.is_empty() { + Err(format!( + "Rating metadata completed with errors:\n{}\n\nSuccessfully wrote {} files", + failed.join("\n"), + written.len() + )) + } else { + Ok(written) + } +} + +#[tauri::command] +fn write_rendered_export( + files: Vec, + destination_folder: String, +) -> Result, String> { + let dest_path = Path::new(&destination_folder); + + if !dest_path.exists() { + return Err("Destination folder does not exist".to_string()); + } + + if !dest_path.is_dir() { + return Err("Destination path is not a directory".to_string()); + } + + let mut processed_files = Vec::new(); + let mut failed_files = Vec::new(); + let base64_engine = base64::engine::general_purpose::STANDARD; + + for file in files { + let payload = file + .data_url + .split_once(',') + .map(|(_, data)| data) + .unwrap_or(file.data_url.as_str()); + + match base64_engine.decode(payload) { + Ok(bytes) => { + let dest_file_path = unique_destination_path(dest_path, &file.file_name); + match fs::write(&dest_file_path, bytes) { + Ok(_) => { + processed_files.push(dest_file_path.display().to_string()); + match write_rendered_export_metadata( + &dest_file_path, + file.rating, + file.metadata_mode.as_deref(), + file.metadata_source_path.as_deref(), + ) { + Ok(metadata_files) => processed_files.extend(metadata_files), + Err(e) => failed_files.push(format!( + "Failed to write metadata for {}: {}", + dest_file_path.display(), + e + )), + } + } + Err(e) => failed_files.push(format!( + "Failed to write {}: {}", + dest_file_path.display(), + e + )), + } + } + Err(e) => failed_files.push(format!("Failed to decode {}: {}", file.file_name, e)), + } + } + + if !failed_files.is_empty() { + Err(format!( + "Rendered export completed with errors:\n{}\n\nSuccessfully wrote {} files", + failed_files.join("\n"), + processed_files.len() + )) + } else if processed_files.is_empty() { + Err("No rendered files were exported".to_string()) + } else { + Ok(processed_files) + } +} + +#[tauri::command] +fn write_rendered_export_stream( + files: Vec, + destination_folder: String, + on_event: Channel, +) -> Result, String> { + let dest_path = Path::new(&destination_folder); + + if !dest_path.exists() { + let error = "Destination folder does not exist".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("writing"), + None, + None, + None, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + if !dest_path.is_dir() { + let error = "Destination path is not a directory".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("writing"), + None, + None, + None, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + let total = files.len(); + let mut processed_files = Vec::new(); + let mut failed_files = Vec::new(); + let base64_engine = base64::engine::general_purpose::STANDARD; + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some("writing"), + Some(0), + Some(total), + None, + None, + None, + ), + ); + + for (index, file) in files.into_iter().enumerate() { + let current = file.file_name.clone(); + send_export_stream_event( + &on_event, + export_event( + "progress", + Some("writing"), + Some(index), + Some(total), + Some(current.clone()), + None, + None, + ), + ); + + let payload = file + .data_url + .split_once(',') + .map(|(_, data)| data) + .unwrap_or(file.data_url.as_str()); + + match base64_engine.decode(payload) { + Ok(bytes) => { + let dest_file_path = unique_destination_path(dest_path, &file.file_name); + match fs::write(&dest_file_path, bytes) { + Ok(_) => { + processed_files.push(dest_file_path.display().to_string()); + match write_rendered_export_metadata( + &dest_file_path, + file.rating, + file.metadata_mode.as_deref(), + file.metadata_source_path.as_deref(), + ) { + Ok(metadata_files) => processed_files.extend(metadata_files), + Err(e) => failed_files.push(format!( + "Failed to write metadata for {}: {}", + dest_file_path.display(), + e + )), + } + } + Err(e) => failed_files.push(format!( + "Failed to write {}: {}", + dest_file_path.display(), + e + )), + } + } + Err(e) => failed_files.push(format!("Failed to decode {}: {}", file.file_name, e)), + } + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some("writing"), + Some(index + 1), + Some(total), + Some(current), + None, + None, + ), + ); + } + + if !failed_files.is_empty() { + let error = format!( + "Rendered export completed with errors:\n{}\n\nSuccessfully wrote {} files", + failed_files.join("\n"), + processed_files.len() + ); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("writing"), + Some(processed_files.len()), + Some(total), + None, + Some(processed_files.clone()), + Some(error.clone()), + ), + ); + Err(error) + } else if processed_files.is_empty() { + let error = "No rendered files were exported".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("writing"), + Some(0), + Some(total), + None, + None, + Some(error.clone()), + ), + ); + Err(error) + } else { + send_export_stream_event( + &on_event, + export_event( + "done", + Some("writing"), + Some(total), + Some(total), + None, + Some(processed_files.clone()), + None, + ), + ); + Ok(processed_files) + } +} + +fn candidate_lightroom_paths() -> Vec { + let mut paths = Vec::new(); + + #[cfg(target_os = "windows")] + { + if let Ok(program_files) = std::env::var("ProgramFiles") { + paths.push( + PathBuf::from(&program_files) + .join("Adobe") + .join("Adobe Lightroom Classic") + .join("Lightroom.exe"), + ); + paths.push( + PathBuf::from(&program_files) + .join("Adobe") + .join("Adobe Lightroom") + .join("Lightroom.exe"), + ); + } + if let Ok(program_files_x86) = std::env::var("ProgramFiles(x86)") { + paths.push( + PathBuf::from(&program_files_x86) + .join("Adobe") + .join("Adobe Lightroom Classic") + .join("Lightroom.exe"), + ); + } + paths.extend(detect_lightroom_paths_from_registry()); + } + + #[cfg(target_os = "macos")] + { + paths.push(PathBuf::from( + "/Applications/Adobe Lightroom Classic/Adobe Lightroom Classic.app", + )); + paths.push(PathBuf::from("/Applications/Adobe Lightroom Classic.app")); + } + + paths +} + +#[cfg(target_os = "windows")] +fn detect_lightroom_paths_from_registry() -> Vec { + let mut paths = Vec::new(); + let script = r#" +$roots = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' +) +Get-ChildItem -Path $roots -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } | + Where-Object { $_.DisplayName -match 'Adobe Lightroom Classic' } | + ForEach-Object { if ($_.InstallLocation) { $_.InstallLocation } } +"#; + let output = Command::new("powershell") + .args([ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + ]) + .output(); + + let Ok(output) = output else { + return paths; + }; + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines() { + let install_location = line.trim(); + if install_location.is_empty() { + continue; + } + let root = PathBuf::from(install_location); + paths.push(root.join("Adobe Lightroom Classic").join("Lightroom.exe")); + paths.push(root.join("Lightroom.exe")); + } + paths +} + +fn detect_lightroom_classic_path() -> Option { + let mut seen = HashSet::new(); + candidate_lightroom_paths() + .into_iter() + .find(|path| seen.insert(path.clone()) && is_lightroom_classic_executable(path)) +} + +fn is_lightroom_classic_executable(path: &Path) -> bool { + if !path.exists() { + return false; + } + + let file_name = path + .file_name() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) + .unwrap_or_default(); + + #[cfg(target_os = "windows")] + { + path.is_file() && file_name == "lightroom.exe" + } + + #[cfg(target_os = "macos")] + { + file_name.contains("lightroom classic") + } + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + { + path.is_file() && file_name.contains("lightroom") + } +} + +#[tauri::command] +fn detect_lightroom_classic() -> Result, String> { + Ok(detect_lightroom_classic_path().map(|path| path.display().to_string())) +} + +#[tauri::command] +fn resolve_lightroom_classic_path(executable_path: Option) -> Option { + let manual_path = executable_path + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .filter(|path| is_lightroom_classic_executable(path)); + manual_path.or_else(detect_lightroom_classic_path) +} + +#[tauri::command] +fn launch_lightroom_classic(executable_path: Option) -> Result, String> { + let detected_path = resolve_lightroom_classic_path(executable_path); + + #[cfg(target_os = "macos")] + { + if let Some(path) = detected_path { + Command::new("open") + .arg(&path) + .spawn() + .map_err(|e| format!("Failed to launch Lightroom Classic: {}", e))?; + return Ok(Some(path.display().to_string())); + } + Command::new("open") + .args(["-a", "Adobe Lightroom Classic"]) + .spawn() + .map_err(|e| format!("Failed to launch Lightroom Classic: {}", e))?; + return Ok(None); + } + + #[cfg(not(target_os = "macos"))] + { + let Some(path) = detected_path else { + return Ok(None); + }; + Command::new(&path) + .spawn() + .map_err(|e| format!("Failed to launch Lightroom Classic: {}", e))?; + Ok(Some(path.display().to_string())) + } +} + +fn files_for_lightroom_import(group: &PhotoGroupInfo) -> Vec<&PhotoFileInfo> { + let mut files = Vec::new(); + if let Some(jpg) = &group.jpg { + files.push(jpg); + } + if let Some(raw) = &group.raw { + files.push(raw); + } + files +} + +fn file_for_lightroom_catalog(group: &PhotoGroupInfo) -> Option<&PhotoFileInfo> { + group.raw.as_ref().or(group.jpg.as_ref()) +} + +fn files_for_lightroom_command_line(groups: &[PhotoGroupInfo]) -> (Vec, Vec) { + let mut import_files = Vec::new(); + let mut warnings = Vec::new(); + let mut seen = HashSet::new(); + + for group in groups { + for file in files_for_lightroom_import(group) { + let path = Path::new(&file.path); + if !path.exists() { + warnings.push(format!("Source file not found: {}", file.path)); + continue; + } + if clamp_rating(group.rating) > 0 { + if let Err(error) = write_rating_to_path(path, group.rating) { + warnings.push(format!( + "Failed to write rating metadata for {}: {}", + path.display(), + error + )); + } + } + } + + let Some(catalog_file) = file_for_lightroom_catalog(group) else { + warnings.push(format!("No RAW or JPG file is available for {}", group.id)); + continue; + }; + let catalog_path = Path::new(&catalog_file.path); + if !catalog_path.exists() { + warnings.push(format!( + "Lightroom import file not found: {}", + catalog_file.path + )); + continue; + } + if seen.insert(catalog_file.path.clone()) { + import_files.push(catalog_file.path.clone()); + } + } + + (import_files, warnings) +} + +#[tauri::command] +fn open_lightroom_source_folder( + groups: Vec, + executable_path: Option, +) -> Result { + let (files, mut warnings) = files_for_lightroom_command_line(&groups); + if files.is_empty() { + return Err("No files are available for Lightroom import".to_string()); + } + + let source_folder = files + .first() + .and_then(|file| Path::new(file).parent()) + .map(Path::to_path_buf) + .ok_or_else(|| "Cannot resolve Lightroom source folder".to_string())?; + let resolved_path = resolve_lightroom_classic_path(executable_path); + + #[cfg(target_os = "macos")] + let launched = { + let mut command = Command::new("open"); + if let Some(path) = &resolved_path { + command.arg(path); + } else { + command.args(["-a", "Adobe Lightroom Classic"]); + } + command.arg("--args"); + command.arg(&source_folder); + match command.spawn() { + Ok(_) => true, + Err(error) => { + warnings.push(format!("Failed to launch Lightroom Classic: {}", error)); + false + } + } + }; + + #[cfg(not(target_os = "macos"))] + let launched = { + if let Some(path) = &resolved_path { + let mut command = Command::new(path); + command.arg(&source_folder); + match command.spawn() { + Ok(_) => true, + Err(error) => { + warnings.push(format!("Failed to launch Lightroom Classic: {}", error)); + false + } + } + } else { + warnings.push("Lightroom Classic was not detected. Open the selected photos folder from Lightroom.".to_string()); + false + } + }; + + Ok(LightroomSourceFolderResult { + source_folder: source_folder.display().to_string(), + files, + launched, + executable_path: resolved_path.map(|path| path.display().to_string()), + warnings, + }) +} + +#[tauri::command] +fn import_to_lightroom_classic( + groups: Vec, + executable_path: Option, +) -> Result { + let resolved_path = resolve_lightroom_classic_path(executable_path); + let (import_files, failed) = files_for_lightroom_command_line(&groups); + + if import_files.is_empty() { + return Err("No files are available for Lightroom import".to_string()); + } + + #[cfg(target_os = "macos")] + { + let mut command = Command::new("open"); + if let Some(path) = &resolved_path { + command.arg("-a").arg(path); + } else { + command.args(["-a", "Adobe Lightroom Classic"]); + } + command.arg("--args"); + if let Some(folder) = import_files + .first() + .and_then(|file| Path::new(file).parent()) + { + command.arg(folder); + } + command + .spawn() + .map_err(|e| format!("Failed to launch Lightroom Classic: {}", e))?; + } + + #[cfg(not(target_os = "macos"))] + { + let Some(path) = &resolved_path else { + return Ok(LightroomImportResult { + files: import_files, + launched: false, + executable_path: None, + warnings: failed, + }); + }; + let mut command = Command::new(&path); + if let Some(folder) = import_files + .first() + .and_then(|file| Path::new(file).parent()) + { + command.arg(folder); + } + command + .spawn() + .map_err(|e| format!("Failed to launch Lightroom Classic: {}", e))?; + } + + Ok(LightroomImportResult { + files: import_files, + launched: true, + executable_path: resolved_path.map(|path| path.display().to_string()), + warnings: failed, + }) +} + +#[tauri::command] +fn export_people_clusters_stream( + clusters: Vec, + destination_folder: String, + on_event: Channel, +) -> Result, String> { + let dest_path = Path::new(&destination_folder); + if !dest_path.exists() { + let error = "Destination folder does not exist".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("copying"), + None, + None, + None, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + if !dest_path.is_dir() { + let error = "Destination path is not a directory".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("copying"), + None, + None, + None, + None, + Some(error.clone()), + ), + ); + return Err(error); + } + + let total = clusters + .iter() + .map(|cluster| cluster.photo_paths.len()) + .sum::(); + let mut processed = 0usize; + let mut processed_files = Vec::new(); + let mut failed_files = Vec::new(); + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some("copying"), + Some(0), + Some(total), + None, + None, + None, + ), + ); + + for cluster in clusters { + let cluster_dir_name = sanitize_path_segment(&cluster.display_name); + let cluster_dir = unique_cluster_destination(dest_path, &cluster_dir_name); + if let Err(error) = fs::create_dir_all(&cluster_dir) { + failed_files.push(format!( + "Failed to create {}: {}", + cluster_dir.display(), + error + )); + continue; + } + + for photo_path in cluster.photo_paths { + let source_path = Path::new(&photo_path); + let current = source_path + .file_name() + .and_then(|name| name.to_str()) + .map(|value| value.to_string()) + .unwrap_or_else(|| photo_path.clone()); + + send_export_stream_event( + &on_event, + export_event( + "progress", + Some("copying"), + Some(processed), + Some(total), + Some(current.clone()), + None, + None, + ), + ); + + if !source_path.exists() || !is_jpeg_path(source_path) { + failed_files.push(format!("Unsupported or missing JPG source: {}", photo_path)); + processed += 1; + continue; + } + + let Some(file_name) = source_path.file_name().and_then(|name| name.to_str()) else { + failed_files.push(format!("Invalid file name: {}", photo_path)); + processed += 1; + continue; + }; + + let destination = unique_destination_path(&cluster_dir, file_name); + match fs::copy(source_path, &destination) { + Ok(_) => processed_files.push(destination.display().to_string()), + Err(error) => failed_files.push(format!( + "Failed to copy {}: {}", + source_path.display(), + error + )), + } + + processed += 1; + send_export_stream_event( + &on_event, + export_event( + "progress", + Some("copying"), + Some(processed), + Some(total), + Some(current), + None, + None, + ), + ); + } + } + + if !failed_files.is_empty() { + let error = format!( + "People export completed with errors:\n{}\n\nSuccessfully processed {} files", + failed_files.join("\n"), + processed_files.len() + ); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("copying"), + Some(processed), + Some(total), + None, + Some(processed_files.clone()), + Some(error.clone()), + ), + ); + Err(error) + } else if processed_files.is_empty() { + let error = "No person photos were exported".to_string(); + send_export_stream_event( + &on_event, + export_event( + "error", + Some("copying"), + Some(processed), + Some(total), + None, + None, + Some(error.clone()), + ), + ); + Err(error) + } else { + send_export_stream_event( + &on_event, + export_event( + "done", + Some("copying"), + Some(processed), + Some(total), + None, + Some(processed_files.clone()), + None, + ), + ); + Ok(processed_files) + } +} + +#[tauri::command] +async fn show_main_window(window: tauri::Window) -> Result<(), String> { + window.show().map_err(|e| e.to_string())?; + window.set_focus().map_err(|e| e.to_string())?; + Ok(()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let builder = tauri::Builder::default() + .plugin(tauri_plugin_window_state::Builder::default().build()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_os::init()); + + // Pro inference state holds the loaded native ONNX session across commands. + #[cfg(feature = "pro")] + let builder = builder.manage(pro_infer::ProInferState::default()); + + #[cfg(feature = "pro")] + let builder = builder.invoke_handler(tauri::generate_handler![ + greet, + read_exif, + scan_folder, + scan_files, + enrich_photo_metadata, + import_folder_stream, + import_files_stream, + extract_raw_embedded_preview, + get_jpeg_thumbnail, + get_app_cache_usage, + clear_app_preview_caches, + detect_rawtherapee_cli, + validate_raw_engine, + get_raw_monitor_cache_entry, + render_raw_monitor_cache_stream, + cancel_raw_monitor_cache_render, + import_monitor_lut, + read_monitor_lut, + clear_raw_monitor_cache, + get_raw_monitor_cache_size, + cleanup_raw_monitor_cache_lru, + move_to_trash, + delete_files_permanently, + export_files, + export_files_stream, + write_rating_metadata, + write_rendered_export, + write_rendered_export_stream, + detect_lightroom_classic, + launch_lightroom_classic, + import_to_lightroom_classic, + open_lightroom_source_folder, + export_people_clusters_stream, + evaluate_ai_analysis, + show_main_window, + pro_infer::pro_infer_init, + pro_infer::pro_infer_batch + ]); + + #[cfg(not(feature = "pro"))] + let builder = builder.invoke_handler(tauri::generate_handler![ + greet, + read_exif, + scan_folder, + scan_files, + enrich_photo_metadata, + import_folder_stream, + import_files_stream, + extract_raw_embedded_preview, + get_jpeg_thumbnail, + get_app_cache_usage, + clear_app_preview_caches, + move_to_trash, + delete_files_permanently, + export_files, + export_files_stream, + write_rating_metadata, + write_rendered_export, + write_rendered_export_stream, + detect_lightroom_classic, + launch_lightroom_classic, + import_to_lightroom_classic, + open_lightroom_source_folder, + export_people_clusters_stream, + evaluate_ai_analysis, + show_main_window + ]); + + builder + .setup(|app| { + #[cfg(desktop)] + { + use tauri::Manager; + let window = app.get_webview_window("main").unwrap(); + if let Err(error) = window.set_background_color(Some(Color(16, 17, 21, 255))) { + eprintln!("Failed to set startup background color: {error}"); + } + + // On macOS, enable native decorations for traffic light buttons + // On Windows/Linux, disable decorations for custom title bar + #[cfg(target_os = "macos")] + { + window.set_decorations(true).unwrap(); + } + + #[cfg(not(target_os = "macos"))] + { + window.set_decorations(false).unwrap(); + } + // Window will be shown after frontend is ready via show_main_window command + } + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_test_dir(name: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("framecull-{name}-{stamp}")); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn raw_copy_exports_target_sidecar_with_rating() { + let dir = temp_test_dir("raw-sidecar"); + let source = dir.join("source.NEF"); + let target = dir.join("target.NEF"); + fs::write(&source, b"raw").unwrap(); + fs::write(&target, b"raw").unwrap(); + + let written = export_raw_sidecar_to_target(&source, &target, "COPY", true, 4) + .unwrap() + .unwrap(); + let text = fs::read_to_string(&written).unwrap(); + assert_eq!(written, dir.join("target.xmp")); + assert!(text.contains("xmp:Rating=\"4\"")); + assert!(!dir.join("source.xmp").exists()); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn raw_copy_preserves_existing_sidecar_and_updates_rating() { + let dir = temp_test_dir("raw-existing-sidecar"); + let source = dir.join("source.NEF"); + let target = dir.join("target.NEF"); + let source_sidecar = dir.join("source.xmp"); + fs::write(&source, b"raw").unwrap(); + fs::write(&target, b"raw").unwrap(); + fs::write(&source_sidecar, build_minimal_xmp(2)).unwrap(); + + let written = export_raw_sidecar_to_target(&source, &target, "COPY", true, 5) + .unwrap() + .unwrap(); + let target_text = fs::read_to_string(&written).unwrap(); + let source_text = fs::read_to_string(&source_sidecar).unwrap(); + assert_eq!(written, dir.join("target.xmp")); + assert!(target_text.contains("xmp:Rating=\"5\"")); + assert!(source_text.contains("xmp:Rating=\"2\"")); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn jpeg_rating_is_embedded_and_read_back() { + let dir = temp_test_dir("jpeg-rating"); + let path = dir.join("rated.jpg"); + fs::write(&path, [0xFF, 0xD8, 0xFF, 0xD9]).unwrap(); + + let written = write_rating_to_path(&path, 5).unwrap(); + assert_eq!(written, path); + assert_eq!(read_rating_from_path(&path), Some(5)); + let bytes = fs::read(&path).unwrap(); + assert!(find_jpeg_xmp_packet(&bytes).is_some()); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn exported_target_jpeg_gets_embedded_rating() { + let dir = temp_test_dir("exported-jpeg-rating"); + let source = dir.join("source.jpg"); + let target = dir.join("target.jpg"); + fs::write(&source, [0xFF, 0xD8, 0xFF, 0xD9]).unwrap(); + fs::write(&target, [0xFF, 0xD8, 0xFF, 0xD9]).unwrap(); + + let written = + write_exported_target_rating_metadata(&source, &target, 3, "COPY", true).unwrap(); + assert_eq!(written, vec![target.clone()]); + assert_eq!(read_rating_from_path(&target), Some(3)); + assert_eq!(read_rating_from_path(&source), None); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn rendered_jpeg_export_metadata_writes_rating() { + let dir = temp_test_dir("rendered-jpeg-rating"); + let target = dir.join("rendered.jpg"); + fs::write(&target, [0xFF, 0xD8, 0xFF, 0xD9]).unwrap(); + + let written = + write_rendered_export_metadata(&target, Some(4), Some("RATING_ONLY"), None).unwrap(); + assert_eq!(written, vec![target.display().to_string()]); + assert_eq!(read_rating_from_path(&target), Some(4)); + + fs::remove_dir_all(dir).unwrap(); + } + + fn test_jpeg_bytes(width: u32, height: u32) -> Vec { + let image = image::RgbImage::from_fn(width, height, |x, y| { + image::Rgb([ + ((x * 17 + y * 3) % 256) as u8, + ((x * 5 + y * 29) % 256) as u8, + ((x * 11 + y * 7) % 256) as u8, + ]) + }); + let mut bytes = Vec::new(); + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut bytes, 82); + encoder.encode_image(&image).unwrap(); + bytes + } + + #[test] + fn raw_preview_prefers_displayable_tiff_jpeg_over_larger_lossless_segment() { + let preview = test_jpeg_bytes(320, 240); + let preview_offset = 128usize; + let lossless_offset = preview_offset + preview.len() + 32; + let lossless_length = preview.len() + 4096; + let mut bytes = vec![0u8; lossless_offset + lossless_length + 32]; + + bytes[0..2].copy_from_slice(b"II"); + bytes[2..4].copy_from_slice(&42u16.to_le_bytes()); + bytes[4..8].copy_from_slice(&8u32.to_le_bytes()); + bytes[8..10].copy_from_slice(&2u16.to_le_bytes()); + + let first_entry = 10usize; + bytes[first_entry..first_entry + 2].copy_from_slice(&0x0201u16.to_le_bytes()); + bytes[first_entry + 2..first_entry + 4].copy_from_slice(&4u16.to_le_bytes()); + bytes[first_entry + 4..first_entry + 8].copy_from_slice(&1u32.to_le_bytes()); + bytes[first_entry + 8..first_entry + 12] + .copy_from_slice(&(preview_offset as u32).to_le_bytes()); + + let second_entry = first_entry + 12; + bytes[second_entry..second_entry + 2].copy_from_slice(&0x0202u16.to_le_bytes()); + bytes[second_entry + 2..second_entry + 4].copy_from_slice(&4u16.to_le_bytes()); + bytes[second_entry + 4..second_entry + 8].copy_from_slice(&1u32.to_le_bytes()); + bytes[second_entry + 8..second_entry + 12] + .copy_from_slice(&(preview.len() as u32).to_le_bytes()); + + bytes[second_entry + 12..second_entry + 16].copy_from_slice(&0u32.to_le_bytes()); + bytes[preview_offset..preview_offset + preview.len()].copy_from_slice(&preview); + + bytes[lossless_offset] = 0xFF; + bytes[lossless_offset + 1] = 0xD8; + bytes[lossless_offset + 2] = 0xFF; + bytes[lossless_offset + 3] = 0xC4; + bytes[lossless_offset + lossless_length - 2] = 0xFF; + bytes[lossless_offset + lossless_length - 1] = 0xD9; + + assert_eq!( + find_largest_embedded_jpeg(&bytes), + Some((preview_offset, preview.len())), + ); + } + + #[test] + fn uppercase_cr2_imports_as_raw_only() { + let dir = temp_test_dir("cr2-import"); + let raw = dir.join("2K9A9785.CR2"); + fs::write(&raw, b"canon raw").unwrap(); + + let groups = collect_photo_groups_from_paths(vec![raw.clone()], |_, _, _| {}); + + assert_eq!(groups.len(), 1); + assert_eq!(groups[0].id, "2K9A9785"); + assert_eq!(groups[0].status, "RAW_ONLY"); + assert!(groups[0].jpg.is_none()); + assert_eq!(groups[0].raw.as_ref().unwrap().extension, "CR2"); + assert_eq!(groups[0].raw.as_ref().unwrap().path, raw.to_string_lossy()); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn recursive_folder_scan_finds_nested_cr2_files() { + let dir = temp_test_dir("recursive-cr2-import"); + let nested = dir.join("nested").join("canon"); + fs::create_dir_all(&nested).unwrap(); + let raw = nested.join("2K9A9785.CR2"); + fs::write(&raw, b"canon raw").unwrap(); + + let files = collect_files_recursive(&dir).unwrap(); + let groups = collect_photo_groups_from_paths(files, |_, _, _| {}); + + assert_eq!(groups.len(), 1); + assert_eq!(groups[0].id, "2K9A9785"); + assert_eq!(groups[0].status, "RAW_ONLY"); + assert_eq!(groups[0].raw.as_ref().unwrap().extension, "CR2"); + + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn lightroom_catalog_file_prefers_raw() { + let group = PhotoGroupInfo { + id: "g1".to_string(), + jpg: Some(PhotoFileInfo { + name: "a.jpg".to_string(), + extension: "jpg".to_string(), + path: "C:\\photos\\a.jpg".to_string(), + size: 1, + modified_ms: None, + }), + raw: Some(PhotoFileInfo { + name: "a.nef".to_string(), + extension: "nef".to_string(), + path: "C:\\photos\\a.nef".to_string(), + size: 2, + modified_ms: None, + }), + status: "UNDECIDED".to_string(), + rating: 4, + exif: None, + }; + + let selected = file_for_lightroom_catalog(&group).unwrap(); + assert_eq!(selected.path, "C:\\photos\\a.nef"); + } + + #[test] + fn lightroom_command_line_uses_original_raw_path_and_writes_ratings() { + let dir = temp_test_dir("lightroom-command-line"); + let jpg = dir.join("a.jpg"); + let raw = dir.join("a.nef"); + fs::write(&jpg, [0xFF, 0xD8, 0xFF, 0xD9]).unwrap(); + fs::write(&raw, b"raw").unwrap(); + + let group = PhotoGroupInfo { + id: "g1".to_string(), + jpg: Some(PhotoFileInfo { + name: "a.jpg".to_string(), + extension: "jpg".to_string(), + path: jpg.display().to_string(), + size: 4, + modified_ms: None, + }), + raw: Some(PhotoFileInfo { + name: "a.nef".to_string(), + extension: "nef".to_string(), + path: raw.display().to_string(), + size: 3, + modified_ms: None, + }), + status: "UNDECIDED".to_string(), + rating: 4, + exif: None, + }; + + let (files, warnings) = files_for_lightroom_command_line(&[group]); + assert!(warnings.is_empty()); + assert_eq!(files, vec![raw.display().to_string()]); + assert_eq!(read_rating_from_path(&jpg), Some(4)); + assert_eq!(read_xmp_sidecar_rating(&raw), Some(4)); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_cache_key_changes_with_engine_version() { + let dir = temp_test_dir("raw-monitor-cache-key"); + let raw = dir.join("a.CR2"); + fs::write(&raw, b"canon raw").unwrap(); + + let first = + raw_monitor_cache_file_name(&raw, "RawTherapee 5.12", RAW_MONITOR_PROFILE_BALANCED) + .unwrap(); + let second = + raw_monitor_cache_file_name(&raw, "RawTherapee 5.13", RAW_MONITOR_PROFILE_BALANCED) + .unwrap(); + + assert_ne!(first, second); + assert!(first.ends_with(".jpg")); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_cache_key_changes_with_profile() { + let dir = temp_test_dir("raw-monitor-cache-profile-key"); + let raw = dir.join("a.CR2"); + fs::write(&raw, b"canon raw").unwrap(); + + let balanced = + raw_monitor_cache_file_name(&raw, "RawTherapee 5.12", RAW_MONITOR_PROFILE_BALANCED) + .unwrap(); + let auto_exposure = raw_monitor_cache_file_name( + &raw, + "RawTherapee 5.12", + RAW_MONITOR_PROFILE_AUTO_EXPOSURE, + ) + .unwrap(); + + assert_ne!(balanced, auto_exposure); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_cache_status_reads_profile_metadata() { + let dir = temp_test_dir("raw-monitor-cache-metadata"); + let cache = dir.join("cache.jpg"); + fs::write(&cache, b"cached jpeg placeholder").unwrap(); + + write_raw_monitor_cache_metadata( + &cache, + RAW_MONITOR_PROFILE_AUTO_EXPOSURE, + "embeddedFallback", + true, + Some(RAW_MONITOR_FALLBACK_DECODE_FAILURE), + ) + .unwrap(); + + let status = raw_monitor_cache_status(&cache); + assert_eq!(status.source, "embeddedFallback"); + assert!(status.fallback); + assert_eq!( + status.fallback_reason.as_deref(), + Some(RAW_MONITOR_FALLBACK_DECODE_FAILURE) + ); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_legacy_nikon_rawtherapee_cache_is_not_reusable() { + let dir = temp_test_dir("raw-monitor-legacy-nef-cache"); + let raw = dir.join("source.NEF"); + let cache = dir.join("cache.jpg"); + fs::write(&raw, b"nikon raw").unwrap(); + fs::write(&cache, test_jpeg_bytes(64, 48)).unwrap(); + fs::write( + raw_monitor_cache_metadata_path(&cache), + br#"{ + "cacheSource": "rawtherapee", + "fallback": false, + "profileId": "FrameCull_Monitor_Balanced_v1", + "writtenAtMs": 1 +}"#, + ) + .unwrap(); + + assert!(!raw_monitor_cache_is_reusable(&raw, &cache)); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_legacy_non_nikon_cache_remains_reusable() { + let dir = temp_test_dir("raw-monitor-legacy-cr3-cache"); + let raw = dir.join("source.CR3"); + let cache = dir.join("cache.jpg"); + fs::write(&raw, b"canon raw").unwrap(); + fs::write(&cache, test_jpeg_bytes(64, 48)).unwrap(); + fs::write( + raw_monitor_cache_metadata_path(&cache), + br#"{ + "cacheSource": "rawtherapee", + "fallback": false, + "profileId": "FrameCull_Monitor_Balanced_v1", + "writtenAtMs": 1 +}"#, + ) + .unwrap(); + + assert!(raw_monitor_cache_is_reusable(&raw, &cache)); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_recent_failure_respects_cooldown() { + let mut table = HashMap::new(); + table.insert( + "nef-cache-key".to_string(), + RawMonitorFailureRecord { + failed_at_ms: 10_000, + error: "decode failed".to_string(), + }, + ); + + assert!(raw_monitor_recent_failure(&table, "nef-cache-key", 10_000 + 60_000).is_some()); + + let cooled_down_at = + 10_000 + (RAW_MONITOR_FAILURE_RETRY_COOLDOWN_SECS * 1000) + 1; + assert!(raw_monitor_recent_failure(&table, "nef-cache-key", cooled_down_at).is_none()); + assert!(raw_monitor_recent_failure(&table, "missing-key", cooled_down_at).is_none()); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_pp3_contains_monitor_defaults() { + let pp3 = raw_monitor_pp3_content(RAW_MONITOR_PROFILE_BALANCED); + + assert!(pp3.contains("[Exposure]")); + assert!(pp3.contains("Auto=false")); + assert!(pp3.contains("HighlightCompr=25")); + assert!(pp3.contains(&format!("Width={}", RAW_MONITOR_MAX_EDGE))); + assert!(pp3.contains("OutputProfile=RTv4_sRGB")); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_auto_exposure_pp3_contains_auto_profile() { + let pp3 = raw_monitor_pp3_content(RAW_MONITOR_PROFILE_AUTO_EXPOSURE); + + assert!(pp3.contains("Auto=true")); + assert!(pp3.contains("Brightness=8")); + assert!(pp3.contains("HighlightCompr=90")); + assert!(pp3.contains("ShadowCompr=55")); + assert!(pp3.contains("OutputProfile=RTv4_sRGB")); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_engine_result_marks_bundled_version_and_source() { + let result = raw_engine_result( + true, + Some("rawtherapee-cli.exe".to_string()), + Some("RawTherapee, version 5.12".to_string()), + Some("BUNDLED"), + Some("ok".to_string()), + ); + + assert!(result.ok); + assert_eq!(result.engine_source.as_deref(), Some("BUNDLED")); + assert_eq!( + result.bundled_engine_version.as_deref(), + Some(RAWTHERAPEE_BUNDLED_VERSION) + ); + } + + #[test] + fn macos_rawtherapee_candidates_match_supported_priority_order() { + let home_dir = PathBuf::from("/Users/framecull-test"); + let candidates = macos_rawtherapee_candidates(Some(home_dir.as_path())); + + assert_eq!( + candidates, + vec![ + ( + PathBuf::from( + "/Users/framecull-test/Library/Application Support/com.framecull.ai.pro/tools/rawtherapee-cli", + ), + "SYSTEM", + ), + ( + PathBuf::from("/usr/local/bin/rawtherapee-cli"), + "SYSTEM", + ), + ( + PathBuf::from("/opt/homebrew/bin/rawtherapee-cli"), + "SYSTEM", + ), + ] + ); + } + + #[cfg(all(feature = "pro", target_os = "windows"))] + #[derive(Clone, Copy)] + enum FakeRawTherapeeMode { + Valid, + Invalid, + DecodeWarning, + } + + #[cfg(all(feature = "pro", target_os = "windows"))] + fn fake_rawtherapee_cli(dir: &Path, mode: FakeRawTherapeeMode) -> PathBuf { + use base64::Engine as _; + + let cli_path = dir.join("rawtherapee-cli.cmd"); + let ps_path = dir.join("fake-rawtherapee.ps1"); + let output_command = if matches!(mode, FakeRawTherapeeMode::Valid | FakeRawTherapeeMode::DecodeWarning) { + let jpeg_b64 = + base64::engine::general_purpose::STANDARD.encode(test_jpeg_bytes(64, 48)); + format!( + "$jpegB64 = '{}'\r\n[IO.File]::WriteAllBytes($out, [Convert]::FromBase64String($jpegB64))\r\n", + jpeg_b64 + ) + } else { + "Set-Content -LiteralPath $out -Value 'not-a-jpeg'\r\n".to_string() + }; + let warning_command = if matches!(mode, FakeRawTherapeeMode::DecodeWarning) { + "[Console]::Error.WriteLine('unknown file: data corrupted at 5014531')\r\n" + } else { + "" + }; + let ps_script = format!( + r#"$rest = $args +if ($rest.Count -gt 0 -and $rest[0] -eq '-v') {{ + Write-Output 'RawTherapee, version 5.12-test' + exit 0 +}} +$out = $null +$pp3 = $null +$raw = $null +$quality = $false +$overwrite = $false +for ($i = 0; $i -lt $rest.Count; $i++) {{ + switch ($rest[$i]) {{ + '-o' {{ $i++; $out = $rest[$i]; continue }} + '-p' {{ $i++; $pp3 = $rest[$i]; continue }} + '-j{quality}' {{ $quality = $true; continue }} + '-Y' {{ $overwrite = $true; continue }} + '-c' {{ $i++; $raw = $rest[$i]; continue }} + }} +}} +if (-not $out) {{ exit 5 }} +if (-not $pp3) {{ exit 6 }} +if (-not $raw) {{ exit 7 }} +if (-not $quality) {{ exit 8 }} +if (-not $overwrite) {{ exit 9 }} +if (-not (Test-Path -LiteralPath $pp3)) {{ exit 10 }} +if (-not (Test-Path -LiteralPath $raw)) {{ exit 11 }} +{output_command}{warning_command}exit $LASTEXITCODE +"#, + quality = RAW_MONITOR_JPEG_QUALITY, + output_command = output_command, + warning_command = warning_command, + ); + fs::write(&ps_path, ps_script).unwrap(); + fs::write( + &cli_path, + "@echo off\r\npowershell -NoProfile -ExecutionPolicy Bypass -File \"%~dp0fake-rawtherapee.ps1\" %*\r\nexit /b %ERRORLEVEL%\r\n", + ) + .unwrap(); + cli_path + } + + #[cfg(all(feature = "pro", target_os = "windows"))] + #[test] + fn raw_monitor_render_invokes_cli_and_writes_valid_jpeg() { + let dir = temp_test_dir("raw-monitor-render"); + let engine = fake_rawtherapee_cli(&dir, FakeRawTherapeeMode::Valid); + let raw = dir.join("source.CR2"); + let pp3 = dir.join("FrameCull_Monitor_v1.pp3"); + let cache = dir.join("cache.jpg"); + fs::write(&raw, b"canon raw").unwrap(); + fs::write(&pp3, raw_monitor_pp3_content(RAW_MONITOR_PROFILE_BALANCED)).unwrap(); + + render_raw_monitor_cache_file(&engine, &pp3, &raw, &cache, RAW_MONITOR_PROFILE_BALANCED) + .unwrap(); + + assert!(cache.is_file()); + assert!(image::open(&cache).is_ok()); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(all(feature = "pro", target_os = "windows"))] + #[test] + fn raw_monitor_render_rejects_unreadable_cli_output() { + let dir = temp_test_dir("raw-monitor-bad-output"); + let engine = fake_rawtherapee_cli(&dir, FakeRawTherapeeMode::Invalid); + let raw = dir.join("source.CR2"); + let pp3 = dir.join("FrameCull_Monitor_v1.pp3"); + let cache = dir.join("cache.jpg"); + fs::write(&raw, b"canon raw").unwrap(); + fs::write(&pp3, raw_monitor_pp3_content(RAW_MONITOR_PROFILE_BALANCED)).unwrap(); + + let error = render_raw_monitor_cache_file( + &engine, + &pp3, + &raw, + &cache, + RAW_MONITOR_PROFILE_BALANCED, + ) + .expect_err("bad CLI output should be rejected"); + + assert!(error.contains("not a readable JPEG")); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(all(feature = "pro", target_os = "windows"))] + #[test] + fn raw_monitor_render_falls_back_to_embedded_preview_when_cli_output_is_bad() { + let dir = temp_test_dir("raw-monitor-embedded-fallback"); + let engine = fake_rawtherapee_cli(&dir, FakeRawTherapeeMode::Invalid); + let raw = dir.join("source.NEF"); + let pp3 = dir.join("FrameCull_Monitor_v1.pp3"); + let cache = dir.join("cache.jpg"); + let preview = test_jpeg_bytes(640, 480); + let mut raw_bytes = b"nikon raw header".to_vec(); + raw_bytes.extend_from_slice(&preview); + raw_bytes.extend_from_slice(b"nikon raw footer"); + fs::write(&raw, raw_bytes).unwrap(); + fs::write(&pp3, raw_monitor_pp3_content(RAW_MONITOR_PROFILE_BALANCED)).unwrap(); + + let result = render_raw_monitor_cache_file( + &engine, + &pp3, + &raw, + &cache, + RAW_MONITOR_PROFILE_BALANCED, + ) + .unwrap(); + + assert!(cache.is_file()); + assert!(image::open(&cache).is_ok()); + assert!(result.status.fallback); + assert_eq!(result.status.source, "embeddedFallback"); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(all(feature = "pro", target_os = "windows"))] + #[test] + fn raw_monitor_decode_warning_replaces_valid_but_corrupt_output() { + let dir = temp_test_dir("raw-monitor-decode-warning-fallback"); + let engine = fake_rawtherapee_cli(&dir, FakeRawTherapeeMode::DecodeWarning); + let raw = dir.join("source.NEF"); + let pp3 = dir.join("FrameCull_Monitor_v1.pp3"); + let cache = dir.join("cache.jpg"); + let preview = test_jpeg_bytes(640, 480); + let mut raw_bytes = b"nikon raw header".to_vec(); + raw_bytes.extend_from_slice(&preview); + raw_bytes.extend_from_slice(b"nikon raw footer"); + fs::write(&raw, raw_bytes).unwrap(); + fs::write(&pp3, raw_monitor_pp3_content(RAW_MONITOR_PROFILE_BALANCED)).unwrap(); + + let result = render_raw_monitor_cache_file( + &engine, + &pp3, + &raw, + &cache, + RAW_MONITOR_PROFILE_BALANCED, + ) + .unwrap(); + + let rendered = image::open(&cache).unwrap(); + assert_eq!(rendered.dimensions(), (640, 480)); + assert!(result.status.fallback); + assert_eq!(result.status.source, "embeddedFallback"); + assert_eq!( + result.status.fallback_reason.as_deref(), + Some(RAW_MONITOR_FALLBACK_DECODE_FAILURE) + ); + + fs::remove_dir_all(dir).unwrap(); + } + + #[cfg(feature = "pro")] + #[test] + fn raw_monitor_decode_failure_detection_catches_corruption_warnings() { + assert!(rawtherapee_reported_decode_failure( + "unknown file: data corrupted at 5014531" + )); + assert!(rawtherapee_reported_decode_failure( + "Decoder error while reading RAW data" + )); + assert!(!rawtherapee_reported_decode_failure( + "Processing completed with neutral profile" + )); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..1aa471b --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + framecull_ai_lib::run() +} diff --git a/src-tauri/src/pro-infer-bench.rs b/src-tauri/src/pro-infer-bench.rs new file mode 100644 index 0000000..cff4608 --- /dev/null +++ b/src-tauri/src/pro-infer-bench.rs @@ -0,0 +1,288 @@ +#![cfg(feature = "pro")] + +// Keep this outside src/bin so Tauri does not auto-bundle the Pro-only utility in Flash builds. + +use std::fs; +use std::path::{Path, PathBuf}; + +use framecull_ai_lib::pro_infer::infer::run_batch; +use framecull_ai_lib::pro_infer::session::init_model; +use framecull_ai_lib::pro_infer::types::ProBatchRequest; +use serde::Serialize; + +#[derive(Debug)] +struct Args { + audit: PathBuf, + manifest: PathBuf, + output: PathBuf, + preview_dir: Option, + batch_size: u32, + limit: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct BenchRow { + photo_id: String, + image_path: String, + aesthetic: Option, + persona_score: Option, + scene_label: Option, + scene_confidence: Option, + semantic_keep_score: Option, + face_validity_score: Option, + composition_score: Option, + moment_score: Option, + lighting_mood_score: Option, + false_face_risk: Option, + error: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct BenchOutput { + manifest_path: String, + active_ep: String, + ep_fallback_chain: Vec, + backbone_version: String, + warmup_ms: f64, + batch_size: u32, + count: usize, + elapsed_ms: f64, + mean_per_image_ms: f64, + results: Vec, +} + +fn main() -> Result<(), String> { + let args = parse_args(std::env::args().skip(1).collect())?; + let audit: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&args.audit).map_err(|e| format!("failed to read audit: {e}"))?, + ) + .map_err(|e| format!("invalid audit json: {e}"))?; + + let summaries = audit["photoSummaries"] + .as_array() + .ok_or_else(|| "audit.photoSummaries missing".to_string())?; + + let mut image_paths: Vec = Vec::new(); + let mut photo_ids: Vec = Vec::new(); + for summary in summaries { + let photo_id = summary["id"].as_str().unwrap_or_default().to_string(); + let source = summary["sourceName"] + .as_str() + .unwrap_or_default() + .to_string(); + let file_name = summary["fileName"].as_str().unwrap_or_default().to_string(); + if photo_id.is_empty() { + continue; + } + let explicit_path = explicit_decodable_path(summary); + let path = pick_decodable_path( + explicit_path.as_deref(), + &source, + &file_name, + &photo_id, + args.preview_dir.as_deref(), + ); + image_paths.push(path); + photo_ids.push(photo_id); + if let Some(limit) = args.limit { + if image_paths.len() >= limit { + break; + } + } + } + + let (mut loaded, caps) = init_model( + args.manifest + .to_str() + .ok_or_else(|| "manifest path is not valid UTF-8".to_string())?, + None, + )?; + + let req = ProBatchRequest { + image_paths: image_paths.clone(), + batch_size: Some(args.batch_size), + heads: None, + }; + let resp = run_batch(&mut loaded, &req); + + let results = resp + .results + .into_iter() + .zip(photo_ids.into_iter()) + .map(|(row, photo_id)| BenchRow { + photo_id, + image_path: row.image_path, + aesthetic: row.aesthetic, + persona_score: row.persona_score, + scene_label: row.scene_label, + scene_confidence: row.scene_confidence, + semantic_keep_score: row.semantic_keep_score, + face_validity_score: row.face_validity_score, + composition_score: row.composition_score, + moment_score: row.moment_score, + lighting_mood_score: row.lighting_mood_score, + false_face_risk: row.false_face_risk, + error: row.error, + }) + .collect::>(); + + let output = BenchOutput { + manifest_path: args.manifest.to_string_lossy().into_owned(), + active_ep: resp.ep, + ep_fallback_chain: caps.ep_fallback_chain, + backbone_version: caps.backbone_version, + warmup_ms: caps.warmup_ms, + batch_size: args.batch_size, + count: results.len(), + elapsed_ms: resp.elapsed_ms, + mean_per_image_ms: if results.is_empty() { + 0.0 + } else { + resp.elapsed_ms / results.len() as f64 + }, + results, + }; + + if let Some(parent) = args.output.parent() { + fs::create_dir_all(parent).map_err(|e| format!("failed to create output dir: {e}"))?; + } + fs::write( + &args.output, + serde_json::to_string_pretty(&output) + .map_err(|e| format!("failed to encode output json: {e}"))?, + ) + .map_err(|e| format!("failed to write output: {e}"))?; + Ok(()) +} + +fn parse_args(argv: Vec) -> Result { + let mut audit: Option = None; + let mut manifest: Option = None; + let mut output: Option = None; + let mut preview_dir: Option = None; + let mut batch_size: u32 = 1; + let mut limit: Option = None; + + let mut idx = 0usize; + while idx < argv.len() { + let key = &argv[idx]; + let has_value = argv + .get(idx + 1) + .map(|value| !value.starts_with("--")) + .unwrap_or(false); + let value = if has_value { + argv.get(idx + 1).cloned() + } else { + None + }; + match key.as_str() { + "--audit" => audit = value.as_deref().map(PathBuf::from), + "--manifest" => manifest = value.as_deref().map(PathBuf::from), + "--output" => output = value.as_deref().map(PathBuf::from), + "--preview-dir" => preview_dir = value.as_deref().map(PathBuf::from), + "--batch-size" => { + batch_size = value + .as_deref() + .ok_or_else(|| "--batch-size requires a value".to_string())? + .parse::() + .map_err(|e| format!("invalid --batch-size: {e}"))?; + } + "--limit" => { + limit = Some( + value + .as_deref() + .ok_or_else(|| "--limit requires a value".to_string())? + .parse::() + .map_err(|e| format!("invalid --limit: {e}"))?, + ); + } + _ => {} + } + idx += if has_value { 2 } else { 1 }; + } + + Ok(Args { + audit: audit.ok_or_else(|| "--audit is required".to_string())?, + manifest: manifest.ok_or_else(|| "--manifest is required".to_string())?, + output: output.ok_or_else(|| "--output is required".to_string())?, + preview_dir, + batch_size, + limit, + }) +} + +fn pick_decodable_path( + explicit_path: Option<&str>, + source: &str, + file_name: &str, + photo_id: &str, + preview_dir: Option<&Path>, +) -> String { + if let Some(explicit_path) = explicit_path { + let path = Path::new(explicit_path); + if is_decodable_image(path) && path.exists() { + return path.to_string_lossy().into_owned(); + } + } + + if let Some(preview_dir) = preview_dir { + for candidate in preview_candidates(preview_dir, file_name, photo_id) { + if candidate.exists() { + return candidate.to_string_lossy().into_owned(); + } + } + } + + let path = Path::new(source); + if is_decodable_image(path) { + return source.to_string(); + } + + let jpg = path.with_extension("jpg"); + if jpg.exists() { + return jpg.to_string_lossy().into_owned(); + } + let jpeg = path.with_extension("jpeg"); + if jpeg.exists() { + return jpeg.to_string_lossy().into_owned(); + } + source.to_string() +} + +fn explicit_decodable_path(summary: &serde_json::Value) -> Option { + for key in [ + "studentPreviewPath", + "previewPath", + "imagePath", + "importPath", + ] { + let value = summary[key].as_str().unwrap_or_default().trim(); + if !value.is_empty() { + return Some(value.to_string()); + } + } + None +} + +fn is_decodable_image(path: &Path) -> bool { + path.extension() + .and_then(|value| value.to_str()) + .map(|ext| { + let lower = ext.to_ascii_lowercase(); + lower == "jpg" || lower == "jpeg" || lower == "png" + }) + .unwrap_or(false) +} + +fn preview_candidates(preview_dir: &Path, file_name: &str, photo_id: &str) -> Vec { + let mut candidates = Vec::new(); + if !file_name.is_empty() { + candidates.push(preview_dir.join(file_name)); + } + for ext in ["jpg", "jpeg", "png"] { + candidates.push(preview_dir.join(format!("{photo_id}.{ext}"))); + } + candidates +} diff --git a/src-tauri/src/pro_infer/ep.rs b/src-tauri/src/pro_infer/ep.rs new file mode 100644 index 0000000..21b8284 --- /dev/null +++ b/src-tauri/src/pro_infer/ep.rs @@ -0,0 +1,80 @@ +//! Execution-provider detection and fallback chain (PRO_MODEL_ARCHITECTURE.md §10.6). +//! +//! The probe order is platform-fixed. Each level is tried in order; a level that +//! reports unavailable or fails to register is recorded with a reason in the +//! fallback chain and the next level is tried. CPU is always last and never +//! fails, so the chain can never end empty. + +use ort::ep::ExecutionProviderDispatch; +use ort::ep::{ExecutionProvider, CPU}; + +#[cfg(target_os = "macos")] +use ort::ep::CoreML; +#[cfg(windows)] +use ort::ep::{DirectML, CUDA}; + +/// A single execution-provider candidate, ordered by preference. +pub struct EpCandidate { + /// Canonical lowercase name reported to the frontend (`activeEp`). + pub name: &'static str, + pub dispatch: ExecutionProviderDispatch, + /// Result of the build-time/availability probe; `Ok` does not guarantee the + /// session will register it, but lets us record why a level was skipped. + pub available: bool, + pub probe_note: String, +} + +/// Build the platform-specific candidate list in preference order. +/// +/// Windows: CUDA -> DirectML -> CPU +/// macOS: CoreML -> CPU +/// other: CPU +pub fn candidate_chain() -> Vec { + let mut chain: Vec = Vec::new(); + + #[cfg(windows)] + { + chain.push(make_candidate("cuda", &CUDA::default(), || { + CUDA::default().build() + })); + chain.push(make_candidate("directml", &DirectML::default(), || { + DirectML::default().build() + })); + } + + #[cfg(target_os = "macos")] + { + chain.push(make_candidate("coreml", &CoreML::default(), || { + CoreML::default().build() + })); + } + + // CPU is always the final, infallible fallback. + chain.push(EpCandidate { + name: "cpu", + dispatch: CPU::default().build(), + available: true, + probe_note: "cpu fallback always available".to_string(), + }); + + chain +} + +#[cfg(any(windows, target_os = "macos"))] +fn make_candidate( + name: &'static str, + probe: &E, + build: impl FnOnce() -> ExecutionProviderDispatch, +) -> EpCandidate { + let (available, probe_note) = match probe.is_available() { + Ok(true) => (true, format!("{name} reported available")), + Ok(false) => (false, format!("{name} not available on this build/host")), + Err(error) => (false, format!("{name} availability probe failed: {error}")), + }; + EpCandidate { + name, + dispatch: build(), + available, + probe_note, + } +} diff --git a/src-tauri/src/pro_infer/infer.rs b/src-tauri/src/pro_infer/infer.rs new file mode 100644 index 0000000..70b1909 --- /dev/null +++ b/src-tauri/src/pro_infer/infer.rs @@ -0,0 +1,260 @@ +//! Batch inference and multi-head output parsing (§10.5/§10.7). + +use std::path::Path; + +use ort::value::Tensor; + +use super::preprocess::preprocess_image; +use super::session::LoadedModel; +use super::types::{ProBatchRequest, ProBatchResponse, ProHeadScores}; + +const DEFAULT_ACCELERATED_BATCH_SIZE: usize = 8; +const DEFAULT_CPU_BATCH_SIZE: usize = 1; + +/// Resolve the effective batch size: explicit request, else a conservative +/// default sized for the 6GB min-spec card (§6.1). +fn effective_batch_size(req: &ProBatchRequest, active_ep: &str) -> usize { + req.batch_size + .map(|value| value.max(1) as usize) + .unwrap_or_else(|| { + if active_ep == "cpu" { + DEFAULT_CPU_BATCH_SIZE + } else { + DEFAULT_ACCELERATED_BATCH_SIZE + } + }) +} + +/// Run a full batch request. Per-image decode failures are isolated into that +/// image's `error` field and never abort the whole batch (§10.9 item 5). +pub fn run_batch(model: &mut LoadedModel, req: &ProBatchRequest) -> ProBatchResponse { + let started = std::time::Instant::now(); + let active_ep = model.active_ep.clone(); + let batch_size = effective_batch_size(req, &active_ep); + let mut results: Vec = Vec::with_capacity(req.image_paths.len()); + + for chunk in req.image_paths.chunks(batch_size) { + run_chunk(model, chunk, &mut results); + } + + ProBatchResponse { + results, + ep: active_ep, + elapsed_ms: started.elapsed().as_secs_f64() * 1000.0, + } +} + +fn run_chunk(model: &mut LoadedModel, chunk: &[String], results: &mut Vec) { + let res = model.manifest.input_resolution as usize; + let channels = model.manifest.channels as usize; + let plane = channels * res * res; + + // Preprocess each path; keep only the ones that decoded so the batch tensor + // stays dense. Failed paths get an immediate error result. + let mut packed: Vec = Vec::with_capacity(chunk.len() * plane); + let mut valid_paths: Vec<&String> = Vec::with_capacity(chunk.len()); + let mut valid_indices: Vec = Vec::with_capacity(chunk.len()); + let mut ordered_results: Vec> = vec![None; chunk.len()]; + + for (index, path) in chunk.iter().enumerate() { + match preprocess_image(Path::new(path), &model.manifest) { + Ok(buffer) => { + packed.extend_from_slice(&buffer); + valid_paths.push(path); + valid_indices.push(index); + } + Err(error) => { + ordered_results[index] = Some(ProHeadScores { + image_path: path.clone(), + error: Some(error), + ..Default::default() + }); + } + } + } + + if !valid_paths.is_empty() { + match infer_packed(model, &packed, valid_paths.len(), channels, res) { + Ok(chunk_results) => { + // Success rows are positional; attach the path for each row. + for ((path, index), mut score) in valid_paths + .iter() + .zip(valid_indices.iter()) + .zip(chunk_results.into_iter()) + { + score.image_path = (*path).clone(); + ordered_results[*index] = Some(score); + } + } + Err(error) => { + // A run-level failure marks every valid image in this chunk; the + // rest of the batch still proceeds. + for (path, index) in valid_paths.iter().zip(valid_indices.iter()) { + ordered_results[*index] = Some(ProHeadScores { + image_path: (*path).clone(), + error: Some(error.clone()), + ..Default::default() + }); + } + } + } + } + + results.extend(ordered_results.into_iter().enumerate().map(|(index, row)| { + row.unwrap_or_else(|| ProHeadScores { + image_path: chunk[index].clone(), + error: Some("internal error: missing inference slot".to_string()), + ..Default::default() + }) + })); +} + +fn infer_packed( + model: &mut LoadedModel, + packed: &[f32], + count: usize, + channels: usize, + res: usize, +) -> Result, String> { + let input = Tensor::from_array(([count, channels, res, res], packed.to_vec())) + .map_err(|error| format!("batch tensor build failed: {error}"))?; + let input_name = model.manifest.input_name.clone(); + + let outputs = model + .session + .run(ort::inputs![input_name.as_str() => input]) + .map_err(|error| format!("inference run failed: {error}"))?; + + let manifest = &model.manifest; + let mut scores: Vec = (0..count).map(|_| ProHeadScores::default()).collect(); + let has_false_face_risk_head = manifest + .heads + .iter() + .any(|head| matches!(head.name.as_str(), "false_face_risk" | "falseFaceRisk")); + + for head in &manifest.heads { + let value = match outputs.get(head.output.as_str()) { + Some(value) => value, + None => continue, + }; + let (shape, data) = value + .try_extract_tensor::() + .map_err(|error| format!("head '{}' extract failed: {error}", head.name))?; + apply_head(head, shape.as_ref(), data, count, &mut scores); + } + + if !has_false_face_risk_head { + for score in scores.iter_mut() { + if score.false_face_risk.is_none() { + if let Some(face_validity) = score.face_validity_score { + score.false_face_risk = Some((1.0 - face_validity).clamp(0.0, 1.0)); + } + } + } + } + + Ok(scores) +} + +fn apply_head( + head: &super::types::ManifestHead, + shape: &[i64], + data: &[f32], + count: usize, + scores: &mut [ProHeadScores], +) { + let per_row = if shape.len() >= 2 { + shape[1..].iter().product::().max(1) as usize + } else { + 1 + }; + + for (row, score) in scores.iter_mut().enumerate().take(count) { + let start = row * per_row; + if start >= data.len() { + break; + } + let slice = &data[start..(start + per_row).min(data.len())]; + match head.kind.as_str() { + "scalar01" => { + let value = slice.first().copied().unwrap_or(0.0).clamp(0.0, 1.0); + assign_scalar(&head.name, value, score); + } + "classifier" => { + let (idx, confidence) = argmax_softmax(slice); + let label = head + .labels + .get(idx) + .cloned() + .unwrap_or_else(|| format!("class_{idx}")); + score.scene_label = Some(label); + score.scene_confidence = Some(confidence); + } + _ => {} + } + } +} + +fn assign_scalar(head_name: &str, value: f32, score: &mut ProHeadScores) { + match head_name { + "aesthetic" => score.aesthetic = Some(value), + "persona" => score.persona_score = Some(value), + "semantic_keep" | "semanticKeepScore" => score.semantic_keep_score = Some(value), + "face_validity" | "faceValidityScore" => score.face_validity_score = Some(value), + "composition" | "compositionScore" => score.composition_score = Some(value), + "moment" | "momentScore" => score.moment_score = Some(value), + "lighting" | "lighting_mood" | "lightingMoodScore" => { + score.lighting_mood_score = Some(value) + } + "false_face_risk" | "falseFaceRisk" => score.false_face_risk = Some(value), + _ => {} + } +} + +fn argmax_softmax(logits: &[f32]) -> (usize, f32) { + if logits.is_empty() { + return (0, 0.0); + } + let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max); + let exps: Vec = logits.iter().map(|v| (v - max).exp()).collect(); + let sum: f32 = exps.iter().sum(); + let mut best_idx = 0; + let mut best_val = f32::NEG_INFINITY; + for (idx, value) in logits.iter().enumerate() { + if *value > best_val { + best_val = *value; + best_idx = idx; + } + } + let confidence = if sum > 0.0 { exps[best_idx] / sum } else { 0.0 }; + (best_idx, confidence) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pro_infer::types::ManifestHead; + + #[test] + fn false_face_head_is_not_forced_to_inverse_face_validity() { + let mut scores = vec![ProHeadScores::default()]; + let face_head = ManifestHead { + name: "face_validity".to_string(), + output: "face_validity".to_string(), + kind: "scalar01".to_string(), + labels: vec![], + }; + let risk_head = ManifestHead { + name: "false_face_risk".to_string(), + output: "false_face_risk".to_string(), + kind: "scalar01".to_string(), + labels: vec![], + }; + + apply_head(&face_head, &[1, 1], &[0.8], 1, &mut scores); + apply_head(&risk_head, &[1, 1], &[0.6], 1, &mut scores); + + assert_eq!(scores[0].face_validity_score, Some(0.8)); + assert_eq!(scores[0].false_face_risk, Some(0.6)); + } +} diff --git a/src-tauri/src/pro_infer/mod.rs b/src-tauri/src/pro_infer/mod.rs new file mode 100644 index 0000000..83221b8 --- /dev/null +++ b/src-tauri/src/pro_infer/mod.rs @@ -0,0 +1,66 @@ +//! Pro native ONNX Runtime inference layer (PRO_MODEL_ARCHITECTURE.md §10). +//! +//! Entire module is gated behind `#[cfg(feature = "pro")]` (applied at the +//! `mod pro_infer` declaration in `lib.rs`), so the Flash build never compiles +//! `ort`/`ndarray` or any of this code. The layer only owns the new distilled +//! multi-head model; YuNet / MediaPipe / SFace / rule engine stay in the wasm +//! worker untouched. + +pub mod ep; +pub mod infer; +pub mod preprocess; +#[cfg(windows)] +pub mod runtime; +pub mod session; +pub mod types; + +#[cfg(test)] +mod tests; + +use tauri::{AppHandle, Manager, State}; + +pub use session::ProInferState; +use types::{ProBatchRequest, ProBatchResponse, ProInferCapabilities}; + +/// Probe execution providers, load backbone + heads from the manifest, warm up, +/// and report the capabilities actually in effect. Initialization failures never +/// panic the host process; they return `Err` and the caller can retry. +#[tauri::command] +pub async fn pro_infer_init( + app: AppHandle, + state: State<'_, ProInferState>, + manifest_path: String, +) -> Result { + let resource_dir = app.path().resource_dir().ok(); + let result = tauri::async_runtime::spawn_blocking(move || { + session::init_model(&manifest_path, resource_dir) + }) + .await + .map_err(|error| format!("pro_infer_init task join failed: {error}"))?; + + let (loaded, capabilities) = result?; + let mut guard = state + .model + .lock() + .map_err(|_| "pro infer state poisoned".to_string())?; + *guard = Some(loaded); + Ok(capabilities) +} + +/// Run batch inference over image paths. Rust owns decode + resize 384 + +/// normalize + batch packing; per-image failures isolate into that image's +/// `error` field instead of failing the whole batch. +#[tauri::command] +pub async fn pro_infer_batch( + state: State<'_, ProInferState>, + req: ProBatchRequest, +) -> Result { + let mut guard = state + .model + .lock() + .map_err(|_| "pro infer state poisoned".to_string())?; + let model = guard + .as_mut() + .ok_or_else(|| "pro_infer not initialized; call pro_infer_init first".to_string())?; + Ok(infer::run_batch(model, &req)) +} diff --git a/src-tauri/src/pro_infer/preprocess.rs b/src-tauri/src/pro_infer/preprocess.rs new file mode 100644 index 0000000..7bf7af7 --- /dev/null +++ b/src-tauri/src/pro_infer/preprocess.rs @@ -0,0 +1,43 @@ +//! Image preprocessing for the Pro native inference layer (§10.7). +//! +//! The frontend hands over file paths only; Rust owns decode + resize to the +//! manifest resolution + normalization + NCHW tensor packing. No pixels cross +//! the Tauri boundary. + +use std::path::Path; + +use image::imageops::FilterType; + +use super::types::ProModelManifest; + +/// Decode an image at `path` and produce a normalized `[3, R, R]` CHW buffer in +/// row-major order (channel-major), ready to be stacked into a batch tensor. +pub fn preprocess_image(path: &Path, manifest: &ProModelManifest) -> Result, String> { + let res = manifest.input_resolution as u32; + let channels = manifest.channels as usize; + if channels != 3 { + return Err(format!( + "unsupported channel count {channels}; placeholder pipeline expects 3" + )); + } + + let decoded = image::open(path).map_err(|error| format!("decode failed: {error}"))?; + let resized = decoded.resize_exact(res, res, FilterType::Triangle); + let rgb = resized.to_rgb8(); + + let mean = manifest.normalize.mean; + let std = manifest.normalize.std; + let pixel_count = (res * res) as usize; + let mut chw = vec![0f32; channels * pixel_count]; + + // Pack channel-major: [R-plane, G-plane, B-plane], normalized per channel. + for (idx, pixel) in rgb.pixels().enumerate() { + for c in 0..3 { + let value = pixel[c] as f32 / 255.0; + let normalized = (value - mean[c]) / std[c]; + chw[c * pixel_count + idx] = normalized; + } + } + + Ok(chw) +} diff --git a/src-tauri/src/pro_infer/runtime.rs b/src-tauri/src/pro_infer/runtime.rs new file mode 100644 index 0000000..4a1e8b2 --- /dev/null +++ b/src-tauri/src/pro_infer/runtime.rs @@ -0,0 +1,148 @@ +//! Windows-only CUDA runtime directory discovery + DLL search path setup. +//! +//! The Pro app bundles a locked user-mode CUDA runtime subset as resources. +//! Before the CUDA EP is probed, we attach that directory to the current +//! process DLL search path so ONNX Runtime can load `onnxruntime_providers_cuda` +//! without requiring a global CUDA Toolkit install. + +use std::env; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; + +use windows_sys::Win32::System::LibraryLoader::{ + AddDllDirectory, SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_APPLICATION_DIR, + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_LIBRARY_SEARCH_USER_DIRS, +}; + +const CUDA_RUNTIME_ENV_VAR: &str = "FRAMECULL_CUDA_RUNTIME_DIR"; +const CUDA_RUNTIME_RESOURCE_DIR: &str = "cuda-runtime/windows-x64/runtime"; +const CUDA_RUNTIME_DEV_VENDOR_DIR: &str = "vendor/nvidia-cuda/windows-x64/runtime"; +const CUDA_RUNTIME_MARKER_DLL: &str = "cublasLt64_12.dll"; + +static REGISTERED_RUNTIME_DIR: OnceLock = OnceLock::new(); + +pub fn prepare_cuda_runtime(resource_dir: Option<&Path>) -> Result, String> { + let Some(runtime_dir) = locate_cuda_runtime_dir(resource_dir) else { + return Ok(None); + }; + + if let Some(existing) = REGISTERED_RUNTIME_DIR.get() { + if paths_equal(existing, &runtime_dir) { + return Ok(Some(existing.clone())); + } + } + + prepend_runtime_dir_to_path(&runtime_dir); + attach_runtime_dir_to_loader(&runtime_dir)?; + let _ = REGISTERED_RUNTIME_DIR.set(runtime_dir.clone()); + Ok(Some(runtime_dir)) +} + +fn locate_cuda_runtime_dir(resource_dir: Option<&Path>) -> Option { + let mut candidates: Vec = Vec::new(); + + if let Some(path) = env::var_os(CUDA_RUNTIME_ENV_VAR) { + if !path.is_empty() { + candidates.push(PathBuf::from(path)); + } + } + + if let Some(resource_dir) = resource_dir { + candidates.push(resource_dir.join(CUDA_RUNTIME_RESOURCE_DIR)); + } + + if let Ok(exe) = env::current_exe() { + if let Some(exe_dir) = exe.parent() { + candidates.push(exe_dir.join(CUDA_RUNTIME_RESOURCE_DIR)); + if let Some(parent) = exe_dir.parent() { + candidates.push(parent.join("resources").join(CUDA_RUNTIME_RESOURCE_DIR)); + } + } + } + + candidates.push(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(CUDA_RUNTIME_DEV_VENDOR_DIR)); + + if let Ok(cwd) = env::current_dir() { + candidates.push(cwd.join(CUDA_RUNTIME_DEV_VENDOR_DIR)); + candidates.push(cwd.join("src-tauri").join(CUDA_RUNTIME_DEV_VENDOR_DIR)); + } + + candidates + .into_iter() + .find(|candidate| is_valid_runtime_dir(candidate)) +} + +fn is_valid_runtime_dir(path: &Path) -> bool { + path.is_dir() && path.join(CUDA_RUNTIME_MARKER_DLL).is_file() +} + +fn prepend_runtime_dir_to_path(runtime_dir: &Path) { + let Some(dir_text) = runtime_dir.to_str() else { + return; + }; + let current = env::var_os("PATH").unwrap_or_default(); + let already_present = env::split_paths(¤t).any(|entry| paths_equal(&entry, runtime_dir)); + if already_present { + return; + } + + let mut parts = vec![runtime_dir.to_path_buf()]; + parts.extend(env::split_paths(¤t)); + if let Ok(joined) = env::join_paths(parts) { + // SAFETY: updating PATH only affects the current process and child + // processes. This is a best-effort supplement to AddDllDirectory. + unsafe { + env::set_var("PATH", joined); + } + } else { + // Fallback for unexpected invalid PATH segments. + let combined = if let Some(existing) = current.to_str() { + format!("{dir_text};{existing}") + } else { + dir_text.to_string() + }; + unsafe { + env::set_var("PATH", combined); + } + } +} + +fn attach_runtime_dir_to_loader(runtime_dir: &Path) -> Result<(), String> { + let mut wide: Vec = OsStr::new(runtime_dir.as_os_str()).encode_wide().collect(); + wide.push(0); + + let flags = LOAD_LIBRARY_SEARCH_DEFAULT_DIRS + | LOAD_LIBRARY_SEARCH_USER_DIRS + | LOAD_LIBRARY_SEARCH_SYSTEM32 + | LOAD_LIBRARY_SEARCH_APPLICATION_DIR; + + unsafe { + if SetDefaultDllDirectories(flags) == 0 { + return Err(format!( + "SetDefaultDllDirectories failed for {:?}: {}", + runtime_dir, + std::io::Error::last_os_error() + )); + } + if AddDllDirectory(wide.as_ptr()) == 0 as _ { + return Err(format!( + "AddDllDirectory failed for {:?}: {}", + runtime_dir, + std::io::Error::last_os_error() + )); + } + } + + Ok(()) +} + +fn paths_equal(a: &Path, b: &Path) -> bool { + match (a.canonicalize(), b.canonicalize()) { + (Ok(a), Ok(b)) => a == b, + _ => a + .to_string_lossy() + .eq_ignore_ascii_case(b.to_string_lossy().as_ref()), + } +} diff --git a/src-tauri/src/pro_infer/session.rs b/src-tauri/src/pro_infer/session.rs new file mode 100644 index 0000000..3d2a508 --- /dev/null +++ b/src-tauri/src/pro_infer/session.rs @@ -0,0 +1,176 @@ +//! Model loading, manifest parsing, EP registration and warmup (§10.5/§10.6). + +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::Instant; + +use ort::session::{builder::GraphOptimizationLevel, Session}; +use ort::value::Tensor; + +use super::ep::candidate_chain; +#[cfg(windows)] +use super::runtime::prepare_cuda_runtime; +use super::types::{ProInferCapabilities, ProModelManifest}; + +/// Live inference session plus the metadata the batch path needs. +pub struct LoadedModel { + pub session: Session, + pub manifest: ProModelManifest, + pub active_ep: String, + pub ep_fallback_chain: Vec, +} + +/// Tauri-managed state. Holds an optional initialized model behind a mutex so +/// `pro_infer_init` can (re)load and `pro_infer_batch` can borrow it mutably for +/// `Session::run`. +#[derive(Default)] +pub struct ProInferState { + pub model: Mutex>, +} + +fn parse_manifest(manifest_path: &Path) -> Result<(ProModelManifest, PathBuf), String> { + let raw = fs::read_to_string(manifest_path) + .map_err(|error| format!("failed to read manifest {manifest_path:?}: {error}"))?; + let manifest: ProModelManifest = + serde_json::from_str(&raw).map_err(|error| format!("invalid manifest json: {error}"))?; + if manifest.input_resolution != 384 { + return Err(format!( + "manifest inputResolution must be 384, got {}", + manifest.input_resolution + )); + } + let model_dir = manifest_path + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from(".")); + let model_path = model_dir.join(&manifest.model); + if !model_path.exists() { + return Err(format!("model file not found: {model_path:?}")); + } + Ok((manifest, model_path)) +} + +/// Initialize the model: probe EPs by the platform fallback chain, register the +/// first that commits, then warm up. Never panics; all failures return `Err` and +/// CPU is the infallible final fallback. +pub fn init_model( + manifest_path: &str, + resource_dir: Option, +) -> Result<(LoadedModel, ProInferCapabilities), String> { + let (manifest, model_path) = parse_manifest(Path::new(manifest_path))?; + let model_bytes = + fs::read(&model_path).map_err(|error| format!("failed to read model: {error}"))?; + + let mut fallback_chain: Vec = Vec::new(); + + #[cfg(windows)] + match prepare_cuda_runtime(resource_dir.as_deref()) { + Ok(Some(runtime_dir)) => { + fallback_chain.push(format!("cuda-runtime: using {}", runtime_dir.display())) + } + Ok(None) => fallback_chain.push( + "cuda-runtime: bundled runtime not found; relying on system DLL path".to_string(), + ), + Err(error) => fallback_chain.push(format!("cuda-runtime: prep failed: {error}")), + } + + let candidates = candidate_chain(); + + for candidate in candidates { + if !candidate.available { + fallback_chain.push(format!( + "{}: skipped ({})", + candidate.name, candidate.probe_note + )); + continue; + } + + let build_result = build_session(&model_bytes, &candidate.dispatch); + + match build_result { + Ok(session) => { + let mut loaded = LoadedModel { + session, + manifest: manifest.clone(), + active_ep: candidate.name.to_string(), + ep_fallback_chain: Vec::new(), + }; + match warmup(&mut loaded) { + Ok(warmup_ms) => { + fallback_chain.push(format!("{}: active", candidate.name)); + loaded.ep_fallback_chain = fallback_chain.clone(); + let capabilities = capabilities_of(&loaded, warmup_ms); + return Ok((loaded, capabilities)); + } + Err(error) => { + fallback_chain.push(format!("{}: warmup failed: {error}", candidate.name)); + } + } + } + Err(error) => { + fallback_chain.push(format!( + "{}: register/commit failed: {error}", + candidate.name + )); + } + } + } + + Err(format!( + "no execution provider could be initialized; chain: {}", + fallback_chain.join(" | ") + )) +} + +/// Build a session for one EP candidate. Kept separate so the differing +/// `BuilderResult` error type does not have to chain through `and_then`. +fn build_session( + model_bytes: &[u8], + dispatch: &ort::ep::ExecutionProviderDispatch, +) -> Result { + let builder = Session::builder().map_err(|error| format!("builder init failed: {error}"))?; + let builder = builder + .with_optimization_level(GraphOptimizationLevel::Level3) + .map_err(|error| format!("optimization level failed: {error}"))?; + let builder = builder + .with_execution_providers([dispatch.clone().error_on_failure()]) + .map_err(|error| format!("ep registration failed: {error}"))?; + builder + .commit_from_memory(model_bytes) + .map_err(|error| format!("commit failed: {error}")) +} + +/// Run a single all-zero batch to force EP graph compilation before real work. +fn warmup(loaded: &mut LoadedModel) -> Result { + let res = loaded.manifest.input_resolution as usize; + let channels = loaded.manifest.channels as usize; + let len = channels * res * res; + let data = vec![0f32; len]; + let input = Tensor::from_array(([1usize, channels, res, res], data)) + .map_err(|error| format!("warmup tensor build failed: {error}"))?; + let input_name = loaded.manifest.input_name.clone(); + + let started = Instant::now(); + loaded + .session + .run(ort::inputs![input_name.as_str() => input]) + .map_err(|error| format!("warmup run failed: {error}"))?; + Ok(started.elapsed().as_secs_f64() * 1000.0) +} + +fn capabilities_of(loaded: &LoadedModel, warmup_ms: f64) -> ProInferCapabilities { + ProInferCapabilities { + active_ep: loaded.active_ep.clone(), + ep_fallback_chain: loaded.ep_fallback_chain.clone(), + backbone_version: loaded.manifest.backbone_version.clone(), + loaded_heads: loaded + .manifest + .heads + .iter() + .map(|h| h.name.clone()) + .collect(), + input_resolution: loaded.manifest.input_resolution, + warmup_ms, + } +} diff --git a/src-tauri/src/pro_infer/tests.rs b/src-tauri/src/pro_infer/tests.rs new file mode 100644 index 0000000..8d6cc77 --- /dev/null +++ b/src-tauri/src/pro_infer/tests.rs @@ -0,0 +1,167 @@ +//! End-to-end tests for the Pro native inference layer against the bundled +//! placeholder model. These cover acceptance items §10.9 #5 (per-image +//! aesthetic + single-image error isolation) and keep the batch path exercised. +//! Full #6 throughput evidence is produced by the release bench because debug +//! builds and GPU fallback states can be noisy. They require the `ort` runtime, +//! so they are gated behind `feature = "pro"` and only run for the pro build. + +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::time::Instant; + +use image::{ImageBuffer, Rgb}; + +use super::session::init_model; +use super::types::ProBatchRequest; +use super::{infer, session::LoadedModel}; + +fn manifest_path() -> PathBuf { + if let Ok(path) = std::env::var("FRAMECULL_PRO_TEST_MANIFEST") { + return PathBuf::from(path); + } + // CARGO_MANIFEST_DIR is `src-tauri`; the placeholder lives under pro-models. + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("pro-models/placeholder/manifest.json") +} + +fn write_test_jpeg(dir: &std::path::Path, name: &str, fill: u8) -> PathBuf { + let path = dir.join(name); + let img: ImageBuffer, Vec> = + ImageBuffer::from_fn(64, 64, |x, _y| Rgb([fill, (x as u8).wrapping_mul(3), 128])); + img.save_with_format(&path, image::ImageFormat::Jpeg) + .expect("write test jpeg"); + path +} + +fn write_corrupt_jpeg(dir: &std::path::Path, name: &str) -> PathBuf { + let path = dir.join(name); + let mut f = fs::File::create(&path).expect("create corrupt file"); + f.write_all(b"this is not a valid jpeg payload").unwrap(); + path +} + +fn load() -> LoadedModel { + let (loaded, caps) = init_model(manifest_path().to_str().unwrap(), None) + .expect("placeholder model should initialize (CPU fallback never fails)"); + assert_eq!( + caps.input_resolution, 384, + "manifest resolution must be 384" + ); + assert!( + !caps.ep_fallback_chain.is_empty(), + "fallback chain recorded" + ); + assert!( + caps.loaded_heads.iter().any(|h| h == "aesthetic"), + "aesthetic head must load" + ); + loaded +} + +#[test] +fn batch_returns_per_image_aesthetic_and_isolates_corrupt() { + let dir = std::env::temp_dir().join(format!("framecull_pro_infer_{}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + + let good_a = write_test_jpeg(&dir, "good_a.jpg", 40); + let bad = write_corrupt_jpeg(&dir, "broken.jpg"); + let good_b = write_test_jpeg(&dir, "good_b.jpg", 200); + + let mut model = load(); + let req = ProBatchRequest { + image_paths: vec![ + good_a.to_string_lossy().into_owned(), + bad.to_string_lossy().into_owned(), + good_b.to_string_lossy().into_owned(), + ], + batch_size: Some(4), + heads: None, + }; + let resp = infer::run_batch(&mut model, &req); + + assert_eq!(resp.results.len(), 3, "every input gets a result row"); + + let find = |needle: &str| { + resp.results + .iter() + .find(|r| r.image_path.contains(needle)) + .unwrap_or_else(|| panic!("missing result for {needle}")) + }; + + // §10.9 #5: good images carry an aesthetic in [0,1] and no error. + for needle in ["good_a", "good_b"] { + let row = find(needle); + let aesthetic = row + .aesthetic + .unwrap_or_else(|| panic!("{needle} aesthetic missing")); + assert!((0.0..=1.0).contains(&aesthetic), "aesthetic in range"); + assert!(row.error.is_none(), "good image has no error"); + } + + // §10.9 #5: the single corrupt image is isolated to its own error field and + // does not abort the rest of the batch. + let broken = find("broken"); + assert!(broken.error.is_some(), "corrupt image reports an error"); + assert!(broken.aesthetic.is_none(), "corrupt image has no score"); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn batch_throughput_beats_single_image_loop() { + let dir = std::env::temp_dir().join(format!("framecull_pro_infer_perf_{}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + + let mut paths: Vec = Vec::new(); + for i in 0..16 { + let p = write_test_jpeg(&dir, &format!("perf_{i}.jpg"), (i * 12) as u8); + paths.push(p.to_string_lossy().into_owned()); + } + + let mut model = load(); + + // Single-image loop: batch_size 1 forces one run per image. + let loop_started = Instant::now(); + let loop_req = ProBatchRequest { + image_paths: paths.clone(), + batch_size: Some(1), + heads: None, + }; + let loop_resp = infer::run_batch(&mut model, &loop_req); + let loop_ms = loop_started.elapsed().as_secs_f64() * 1000.0; + + // Batched path: a single run over the whole set. + let batch_started = Instant::now(); + let batch_req = ProBatchRequest { + image_paths: paths.clone(), + batch_size: Some(16), + heads: None, + }; + let batch_resp = infer::run_batch(&mut model, &batch_req); + let batch_ms = batch_started.elapsed().as_secs_f64() * 1000.0; + + assert_eq!(loop_resp.results.len(), paths.len()); + assert_eq!(batch_resp.results.len(), paths.len()); + + if std::env::var("FRAMECULL_PRO_TEST_MANIFEST").is_ok() { + eprintln!( + "external manifest benchmark: ep={} batch={batch_ms:.1}ms loop={loop_ms:.1}ms; detailed throughput is covered by bench-pro-persona.mjs", + model.active_ep + ); + let _ = fs::remove_dir_all(&dir); + return; + } + + // §10.9 #6: the batched path should be no slower than the per-image loop. + // Decode dominates here, but batched inference still avoids per-image run + // overhead; allow a small slack for timer noise on tiny placeholder graphs. + // The release bench is the authoritative throughput check. In debug unit + // tests, decode overhead and DirectML/CUDA fallback probing can dominate a + // tiny placeholder graph, so only guard against a severe batch regression. + assert!( + batch_ms <= loop_ms * 1.35, + "batch ({batch_ms:.1}ms) regressed too far beyond single-image loop ({loop_ms:.1}ms)" + ); + + let _ = fs::remove_dir_all(&dir); +} diff --git a/src-tauri/src/pro_infer/types.rs b/src-tauri/src/pro_infer/types.rs new file mode 100644 index 0000000..9dda2cb --- /dev/null +++ b/src-tauri/src/pro_infer/types.rs @@ -0,0 +1,113 @@ +//! Shared serde structures for the Pro native inference layer. +//! +//! Field names map one-to-one to the TypeScript interfaces in `src/types.ts` +//! (see PRO_MODEL_ARCHITECTURE.md §10.5). Do not rename fields without updating +//! the frontend contract in lockstep. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProInferCapabilities { + pub active_ep: String, + pub ep_fallback_chain: Vec, + pub backbone_version: String, + pub loaded_heads: Vec, + pub input_resolution: u32, + pub warmup_ms: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProBatchRequest { + pub image_paths: Vec, + #[serde(default)] + pub batch_size: Option, + #[serde(default)] + pub heads: Option>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProHeadScores { + pub image_path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub aesthetic: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scene_label: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub scene_confidence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub persona_score: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic_keep_score: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub face_validity_score: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub composition_score: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub moment_score: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lighting_mood_score: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub false_face_risk: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProBatchResponse { + pub results: Vec, + pub ep: String, + pub elapsed_ms: f64, +} + +/// Head descriptor parsed from `manifest.json`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ManifestHead { + pub name: String, + pub output: String, + pub kind: String, + #[serde(default)] + pub labels: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ManifestNormalize { + pub mean: [f32; 3], + pub std: [f32; 3], +} + +/// Parsed `manifest.json`. Swapping in a real model only edits this file plus +/// the referenced model blob, never the Rust code. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProModelManifest { + #[serde(default = "default_schema_version")] + pub schema_version: u32, + pub backbone_version: String, + pub model: String, + #[serde(default)] + pub sha256: Option, + #[serde(default = "default_input_name")] + pub input_name: String, + pub input_resolution: u32, + #[serde(default = "default_channels")] + pub channels: u32, + pub normalize: ManifestNormalize, + pub heads: Vec, +} + +fn default_schema_version() -> u32 { + 1 +} + +fn default_input_name() -> String { + "pixel_values".to_string() +} + +fn default_channels() -> u32 { + 3 +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..b9736e1 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "FrameCull AI Flash", + "version": "0.1.6", + "identifier": "com.framecull.ai.flash", + "build": { + "beforeDevCommand": "pnpm run dev", + "devUrl": "http://127.0.0.1:3000", + "beforeBuildCommand": "pnpm run build:release:flash", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "FrameCull AI Flash", + "width": 1440, + "height": 900, + "minWidth": 900, + "minHeight": 600, + "decorations": false, + "transparent": false, + "visible": false, + "center": true, + "backgroundColor": "#101115" + } + ], + "security": { + "csp": null, + "assetProtocol": { + "enable": true, + "scope": ["**"] + } + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src-tauri/tauri.flash.conf.json b/src-tauri/tauri.flash.conf.json new file mode 100644 index 0000000..a1ae297 --- /dev/null +++ b/src-tauri/tauri.flash.conf.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "FrameCull AI Flash", + "identifier": "com.framecull.ai.flash", + "build": { + "beforeBuildCommand": "pnpm run build:release:flash" + } +} diff --git a/src-tauri/tauri.pro.conf.json b/src-tauri/tauri.pro.conf.json new file mode 100644 index 0000000..8131617 --- /dev/null +++ b/src-tauri/tauri.pro.conf.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "FrameCull AI Pro", + "identifier": "com.framecull.ai.pro", + "app": { + "windows": [ + { + "label": "main", + "title": "FrameCull AI Pro" + } + ] + }, + "build": { + "beforeBuildCommand": "pnpm run build:release:pro" + }, + "bundle": { + "resources": { + "vendor/rawtherapee/windows-x64/RawTherapee_5.12_win64_release": "raw-engines/rawtherapee/windows-x64/RawTherapee_5.12_win64_release", + "vendor/rawtherapee/THIRD_PARTY_NOTICES.txt": "raw-engines/rawtherapee/THIRD_PARTY_NOTICES.txt", + "vendor/rawtherapee/rawtherapee-5.12-win64.json": "raw-engines/rawtherapee/rawtherapee-5.12-win64.json", + "pro-models/placeholder/model.onnx": "pro-models/placeholder/model.onnx", + "pro-models/placeholder/manifest.json": "pro-models/placeholder/manifest.json", + "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx": "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx", + "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json": "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json" + } + } +} diff --git a/src-tauri/tauri.pro.macos.conf.json b/src-tauri/tauri.pro.macos.conf.json new file mode 100644 index 0000000..2944159 --- /dev/null +++ b/src-tauri/tauri.pro.macos.conf.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "FrameCull AI Pro", + "identifier": "com.framecull.ai.pro", + "app": { + "windows": [ + { + "label": "main", + "title": "FrameCull AI Pro" + } + ] + }, + "build": { + "beforeBuildCommand": "pnpm run build:release:pro:macos" + }, + "bundle": { + "resources": { + "pro-models/placeholder/model.onnx": "pro-models/placeholder/model.onnx", + "pro-models/placeholder/manifest.json": "pro-models/placeholder/manifest.json", + "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx": "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx", + "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json": "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json" + } + } +} diff --git a/src-tauri/tauri.pro.macos.x64.conf.json b/src-tauri/tauri.pro.macos.x64.conf.json new file mode 100644 index 0000000..14dae2a --- /dev/null +++ b/src-tauri/tauri.pro.macos.x64.conf.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "FrameCull AI Pro", + "identifier": "com.framecull.ai.pro", + "app": { + "windows": [ + { + "label": "main", + "title": "FrameCull AI Pro" + } + ] + }, + "build": { + "beforeBuildCommand": "pnpm run build:release:pro:macos" + }, + "bundle": { + "resources": { + "pro-models/placeholder/model.onnx": "pro-models/placeholder/model.onnx", + "pro-models/placeholder/manifest.json": "pro-models/placeholder/manifest.json", + "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx": "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/model.int8.onnx", + "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json": "pro-models/semantic_student_v2_grounded_convnext_v14_five_mountain_region/manifest.int8.json", + "vendor/onnxruntime/onnxruntime-1.23.2-macos-x64.json": "onnxruntime/macos-x64/runtime-manifest.json", + "vendor/onnxruntime/macos-x64/LICENSE": "onnxruntime/macos-x64/LICENSE", + "vendor/onnxruntime/macos-x64/ThirdPartyNotices.txt": "onnxruntime/macos-x64/ThirdPartyNotices.txt", + "vendor/onnxruntime/macos-x64/VERSION_NUMBER": "onnxruntime/macos-x64/VERSION_NUMBER" + }, + "macOS": { + "frameworks": ["vendor/onnxruntime/macos-x64/libonnxruntime.1.23.2.dylib"] + } + } +} diff --git a/src-tauri/vendor/onnxruntime/onnxruntime-1.23.2-macos-x64.json b/src-tauri/vendor/onnxruntime/onnxruntime-1.23.2-macos-x64.json new file mode 100644 index 0000000..a6e4c9f --- /dev/null +++ b/src-tauri/vendor/onnxruntime/onnxruntime-1.23.2-macos-x64.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "name": "Microsoft ONNX Runtime", + "version": "1.23.2", + "platform": "macos-x64", + "artifact": "onnxruntime-osx-x86_64-1.23.2.tgz", + "sourceUrl": "https://github.com/microsoft/onnxruntime/releases/download/v1.23.2/onnxruntime-osx-x86_64-1.23.2.tgz", + "releaseUrl": "https://github.com/microsoft/onnxruntime/releases/tag/v1.23.2", + "sha256": "d10359e16347b57d9959f7e80a225a5b4a66ed7d7e007274a15cae86836485a6", + "dylib": "libonnxruntime.1.23.2.dylib" +} diff --git a/src-tauri/vendor/rawtherapee/THIRD_PARTY_NOTICES.txt b/src-tauri/vendor/rawtherapee/THIRD_PARTY_NOTICES.txt new file mode 100644 index 0000000..6d7c384 --- /dev/null +++ b/src-tauri/vendor/rawtherapee/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,21 @@ +FrameCull AI Pro third-party RAW engine notice + +RawTherapee +- Version: 5.12 Windows 64-bit release +- Upstream: https://github.com/RawTherapee/RawTherapee +- Release: https://github.com/RawTherapee/RawTherapee/releases/tag/5.12 +- Bundled artifact: RawTherapee_5.12_win64_release.zip +- SHA-256: a6de1797da462975435846db7b79a981557350af1bdac07525bf6884ede805dd +- License: GNU General Public License version 3 + +FrameCull AI Pro uses the bundled RawTherapee command-line runtime only to +generate local RAW monitor preview caches. The cache is for viewing while +culling and does not modify originals, XMP sidecars, Lightroom catalogs, AI +analysis inputs, or exported originals. + +RawTherapee source code is available from the upstream project: +https://github.com/RawTherapee/RawTherapee + +The bundled RawTherapee runtime includes its own LICENSE, AUTHORS, +AboutThisBuild.txt, RELEASE_NOTES.txt, and dependency license files in the +RawTherapee directory. diff --git a/src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json b/src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json new file mode 100644 index 0000000..87559a3 --- /dev/null +++ b/src-tauri/vendor/rawtherapee/rawtherapee-5.12-macos-universal.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "name": "RawTherapee", + "version": "5.12", + "platform": "macos-universal", + "artifact": "RawTherapee_macOS_15.4_Universal_5.12.zip", + "sourceUrl": "https://github.com/RawTherapee/RawTherapee/releases/download/5.12/RawTherapee_macOS_15.4_Universal_5.12.zip", + "checksumUrl": "https://github.com/RawTherapee/RawTherapee/releases/download/5.12/RawTherapee_macOS_15.4_Universal_5.12.zip.sha256", + "releaseUrl": "https://github.com/RawTherapee/RawTherapee/releases/tag/5.12", + "sha256": "2f284d1c023f53f0c492aecc3f7635d6b7807ef22d5413ee55715d81e81fe688" +} diff --git a/src-tauri/vendor/rawtherapee/rawtherapee-5.12-win64.json b/src-tauri/vendor/rawtherapee/rawtherapee-5.12-win64.json new file mode 100644 index 0000000..ffefd0e --- /dev/null +++ b/src-tauri/vendor/rawtherapee/rawtherapee-5.12-win64.json @@ -0,0 +1,12 @@ +{ + "name": "RawTherapee", + "version": "5.12", + "platform": "windows-x64", + "artifact": "RawTherapee_5.12_win64_release.zip", + "sourceUrl": "https://github.com/RawTherapee/RawTherapee/releases/download/5.12/RawTherapee_5.12_win64_release.zip", + "releaseUrl": "https://github.com/RawTherapee/RawTherapee/releases/tag/5.12", + "sha256": "a6de1797da462975435846db7b79a981557350af1bdac07525bf6884ede805dd", + "license": "GPL-3.0", + "cliRelativePath": "windows-x64/RawTherapee_5.12_win64_release/rawtherapee-cli.exe", + "resourceRelativePath": "raw-engines/rawtherapee/windows-x64/RawTherapee_5.12_win64_release/rawtherapee-cli.exe" +} diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..dea7172 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,1449 @@ +import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import { Channel, invoke } from '@tauri-apps/api/core'; +import { revealItemInDir } from '@tauri-apps/plugin-opener'; +import { open } from '@tauri-apps/plugin-dialog'; +import { SelectionState, GroupStatus, ExportOptions, PhotoRating, RawDecodeProgress, ImportProgress, ExportProgress, ExportStreamEvent, PhotoGroup, type LightroomSourceFolderResult } from './types'; +import Viewer, { type ViewerAiMode } from './components/Viewer'; +import { Toolbar } from './components/Toolbar'; +import { Filmstrip } from './components/Filmstrip'; +import { EmptyState } from './components/EmptyState'; +import { ExportProgressOverlay } from './components/ExportProgressOverlay'; +import { NotificationCenter, type AppNotification, type NotificationKind } from './components/NotificationCenter'; +import { PeopleSplitWorkspace } from './components/PeopleSplitWorkspace'; +import { DuplicateReviewWorkspace } from './components/DuplicateReviewWorkspace'; +import ConfirmationModal from './components/ConfirmationModal'; +import SettingsPanel from './components/SettingsPanel'; +import AiSettingsPanel from './components/AiSettingsPanel'; +import { getCurrentWindow } from '@tauri-apps/api/window'; +import { getTranslations, Language } from './i18n'; +import { usePlatform } from './hooks/usePlatform'; +import { useTheme } from './hooks/useTheme'; +import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; +import { toRustGroups, usePhotoState } from './hooks/usePhotoState'; +import { useModalState } from './hooks/useModalState'; +import { usePhotoNavigation } from './hooks/usePhotoNavigation'; +import { useAiCulling } from './hooks/useAiCulling'; +import { usePeopleSplit } from './hooks/usePeopleSplit'; +import { renderGroupForExport } from './utils/aiImage'; +import { clearAppCaches, getAppCacheUsage, type AppCacheUsage } from './utils/cacheMaintenance'; +import { hasTauriRuntime } from './utils/tauriRuntime'; +import { cancelRawPreloads, decodeRawFile, preloadRawWindow, subscribeRawDecodeProgress } from './utils/rawLoader'; +import { getDisplayPreviewUrl, loadDisplayImage, preloadDisplayWindow } from './utils/imagePreloader'; +import { readStorage } from './utils/storage'; +import { buildEditionAiPickedPhotoIds } from '@edition/buildAiPickedPhotoIds'; +import { useRawMonitorFeature } from '@edition/useRawMonitorFeature'; + +const LANGUAGE_STORAGE_KEY = 'framecull-language'; +const LIGHTROOM_PATH_STORAGE_KEY = 'framecull-lightroom-classic-path'; + +const SPLASH_MIN_VISIBLE_MS = 2400; +const SPLASH_FIRST_PAINT_DELAY_MS = 180; +type WorkspaceMode = 'CULLING' | 'PEOPLE_SPLIT'; + +const IDLE_EXPORT_PROGRESS: ExportProgress = { + phase: 'idle', + total: 0, + processed: 0, + running: false, +}; + +function getSelectionTargetAfterRemoval( + visiblePhotos: PhotoGroup[], + currentPhotoId: string | undefined, + removedIds: ReadonlySet, +) { + if (!currentPhotoId) { + return visiblePhotos.find(photo => !removedIds.has(photo.id))?.id ?? null; + } + + if (!removedIds.has(currentPhotoId)) { + return currentPhotoId; + } + + const currentIndex = visiblePhotos.findIndex(photo => photo.id === currentPhotoId); + const searchStart = currentIndex === -1 ? 0 : currentIndex + 1; + const nextPhoto = visiblePhotos.slice(searchStart).find(photo => !removedIds.has(photo.id)); + if (nextPhoto) return nextPhoto.id; + + const previousPhoto = visiblePhotos.slice(0, Math.max(0, searchStart - 1)).reverse().find(photo => !removedIds.has(photo.id)); + return previousPhoto?.id ?? null; +} + +async function launchLightroomAfterExport(options: ExportOptions): Promise> { + if (options.exportTarget !== 'LIGHTROOM_CLASSIC' || !options.launchLightroom) { + return { lightroomLaunchStatus: 'NOT_REQUESTED' }; + } + + const savedPath = readStorage(LIGHTROOM_PATH_STORAGE_KEY) || options.lightroomExecutablePath; + try { + const launchedPath = await invoke('launch_lightroom_classic', { + executablePath: savedPath || null, + }); + if (launchedPath) { + localStorage.setItem(LIGHTROOM_PATH_STORAGE_KEY, launchedPath); + return { + lightroomLaunchStatus: 'LAUNCHED', + lightroomExecutablePath: launchedPath, + }; + } + return { + lightroomLaunchStatus: 'NOT_FOUND', + lightroomMessage: 'Lightroom Classic was not found. Open the selected photos folder from Lightroom.', + }; + } catch (error) { + return { + lightroomLaunchStatus: 'ERROR', + lightroomMessage: error instanceof Error ? error.message : String(error), + }; + } +} + +function formatBytesForLog(bytes: number) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; +} + +const App: React.FC = () => { + // Settings + const { theme, themeMode, setThemeMode } = useTheme(); + const [language, setLanguage] = useState(() => { + const saved = readStorage(LANGUAGE_STORAGE_KEY); + return (saved === 'zh' || saved === 'en') ? saved : 'zh'; + }); + const [viewerAiMode, setViewerAiMode] = useState('AI'); + const [rawDecodeProgress, setRawDecodeProgress] = useState({ + total: 0, + processed: 0, + queued: 0, + active: 0, + running: false, + }); + const [isMaximized, setIsMaximized] = useState(false); + const [initialImportActive, setInitialImportActive] = useState(false); + const [initialImportSelectionId, setInitialImportSelectionId] = useState(null); + const [initialImportPreloadProgress, setInitialImportPreloadProgress] = useState(null); + const initialPreloadStartedRef = useRef(false); + const latestPhotosRef = useRef([]); + const [exportProgress, setExportProgress] = useState(IDLE_EXPORT_PROGRESS); + const [notifications, setNotifications] = useState([]); + // Custom hooks for state management + const photoState = usePhotoState(); + const modalState = useModalState(); + const aiCulling = useAiCulling(photoState.photos, photoState.updatePhotoAiAnalysis); + const duplicateBestPhotoIds = useMemo( + () => new Set(aiCulling.duplicateGroups.map(group => group.bestPhotoId).filter((id): id is string => Boolean(id))), + [aiCulling.duplicateGroups], + ); + const aiPickedPhotoIds = useMemo( + () => aiCulling.duplicateStatus === 'READY' + ? buildEditionAiPickedPhotoIds( + photoState.photos, + duplicateBestPhotoIds, + aiCulling.duplicatePhotoIds, + aiCulling.settings.aiPickTargetRatio, + aiCulling.duplicateGroups, + aiCulling.settings, + ) + : new Set(), + [ + photoState.photos, + duplicateBestPhotoIds, + aiCulling.duplicatePhotoIds, + aiCulling.duplicateGroups, + aiCulling.duplicateStatus, + aiCulling.settings.aiPickTargetRatio, + aiCulling.settings.proPersonaRanking, + ], + ); + const navigation = usePhotoNavigation( + photoState.photos, + aiCulling.duplicatePhotoIds, + duplicateBestPhotoIds, + aiCulling.duplicateStatus === 'READY', + aiPickedPhotoIds, + ); + const peopleSplit = usePeopleSplit(photoState.photos, { + aiCullingRunning: aiCulling.progress.running && !aiCulling.progress.paused, + }); + const [workspaceMode, setWorkspaceMode] = useState('CULLING'); + const [showAiSettings, setShowAiSettings] = useState(false); + const [appCacheUsage, setAppCacheUsage] = useState(null); + const [appCacheUsageBusy, setAppCacheUsageBusy] = useState(false); + const t = getTranslations(language); + const runningInTauri = hasTauriRuntime(); + + // Platform detection + const { isMacOS } = usePlatform(); + + useEffect(() => { + latestPhotosRef.current = photoState.photos; + }, [photoState.photos]); + + const dismissNotification = (id: string) => { + setNotifications(prev => prev.filter(notification => notification.id !== id)); + }; + + const notify = ({ + kind, + title, + message, + detail, + autoDismissMs, + }: { + kind: NotificationKind; + title: string; + message?: string; + detail?: string; + autoDismissMs?: number; + }) => { + const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const next: AppNotification = { + id, + kind, + title, + message, + detail, + createdAt: Date.now(), + autoDismissMs, + }; + setNotifications(prev => [...prev, next]); + }; + + const rawMonitorFeature = useRawMonitorFeature({ + photos: photoState.photos, + filteredPhotos: navigation.filteredPhotos, + selectedIndex: navigation.selectedIndex, + language, + notify, + }); + const { + rawEngineSettings, + rawEngineBusy, + rawMonitorProgress, + rawMonitorCacheSizeBytes, + rawMonitorCacheBusy, + viewerPreview: rawMonitorPreview, + onDetectRawEngine: handleDetectRawEngine, + onChooseRawEngine: handleChooseRawEngine, + onClearRawEngine: handleClearRawEngine, + onRefreshRawMonitorCacheSize: handleRefreshRawMonitorCacheSize, + onCleanupRawMonitorCache: handleCleanupRawMonitorCache, + clearCache: clearRawMonitorCache, + } = rawMonitorFeature; + + const refreshAppCacheUsage = useCallback(async () => { + setAppCacheUsageBusy(true); + try { + setAppCacheUsage(await getAppCacheUsage()); + } catch (error) { + console.warn('Failed to refresh app cache usage:', error); + } finally { + setAppCacheUsageBusy(false); + } + }, []); + + useEffect(() => { + if (!modalState.showSettings) return; + void refreshAppCacheUsage(); + }, [modalState.showSettings, refreshAppCacheUsage]); + + + useEffect(() => { + const timers = notifications + .filter(notification => typeof notification.autoDismissMs === 'number' && notification.autoDismissMs > 0) + .map(notification => window.setTimeout(() => { + dismissNotification(notification.id); + }, notification.autoDismissMs)); + + return () => { + timers.forEach(timer => window.clearTimeout(timer)); + }; + }, [notifications]); + + + const completeInitialImport = (selectedId: string | null, fallbackIndex = 0) => { + setInitialImportActive(false); + setInitialImportPreloadProgress(null); + initialPreloadStartedRef.current = false; + if (selectedId) { + navigation.selectPhotoById(selectedId); + } else if (photoState.photos.length > 0) { + navigation.setSelectedIndex(fallbackIndex); + } else { + navigation.setSelectedIndex(null); + } + }; + + const prepareInitialPreviewWindow = async (selectedId: string | null) => { + const targetPhotos = photoState.photos; + if (targetPhotos.length === 0) { + completeInitialImport(selectedId); + return; + } + + let startIndex = 0; + if (selectedId) { + const foundIndex = targetPhotos.findIndex(photo => photo.id === selectedId); + if (foundIndex >= 0) startIndex = foundIndex; + } + + const previewCandidates = targetPhotos.slice(startIndex, startIndex + 2); + const preloadTotal = Math.max(1, previewCandidates.length); + + setInitialImportPreloadProgress({ + phase: 'preload', + total: preloadTotal, + processed: 0, + running: true, + current: targetPhotos[startIndex]?.id, + }); + + preloadDisplayWindow(targetPhotos, startIndex, { ahead: 3, behind: 0 }); + preloadRawWindow(targetPhotos, startIndex, { ahead: 2, behind: 0, includeCurrent: false }); + + const preloadTasks = previewCandidates.map(async (group, index) => { + setInitialImportPreloadProgress(prev => prev ? { + ...prev, + processed: index, + current: group.id, + } : prev); + + const tasks: Promise[] = []; + if (group.raw?.path) { + tasks.push( + decodeRawFile(group.raw.path, false, { + priority: index === 0 ? 'high' : 'low', + silent: true, + fallbackToWorker: index === 0, + }) + .catch(() => null) + ); + } + const displayUrl = getDisplayPreviewUrl(group); + if (displayUrl) { + tasks.push(loadDisplayImage(displayUrl).catch(() => null)); + } + await Promise.all(tasks); + }); + + await Promise.race([ + Promise.all(preloadTasks), + new Promise(resolve => window.setTimeout(resolve, 1400)), + ]); + + setInitialImportPreloadProgress(prev => prev ? { + ...prev, + phase: 'done', + running: false, + processed: prev.total, + } : prev); + window.setTimeout(() => completeInitialImport(selectedId, startIndex), 140); + }; + + const handleMinimize = () => { + if (!hasTauriRuntime()) return; + getCurrentWindow().minimize(); + }; + + const handleMaximize = async () => { + if (!hasTauriRuntime()) return; + const appWindow = getCurrentWindow(); + const isMaximized = await appWindow.isMaximized(); + if (isMaximized) { + await appWindow.unmaximize(); + } else { + await appWindow.maximize(); + } + setIsMaximized(await appWindow.isMaximized()); + }; + + const handleClose = () => { + if (!hasTauriRuntime()) return; + getCurrentWindow().close(); + }; + + // Persist language setting + useEffect(() => { + localStorage.setItem(LANGUAGE_STORAGE_KEY, language); + }, [language]); + + useEffect(() => subscribeRawDecodeProgress(setRawDecodeProgress), []); + + useEffect(() => { + if (!runningInTauri) return; + let disposed = false; + let unlisten: (() => void) | undefined; + const appWindow = getCurrentWindow(); + const syncMaximizedState = async () => { + try { + const maximized = await appWindow.isMaximized(); + if (!disposed) setIsMaximized(maximized); + } catch { + if (!disposed) setIsMaximized(false); + } + }; + + void syncMaximizedState(); + void appWindow.onResized(() => { + void syncMaximizedState(); + }).then(cleanup => { + if (disposed) cleanup(); + else unlisten = cleanup; + }); + + return () => { + disposed = true; + unlisten?.(); + }; + }, [runningInTauri]); + + useEffect(() => { + if (initialImportActive) return; + if (aiCulling.progress.running) { + cancelRawPreloads(); + } else { + preloadRawWindow(navigation.filteredPhotos, navigation.selectedIndex, { + ahead: 2, + behind: 0, + includeCurrent: false, + }); + } + const displayWindow = aiCulling.progress.running + ? { ahead: 2, behind: 0, includeCurrent: false } + : { ahead: 5, behind: 1, includeCurrent: false }; + preloadDisplayWindow(navigation.filteredPhotos, navigation.selectedIndex, displayWindow); + }, [aiCulling.progress.running, initialImportActive, navigation.filteredPhotos, navigation.selectedIndex]); + + // Auto-select first photo when photos are available + useEffect(() => { + if (initialImportActive) return; + navigation.autoSelectFirst(); + }, [initialImportActive, photoState.photos.length]); + + useEffect(() => { + if (!initialImportActive || initialPreloadStartedRef.current) return; + if (photoState.photos.length === 0) return; + if (!initialImportSelectionId && photoState.importProgress.phase !== 'done') return; + + const selectedId = initialImportSelectionId || photoState.photos[0]?.id || null; + initialPreloadStartedRef.current = true; + void prepareInitialPreviewWindow(selectedId).catch(error => { + console.warn('Initial preview preparation failed:', error); + completeInitialImport(selectedId); + }); + }, [initialImportActive, initialImportSelectionId, photoState.photos, photoState.importProgress.phase]); + + // Show window after app is ready + useEffect(() => { + const splashStartedAt = performance.now(); + let disposed = false; + let hideTimer: number | undefined; + let showTimer: number | undefined; + let imageTimer: number | undefined; + + const hideLoading = () => { + if (disposed) return; + const loadingEl = document.getElementById('app-loading'); + if (loadingEl) { + loadingEl.classList.add('fade-out'); + setTimeout(() => { + loadingEl.style.display = 'none'; + }, 380); + } + }; + + const scheduleHideLoading = () => { + const elapsed = performance.now() - splashStartedAt; + hideTimer = window.setTimeout(hideLoading, Math.max(0, SPLASH_MIN_VISIBLE_MS - elapsed)); + }; + + const waitForSplashPaint = (callback: () => void) => { + const runAfterPaint = () => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + showTimer = window.setTimeout(callback, SPLASH_FIRST_PAINT_DELAY_MS); + }); + }); + }; + + const loadingMark = document.querySelector('#app-loading img'); + if (!loadingMark || loadingMark.complete) { + runAfterPaint(); + return; + } + + let settled = false; + const complete = () => { + if (settled) return; + settled = true; + loadingMark.removeEventListener('load', complete); + loadingMark.removeEventListener('error', complete); + if (imageTimer !== undefined) window.clearTimeout(imageTimer); + runAfterPaint(); + }; + + loadingMark.addEventListener('load', complete, { once: true }); + loadingMark.addEventListener('error', complete, { once: true }); + imageTimer = window.setTimeout(complete, 420); + }; + + const showWindow = async () => { + if (disposed) return; + try { + if (runningInTauri) { + await invoke('show_main_window'); + } + } catch (error) { + console.error('Failed to show window:', error); + } finally { + scheduleHideLoading(); + } + }; + + waitForSplashPaint(() => { void showWindow(); }); + return () => { + disposed = true; + if (hideTimer !== undefined) window.clearTimeout(hideTimer); + if (showTimer !== undefined) window.clearTimeout(showTimer); + if (imageTimer !== undefined) window.clearTimeout(imageTimer); + }; + }, [runningInTauri]); + + // Import handlers + const handleImportFiles = async () => { + try { + const isInitialImport = photoState.photos.length === 0; + if (isInitialImport) { + setInitialImportActive(true); + setInitialImportSelectionId(null); + setInitialImportPreloadProgress(null); + initialPreloadStartedRef.current = false; + } + const firstNewGroupId = await photoState.importFiles(); + if (isInitialImport) { + if (firstNewGroupId) { + setInitialImportSelectionId(firstNewGroupId); + } else { + window.setTimeout(() => { + const fallbackId = latestPhotosRef.current[0]?.id || null; + if (fallbackId) { + setInitialImportSelectionId(fallbackId); + } else { + setInitialImportActive(false); + } + }, 80); + } + } else if (firstNewGroupId) { + navigation.selectPhotoById(firstNewGroupId); + } else if (navigation.selectedIndex === null && photoState.photos.length > 0) { + navigation.autoSelectFirst(); + } + } catch (error) { + setInitialImportActive(false); + setInitialImportSelectionId(null); + setInitialImportPreloadProgress(null); + initialPreloadStartedRef.current = false; + console.error('Failed to import files:', error); + notify({ + kind: 'error', + title: t.messages.importFailed, + detail: error instanceof Error ? error.message : String(error), + }); + } + }; + + const handleImportFolder = async () => { + try { + const isInitialImport = photoState.photos.length === 0; + if (isInitialImport) { + setInitialImportActive(true); + setInitialImportSelectionId(null); + setInitialImportPreloadProgress(null); + initialPreloadStartedRef.current = false; + } + const firstNewGroupId = await photoState.importFolder(); + if (isInitialImport) { + if (firstNewGroupId) { + setInitialImportSelectionId(firstNewGroupId); + } else { + window.setTimeout(() => { + const fallbackId = latestPhotosRef.current[0]?.id || null; + if (fallbackId) { + setInitialImportSelectionId(fallbackId); + } else { + setInitialImportActive(false); + } + }, 80); + } + } else if (firstNewGroupId) { + navigation.selectPhotoById(firstNewGroupId); + } else if (navigation.selectedIndex === null && photoState.photos.length > 0) { + navigation.autoSelectFirst(); + } + } catch (error) { + setInitialImportActive(false); + setInitialImportSelectionId(null); + setInitialImportPreloadProgress(null); + initialPreloadStartedRef.current = false; + console.error('Failed to import folder:', error); + notify({ + kind: 'error', + title: t.messages.importFailed, + detail: error instanceof Error ? error.message : String(error), + }); + } + }; + + // Delete handlers + const handleDeleteRejected = async () => { + try { + const rejectedIds = new Set(photoState.photos + .filter(photo => photo.selection === SelectionState.REJECTED) + .map(photo => photo.id)); + const nextSelectionId = getSelectionTargetAfterRemoval( + navigation.filteredPhotos, + navigation.currentPhoto?.id, + rejectedIds, + ); + const deletedCount = await photoState.deleteRejectedPhotos(); + modalState.setShowDeleteConfirm(false); + if (nextSelectionId) { + navigation.selectPhotoById(nextSelectionId); + } else { + navigation.setSelectedIndex(null); + } + console.log(`Successfully moved ${deletedCount} files to trash`); + } catch (error) { + console.error('Failed to move files to trash:', error); + modalState.setShowDeleteConfirm(false); + + // Show force delete confirmation + const rejectedGroups = photoState.photos.filter(p => p.selection === SelectionState.REJECTED); + modalState.setGroupsToForceDelete(rejectedGroups); + modalState.setShowForceDeleteConfirm(true); + notify({ + kind: 'warning', + title: language === 'zh' ? '移动到回收站失败' : 'Move to trash failed', + message: language === 'zh' ? '\u5df2\u5207\u6362\u4e3a\u6c38\u4e45\u5220\u9664\u786e\u8ba4\u3002\u8bf7\u518d\u6b21\u786e\u8ba4\u6b64\u64cd\u4f5c\u3002' : 'Switched to permanent delete confirmation. Please confirm again.', + detail: error instanceof Error ? error.message : String(error), + }); + } + }; + + const handleOrphanDeleteStart = (type: 'RAW' | 'JPG') => { + const orphanGroups = photoState.photos.filter(p => { + if (type === 'RAW') { + return p.status === GroupStatus.RAW_ONLY; + } else { + return p.status === GroupStatus.JPG_ONLY; + } + }); + + if (orphanGroups.length === 0) { + notify({ + kind: 'info', + title: type === 'RAW' ? t.messages.noOrphanRawFiles : t.messages.noOrphanJpgFiles, + autoDismissMs: 4500, + }); + return; + } + + modalState.setOrphanDeleteType(type); + modalState.setShowOrphanDeleteConfirm(true); + }; + + const handleOrphanDelete = async () => { + if (!modalState.orphanDeleteType) { + modalState.setShowOrphanDeleteConfirm(false); + return; + } + + try { + const deletedCount = await photoState.deleteOrphanPhotos(modalState.orphanDeleteType); + modalState.setShowOrphanDeleteConfirm(false); + modalState.setOrphanDeleteType(null); + + if (photoState.photos.length > deletedCount) { + navigation.setSelectedIndex(0); + } else { + navigation.setSelectedIndex(null); + } + + notify({ + kind: 'success', + title: t.messages.orphanDeleteSuccess, + message: `${deletedCount} ${language === 'zh' ? '\u4e2a\u6587\u4ef6\u5df2\u79fb\u81f3\u56de\u6536\u7ad9' : 'files moved to trash'}`, + autoDismissMs: 4500, + }); + console.log(`Successfully moved ${deletedCount} orphan files to trash`); + } catch (error) { + console.error('Failed to move orphan files to trash:', error); + modalState.setShowOrphanDeleteConfirm(false); + + // Show force delete confirmation + if (modalState.orphanDeleteType) { + const orphanGroups = photoState.photos.filter(p => { + if (modalState.orphanDeleteType === 'RAW') { + return p.status === GroupStatus.RAW_ONLY; + } else { + return p.status === GroupStatus.JPG_ONLY; + } + }); + modalState.setGroupsToForceDelete(orphanGroups); + modalState.setShowForceDeleteConfirm(true); + notify({ + kind: 'warning', + title: language === 'zh' ? '移动到回收站失败' : 'Move to trash failed', + message: language === 'zh' ? '\u5df2\u5207\u6362\u4e3a\u6c38\u4e45\u5220\u9664\u786e\u8ba4\u3002\u8bf7\u518d\u6b21\u786e\u8ba4\u6b64\u64cd\u4f5c\u3002' : 'Switched to permanent delete confirmation. Please confirm again.', + detail: error instanceof Error ? error.message : String(error), + }); + } else { + modalState.setOrphanDeleteType(null); + } + } + }; + + const handleForceDelete = async () => { + try { + const removedIds = new Set(modalState.groupsToForceDelete.map(group => group.id)); + const nextSelectionId = getSelectionTargetAfterRemoval( + navigation.filteredPhotos, + navigation.currentPhoto?.id, + removedIds, + ); + const deletedCount = await photoState.forceDeletePhotos(modalState.groupsToForceDelete); + modalState.setShowForceDeleteConfirm(false); + modalState.setGroupsToForceDelete([]); + modalState.setOrphanDeleteType(null); + + if (nextSelectionId) { + navigation.selectPhotoById(nextSelectionId); + } else { + navigation.setSelectedIndex(null); + } + + console.log(`Successfully deleted ${deletedCount} files permanently`); + } catch (error) { + console.error('Failed to force delete files:', error); + notify({ + kind: 'error', + title: t.messages.deleteFailed, + detail: error instanceof Error ? error.message : String(error), + }); + modalState.setShowForceDeleteConfirm(false); + modalState.setGroupsToForceDelete([]); + modalState.setOrphanDeleteType(null); + } + }; + + // Export handlers + const showExportError = (error: unknown, destinationFolder?: string) => { + const message = error instanceof Error ? error.message : String(error); + setExportProgress({ + phase: 'error', + total: 0, + processed: 0, + running: false, + destinationFolder, + error: message, + }); + }; + + const applyExportStreamEvent = (payload: ExportStreamEvent, destinationFolder: string) => { + setExportProgress(prev => { + const nextPhase = payload.phase || prev.phase; + const processed = payload.processed ?? prev.processed; + const total = payload.total ?? prev.total; + const current = payload.current ?? prev.current; + const files = payload.files ?? prev.files; + + if (payload.kind === 'done') { + return { + phase: 'done', + total: total || files?.length || processed, + processed: processed || files?.length || total, + current, + destinationFolder, + running: false, + files, + }; + } + + if (payload.kind === 'error') { + return { + phase: 'error', + total, + processed, + current, + destinationFolder, + running: false, + files, + error: payload.error || 'Export failed', + }; + } + + return { + ...prev, + phase: nextPhase === 'idle' || nextPhase === 'done' || nextPhase === 'error' ? 'copying' : nextPhase, + total, + processed, + current, + destinationFolder, + running: true, + }; + }); + }; + + const handleExportStart = () => { + if (navigation.currentSelectionTarget.length === 0) { + showExportError(t.messages.noPhotosToExport); + return; + } + modalState.setShowExportConfirm(true); + }; + + const handleExport = async (options?: ExportOptions) => { + if (!options) { + modalState.setShowExportConfirm(false); + return; + } + + try { + const targetGroups = navigation.currentSelectionTarget; + if (targetGroups.length === 0) { + modalState.setShowExportConfirm(false); + showExportError(t.messages.noPhotosToExport, options.destinationFolder); + return; + } + + const exportMode = options.mode; + const exportIntent = options.intent; + const movedGroupCount = targetGroups.length; + const isLightroomDirectImport = exportIntent === 'LIGHTROOM_IMPORT' || options.exportTarget === 'LIGHTROOM_CLASSIC'; + const isRenderedExport = exportIntent === 'RENDER_COPY'; + const sourceExportMode = isSourceExportMode(exportMode) ? exportMode : 'BOTH'; + let finalExportedFiles: string[] = []; + + modalState.setShowExportConfirm(false); + if (isLightroomDirectImport) { + setExportProgress({ + phase: 'preparing', + total: targetGroups.length, + processed: 0, + exportTarget: 'LIGHTROOM_CLASSIC', + lightroomMode: 'SOURCE_FOLDER', + running: true, + }); + const savedPath = readStorage(LIGHTROOM_PATH_STORAGE_KEY) || options.lightroomExecutablePath; + const result = await invoke('open_lightroom_source_folder', { + groups: toRustGroups(targetGroups), + executablePath: savedPath || null, + }); + if (result.executablePath) { + localStorage.setItem(LIGHTROOM_PATH_STORAGE_KEY, result.executablePath); + } + const lightroomMessage = (result.warnings || []).filter(Boolean).join('\n') || undefined; + setExportProgress({ + phase: 'done', + total: result.files.length, + processed: result.files.length, + exportTarget: 'LIGHTROOM_CLASSIC', + lightroomMode: 'SOURCE_FOLDER', + lightroomLaunchStatus: result.launched ? 'LAUNCHED' : 'NOT_FOUND', + destinationFolder: result.sourceFolder, + lightroomExecutablePath: result.executablePath, + lightroomMessage, + running: false, + files: result.files, + }); + if (!result.launched) { + try { + await revealItemInDir(result.files); + } catch (revealError) { + console.warn('Failed to reveal Lightroom import source files:', revealError); + } + notify({ + kind: 'warning', + title: language === 'zh' ? '未检测到 Lightroom Classic' : 'Lightroom Classic was not detected', + message: language === 'zh' + ? '\u5df2\u5199\u5165\u661f\u7ea7\u5143\u6570\u636e\uff0c\u5e76\u6253\u5f00\u6240\u9009\u7167\u7247\u6240\u5728\u6587\u4ef6\u5939\u3002' + : 'Ratings were written and the source folder was revealed.', + autoDismissMs: 8000, + }); + } + console.log('Lightroom source folder opened'); + return; + } + setExportProgress({ + phase: 'preparing', + total: isRenderedExport ? targetGroups.length : countSourceExportFiles(targetGroups, sourceExportMode), + processed: 0, + destinationFolder: options.destinationFolder, + exportTarget: options.exportTarget, + lightroomMode: options.lightroomMode, + running: true, + }); + + if (isRenderedExport) { + const format = exportMode === 'RENDER_TIFF' + ? 'tiff' + : exportMode === 'RENDER_PNG' + ? 'png' + : 'jpeg'; + const renderedFiles: Awaited>[] = []; + + for (const [index, group] of targetGroups.entries()) { + const current = group.jpg?.name || group.raw?.name || group.id; + setExportProgress({ + phase: 'rendering', + total: targetGroups.length, + processed: index, + current, + destinationFolder: options.destinationFolder, + running: true, + }); + const renderedFile = await renderGroupForExport(group, format, { + jpegQuality: options.jpegQuality ?? 100, + fileNameBase: getRenderedExportFileNameBase(options, group, index, targetGroups.length), + metadataMode: options.metadataMode ?? 'NONE', + colorSpace: options.colorSpace ?? 'SRGB', + }); + renderedFiles.push(renderedFile); + setExportProgress({ + phase: 'rendering', + total: targetGroups.length, + processed: index + 1, + current: renderedFile.fileName, + destinationFolder: options.destinationFolder, + running: true, + }); + } + + const channel = new Channel((payload) => { + applyExportStreamEvent(payload, options.destinationFolder); + }); + const exportedFiles = await invoke('write_rendered_export_stream', { + files: renderedFiles, + destinationFolder: options.destinationFolder, + onEvent: channel, + }); + finalExportedFiles = exportedFiles; + setExportProgress({ + phase: 'done', + total: exportedFiles.length, + processed: exportedFiles.length, + destinationFolder: options.destinationFolder, + exportTarget: options.exportTarget, + lightroomMode: options.lightroomMode, + running: false, + files: exportedFiles, + }); + } else { + const phase = options.operation === 'MOVE' ? 'moving' : 'copying'; + setExportProgress(prev => ({ ...prev, phase, running: true })); + const channel = new Channel((payload) => { + applyExportStreamEvent(payload, options.destinationFolder); + }); + const exportedFiles = await photoState.exportPhotosStream( + targetGroups, + sourceExportMode, + options.operation, + options.destinationFolder, + channel, + options.includeRawSidecars ?? true + ); + finalExportedFiles = exportedFiles; + setExportProgress({ + phase: 'done', + total: exportedFiles.length, + processed: exportedFiles.length, + destinationFolder: options.destinationFolder, + exportTarget: options.exportTarget, + lightroomMode: options.lightroomMode, + running: false, + files: exportedFiles, + }); + } + + const lightroomResult = await launchLightroomAfterExport(options); + setExportProgress(prev => ({ + ...prev, + phase: 'done', + total: prev.total || finalExportedFiles.length, + processed: prev.processed || finalExportedFiles.length, + destinationFolder: options.destinationFolder, + exportTarget: options.exportTarget, + lightroomMode: options.lightroomMode, + running: false, + files: prev.files || finalExportedFiles, + ...lightroomResult, + })); + + if (lightroomResult.lightroomLaunchStatus === 'NOT_FOUND' || lightroomResult.lightroomLaunchStatus === 'ERROR') { + try { + await revealItemInDir(options.destinationFolder); + } catch (revealError) { + console.warn('Failed to reveal Lightroom folder:', revealError); + } + notify({ + kind: 'warning', + title: language === 'zh' ? 'Lightroom \u672a\u542f\u52a8' : 'Lightroom was not launched', + message: language === 'zh' + ? '\u5bfc\u51fa\u6587\u4ef6\u548c\u661f\u7ea7\u5df2\u51c6\u5907\u597d\uff0c\u8bf7\u5728 Lightroom Classic \u4e2d\u6253\u5f00\u76ee\u6807\u76ee\u5f55\u3002' + : 'The exported files and ratings are ready. Open the destination folder from Lightroom Classic.', + detail: lightroomResult.lightroomMessage, + autoDismissMs: 8000, + }); + } + + if (options.intent === 'MOVE_ORIGINALS' && options.operation === 'MOVE') { + navigation.setSelectedIndex(photoState.photos.length > movedGroupCount ? 0 : null); + } + + console.log('Export completed'); + } catch (error) { + console.error('Failed to export files:', error); + modalState.setShowExportConfirm(false); + showExportError(error, options.destinationFolder); + } + }; + + const handleCloseExportProgress = () => { + if (exportProgress.running) return; + setExportProgress(IDLE_EXPORT_PROGRESS); + }; + + const handleRevealExportResult = async () => { + const revealTarget = exportProgress.exportTarget === 'LIGHTROOM_CLASSIC' + ? (exportProgress.files?.length ? exportProgress.files : exportProgress.destinationFolder) + : exportProgress.files?.length + ? exportProgress.files + : exportProgress.destinationFolder; + if (!revealTarget) return; + + try { + await revealItemInDir(revealTarget); + } catch (error) { + console.error('Failed to reveal export result:', error); + notify({ + kind: 'error', + title: language === 'zh' ? '无法显示导出结果' : 'Failed to show export result', + message: language === 'zh' ? '\u5bfc\u51fa\u5df2\u5b8c\u6210\uff0c\u4f46\u65e0\u6cd5\u5728\u8d44\u6e90\u7ba1\u7406\u5668\u4e2d\u5b9a\u4f4d\u6587\u4ef6\u3002' : 'Export completed, but the result could not be revealed in the file manager.', + detail: error instanceof Error ? error.message : String(error), + }); + } + }; + + const handlePeopleExport = async () => { + if (peopleSplit.selectedClusters.length === 0) { + notify({ + kind: 'info', + title: language === 'zh' ? '\u8bf7\u5148\u9009\u62e9\u4eba\u7269\u7ec4' : 'Select at least one person', + autoDismissMs: 3200, + }); + return; + } + + try { + const destinationFolder = await open({ + directory: true, + multiple: false, + }); + if (!destinationFolder || Array.isArray(destinationFolder)) return; + + setExportProgress({ + phase: 'copying', + total: peopleSplit.selectedClusters.reduce((count, cluster) => count + cluster.photoCount, 0), + processed: 0, + destinationFolder, + running: true, + }); + + const channel = new Channel((payload) => { + applyExportStreamEvent(payload, destinationFolder); + }); + const exportedFiles = await peopleSplit.exportClusters( + peopleSplit.selectedClusters, + destinationFolder, + channel, + ); + setExportProgress({ + phase: 'done', + total: exportedFiles.length, + processed: exportedFiles.length, + destinationFolder, + running: false, + files: exportedFiles, + }); + } catch (error) { + console.error('Failed to export people clusters:', error); + showExportError(error); + } + }; + + const handleUpdateRating = (rating: PhotoRating) => { + navigation.updateRatingForSelection(rating, (photoIds, nextRating) => { + void photoState.updatePhotoRating(photoIds, nextRating).catch(error => { + console.error('Failed to write rating metadata:', error); + notify({ + kind: 'error', + title: language === 'zh' ? '\u5199\u5165\u661f\u7ea7\u5143\u6570\u636e\u5931\u8d25' : 'Failed to write rating metadata', + message: language === 'zh' ? '\u754c\u9762\u8bc4\u5206\u5df2\u56de\u9000\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002' : 'The visible rating has been rolled back. Please try again.', + detail: error instanceof Error ? error.message : String(error), + }); + }); + }); + }; + + const handleClearCaches = async () => { + const result = await clearAppCaches(); + await clearRawMonitorCache(); + photoState.clearCurrentSessionMarks(); + void refreshAppCacheUsage(); + if (handleRefreshRawMonitorCacheSize) { + void handleRefreshRawMonitorCacheSize(); + } + notify({ + kind: 'success', + title: language === 'zh' ? '\u7f13\u5b58\u5df2\u6e05\u9664' : 'Caches cleared', + message: language === 'zh' + ? '\u5df2\u6e05\u9664 AI \u5206\u6790\u3001\u7b5b\u7247\u72b6\u6001\u548c\u9884\u89c8\u7f13\u5b58\u3002\u8bed\u8a00\u3001\u4e3b\u9898\u3001AI \u8bbe\u7f6e\u548c Lightroom \u8def\u5f84\u5df2\u4fdd\u7559\u3002' + : 'AI analysis, culling state, and preview caches were cleared. Language, theme, AI settings, and Lightroom path were kept.', + detail: `persistent=${result.clearedPersistent}; disk=${formatBytesForLog(result.clearedDiskBytes)}; memory=${result.clearedMemory}`, + autoDismissMs: 2600, + }); + }; + + const toggleViewerAiMode = () => { + setViewerAiMode(mode => mode === 'AI' ? 'ORIGINAL' : 'AI'); + }; + + // Keyboard shortcuts + useKeyboardShortcuts({ + enabled: workspaceMode === 'CULLING' && navigation.selectedIndex !== null, + onNavigate: navigation.navigate, + onUpdateSelection: (state: SelectionState) => { + const updater = navigation.filter === 'AI_REVIEW' + ? photoState.reviewAiPhoto + : photoState.updatePhotoSelection; + navigation.updateSelectionWithAnimation(state, updater); + }, + onUpdateRating: handleUpdateRating, + onToggleAiOverlay: toggleViewerAiMode, + onSelectAll: navigation.selectAllFilteredPhotos, + onClearSelection: navigation.clearMultiSelection, + }); + + const toolbarRawDecodeProgress = aiCulling.progress.running + ? { total: 0, processed: 0, queued: 0, active: 0, running: false } + : rawDecodeProgress; + const showInitialImportSurface = photoState.photos.length === 0 || initialImportActive; + const initialImportProgress = initialImportPreloadProgress || photoState.importProgress; + const filmstripStats = { + ...photoState.stats, + duplicates: aiCulling.duplicatePhotoIds.size, + aiPicked: aiPickedPhotoIds.size, + }; + + return ( +
+
+ {/* Top Nav */} + { void aiCulling.start(); }} + onAiPause={aiCulling.pause} + onAiResume={aiCulling.resume} + onAiSettingsClick={() => setShowAiSettings(true)} + stats={filmstripStats} + selectionCount={navigation.currentSelectionTarget.length} + peopleActive={workspaceMode === 'PEOPLE_SPLIT'} + peopleCount={peopleSplit.state.clusters.length} + onPeopleClick={() => setWorkspaceMode(mode => mode === 'PEOPLE_SPLIT' ? 'CULLING' : 'PEOPLE_SPLIT')} + onDeleteRejected={() => modalState.setShowDeleteConfirm(true)} + onExportClick={handleExportStart} + onSettingsClick={() => modalState.setShowSettings(!modalState.showSettings)} + isMacOS={isMacOS} + isMaximized={isMaximized} + onMinimize={handleMinimize} + onMaximize={handleMaximize} + onClose={handleClose} + /> + + {/* Main Workspace */} +
+ {workspaceMode === 'PEOPLE_SPLIT' ? ( + { void peopleSplit.start(); }} + onStop={peopleSplit.stop} + onRenameCluster={peopleSplit.renameCluster} + onMergeClusters={peopleSplit.mergeClusters} + onMoveFace={peopleSplit.moveFace} + onCreatePersonFromFace={peopleSplit.createPersonFromFace} + onToggleClusterSelection={peopleSplit.toggleClusterSelection} + onSetSelectedClusterIds={peopleSplit.setSelectedClusterIds} + onExportSelected={() => { void handlePeopleExport(); }} + aiViewMode={viewerAiMode} + onAiViewModeChange={setViewerAiMode} + onFocusPhoto={navigation.selectPhotoById} + onUpdatePhotoSelection={photoState.updatePhotoSelection} + onUpdatePhotoRating={(photoId, rating) => photoState.updatePhotoRating([photoId], rating)} + /> + ) : showInitialImportSurface ? ( + + ) : ( +
+ + +
+ {navigation.filter === 'DUPLICATES' ? ( + { + if (navigation.currentPhoto?.id === photoId) { + navigation.updateSelectionWithAnimation(state, photoState.updatePhotoSelection); + return; + } + photoState.updatePhotoSelection(photoId, state); + }} + onUpdateRating={(photoIds, rating) => { + if (photoIds.length === 1 && navigation.currentPhoto?.id === photoIds[0]) { + handleUpdateRating(rating); + return; + } + photoState.updatePhotoRating(photoIds, rating); + }} + onAiStart={() => { void aiCulling.start(); }} + /> + ) : navigation.filteredPhotos.length === 0 ? ( + + ) : navigation.currentPhoto && ( + { + navigation.updateSelectionWithAnimation(state, photoState.updatePhotoSelection); + }} + onAiReview={(state: SelectionState) => { + navigation.updateSelectionWithAnimation(state, photoState.reviewAiPhoto); + }} + theme={theme} + language={language} + onUpdateRating={handleUpdateRating} + aiViewMode={viewerAiMode} + onAiViewModeChange={setViewerAiMode} + isAiReviewMode={navigation.filter === 'AI_REVIEW'} + rawMonitorPreview={rawMonitorPreview} + /> + )} +
+
+ )} +
+
+ + + + {/* Modals */} + {modalState.showDeleteConfirm && ( + p.selection === SelectionState.REJECTED)} + onConfirm={handleDeleteRejected} + onCancel={() => modalState.setShowDeleteConfirm(false)} + theme={theme} + language={language} + /> + )} + + {modalState.showExportConfirm && ( + modalState.setShowExportConfirm(false)} + theme={theme} + language={language} + /> + )} + + {modalState.showOrphanDeleteConfirm && modalState.orphanDeleteType && ( + { + if (modalState.orphanDeleteType === 'RAW') { + return p.status === GroupStatus.RAW_ONLY; + } else { + return p.status === GroupStatus.JPG_ONLY; + } + })} + orphanDeleteKind={modalState.orphanDeleteType} + onConfirm={handleOrphanDelete} + onCancel={() => { + modalState.setShowOrphanDeleteConfirm(false); + modalState.setOrphanDeleteType(null); + }} + theme={theme} + language={language} + /> + )} + + {modalState.showForceDeleteConfirm && ( + { + modalState.setShowForceDeleteConfirm(false); + modalState.setGroupsToForceDelete([]); + modalState.setOrphanDeleteType(null); + }} + theme={theme} + language={language} + /> + )} + + {/* Settings Panel */} + modalState.setShowSettings(false)} + theme={theme} + themeMode={themeMode} + language={language} + onThemeModeChange={setThemeMode} + onLanguageChange={setLanguage} + orphanStats={{ + raw: photoState.stats.orphanRaw, + jpg: photoState.stats.orphanJpg, + }} + onDeleteOrphanRaw={() => handleOrphanDeleteStart('RAW')} + onDeleteOrphanJpg={() => handleOrphanDeleteStart('JPG')} + onClearCaches={handleClearCaches} + appCacheUsage={appCacheUsage} + appCacheUsageBusy={appCacheUsageBusy} + onRefreshAppCacheUsage={() => { void refreshAppCacheUsage(); }} + rawEngineSettings={rawEngineSettings} + rawEngineBusy={rawEngineBusy} + rawMonitorProgress={rawMonitorProgress} + rawMonitorCacheSizeBytes={rawMonitorCacheSizeBytes} + rawMonitorCacheBusy={rawMonitorCacheBusy} + onDetectRawEngine={handleDetectRawEngine ? () => { void handleDetectRawEngine(); } : undefined} + onChooseRawEngine={handleChooseRawEngine ? () => { void handleChooseRawEngine(); } : undefined} + onClearRawEngine={handleClearRawEngine ? () => { void handleClearRawEngine(); } : undefined} + onRefreshRawMonitorCacheSize={handleRefreshRawMonitorCacheSize ? () => { void handleRefreshRawMonitorCacheSize(); } : undefined} + onCleanupRawMonitorCache={handleCleanupRawMonitorCache ? () => { void handleCleanupRawMonitorCache(); } : undefined} + onClearRawMonitorCache={() => { void clearRawMonitorCache(); }} + /> + + setShowAiSettings(false)} + theme={theme} + language={language} + settings={aiCulling.settings} + onSettingsChange={aiCulling.setSettings} + /> + + {exportProgress.phase !== 'idle' && ( + + )} +
+ ); +}; + +export default App; + +function countSourceExportFiles(groups: PhotoGroup[], mode: Exclude) { + return groups.reduce((count, group) => { + if (mode === 'JPG') return count + (group.jpg ? 1 : 0); + if (mode === 'RAW') return count + (group.raw ? 1 : 0); + return count + (group.jpg ? 1 : 0) + (group.raw ? 1 : 0); + }, 0); +} + +function getRenderedExportFileNameBase(options: ExportOptions, group: PhotoGroup, index: number, total: number) { + const baseName = options.renameBaseName?.trim(); + if (!options.renameEnabled || !baseName) return group.id; + if (total <= 1) return baseName; + return `${baseName}-${String(index + 1).padStart(3, '0')}`; +} + +function isSourceExportMode(mode: ExportOptions['mode']): mode is Exclude { + return mode === 'JPG' || mode === 'RAW' || mode === 'BOTH'; +} diff --git a/src/assets/brand/framecull-icon-source.png b/src/assets/brand/framecull-icon-source.png new file mode 100644 index 0000000..a623255 Binary files /dev/null and b/src/assets/brand/framecull-icon-source.png differ diff --git a/src/assets/brand/framecull-mark.png b/src/assets/brand/framecull-mark.png new file mode 100644 index 0000000..326f8ec Binary files /dev/null and b/src/assets/brand/framecull-mark.png differ diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..76c4de9 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/AiFloatingPanel.test.ts b/src/components/AiFloatingPanel.test.ts new file mode 100644 index 0000000..8eb53f8 --- /dev/null +++ b/src/components/AiFloatingPanel.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; +import { + DEFAULT_POSITION, + PANEL_WIDTH, + clampPanelPosition, + formatElapsedTime, + getDisplayedElapsedMs, + getEdgeForPosition, + getRestoredPosition, + parseStoredPanelState, +} from './AiFloatingPanel'; + +const VIEWPORT_WIDTH = 1440; +const VIEWPORT_HEIGHT = 900; + +describe('AiFloatingPanel edge docking helpers', () => { + it('returns left edge when dropped at the minimum x bound', () => { + expect(getEdgeForPosition({ x: 8, y: 92 }, VIEWPORT_WIDTH, PANEL_WIDTH)).toBe('left'); + }); + + it('returns right edge when dropped at the maximum x bound', () => { + const clamped = clampPanelPosition({ x: 9999, y: 92 }, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, PANEL_WIDTH); + expect(getEdgeForPosition(clamped, VIEWPORT_WIDTH, PANEL_WIDTH)).toBe('right'); + }); + + it('does not auto-hide the default position', () => { + expect(getEdgeForPosition(DEFAULT_POSITION, VIEWPORT_WIDTH, PANEL_WIDTH)).toBeNull(); + }); + + it('restores from the left edge into a safe visible inset', () => { + const restored = getRestoredPosition('left', 92, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, PANEL_WIDTH); + expect(restored.x).toBeGreaterThan(8); + expect(getEdgeForPosition(restored, VIEWPORT_WIDTH, PANEL_WIDTH)).toBeNull(); + }); + + it('restores from the right edge into a safe visible inset', () => { + const restored = getRestoredPosition('right', 92, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, PANEL_WIDTH); + const clampedMax = clampPanelPosition({ x: 9999, y: 92 }, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, PANEL_WIDTH); + expect(restored.x).toBeLessThan(clampedMax.x); + expect(getEdgeForPosition(restored, VIEWPORT_WIDTH, PANEL_WIDTH)).toBeNull(); + }); + + it('parses legacy stored positions without losing compatibility', () => { + const parsed = parseStoredPanelState(JSON.stringify({ x: 18, y: 92 }), VIEWPORT_WIDTH, VIEWPORT_HEIGHT, PANEL_WIDTH); + expect(parsed.position).toEqual(DEFAULT_POSITION); + expect(parsed.hiddenEdge).toBeNull(); + }); + + it('formats elapsed time as m:ss below one hour', () => { + expect(formatElapsedTime(65_000)).toBe('1:05'); + }); + + it('formats elapsed time as h:mm:ss at one hour and above', () => { + expect(formatElapsedTime(3_665_000)).toBe('1:01:05'); + }); + + it('uses live elapsed time while running and freezes elapsed time while paused', () => { + expect(getDisplayedElapsedMs({ + total: 100, + processed: 10, + running: true, + paused: false, + startedAt: 1_000, + pausedTotalMs: 200, + elapsedMs: 500, + }, 4_000)).toBe(2_800); + + expect(getDisplayedElapsedMs({ + total: 100, + processed: 10, + running: true, + paused: true, + startedAt: 1_000, + pausedTotalMs: 200, + elapsedMs: 1_750, + }, 8_000)).toBe(1_750); + }); +}); diff --git a/src/components/AiFloatingPanel.tsx b/src/components/AiFloatingPanel.tsx new file mode 100644 index 0000000..e2bd8a3 --- /dev/null +++ b/src/components/AiFloatingPanel.tsx @@ -0,0 +1,553 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ChevronLeft, ChevronRight, Pause, Play, SlidersHorizontal, WandSparkles } from 'lucide-react'; +import type { AiProgress } from '../types'; +import type { Language } from '../i18n'; +import { AppIcon } from './ui/AppIcon'; +import { glassInteractive, glassSubtle, glassSurface } from './ui/chrome'; +import { readStorage } from '../utils/storage'; + +interface AiFloatingPanelProps { + theme: 'light' | 'dark'; + language: Language; + stats: { + total: number; + aiReview: number; + }; + progress: AiProgress; + onAiStart: () => void; + onAiPause: () => void; + onAiResume: () => void; + onAiSettingsClick: () => void; +} + +export type PanelEdge = 'left' | 'right'; +export type PanelPosition = { x: number; y: number }; +export type PanelDockState = { hiddenEdge: PanelEdge | null }; +export type PanelStorageState = PanelDockState & { position: PanelPosition }; + +type PanelDragState = { + pointerId: number; + startX: number; + startY: number; + origin: PanelPosition; + current: PanelPosition; +}; + +export const PANEL_WIDTH = 268; +export const PANEL_HEIGHT = 272; +export const DEFAULT_POSITION: PanelPosition = { x: 18, y: 150 }; +const STORAGE_KEY = 'framecull-ai-floating-panel-position'; +const EDGE_MIN_X = 8; +const EDGE_MIN_Y = 96; +const EDGE_GUTTER = 8; +const RESTORE_INSET = 18; +const EDGE_TAB_HEIGHT = 78; +const TAB_PULL_OFFSET = 18; + +const copy = { + zh: { + title: 'AI\u6311\u56fe', + review: '\u5f85\u590d\u67e5', + start: '\u5f00\u59cb', + pause: '\u6682\u505c', + resume: '\u7ee7\u7eed', + settings: 'AI\u8bbe\u7f6e', + active: '\u5f53\u524d', + idle: '\u5c31\u7eea', + empty: '\u65e0\u7167\u7247', + scanned: '\u5df2\u7b5b', + elapsed: '\u8fd0\u884c\u65f6\u95f4', + remaining: '\u9884\u8ba1\u5269\u4f59', + noEstimate: '--', + proScoringPhase: '\u6574\u7406 Pro \u6a21\u578b\u5206\u6570', + proScoringState: '\u6536\u5c3e\u4e2d', + engineInit: 'AI \u7f8e\u5b66\u5f15\u64ce\u542f\u52a8\u4e2d', + engineInitShort: '\u7f8e\u5b66\u5f15\u64ce', + engineInitDetail: '\u6b63\u5728\u52a0\u8f7d\u672c\u5730\u5ba1\u7f8e\u6a21\u578b\u4e0e\u7b5b\u7247\u89c4\u5219', + engineInitState: '\u542f\u52a8\u4e2d', + duplicatePhase: '分析重复照片', + collapse: '\u6536\u8d77', + restore: '\u6062\u590d\u9762\u677f', + }, + en: { + title: 'AI Cull', + review: 'Review', + start: 'Start', + pause: 'Pause', + resume: 'Resume', + settings: 'AI Settings', + active: 'Active', + idle: 'Ready', + empty: 'No photos', + scanned: 'Scanned', + elapsed: 'Elapsed', + remaining: 'Remaining', + noEstimate: '--', + proScoringPhase: 'Finalizing Pro model scores', + proScoringState: 'Finishing', + engineInit: 'Starting AI aesthetic engine', + engineInitShort: 'Aesthetic engine', + engineInitDetail: 'Loading local aesthetic model and culling rules', + engineInitState: 'Starting', + duplicatePhase: 'Analyzing duplicates', + collapse: 'Collapse', + restore: 'Restore panel', + }, +}; + +export const AiFloatingPanel: React.FC = ({ + theme, + language, + stats, + progress, + onAiStart, + onAiPause, + onAiResume, + onAiSettingsClick, +}) => { + const text = copy[language]; + const panelRef = useRef(null); + const dragRef = useRef(null); + const tabCleanupRef = useRef<(() => void) | null>(null); + const [panelState, setPanelState] = useState(() => loadPanelState()); + const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth); + const [mobileOpen, setMobileOpen] = useState(false); + const [timeNow, setTimeNow] = useState(() => Date.now()); + const isCompact = viewportWidth < 720; + const expanded = !isCompact || mobileOpen; + const hiddenEdge = !isCompact ? panelState.hiddenEdge : null; + const progressPercent = useMemo(() => { + if (!progress.total) return 0; + return Math.round((progress.processed / progress.total) * 100); + }, [progress.processed, progress.total]); + const progressTotal = progress.total || stats.total; + const engineInitializing = progress.running && progress.phase === 'AI_ENGINE_INIT'; + const proScoring = progress.running && progress.phase === 'PRO_MODEL_SCORING'; + const displayPercent = engineInitializing ? 16 : proScoring ? 98 : progressPercent; + const scannedValue = `${Math.min(progress.processed, progressTotal)}/${progressTotal}`; + const elapsedMs = getDisplayedElapsedMs(progress, timeNow); + const elapsedValue = formatElapsedTime(elapsedMs); + const remainingMs = estimateRemainingMs(elapsedMs, progress.processed, progressTotal); + const remainingValue = remainingMs === null ? text.noEstimate : formatElapsedTime(remainingMs); + const activeValue = engineInitializing + ? text.engineInitDetail + : proScoring + ? text.proScoringPhase + : progress.phase === 'DUPLICATE_GROUPING' + ? text.duplicatePhase + : progress.activeId || (stats.total === 0 ? text.empty : text.idle); + + const panelWidth = useCallback(() => panelRef.current?.offsetWidth || PANEL_WIDTH, []); + const panelHeight = useCallback(() => panelRef.current?.offsetHeight || PANEL_HEIGHT, []); + const clampForWindow = useCallback((next: PanelPosition) => { + return clampPanelPosition(next, window.innerWidth, window.innerHeight, panelWidth(), panelHeight()); + }, [panelHeight, panelWidth]); + + useEffect(() => { + const handleResize = () => { + setViewportWidth(window.innerWidth); + setPanelState(prev => ({ + ...prev, + position: clampForWindow(prev.position), + })); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [clampForWindow]); + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(panelState)); + }, [panelState]); + + useEffect(() => { + return () => { + tabCleanupRef.current?.(); + }; + }, []); + + useEffect(() => { + if (!progress.running || progress.paused) return; + setTimeNow(Date.now()); + const timer = window.setInterval(() => setTimeNow(Date.now()), 1000); + return () => window.clearInterval(timer); + }, [progress.paused, progress.running]); + + const restoreFromEdge = useCallback((edge: PanelEdge) => { + setPanelState(prev => ({ + hiddenEdge: null, + position: getRestoredPosition(edge, prev.position.y, window.innerWidth, window.innerHeight, panelWidth(), panelHeight()), + })); + }, [panelHeight, panelWidth]); + + const handlePointerDown = useCallback((event: React.PointerEvent) => { + if (isCompact && !expanded) return; + const target = event.target as HTMLElement; + if (target.closest('button')) return; + const origin = panelState.position; + dragRef.current = { + pointerId: event.pointerId, + startX: event.clientX, + startY: event.clientY, + origin, + current: origin, + }; + event.currentTarget.setPointerCapture(event.pointerId); + }, [expanded, isCompact, panelState.position]); + + const handlePointerMove = useCallback((event: React.PointerEvent) => { + const drag = dragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + const next = clampForWindow({ + x: drag.origin.x + event.clientX - drag.startX, + y: drag.origin.y + event.clientY - drag.startY, + }); + drag.current = next; + setPanelState({ hiddenEdge: null, position: next }); + }, [clampForWindow]); + + const endDrag = useCallback((event: React.PointerEvent) => { + const drag = dragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + + try { + event.currentTarget.releasePointerCapture(event.pointerId); + } catch { + // Pointer capture may already be released by the browser. + } + + const finalPosition = drag.current; + const nextEdge = !isCompact + ? getEdgeForPosition(finalPosition, window.innerWidth, panelWidth()) + : null; + setPanelState({ + hiddenEdge: nextEdge, + position: finalPosition, + }); + dragRef.current = null; + }, [isCompact, panelWidth]); + + const handleEdgePointerDown = useCallback((event: React.PointerEvent, edge: PanelEdge) => { + event.preventDefault(); + tabCleanupRef.current?.(); + + const drag = { + startX: event.clientX, + startY: event.clientY, + originY: panelState.position.y, + opened: false, + }; + + const handleMove = (moveEvent: PointerEvent) => { + const distance = Math.hypot(moveEvent.clientX - drag.startX, moveEvent.clientY - drag.startY); + if (!drag.opened && distance < 4) return; + drag.opened = true; + + const width = panelWidth(); + const height = panelHeight(); + const bounds = getPanelBounds(window.innerWidth, window.innerHeight, width, height); + const rawX = edge === 'left' + ? Math.max(bounds.minX + RESTORE_INSET, moveEvent.clientX - TAB_PULL_OFFSET) + : Math.min(bounds.maxX - RESTORE_INSET, moveEvent.clientX - width + TAB_PULL_OFFSET); + const next = clampPanelPosition({ + x: rawX, + y: drag.originY + moveEvent.clientY - drag.startY, + }, window.innerWidth, window.innerHeight, width, height); + + setPanelState({ hiddenEdge: null, position: next }); + }; + + const cleanup = () => { + window.removeEventListener('pointermove', handleMove); + window.removeEventListener('pointerup', handleUp); + tabCleanupRef.current = null; + }; + + const handleUp = () => { + cleanup(); + if (!drag.opened) { + restoreFromEdge(edge); + } + }; + + tabCleanupRef.current = cleanup; + window.addEventListener('pointermove', handleMove); + window.addEventListener('pointerup', handleUp); + }, [panelHeight, panelState.position.y, panelWidth, restoreFromEdge]); + + if (!expanded) { + return ( + + ); + } + + if (hiddenEdge) { + return ( + + ); + } + + return ( +
+
+
+ + + +
+ {engineInitializing ? text.engineInit : text.title} +
+
+ {isCompact && ( + + )} +
+ +
+
+
+
+
+
+ {engineInitializing ? text.engineInitState : proScoring ? text.proScoringState : `${progressPercent}%`} +
+
+ +
+ + + + +
+ +
+ + {text.active}: + + + {activeValue} + +
+ +
+ + +
+
+
+ ); +}; + +const MetricCell = ({ theme, label, value }: { theme: 'light' | 'dark'; label: string; value: string }) => ( +
+
{label}
+
{value}
+
+); + +export function getDisplayedElapsedMs(progress: AiProgress, nowMs: number) { + if (!progress.running || progress.paused || progress.startedAt === undefined) { + return progress.elapsedMs ?? 0; + } + return Math.max(0, nowMs - progress.startedAt - (progress.pausedTotalMs ?? 0)); +} + +function estimateRemainingMs(elapsedMs: number, processed: number, total: number) { + if (processed <= 0 || total <= processed || elapsedMs < 1000) return null; + return Math.max(0, Math.round((elapsedMs / processed) * (total - processed))); +} + +export function formatElapsedTime(ms: number) { + const totalSeconds = Math.max(0, Math.floor(ms / 1000)); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + if (hours > 0) { + return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + } + return `${minutes}:${String(seconds).padStart(2, '0')}`; +} + +export function getPanelBounds(viewportWidth: number, viewportHeight: number, panelWidth = PANEL_WIDTH, panelHeight = PANEL_HEIGHT) { + const maxX = Math.max(EDGE_MIN_X, viewportWidth - panelWidth - EDGE_GUTTER); + const maxY = Math.max(EDGE_MIN_Y, viewportHeight - panelHeight - EDGE_GUTTER); + return { + minX: EDGE_MIN_X, + minY: EDGE_MIN_Y, + maxX, + maxY, + }; +} + +export function clampPanelPosition(position: PanelPosition, viewportWidth: number, viewportHeight: number, panelWidth = PANEL_WIDTH, panelHeight = PANEL_HEIGHT): PanelPosition { + const bounds = getPanelBounds(viewportWidth, viewportHeight, panelWidth, panelHeight); + return { + x: Math.min(Math.max(bounds.minX, position.x), bounds.maxX), + y: Math.min(Math.max(bounds.minY, position.y), bounds.maxY), + }; +} + +export function getEdgeForPosition(position: PanelPosition, viewportWidth: number, panelWidth = PANEL_WIDTH): PanelEdge | null { + const maxX = Math.max(EDGE_MIN_X, viewportWidth - panelWidth - EDGE_GUTTER); + if (position.x <= EDGE_MIN_X) return 'left'; + if (position.x >= maxX) return 'right'; + return null; +} + +export function getRestoredPosition(edge: PanelEdge, y: number, viewportWidth: number, viewportHeight: number, panelWidth = PANEL_WIDTH, panelHeight = PANEL_HEIGHT): PanelPosition { + const bounds = getPanelBounds(viewportWidth, viewportHeight, panelWidth, panelHeight); + const x = edge === 'left' + ? bounds.minX + RESTORE_INSET + : bounds.maxX - RESTORE_INSET; + return clampPanelPosition({ x, y }, viewportWidth, viewportHeight, panelWidth, panelHeight); +} + +export function parseStoredPanelState(raw: string | null, viewportWidth: number, viewportHeight: number, panelWidth = PANEL_WIDTH, panelHeight = PANEL_HEIGHT): PanelStorageState { + if (!raw) { + return { + position: clampPanelPosition(DEFAULT_POSITION, viewportWidth, viewportHeight, panelWidth, panelHeight), + hiddenEdge: null, + }; + } + + try { + const parsed = JSON.parse(raw) as Partial; + const legacyX = typeof parsed.x === 'number' ? parsed.x : undefined; + const legacyY = typeof parsed.y === 'number' ? parsed.y : undefined; + const position = parsed.position && typeof parsed.position.x === 'number' && typeof parsed.position.y === 'number' + ? parsed.position + : legacyX !== undefined && legacyY !== undefined + ? { x: legacyX, y: legacyY } + : DEFAULT_POSITION; + const hiddenEdge = parsed.hiddenEdge === 'left' || parsed.hiddenEdge === 'right' + ? parsed.hiddenEdge + : null; + + return { + position: clampPanelPosition(migrateStoredPanelPosition(position, hiddenEdge), viewportWidth, viewportHeight, panelWidth, panelHeight), + hiddenEdge, + }; + } catch { + return { + position: clampPanelPosition(DEFAULT_POSITION, viewportWidth, viewportHeight, panelWidth, panelHeight), + hiddenEdge: null, + }; + } +} + +function migrateStoredPanelPosition(position: PanelPosition, hiddenEdge: PanelEdge | null): PanelPosition { + if (hiddenEdge) return position; + if (position.y < 132) { + return { ...position, y: DEFAULT_POSITION.y }; + } + return position; +} + +function loadPanelState(): PanelStorageState { + return parseStoredPanelState(readStorage(STORAGE_KEY), window.innerWidth, window.innerHeight); +} + +function clampBookmarkY(y: number) { + const maxY = Math.max(EDGE_MIN_Y, window.innerHeight - EDGE_TAB_HEIGHT - EDGE_GUTTER); + return Math.min(Math.max(EDGE_MIN_Y, y), maxY); +} diff --git a/src/components/AiSettingsPanel.tsx b/src/components/AiSettingsPanel.tsx new file mode 100644 index 0000000..7a34cfe --- /dev/null +++ b/src/components/AiSettingsPanel.tsx @@ -0,0 +1,424 @@ +import React from 'react'; +import { CopyCheck, Cpu, EyeOff, Focus, Moon, SunMedium, X } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import { AiIssueCode, AiSensitivity, AiSettings, DuplicateSensitivity } from '../types'; +import { aiIssueLabel, aiSensitivityLabel, duplicateSensitivityLabel } from '../utils/aiLabels'; +import { Language } from '../i18n'; +import { AppIcon } from './ui/AppIcon'; +import { chromeGlass, glassInteractive, glassSubtle, modalBackdrop } from './ui/chrome'; +import { IS_PRO_EDITION } from '../utils/appInfo'; + +interface AiSettingsPanelProps { + isOpen: boolean; + onClose: () => void; + theme: 'light' | 'dark'; + language: Language; + settings: AiSettings; + onSettingsChange: (settings: AiSettings) => void; +} + +const CHECKS: AiIssueCode[] = ['OUT_OF_FOCUS', 'UNDER_EXPOSED', 'OVER_EXPOSED', 'EYES_CLOSED']; +const SENSITIVITIES: AiSensitivity[] = ['weak', 'standard', 'strong']; +const DUPLICATE_SENSITIVITIES: DuplicateSensitivity[] = ['off', 'loose', 'standard', 'strict']; + +const copy = { + zh: { + title: '\u672c\u5730 AI \u7b5b\u7247', + subtitle: '\u5931\u7126 / \u66dd\u5149 / \u95ed\u773c', + checks: '\u68c0\u6d4b\u9879', + sensitivity: '\u654f\u611f\u5ea6', + allSensitivity: '\u5168\u90e8\u540c\u6b65\u4e3a', + itemSensitivity: '\u5355\u9879\u654f\u611f\u5ea6', + aiPicks: 'AI\u7cbe\u9009', + aiPickTarget: '\u7cbe\u9009\u4fdd\u7559\u6bd4\u4f8b', + aiPickTargetHint: '\u91cd\u590d / \u8fde\u62cd\u5b50\u7ec4\u5148\u4fdd\u7559\u53ef\u7528\u4ee3\u8868\uff0c\u666e\u901a\u5355\u5f20\u518d\u6309\u6bd4\u4f8b\u8865\u8db3\u3002', + proPersonaRanking: 'Pro persona 灰度排序', + proPersonaRankingHint: '默认关闭。开启后仅 Pro 使用 student persona 分数参与 AI 精选排序,Flash 和旧规则不受影响。', + duplicates: '重复照片', + duplicateHint: '并入 AI 筛图流程,整批分析完成后生成重复组和奖杯推荐。', + recommendBest: '每组推荐 1 张最佳', + close: '\u5173\u95ed', + enabled: '\u5f00', + disabled: '\u5173', + local: '\u672c\u5730\u6a21\u578b', + footer: '\u4ec5\u6807\u8bb0\uff0c\u4e0d\u81ea\u52a8\u5220\u9664\u6216\u79fb\u52a8\u539f\u56fe', + }, + en: { + title: 'Local AI Culling', + subtitle: 'Focus / Exposure / Eyes', + checks: 'Checks', + sensitivity: 'Sensitivity', + allSensitivity: 'Set all to', + itemSensitivity: 'Item sensitivity', + aiPicks: 'AI Picks', + aiPickTarget: 'Pick target', + aiPickTargetHint: 'Duplicate and burst subgroups keep usable representatives first, then solo photos fill this target.', + proPersonaRanking: 'Pro persona rollout ranking', + proPersonaRankingHint: 'Off by default. Pro only; uses the student persona score in AI Pick ranking while Flash and default rules stay unchanged.', + duplicates: 'Duplicates', + duplicateHint: 'Runs inside AI culling, then creates duplicate groups and trophy recommendations.', + recommendBest: 'Recommend one best per group', + close: 'Close', + enabled: 'On', + disabled: 'Off', + local: 'Local model', + footer: 'Marks only. Original files are never deleted or moved automatically.', + }, +}; + +const AiSettingsPanel: React.FC = ({ + isOpen, + onClose, + theme, + language, + settings, + onSettingsChange, +}) => { + if (!isOpen) return null; + + const text = copy[language]; + + const updateEnabled = (code: AiIssueCode, enabled: boolean) => { + onSettingsChange({ + ...settings, + enabledChecks: { + ...settings.enabledChecks, + [code]: enabled, + }, + }); + }; + + const updateAllSensitivity = (sensitivity: AiSensitivity) => { + onSettingsChange({ + ...settings, + sensitivity, + sensitivityByCheck: { + OUT_OF_FOCUS: sensitivity, + UNDER_EXPOSED: sensitivity, + OVER_EXPOSED: sensitivity, + EYES_CLOSED: sensitivity, + }, + }); + }; + + const updateCheckSensitivity = (code: AiIssueCode, sensitivity: AiSensitivity) => { + onSettingsChange({ + ...settings, + sensitivityByCheck: { + ...settings.sensitivityByCheck, + [code]: sensitivity, + }, + }); + }; + + const updateDuplicateSensitivity = (duplicateSensitivity: DuplicateSensitivity) => { + onSettingsChange({ + ...settings, + duplicateSensitivity, + }); + }; + + const updateDuplicateRecommendation = (duplicateAlwaysRecommendOne: boolean) => { + onSettingsChange({ + ...settings, + duplicateAlwaysRecommendOne, + }); + }; + + const updateAiPickTargetRatio = (value: number) => { + onSettingsChange({ + ...settings, + aiPickTargetRatio: Math.max(0.1, Math.min(0.7, value)), + }); + }; + + const updateProPersonaRanking = (enabled: boolean) => { + onSettingsChange({ + ...settings, + proPersonaRanking: { + ...settings.proPersonaRanking, + enabled, + }, + }); + }; + + return ( + <> +
+ + + ); +}; + +function iconForCheck(code: AiIssueCode): LucideIcon { + if (code === 'OUT_OF_FOCUS') return Focus; + if (code === 'UNDER_EXPOSED') return Moon; + if (code === 'OVER_EXPOSED') return SunMedium; + return EyeOff; +} + +export default AiSettingsPanel; diff --git a/src/components/ConfirmationModal.tsx b/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000..055fd05 --- /dev/null +++ b/src/components/ConfirmationModal.tsx @@ -0,0 +1,1062 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { open } from '@tauri-apps/plugin-dialog'; +import type { LucideIcon } from 'lucide-react'; +import { + AlertTriangle, + Check, + CheckCircle2, + ChevronDown, + FolderOpen, + Image, + Send, + Star, + Trash2, +} from 'lucide-react'; +import { + ExportColorSpace, + ExportIntent, + ExportTarget, + ExportMetadataMode, + ExportMode, + ExportOperation, + ExportOptions, + PhotoGroup, +} from '../types'; +import { Language } from '../i18n'; +import { AppIcon } from './ui/AppIcon'; +import { chromeGlass, glassSubtle, glassPopover, modalBackdrop } from './ui/chrome'; + +interface ConfirmationModalProps { + groups: PhotoGroup[]; + onConfirm: (value?: ExportOptions) => void; + onCancel: () => void; + type: 'delete' | 'export' | 'forceDelete'; + orphanDeleteKind?: 'RAW' | 'JPG' | null; + theme: 'light' | 'dark'; + language?: Language; +} + +const labels = { + zh: { + cancel: '取消', + deleteTitle: '移动到回收站', + deleteBody: '即将把这些已弃用照片移动到系统回收站。', + deleteConfirm: '移到回收站', + orphanRawTitle: '清理孤立 RAW', + orphanJpgTitle: '清理孤立 JPG', + orphanRawBody: '即将把所有没有配对 JPG 的 RAW 文件移动到系统回收站。这是文件维护操作,不会按 AI 标签处理照片。', + orphanJpgBody: '即将把所有没有配对 RAW 的 JPG 文件移动到系统回收站。这是文件维护操作,不会按 AI 标签处理照片。', + orphanConfirm: '确认清理', + forceTitle: '永久删除', + forceBody: '无法移动到回收站。如果继续,文件会被直接删除,此操作不可撤销。', + forceConfirm: '直接删除', + exportTitle: '导出选中照片', + exportTarget: '导出目标', + exportToFolder: '普通文件夹', + exportToFolderHint: '只导出到本地目录,完成后可手动打开。', + exportToLightroom: 'Lightroom Classic', + exportToLightroomHint: '写入星级并打开所选照片所在文件夹。', + lightroomQuickTitle: '打开 Lightroom Classic', + lightroomQuickHint: '', + exportMethod: '导出方式', + exportCopy: '导出副本', + moveOriginals: '原始文件', + exportCopyHint: '重新渲染为新文件,原片留在当前位置。', + moveOriginalsHint: '复制或移动原始 JPG/RAW,RAW 的 XMP 会一并处理。', + operation: '处理方式', + copyOriginals: '复制原片', + copyOriginalsHint: '原片留在当前位置,目标目录写入星级副本。', + moveOriginalsOperation: '移动原片', + moveOriginalsOperationHint: '把原始文件移动到目标目录。', + lightroomHandoff: 'Lightroom', + lightroomImportFolder: '源文件夹', + lightroomImportFolderHint: '启动 Lightroom Classic,并打开当前所选照片所在文件夹。', + lightroomWatchedFolder: '源文件夹', + lightroomWatchedFolderHint: '启动 Lightroom Classic,并打开当前所选照片所在文件夹。', + launchLightroom: '导出完成后启动 Lightroom Classic', + lightroomHonestHint: 'FrameCull 会把当前星级写入照片元数据,并打开 Lightroom Classic 与源文件夹。', + fileSettings: '文件设置', + fileNaming: '文件命名', + metadata: '元数据', + destination: '目标目录', + imageFormat: '图像格式', + quality: '品质', + colorSpace: '色彩空间', + renameAs: '重新命名', + nameAs: '命名为', + renameHint: '多张导出会自动追加序号,例如:婚礼精选-001.jpg。', + extensionExample: '示例', + chooseFolder: '选择目录', + noFolder: '尚未选择目录', + exportConfirm: '开始导出', + importLightroomConfirm: '打开 LR', + groups: '照片组', + files: '个文件', + andMore: '还有', + more: '项', + renderJpeg: 'JPEG', + renderTiff: 'TIFF', + renderPng: 'PNG', + sourceJpg: 'JPG', + sourceRaw: 'RAW', + sourceBoth: 'RAW + JPG', + metadataStrategy: '保留策略', + noMetadata: '不保留元数据', + noMetadataHint: '只生成图像文件,不写入 FrameCull 星级,也不复制 EXIF 或 XMP。', + ratingMetadata: '写入星级', + ratingMetadataHint: '只写入 FrameCull 当前星级。JPEG 写入文件内,TIFF/PNG 生成同名 .xmp。', + captureAndRatingMetadata: '保留拍摄信息并写入星级', + captureAndRatingMetadataHint: '仅 JPEG / sRGB 可用。复制原 JPG 的 EXIF/XMP(时间、相机、镜头、GPS 等)并写入当前星级。', + sourceMetadata: '原始文件并写入星级', + sourceMetadataHint: '原 JPG/RAW 不重新编码。复制或移动到目标目录后,会把 FrameCull 当前星级写入目标 JPG 或 RAW 同名 .xmp,并保留原有拍摄时间、位置信息和其他元数据。', + colorSpaceSrgb: 'sRGB IEC61966-2.1', + colorSpaceAdobe: 'Adobe RGB (1998)', + colorSpaceHint: 'JPEG/TIFF/PNG 均可输出 sRGB 或 Adobe RGB (1998)。', + selectDestinationFirst: '选择目标目录后即可开始导出。', + lightroomReadySummary: '写入星级,并打开 Lightroom Classic 到所选照片文件夹。', + willRender: '将生成', + willMove: '将处理', + willCopy: '将复制', + lightroomSummary: '并打开 Lightroom Classic', + toDestination: '到目标目录', + }, + en: { + cancel: 'Cancel', + deleteTitle: 'Move to Trash', + deleteBody: 'These rejected photos will be moved to the system trash.', + deleteConfirm: 'Move to Trash', + orphanRawTitle: 'Clean orphan RAW', + orphanJpgTitle: 'Clean orphan JPG', + orphanRawBody: 'All RAW files without a paired JPG will be moved to the system trash. This is file maintenance and does not act on AI labels.', + orphanJpgBody: 'All JPG files without a paired RAW will be moved to the system trash. This is file maintenance and does not act on AI labels.', + orphanConfirm: 'Clean Files', + forceTitle: 'Permanent Delete', + forceBody: 'Moving to trash failed. Continuing will permanently delete these files and cannot be undone.', + forceConfirm: 'Delete Permanently', + exportTitle: 'Export Selected Photos', + exportTarget: 'Export target', + exportToFolder: 'Folder', + exportToFolderHint: 'Export to a local folder and open it manually.', + exportToLightroom: 'Lightroom Classic', + exportToLightroomHint: 'Write ratings and open the selected photos folder.', + lightroomQuickTitle: 'Open Lightroom Classic', + lightroomQuickHint: '', + exportMethod: 'Export method', + exportCopy: 'Export copy', + moveOriginals: 'Original files', + exportCopyHint: 'Render new files and leave originals in place.', + moveOriginalsHint: 'Copy or move original JPG/RAW files and handle RAW XMP sidecars.', + operation: 'Operation', + copyOriginals: 'Copy originals', + copyOriginalsHint: 'Keep originals in place and write ratings to exported targets.', + moveOriginalsOperation: 'Move originals', + moveOriginalsOperationHint: 'Move original files to the destination folder.', + lightroomHandoff: 'Lightroom', + lightroomImportFolder: 'Import folder', + lightroomImportFolderHint: 'Launch Lightroom Classic and open the source folder.', + lightroomWatchedFolder: 'Watched folder', + lightroomWatchedFolderHint: 'Launch Lightroom Classic and open the selected photos folder.', + launchLightroom: 'Launch Lightroom Classic after export', + lightroomHonestHint: 'FrameCull writes ratings and opens Lightroom at the source folder.', + fileSettings: 'File settings', + fileNaming: 'File naming', + metadata: 'Metadata', + destination: 'Destination', + imageFormat: 'Image format', + quality: 'Quality', + colorSpace: 'Color space', + renameAs: 'Rename', + nameAs: 'Name as', + renameHint: 'Multiple exports append a sequence, for example: wedding-picks-001.jpg.', + extensionExample: 'Example', + chooseFolder: 'Choose Folder', + noFolder: 'No folder selected', + exportConfirm: 'Start Export', + importLightroomConfirm: 'Open LR', + groups: 'photo groups', + files: 'files', + andMore: 'and', + more: 'more', + renderJpeg: 'JPEG', + renderTiff: 'TIFF', + renderPng: 'PNG', + sourceJpg: 'JPG', + sourceRaw: 'RAW', + sourceBoth: 'RAW + JPG', + metadataStrategy: 'Policy', + noMetadata: 'No metadata', + noMetadataHint: 'Create image files only, without FrameCull rating, EXIF, or XMP.', + ratingMetadata: 'Write rating', + ratingMetadataHint: 'Writes only the current FrameCull rating. JPEG embeds it; TIFF/PNG get matching .xmp sidecars.', + captureAndRatingMetadata: 'Keep capture info and write rating', + captureAndRatingMetadataHint: 'JPEG / sRGB only. Copies source JPG EXIF/XMP such as time, camera, lens, and GPS, then writes the current rating.', + sourceMetadata: 'Original files with ratings', + sourceMetadataHint: 'Original JPG/RAW files are not re-encoded. After copying or moving to the destination, FrameCull writes the current rating into target JPG or RAW .xmp while preserving capture time, location, and other original metadata.', + colorSpaceSrgb: 'sRGB IEC61966-2.1', + colorSpaceAdobe: 'Adobe RGB (1998)', + colorSpaceHint: 'JPEG, TIFF, and PNG can export sRGB or Adobe RGB (1998).', + selectDestinationFirst: 'Choose a destination folder to start export.', + lightroomReadySummary: 'Write ratings and open Lightroom Classic at the selected photos folder.', + willRender: 'Will create', + willMove: 'Will process', + willCopy: 'Will copy', + lightroomSummary: 'and open Lightroom Classic', + toDestination: 'to the destination folder', + }, +} as const; + +type CopyKey = keyof typeof labels.zh; +type CopyTable = Record; + +const renderModeOptions: Array<{ value: ExportMode; labelKey: CopyKey }> = [ + { value: 'RENDER_JPG', labelKey: 'renderJpeg' }, + { value: 'RENDER_TIFF', labelKey: 'renderTiff' }, + { value: 'RENDER_PNG', labelKey: 'renderPng' }, +]; + +const baseOriginalModeOptions: Array<{ value: ExportMode; labelKey: CopyKey }> = [ + { value: 'JPG', labelKey: 'sourceJpg' }, + { value: 'RAW', labelKey: 'sourceRaw' }, + { value: 'BOTH', labelKey: 'sourceBoth' }, +]; + +const ConfirmationModal: React.FC = ({ + groups, + onConfirm, + onCancel, + type, + orphanDeleteKind = null, + theme, + language = 'zh', +}) => { + const [exportTarget, setExportTarget] = useState('FOLDER'); + const [intent, setIntent] = useState('RENDER_COPY'); + const [mode, setMode] = useState('RENDER_JPG'); + const [sourceOperation, setSourceOperation] = useState('MOVE'); + const [jpegQuality, setJpegQuality] = useState(100); + const [colorSpace, setColorSpace] = useState('SRGB'); + const [metadataMode, setMetadataMode] = useState('RATING_ONLY'); + const [renameEnabled, setRenameEnabled] = useState(false); + const [renameBaseName, setRenameBaseName] = useState(''); + const [destinationFolder, setDestinationFolder] = useState(''); + const [formatMenuOpen, setFormatMenuOpen] = useState(false); + const text = labels[language]; + const isDark = theme === 'dark'; + const isRenderIntent = intent === 'RENDER_COPY'; + const isLightroomTarget = exportTarget === 'LIGHTROOM_CLASSIC'; + const isLightroomImport = intent === 'LIGHTROOM_IMPORT' || isLightroomTarget; + const originalAvailability = useMemo(() => getOriginalAvailability(groups), [groups]); + const originalModeOptions = useMemo( + () => getAvailableOriginalModeOptions(originalAvailability), + [originalAvailability], + ); + const modeOptions = isRenderIntent ? renderModeOptions : originalModeOptions; + const operation: ExportOperation = isRenderIntent ? 'COPY' : sourceOperation; + const supportsCaptureMetadata = isRenderIntent + && mode === 'RENDER_JPG' + && colorSpace === 'SRGB' + && groups.every(group => Boolean(group.jpg?.path)); + const exportedFileCount = useMemo( + () => countExportFiles(groups, mode), + [groups, mode], + ); + const currentModeLabel = text[modeOptions.find(option => option.value === mode)?.labelKey || 'renderJpeg']; + const exportSummary = buildExportSummary({ + text, + modeLabel: currentModeLabel, + groupsCount: groups.length, + fileCount: exportedFileCount, + intent, + operation, + exportTarget, + hasDestination: Boolean(destinationFolder), + }); + const isOrphanDelete = type === 'delete' && orphanDeleteKind !== null; + const title = isOrphanDelete + ? (orphanDeleteKind === 'RAW' ? text.orphanRawTitle : text.orphanJpgTitle) + : type === 'delete' + ? text.deleteTitle + : type === 'forceDelete' + ? text.forceTitle + : text.exportTitle; + const icon = type === 'delete' + ? Trash2 + : type === 'forceDelete' + ? AlertTriangle + : Send; + const confirmLabel = isOrphanDelete + ? text.orphanConfirm + : type === 'delete' + ? text.deleteConfirm + : type === 'forceDelete' + ? text.forceConfirm + : isLightroomImport + ? text.importLightroomConfirm + : text.exportConfirm; + + useEffect(() => { + if (type !== 'export') return; + if (modeOptions.some(option => option.value === mode)) return; + setMode(isRenderIntent ? 'RENDER_JPG' : getDefaultOriginalMode(originalAvailability)); + }, [isRenderIntent, mode, modeOptions, originalAvailability, type]); + + const chooseDestination = async () => { + const folder = await open({ + directory: true, + multiple: false, + title: text.destination, + }); + if (typeof folder === 'string') setDestinationFolder(folder); + }; + + const handleIntentChange = (nextIntent: ExportIntent) => { + setIntent(nextIntent); + setMode(nextIntent === 'RENDER_COPY' ? 'RENDER_JPG' : getDefaultOriginalMode(originalAvailability)); + setMetadataMode(nextIntent === 'RENDER_COPY' ? 'RATING_ONLY' : 'ALL'); + if (nextIntent === 'MOVE_ORIGINALS') { + setSourceOperation(isLightroomTarget ? 'COPY' : 'MOVE'); + } + setFormatMenuOpen(false); + }; + + const handleExportTargetChange = (nextTarget: ExportTarget) => { + setExportTarget(nextTarget); + if (nextTarget === 'LIGHTROOM_CLASSIC') { + setIntent('LIGHTROOM_IMPORT'); + setMode(getDefaultOriginalMode(originalAvailability)); + setSourceOperation('COPY'); + setMetadataMode('ALL'); + } else if (intent === 'LIGHTROOM_IMPORT') { + setIntent('RENDER_COPY'); + setMode('RENDER_JPG'); + setSourceOperation('MOVE'); + setMetadataMode('RATING_ONLY'); + } else if (intent === 'MOVE_ORIGINALS') { + setSourceOperation('MOVE'); + } + setFormatMenuOpen(false); + }; + + const handleModeChange = (nextMode: ExportMode) => { + setMode(nextMode); + setFormatMenuOpen(false); + if (nextMode !== 'RENDER_JPG' && metadataMode === 'CAPTURE_INFO_AND_RATING') { + setMetadataMode('RATING_ONLY'); + } + }; + + const handleColorSpaceChange = (value: ExportColorSpace) => { + setColorSpace(value); + if (value !== 'SRGB' && metadataMode === 'CAPTURE_INFO_AND_RATING') { + setMetadataMode('RATING_ONLY'); + } + }; + + const handleQualityChange = (value: number) => { + setJpegQuality(Math.min(100, Math.max(1, Math.round(value)))); + }; + + const handleConfirm = () => { + if (type !== 'export') { + onConfirm(); + return; + } + + if (!destinationFolder && !isLightroomImport) return; + onConfirm({ + intent, + mode, + operation, + destinationFolder: isLightroomImport ? '' : destinationFolder, + exportTarget, + lightroomMode: isLightroomImport ? 'SOURCE_FOLDER' : undefined, + launchLightroom: isLightroomImport ? true : undefined, + jpegQuality, + colorSpace, + metadataMode: isRenderIntent ? metadataMode : 'ALL', + renameEnabled, + renameBaseName: renameBaseName.trim(), + includeRawSidecars: !isRenderIntent, + }); + }; + + return ( +
+
+
+

+ + {title} +

+ + {type === 'export' && ( +
+ {groups.length} {text.groups}, {exportedFileCount} {text.files} +
+ )} + +
+ {type === 'delete' && ( +

{isOrphanDelete + ? (orphanDeleteKind === 'RAW' ? text.orphanRawBody : text.orphanJpgBody) + : text.deleteBody} +

+ )} + {type === 'forceDelete' &&

{text.forceBody}

} +
+
+ + {type === 'export' && ( +
+ + + + + {!isLightroomImport && ( +
+ + + +
+ )} + +
+ {!isLightroomImport && isRenderIntent && ( + setRenameEnabled(value => !value)} + className={`flex h-6 w-6 items-center justify-center rounded-md transition-colors ${ + isDark ? 'hover:bg-white/[0.055]' : 'hover:bg-white/60' + }`} + > + + + } + > +
+
+ {text.nameAs} + setRenameBaseName(event.target.value)} + disabled={!renameEnabled} + placeholder="Wedding Picks" + className={inputClass(theme)} + style={{ colorScheme: isDark ? 'dark' : 'light' }} + /> +
+
+ {text.renameHint} + {renameEnabled && renameBaseName.trim() && ( + + {text.extensionExample}: {buildRenamePreview(renameBaseName.trim(), mode)} + + )} +
+
+
+ )} + + {!isLightroomImport && ( + + {!isRenderIntent && ( + + + + )} + + + setFormatMenuOpen(value => !value)} + onChange={handleModeChange} + /> + + + {mode === 'RENDER_JPG' && ( + +
+
+ handleQualityChange(Number(event.target.value))} + className={`export-quality-slider w-full ${isDark ? 'dark' : 'light'}`} + style={{ colorScheme: isDark ? 'dark' : 'light', '--quality': `${jpegQuality}%` } as React.CSSProperties} + /> +
+ handleQualityChange(Number(event.target.value || 1))} + className={`${compactInputClass(theme)} h-7 w-[68px] text-right font-medium tabular-nums`} + style={{ colorScheme: isDark ? 'dark' : 'light' }} + /> +
+
+ )} + + {isRenderIntent && ( + +
+ +
{text.colorSpaceHint}
+
+
+ )} +
+ )} + + {!isLightroomImport && ( + + {isRenderIntent ? ( +
+ {supportsCaptureMetadata && ( + setMetadataMode('CAPTURE_INFO_AND_RATING')} + /> + )} + setMetadataMode('RATING_ONLY')} + /> + setMetadataMode('NONE')} + /> +
+ ) : ( + + )} +
+ )} + + {!isLightroomImport && ( + +
+
+ {destinationFolder || text.noFolder} +
+ +
+
+ )} +
+
+ )} + + {type !== 'export' && ( +
+
+ {groups.slice(0, 24).map(group => ( +
+ {group.id} +
+ ))} + {groups.length > 24 && ( +
+ {text.andMore} {groups.length - 24} {text.more} +
+ )} +
+
+ )} + +
+ {type === 'export' && ( +
+ {exportSummary} +
+ )} +
+ + +
+
+
+
+ ); +}; + +const SegmentedChoice = ({ + value, + options, + theme, + onChange, +}: { + value: T; + options: Array<{ value: T; label: string; hint: string }>; + theme: 'light' | 'dark'; + onChange: (value: T) => void; +}) => { + const isDark = theme === 'dark'; + return ( +
+ {options.map(option => { + const active = value === option.value; + return ( + + ); + })} +
+ ); +}; + +const ExportSection = ({ + title, + theme, + headerAction, + children, +}: { + title: string; + theme: 'light' | 'dark'; + headerAction?: React.ReactNode; + children: React.ReactNode; +}) => { + const isDark = theme === 'dark'; + return ( +
+
+
+ {title} +
+ {headerAction} +
+
{children}
+
+ ); +}; + +const ExportField = ({ + label, + theme, + children, +}: { + label: string; + theme: 'light' | 'dark'; + children: React.ReactNode; +}) => { + const isDark = theme === 'dark'; + return ( + + ); +}; + +const FormatSelect = ({ + value, + open, + options, + text, + theme, + onToggle, + onChange, +}: { + value: ExportMode; + open: boolean; + options: Array<{ value: ExportMode; labelKey: CopyKey }>; + text: CopyTable; + theme: 'light' | 'dark'; + onToggle: () => void; + onChange: (value: ExportMode) => void; +}) => { + const isDark = theme === 'dark'; + const current = options.find(option => option.value === value) ?? options[0]; + return ( +
+ + + {open && ( +
+
+ {options.map(option => { + const active = option.value === value; + return ( + + ); + })} +
+
+ )} +
+ ); +}; + +const MetadataChoice = ({ + active, + icon, + title, + hint, + theme, + onClick, +}: { + active: boolean; + icon: LucideIcon; + title: string; + hint?: string; + theme: 'light' | 'dark'; + onClick?: () => void; +}) => { + const isDark = theme === 'dark'; + const Tag = onClick ? 'button' : 'div'; + return ( + + + + {title} + {hint && ( + + {hint} + + )} + + {active && ( + + )} + + ); +}; + +const ColorSpaceSwitch = ({ + value, + disabled, + text, + theme, + onChange, +}: { + value: ExportColorSpace; + disabled: boolean; + text: CopyTable; + theme: 'light' | 'dark'; + onChange: (value: ExportColorSpace) => void; +}) => { + const isDark = theme === 'dark'; + return ( +
+ {([ + { value: 'SRGB' as const, label: text.colorSpaceSrgb }, + { value: 'ADOBE_RGB' as const, label: text.colorSpaceAdobe }, + ]).map(option => { + const active = value === option.value; + const blocked = disabled && option.value === 'ADOBE_RGB'; + return ( + + ); + })} +
+ ); +}; + +function inputClass(theme: 'light' | 'dark') { + return `h-8 w-full rounded-md px-2.5 text-[13px] outline-none transition-colors disabled:cursor-not-allowed disabled:opacity-45 ${ + theme === 'dark' + ? 'bg-[#22252a]/95 text-zinc-100 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)] placeholder:text-zinc-600 focus:shadow-[inset_0_0_0_1px_rgba(103,232,249,0.34),0_0_0_2px_rgba(103,232,249,0.08)]' + : 'bg-slate-100/75 text-slate-950 shadow-[inset_0_0_0_1px_rgba(100,116,139,0.24)] placeholder:text-slate-400 focus:shadow-[inset_0_0_0_1px_rgba(8,145,178,0.40),0_0_0_2px_rgba(8,145,178,0.10)]' + }`; +} + +function compactInputClass(theme: 'light' | 'dark') { + return `h-8 rounded-md px-2.5 text-[13px] outline-none transition-colors disabled:cursor-not-allowed disabled:opacity-45 ${ + theme === 'dark' + ? 'bg-[#22252a]/95 text-zinc-100 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)] focus:shadow-[inset_0_0_0_1px_rgba(103,232,249,0.34),0_0_0_2px_rgba(103,232,249,0.08)]' + : 'bg-slate-100/75 text-slate-950 shadow-[inset_0_0_0_1px_rgba(100,116,139,0.24)] focus:shadow-[inset_0_0_0_1px_rgba(8,145,178,0.40),0_0_0_2px_rgba(8,145,178,0.10)]' + }`; +} + +function countExportFiles(groups: PhotoGroup[], mode: ExportMode) { + if (mode === 'RENDER_JPG' || mode === 'RENDER_TIFF' || mode === 'RENDER_PNG') return groups.length; + return groups.reduce((count, group) => { + if (mode === 'JPG') return count + (group.jpg ? 1 : 0); + if (mode === 'RAW') return count + (group.raw ? 1 : 0); + return count + (group.jpg ? 1 : 0) + (group.raw ? 1 : 0); + }, 0); +} + +type OriginalAvailability = { + hasJpg: boolean; + hasRaw: boolean; + hasCompletePair: boolean; +}; + +function getOriginalAvailability(groups: PhotoGroup[]): OriginalAvailability { + return groups.reduce((availability, group) => ({ + hasJpg: availability.hasJpg || Boolean(group.jpg), + hasRaw: availability.hasRaw || Boolean(group.raw), + hasCompletePair: availability.hasCompletePair || Boolean(group.jpg && group.raw), + }), { + hasJpg: false, + hasRaw: false, + hasCompletePair: false, + }); +} + +function getAvailableOriginalModeOptions(availability: OriginalAvailability) { + return baseOriginalModeOptions.filter(option => { + if (option.value === 'JPG') return availability.hasJpg; + if (option.value === 'RAW') return availability.hasRaw; + if (option.value === 'BOTH') return availability.hasCompletePair; + return false; + }); +} + +function getDefaultOriginalMode(availability: OriginalAvailability): ExportMode { + if (availability.hasCompletePair) return 'BOTH'; + if (availability.hasRaw) return 'RAW'; + if (availability.hasJpg) return 'JPG'; + return 'JPG'; +} + +function buildRenamePreview(baseName: string, mode: ExportMode) { + const extension = mode === 'RENDER_TIFF' ? 'tiff' : mode === 'RENDER_PNG' ? 'png' : 'jpg'; + return `${baseName}-001.${extension}`; +} + +function buildExportSummary({ + text, + modeLabel, + groupsCount, + fileCount, + intent, + operation, + exportTarget, + hasDestination, +}: { + text: CopyTable; + modeLabel: string; + groupsCount: number; + fileCount: number; + intent: ExportIntent; + operation: ExportOperation; + exportTarget: ExportTarget; + hasDestination: boolean; +}) { + if (exportTarget === 'LIGHTROOM_CLASSIC') return text.lightroomReadySummary; + if (!hasDestination) return text.selectDestinationFirst; + + const action = intent === 'RENDER_COPY' + ? text.willRender + : operation === 'COPY' + ? text.willCopy + : text.willMove; + return `${action} ${groupsCount} ${text.groups}, ${fileCount} ${text.files} (${modeLabel}) ${text.toDestination}`; +} + +export default ConfirmationModal; diff --git a/src/components/DetailOverlay.tsx b/src/components/DetailOverlay.tsx new file mode 100644 index 0000000..07c43a5 --- /dev/null +++ b/src/components/DetailOverlay.tsx @@ -0,0 +1,116 @@ + +import React from 'react'; +import { PhotoGroup, SelectionState } from '../types'; +import { formatSize } from '../utils/fileHelpers'; + +interface DetailOverlayProps { + group: PhotoGroup; + onClose: () => void; + onToggleSelection: (state: SelectionState) => void; +} + +const DetailOverlay: React.FC = ({ group, onClose, onToggleSelection }) => { + return ( +
+ {/* Main Image Area */} +
+ + + {group.id} + + {/* Floating Controls */} +
+ + + +
+
+ + {/* Info Sidebar */} +
+
+

{group.id}

+

{group.status.replace('_', ' ')}

+
+ +
+ + + + +
+ +
+
+

Camera

+

{group.exif?.model}

+
+
+

Lens

+

{group.exif?.lens}

+
+
+

Captured

+

{group.exif?.dateTime}

+
+
+ +
+

Files

+ {group.jpg && ( +
+ JPG + {formatSize(group.jpg.size)} +
+ )} + {group.raw && ( +
+ {group.raw.extension} (RAW) + {formatSize(group.raw.size)} +
+ )} +
+ +
+ Tip: Use [P] for Pick, [X] for Reject, [U] for Unmark +
+
+
+ ); +}; + +const ExifItem = ({ label, value, icon }: { label: string, value?: string, icon: string }) => ( +
+
+ + {label} +
+ {value || '--'} +
+); + +export default DetailOverlay; diff --git a/src/components/DuplicateReviewWorkspace.tsx b/src/components/DuplicateReviewWorkspace.tsx new file mode 100644 index 0000000..bc83e7a --- /dev/null +++ b/src/components/DuplicateReviewWorkspace.tsx @@ -0,0 +1,553 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ArrowLeft, CheckCircle2, Images, RotateCw, Trophy, WandSparkles, type LucideIcon } from 'lucide-react'; +import { DuplicateGroup, DuplicateReviewStatus, PhotoGroup, SelectionState, type PhotoRating } from '../types'; +import { Language } from '../i18n'; +import { AppIcon } from './ui/AppIcon'; +import { glassInteractive, glassSurface } from './ui/chrome'; +import { aiIssueLabel } from '../utils/aiLabels'; +import { preloadJpegThumbnail } from '../utils/jpegThumbnailLoader'; +import { decodeRawFile, getThumbnailFromCache } from '../utils/rawLoader'; +import LazyThumbnail from './LazyThumbnail'; +import Viewer, { type ViewerAiMode } from './Viewer'; + +interface DuplicateReviewWorkspaceProps { + theme: 'light' | 'dark'; + language: Language; + photos: PhotoGroup[]; + groups: DuplicateGroup[]; + status: DuplicateReviewStatus; + selectedPhotoId?: string; + aiRunning: boolean; + aiViewMode: ViewerAiMode; + onAiViewModeChange: (mode: ViewerAiMode) => void; + onSelectPhoto: (photoId: string) => void; + onNavigatePhoto?: (direction: 'prev' | 'next') => void; + onUpdateSelection: (photoId: string, state: SelectionState) => void; + onUpdateRating: (photoIds: string[], rating: PhotoRating) => void | Promise; + onAiStart: () => void; +} + +const copy = { + zh: { + title: '重复照片选优', + subtitle: 'AI 筛图完成后按相似画面分组,每组只给出一张最佳候选。', + waitingTitle: '等待 AI 筛图完成', + waitingDescription: '重复识别已并入 AI 筛图流程。完成后这里会自动显示重复组和奖杯推荐。', + disabledTitle: '重复检测未启用', + disabledDescription: '在 AI 设置中打开重复检测后,重新运行 AI 筛图即可生成重复组。', + emptyTitle: '没有发现重复照片', + emptyDescription: '当前敏感度下没有可合并比较的重复组,可以在 AI 设置中调到“轻度相似”。', + startAi: '开始 AI 筛图', + group: '重复组', + photos: '张', + similarity: '相似度', + best: '推荐', + bestCurrent: '当前为推荐图', + back: '返回重复组', + open: '双击查看大图', + manualPick: '人工保留优先', + rating: '星级优先', + aiClear: 'AI 正常优先', + quality: '质量评分优先', + }, + en: { + title: 'Duplicate Review', + subtitle: 'After AI culling, similar frames are grouped and one best candidate is recommended.', + waitingTitle: 'Waiting for AI culling', + waitingDescription: 'Duplicate detection runs inside AI culling. Groups and trophies appear here when it finishes.', + disabledTitle: 'Duplicate detection is off', + disabledDescription: 'Turn it on in AI settings, then run AI culling again to create duplicate groups.', + emptyTitle: 'No duplicates found', + emptyDescription: 'No duplicate groups matched the current sensitivity. Try Loose similarity in AI settings.', + startAi: 'Start AI culling', + group: 'Group', + photos: 'photos', + similarity: 'Similarity', + best: 'Best', + bestCurrent: 'Recommended frame', + back: 'Back to groups', + open: 'Double-click to open', + manualPick: 'Manual pick wins', + rating: 'Rating wins', + aiClear: 'AI clear wins', + quality: 'Quality score wins', + }, +}; + +export const DuplicateReviewWorkspace: React.FC = ({ + theme, + language, + photos, + groups, + status, + selectedPhotoId, + aiRunning, + aiViewMode, + onAiViewModeChange, + onSelectPhoto, + onNavigatePhoto, + onUpdateSelection, + onUpdateRating, + onAiStart, +}) => { + const text = copy[language]; + const isDark = theme === 'dark'; + const [viewerOpen, setViewerOpen] = useState(false); + const scrollContainerRef = useRef(null); + const groupRefs = useRef(new Map()); + const photoRefs = useRef(new Map()); + const scrollAlignFrameRef = useRef(null); + const scrollAlignRetryRef = useRef(null); + const photoMap = useMemo(() => new Map(photos.map(photo => [photo.id, photo])), [photos]); + const duplicatePhotos = useMemo( + () => groups + .flatMap(group => group.photoIds) + .map(id => photoMap.get(id)) + .filter((photo): photo is PhotoGroup => Boolean(photo)), + [groups, photoMap], + ); + const selectedGroup = useMemo( + () => selectedPhotoId ? groups.find(group => group.photoIds.includes(selectedPhotoId)) ?? null : null, + [groups, selectedPhotoId], + ); + const bestPhotoIds = useMemo( + () => new Set(groups.map(group => group.bestPhotoId).filter((id): id is string => Boolean(id))), + [groups], + ); + const selectedDuplicateIndex = useMemo( + () => selectedPhotoId ? duplicatePhotos.findIndex(photo => photo.id === selectedPhotoId) : 0, + [duplicatePhotos, selectedPhotoId], + ); + const focusedPhoto = viewerOpen && selectedPhotoId ? photoMap.get(selectedPhotoId) ?? null : null; + const focusedIsBest = focusedPhoto ? bestPhotoIds.has(focusedPhoto.id) : false; + + useEffect(() => { + if (duplicatePhotos.length === 0) return; + preloadDuplicateThumbnailWindow(duplicatePhotos, selectedDuplicateIndex >= 0 ? selectedDuplicateIndex : 0, { ahead: 42, behind: 18 }); + }, [duplicatePhotos, selectedDuplicateIndex]); + + const alignSelectedPhoto = useCallback((behavior: ScrollBehavior = 'smooth') => { + if (!selectedGroup) return false; + const scroller = scrollContainerRef.current; + if (!scroller) return false; + + const element = selectedPhotoId + ? photoRefs.current.get(selectedPhotoId) ?? groupRefs.current.get(selectedGroup.id) + : groupRefs.current.get(selectedGroup.id); + if (!element) return false; + + const scrollerRect = scroller.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + if (elementRect.height <= 0) return false; + + const targetTop = Math.max( + 0, + Math.min( + scroller.scrollHeight - scroller.clientHeight, + scroller.scrollTop + elementRect.top - scrollerRect.top - (scrollerRect.height - elementRect.height) / 2, + ), + ); + + if (Math.abs(targetTop - scroller.scrollTop) <= 8) return true; + scroller.scrollTo({ top: targetTop, behavior }); + return true; + }, [selectedGroup, selectedPhotoId]); + + useEffect(() => { + if (!selectedGroup) return; + + if (scrollAlignFrameRef.current !== null) { + window.cancelAnimationFrame(scrollAlignFrameRef.current); + } + if (scrollAlignRetryRef.current !== null) { + window.clearTimeout(scrollAlignRetryRef.current); + } + + scrollAlignFrameRef.current = window.requestAnimationFrame(() => { + scrollAlignFrameRef.current = null; + const aligned = alignSelectedPhoto('smooth'); + if (aligned) return; + + scrollAlignRetryRef.current = window.setTimeout(() => { + scrollAlignRetryRef.current = null; + alignSelectedPhoto('smooth'); + }, 80); + }); + }, [alignSelectedPhoto, selectedGroup?.id, selectedPhotoId]); + + useEffect(() => { + if (!viewerOpen || !onNavigatePhoto) return; + + const handleViewerKeyDown = (event: KeyboardEvent) => { + const activeTag = document.activeElement?.tagName || ''; + const isEditingText = document.activeElement instanceof HTMLElement && document.activeElement.isContentEditable; + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(activeTag) || isEditingText) return; + if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; + + event.preventDefault(); + event.stopPropagation(); + onNavigatePhoto(event.key === 'ArrowRight' ? 'next' : 'prev'); + }; + + window.addEventListener('keydown', handleViewerKeyDown, true); + return () => window.removeEventListener('keydown', handleViewerKeyDown, true); + }, [onNavigatePhoto, viewerOpen]); + + useEffect(() => () => { + if (scrollAlignFrameRef.current !== null) window.cancelAnimationFrame(scrollAlignFrameRef.current); + if (scrollAlignRetryRef.current !== null) window.clearTimeout(scrollAlignRetryRef.current); + }, []); + + useEffect(() => { + if (viewerOpen && selectedPhotoId && !photoMap.has(selectedPhotoId)) setViewerOpen(false); + }, [photoMap, selectedPhotoId, viewerOpen]); + + if (focusedPhoto) { + return ( +
+ + {focusedIsBest && ( +
+ + {text.bestCurrent} +
+ )} + onUpdateSelection(focusedPhoto.id, state)} + theme={theme} + language={language} + onUpdateRating={rating => { void onUpdateRating([focusedPhoto.id], rating); }} + aiViewMode={aiViewMode} + onAiViewModeChange={onAiViewModeChange} + /> +
+ ); + } + + if (status === 'DISABLED') { + return ; + } + + if (aiRunning || status === 'ANALYZING' || status === 'IDLE') { + return ( + + ); + } + + if (groups.length === 0) { + return ; + } + + return ( +
+
+
+
+ +

{text.title}

+ + {groups.length} + +
+

{text.subtitle}

+
+
+ {duplicatePhotos.length} {text.photos} +
+
+ +
+
+ {groups.map((group, index) => ( + { + if (element) groupRefs.current.set(group.id, element); + else groupRefs.current.delete(group.id); + }} + group={group} + index={index} + photoMap={photoMap} + selectedPhotoId={selectedPhotoId} + theme={theme} + language={language} + labels={text} + photoRefCallback={(photoId, element) => { + if (element) photoRefs.current.set(photoId, element); + else photoRefs.current.delete(photoId); + }} + onSelectPhoto={onSelectPhoto} + onOpenPhoto={photoId => { + onSelectPhoto(photoId); + setViewerOpen(true); + }} + /> + ))} +
+
+
+ ); +}; + +const DuplicateGroupSection = ({ + refCallback, + group, + index, + photoMap, + selectedPhotoId, + theme, + language, + labels, + photoRefCallback, + onSelectPhoto, + onOpenPhoto, +}: { + refCallback: (element: HTMLElement | null) => void; + group: DuplicateGroup; + index: number; + photoMap: Map; + selectedPhotoId?: string; + theme: 'light' | 'dark'; + language: Language; + labels: typeof copy.zh; + photoRefCallback: (photoId: string, element: HTMLElement | null) => void; + onSelectPhoto: (photoId: string) => void; + onOpenPhoto: (photoId: string) => void; +}) => { + const isDark = theme === 'dark'; + const photos = group.photoIds.map(id => photoMap.get(id)).filter((photo): photo is PhotoGroup => Boolean(photo)); + const bestMatch = group.matches.find(match => match.photoId === group.bestPhotoId); + + return ( +
+
+
+ + {labels.group} {index + 1} + + + {photos.length} {labels.photos} + + + {labels.similarity} {Math.round(group.similarity * 100)}% + +
+ {bestMatch && ( + + + {bestReasonLabel(bestMatch.reason, labels)} + + )} +
+
+ {photos.map(photo => { + const match = group.matches.find(item => item.photoId === photo.id); + return ( + photoRefCallback(photo.id, element)} + onSelect={() => onSelectPhoto(photo.id)} + onOpen={() => onOpenPhoto(photo.id)} + /> + ); + })} +
+
+ ); +}; + +const DuplicateTile = ({ + photo, + match, + active, + best, + eager, + theme, + language, + labels, + refCallback, + onSelect, + onOpen, +}: { + photo: PhotoGroup; + match?: DuplicateGroup['matches'][number]; + active: boolean; + best: boolean; + eager: boolean; + theme: 'light' | 'dark'; + language: Language; + labels: typeof copy.zh; + refCallback: (element: HTMLElement | null) => void; + onSelect: () => void; + onOpen: () => void; +}) => { + const isDark = theme === 'dark'; + const issues = photo.ai?.issues ?? []; + + return ( + + ); +}; + +const DuplicateStatusSurface = ({ + theme, + title, + description, + icon, + actionLabel, + onAction, + loading = false, +}: { + theme: 'light' | 'dark'; + title: string; + description: string; + icon: LucideIcon; + actionLabel?: string; + onAction?: () => void; + loading?: boolean; +}) => { + const isDark = theme === 'dark'; + return ( +
+
+ + + +

{title}

+

{description}

+ {actionLabel && onAction && ( + + )} +
+
+ ); +}; + +function bestReasonLabel(reason: string | undefined, labels: typeof copy.zh) { + if (reason === 'manual-pick') return labels.manualPick; + if (reason === 'rating') return labels.rating; + if (reason === 'ai-clear') return labels.aiClear; + return labels.quality; +} + +function preloadDuplicateThumbnailWindow( + photos: PhotoGroup[], + currentIndex: number, + options: { ahead: number; behind: number }, +) { + if (photos.length === 0 || currentIndex < 0) return; + + const maxOffset = Math.max(options.ahead, options.behind); + for (let offset = 0; offset <= maxOffset; offset += 1) { + if (offset <= options.ahead) { + preloadDuplicateThumbnail(photos[currentIndex + offset], offset === 0); + } + if (offset > 0 && offset <= options.behind) { + preloadDuplicateThumbnail(photos[currentIndex - offset], false); + } + } +} + +function preloadDuplicateThumbnail(photo: PhotoGroup | undefined, highPriority: boolean) { + if (!photo) return; + + if (photo.jpg?.path && photo.jpg.previewUrl) { + preloadJpegThumbnail(photo.jpg.path, photo.jpg.previewUrl, 360, highPriority ? 'high' : 'low'); + return; + } + + if (!photo.raw?.path || getThumbnailFromCache(photo.raw.path)) return; + void decodeRawFile(photo.raw.path, true, { + priority: highPriority ? 'high' : 'low', + silent: true, + }).catch(() => undefined); +} diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 0000000..2b32267 --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,503 @@ +import React from 'react'; +import { + Check, + FilePlus2, + FolderOpen, + Image, + PanelRight, + ScanLine, +} from 'lucide-react'; +import { Language } from '../i18n'; +import { ImportProgress, PhotoFilter } from '../types'; +import { AppIcon } from './ui/AppIcon'; +import { BrandLogo } from './ui/BrandLogo'; +import { glassInteractive, glassSurface, photoOverlay } from './ui/chrome'; + +interface EmptyStateProps { + theme: 'light' | 'dark'; + t: any; + language: Language; + filter: PhotoFilter; + hasPhotos: boolean; + onImportFiles: () => void; + onImportFolder: () => void; + importProgress?: ImportProgress; + initialImportActive?: boolean; +} + +const IDLE_IMPORT_PROGRESS: ImportProgress = { + phase: 'idle', + total: 0, + processed: 0, + running: false, +}; + +const copy = { + zh: { + title: '准备导入照片', + description: '选择文件或文件夹。FrameCull AI 会先完成本地扫描、RAW/JPG 匹配和首屏预览准备,再进入选图界面。', + importFiles: '导入文件', + importFilesHint: '选择单张或多张照片', + importFolder: '导入文件夹', + importFolderHint: '扫描一个拍摄目录', + progressTitle: '导入工作台', + progressIdle: '等待照片进入队列', + scan: '扫描文件', + pair: '匹配 RAW/JPG', + metadata: '读取元数据', + preload: '准备预览', + done: '准备完成', + currentFile: '当前文件', + filteredAction: '继续导入', + aiReviewTitle: '暂无 AI 待复查照片', + aiReviewDescription: 'AI 还没有标记复查线索,或当前线索已经完成了人工复查。', + aiNormalTitle: '暂无 AI 正常照片', + aiNormalDescription: '先运行 AI 筛图,或放宽当前星级筛选条件。', + aiPickedTitle: '暂无 AI 精选照片', + aiPickedDescription: '先运行 AI 筛图。重复照片只会收录奖杯推荐的单张,有复查问题的照片不会进入这里。', + duplicateTitle: '暂无重复照片', + duplicateDescription: 'AI 筛图完成后会生成重复组。若没有结果,说明当前敏感度下未发现可合并比较的重复照片。', + groupPhotoTitle: '当前没有合照', + groupPhotoDescription: '这批照片里暂时没有识别到稳定的合照。可以试试导入更多照片,或先运行 AI 筛图后再查看。', + }, + en: { + title: 'Ready to import photos', + description: 'Choose files or a folder. FrameCull AI finishes local scanning, RAW/JPG pairing, and first-screen preview prep before opening the review workspace.', + importFiles: 'Import Files', + importFilesHint: 'Choose one or many photos', + importFolder: 'Import Folder', + importFolderHint: 'Scan a shoot directory', + progressTitle: 'Import workbench', + progressIdle: 'Waiting for photos', + scan: 'Scan files', + pair: 'Pair RAW/JPG', + metadata: 'Read metadata', + preload: 'Prepare previews', + done: 'Ready', + currentFile: 'Current file', + filteredAction: 'Import more', + aiReviewTitle: 'No AI review photos', + aiReviewDescription: 'AI has not flagged review evidence yet, or every flagged photo has already been reviewed.', + aiNormalTitle: 'No AI clear photos', + aiNormalDescription: 'Run AI culling first, or loosen the current star rating filter.', + aiPickedTitle: 'No AI picks yet', + aiPickedDescription: 'Run AI culling first. Duplicate groups only include the trophy best frame, and photos with review issues stay out of this view.', + duplicateTitle: 'No duplicate photos', + duplicateDescription: 'Duplicate groups appear after AI culling completes. If this stays empty, no repeat candidates matched the current sensitivity.', + groupPhotoTitle: 'No group portraits here', + groupPhotoDescription: 'This batch does not have a stable group portrait match yet. Try importing more photos, or run AI culling before checking again.', + }, +}; + +export const EmptyState: React.FC = ({ + theme, + t, + language, + filter, + hasPhotos, + onImportFiles, + onImportFolder, + importProgress = IDLE_IMPORT_PROGRESS, + initialImportActive = false, +}) => { + const text = copy[language]; + const isDark = theme === 'dark'; + const showImportProgress = initialImportActive && importProgress.phase !== 'idle'; + + if (!hasPhotos) { + return ( +
+
+ {showImportProgress ? ( + + ) : ( +
+
+
+ +

+ {text.title} +

+

+ {text.description} +

+
+ + +
+
+
+ )} +
+
+ ); + } + + const filtered = getFilteredCopy(filter, t, text); + + return ( +
+ +
+
+ +
+

+ {filtered.title} +

+

+ {filtered.description} +

+
+ + +
+
+
+ ); +}; + +function getFilteredCopy(filter: PhotoFilter, t: any, text: typeof copy.zh) { + if (filter === 'PICKED') return t.emptyState.picked; + if (filter === 'REJECTED') return t.emptyState.rejected; + if (filter === 'UNMARKED') return t.emptyState.unmarked; + if (filter === 'ORPHANS') return t.emptyState.orphans; + if (filter === 'AI_REVIEW') { + return { + title: text.aiReviewTitle, + description: text.aiReviewDescription, + }; + } + if (filter === 'AI_NORMAL') { + return { + title: text.aiNormalTitle, + description: text.aiNormalDescription, + }; + } + if (filter === 'AI_PICKED') { + return { + title: text.aiPickedTitle, + description: text.aiPickedDescription, + }; + } + if (filter === 'DUPLICATES') { + return { + title: text.duplicateTitle, + description: text.duplicateDescription, + }; + } + if (filter === 'GROUP_PHOTO') { + return { + title: text.groupPhotoTitle, + description: text.groupPhotoDescription, + }; + } + return t.emptyState.all; +} + +const WorkspaceBackdrop = ({ theme, subtle = false }: { theme: 'light' | 'dark'; subtle?: boolean }) => { + const isDark = theme === 'dark'; + return ( +