diff --git a/.github/workflows/direct.yml b/.github/workflows/direct.yml new file mode 100644 index 0000000..d72b46d --- /dev/null +++ b/.github/workflows/direct.yml @@ -0,0 +1,101 @@ +name: Direct mode +# Builds AXRB Direct (the emulator-free runner) whenever it or what it embeds +# changes; releases build it as part of the launcher setup (release.yml). +on: + push: + paths: ['direct/**', 'runtime/**', 'protocol/**', 'scripts/build/build_direct.ps1', 'scripts/build/direct_deps.ps1', + 'scripts/run/run_direct_game.ps1', '.github/workflows/direct.yml'] + pull_request: + paths: ['direct/**', 'runtime/**', 'protocol/**', 'scripts/build/build_direct.ps1', 'scripts/build/direct_deps.ps1', + 'scripts/run/run_direct_game.ps1', '.github/workflows/direct.yml'] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: direct-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + build: + runs-on: windows-2022 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: android-actions/setup-android@v3 + with: + packages: 'platform-tools' + - name: Install Android build dependencies + shell: pwsh + run: | + sdkmanager 'platforms;android-29' 'platforms;android-36' 'build-tools;36.1.0' 'build-tools;35.0.0' 'ndk;29.0.14206865' 'cmake;3.22.1' + if ($LASTEXITCODE -ne 0) { throw 'Android SDK installation failed' } + "ANDROID_NDK_HOME=$env:ANDROID_HOME/ndk/29.0.14206865" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + $target = Join-Path $env:LOCALAPPDATA 'Android/Sdk' + if (!(Test-Path -LiteralPath $target)) { + New-Item -ItemType Directory -Force (Split-Path $target) | Out-Null + New-Item -ItemType Junction -Path $target -Target $env:ANDROID_HOME | Out-Null + } + - uses: actions/cache@v4 + with: + path: out/deps + key: direct-deps-${{ hashFiles('scripts/build/direct_deps.ps1', 'scripts/build/build_direct.ps1', 'direct/tools/make_sysroot.py') }} + - name: Build the arm64 OpenXR runtime + shell: powershell + run: ./runtime/apk/build_apk.ps1 -Abi arm64-v8a -Sdk "$env:ANDROID_HOME" -NdkVersion 29.0.14206865 + - name: Build direct mode + shell: powershell + run: ./scripts/build/build_direct.ps1 -Sdk "$env:ANDROID_HOME" + - name: Check native time conversions and translators + shell: powershell + run: | + cmake --build out/distribution/build-direct --config Release --target axrb-time-math-test + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + ctest --test-dir out/distribution/build-direct -C Release --output-on-failure + exit $LASTEXITCODE + - name: Check the system root builder + run: python -m unittest discover -s direct/tests -p test_art_session.py + - name: Check explicit companion APK staging and patch probes + shell: powershell + run: | + python -m unittest discover -s direct/tests -p test_companion_apks.py + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + ./direct/tests/patch_probe.ps1 + exit $LASTEXITCODE + - name: Build and test the experimental native x64 boundary + shell: powershell + run: | + cmake -S direct/x64 -B out/native-x64 -G "Visual Studio 17 2022" -A x64 "-DAXRB_NDK=$env:ANDROID_NDK_HOME" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cmake --build out/native-x64 --config Release --parallel + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + ctest --test-dir out/native-x64 -C Release --output-on-failure + exit $LASTEXITCODE + - name: Exercise real ART, JNI, Binder and AOSP framework + shell: powershell + run: | + python direct/tools/test_art.py --runner out/direct/axrb-direct.exe --sysroot out/deps/aosp-36-r02-art --work out/art-tests --ndk "$env:ANDROID_HOME/ndk/29.0.14206865" --build-tools "$env:ANDROID_HOME/build-tools/35.0.0" --framework + exit $LASTEXITCODE + - name: Exercise explicit companion package contexts and code loading + shell: powershell + run: | + python direct/tools/test_companion_apks.py --direct out/direct --sysroot out/deps/aosp-36-r02-art --work out/companion-apk-probe --android-jar "$env:ANDROID_HOME/platforms/android-36/android.jar" --build-tools "$env:ANDROID_HOME/build-tools/35.0.0" + exit $LASTEXITCODE + - name: Exercise Digitalis with real Android probes + shell: powershell + run: | + $env:AXRB_DIRECT_TRANSLATOR = 'digitalis' + python direct/tools/test_art.py --runner out/direct/axrb-direct.exe --sysroot out/deps/aosp-36-r02-art --work out/art-tests-digitalis --ndk "$env:ANDROID_HOME/ndk/29.0.14206865" --build-tools "$env:ANDROID_HOME/build-tools/35.0.0" --framework + exit $LASTEXITCODE + - uses: actions/upload-artifact@v4 + with: + name: axrb-direct-${{ github.sha }} + if-no-files-found: error + path: out/direct/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ffb7fa..79c329e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: needs: version if: needs.version.outputs.changed == 'true' runs-on: windows-2022 - timeout-minutes: 90 + timeout-minutes: 120 env: RELEASE_BUILD: ${{ github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch && github.repository == 'Android-XR-Bridge/AXRB' }} steps: @@ -58,7 +58,7 @@ jobs: - name: Install Android build dependencies shell: pwsh run: | - sdkmanager 'platforms;android-29' 'build-tools;36.1.0' 'ndk;27.3.13750724' 'cmake;3.22.1' + sdkmanager 'platforms;android-29' 'platforms;android-36' 'build-tools;36.1.0' 'build-tools;35.0.0' 'ndk;29.0.14206865' 'cmake;3.22.1' if ($LASTEXITCODE -ne 0) { throw 'Android SDK installation failed' } $target = Join-Path $env:LOCALAPPDATA 'Android/Sdk' if (!(Test-Path -LiteralPath $target)) { @@ -95,6 +95,11 @@ jobs: "CSC_LINK=$file" >> $env:GITHUB_ENV "CSC_KEY_PASSWORD=$env:CERT_PASSWORD" >> $env:GITHUB_ENV } + # Direct mode builds Dynarmic and fetches pinned Boost headers (scripts/build/direct_deps.ps1). + - uses: actions/cache@v4 + with: + path: out/deps + key: direct-deps-${{ hashFiles('scripts/build/direct_deps.ps1') }} - name: Build complete setup shell: powershell run: ./scripts/build/build-launcher.ps1 -Setup -KeepUnpacked @@ -104,8 +109,8 @@ jobs: overwrite: true if-no-files-found: error path: | - out/releases/AXRB-Setup-*.exe - out/releases/AXRB-Setup-*.exe.blockmap + out/releases/AXRB-Emulatorless-Setup-*.exe + out/releases/AXRB-Emulatorless-Setup-*.exe.blockmap out/releases/*-source.zip out/releases/*-setup.txt out/releases/*.yml diff --git a/.gitignore b/.gitignore index 0dfcee9..37cc7e1 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ axrb-windows-bridge.*.log *.pdb *.ilk *.obj +# Prebuilt ISPC objects (scripts/build/build_bc7e.py) +!runtime/vulkan/texture/bc7e/win64/*.obj *.VC.db *.VC.VC.opendb diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf65193..f677ad1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,10 @@ # Contributing to AXRB -Read [LICENSE](LICENSE) and the [coverage list](licenses/source-available-files.txt) -before copying code between components. - -For source-available components, public contribution forks and pull requests -are welcome under section 3 of the -[AXRB Source-Available License](licenses/AXRB-Source-Available-1.0.txt). -That permission covers contributions and review, not separately distributed -modified products. You retain ownership of your contributions. - -Launcher contributions remain GPL-3.0-or-later. Do not copy restricted native -code into the GPL launcher or GPL-derived code into restricted components. -Third-party and previously MIT-licensed material retains its existing terms. +AXRB is MIT-licensed, except for the launcher in `launcher/`, which is +GPL-3.0-or-later. Contributions are accepted under the license of the +component they change. Do not copy GPL-covered launcher code into the +MIT-licensed components. Only submit material you have the right to contribute. Identify third-party code and preserve upstream notices. Do not include credentials, game assets, -private logs or personal runtime data. Changes to the coverage list require -an ownership and license check; new files are not automatically covered. - -Request separate distribution permissions through this repository's maintainers. +private logs or personal runtime data. diff --git a/LICENSE b/LICENSE index 9dcf401..413c51f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,37 +1,21 @@ -AXRB licensing map -================== - -This repository contains separately licensed components. It is not licensed -in its entirety under MIT or under the AXRB Source-Available License. - -1. New AXRB-owned contributions in licenses/source-available-files.txt, - introduced after baseline commit - 19653e2c29e9cd93c25f8d0c906fa54e4c05b3c3, are offered under the AXRB - Source-Available License 1.0 in licenses/AXRB-Source-Available-1.0.txt. - Only contributions whose relevant rights the Licensor controls are covered. - Listing a file does not assert ownership of its pre-existing contents. - -2. The launcher is distributed under GPL-3.0-or-later; see launcher/LICENSE - and launcher/THIRD_PARTY_NOTICES.md. The source-available restrictions do - not apply to the launcher, its modifications, or other GPL-covered work. - Previously MIT-licensed launcher portions retain their separate grant. - -3. Previously released MIT code retains the permissions granted with those - copies. The original MIT notice is preserved in licenses/MIT-legacy.txt. - This change does not revoke or narrow any earlier license grant. - -4. Third-party code, imported experiments, generated third-party material and - dependencies retain their respective licenses and notices. Those notices - take precedence over the coverage list. See launcher/THIRD_PARTY_NOTICES.md - and the individual source files. This license grants no third-party rights. - -5. Unlisted AXRB-owned files retain the previous MIT terms unless they carry - another explicit license. New files are not automatically source-available: - they must be deliberately added to the coverage list after a rights check. - -Restrictions apply to covered code, not merely to the AXRB name: renaming a -derivative does not remove them. Independent programs, including the GPL -launcher, retain their own rights even when distributed alongside AXRB. - -Read CONTRIBUTING.md before submitting changes. Request separate permissions -from the maintainers at https://github.com/Android-XR-Bridge/AXRB. +MIT License + +Copyright (c) 2026 Feline Reintgen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6bc08af..a38c86b 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,10 @@ copyrighted game files. ## License and attribution -AXRB uses multiple licenses; see [LICENSE](LICENSE) for the exact scope. -Designated new AXRB-owned contributions are source-available: use, unchanged -forks and contributions to AXRB are allowed, while distributing modified -standalone projects requires separate permission. These restrictions do not -apply to the GPL launcher, third-party code, or earlier MIT releases. +AXRB is released under the [MIT License](LICENSE). The launcher in +`launcher/` is the exception: it contains GPL-licensed code and is +distributed under GPL-3.0-or-later ([launcher/LICENSE](launcher/LICENSE)). +Third-party code keeps its own license and notices. See [CONTRIBUTING.md](CONTRIBUTING.md) before submitting changes. Third-party components and RiftLift attribution are listed in [launcher/THIRD_PARTY_NOTICES.md](launcher/THIRD_PARTY_NOTICES.md). diff --git a/direct/CMakeLists.txt b/direct/CMakeLists.txt new file mode 100644 index 0000000..4b3a9e6 --- /dev/null +++ b/direct/CMakeLists.txt @@ -0,0 +1,140 @@ +cmake_minimum_required(VERSION 3.24) +project(axrb_direct LANGUAGES C CXX) + +# AXRB Direct: runs an Android ARM64 application in an ordinary Windows +# process. Guest code executes through Dynarmic; the Android userspace is the +# unmodified AOSP arm64 bionic/linker from the SDK system image, with Linux +# system calls answered here. + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +get_filename_component(AXRB_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) +set(DYNARMIC_SOURCE "${AXRB_ROOT}/third_party/dynarmic" CACHE PATH "Pinned Dynarmic checkout") +set(BOOST_ROOT "${AXRB_ROOT}/third_party/boost_1_84_0" CACHE PATH "Boost headers for Dynarmic") +set(Boost_INCLUDE_DIR "${BOOST_ROOT}" CACHE PATH "") +set(Boost_NO_BOOST_CMAKE ON CACHE BOOL "") + +set(DYNARMIC_FRONTENDS A64 CACHE STRING "" FORCE) +set(DYNARMIC_TESTS OFF CACHE BOOL "" FORCE) +set(DYNARMIC_USE_BUNDLED_EXTERNALS ON CACHE BOOL "" FORCE) +set(DYNARMIC_WARNINGS_AS_ERRORS OFF CACHE BOOL "" FORCE) +set(CMAKE_POLICY_VERSION_MINIMUM 3.5) +set(CMAKE_POLICY_DEFAULT_CMP0167 OLD) +add_subdirectory("${DYNARMIC_SOURCE}" dynarmic EXCLUDE_FROM_ALL) + +set(AXRB_NDK "$ENV{ANDROID_NDK_HOME}" CACHE PATH "Android NDK used to build the guest-side helpers") +if(NOT AXRB_NDK) + file(GLOB _ndks "$ENV{LOCALAPPDATA}/Android/Sdk/ndk/*") + list(SORT _ndks COMPARE NATURAL ORDER DESCENDING) + list(GET _ndks 0 AXRB_NDK) +endif() +set(NDK_BIN "${AXRB_NDK}/toolchains/llvm/prebuilt/windows-x86_64/bin") +if(NOT EXISTS "${NDK_BIN}/clang.exe") + message(FATAL_ERROR "AXRB_NDK must name an installed Android NDK (found '${AXRB_NDK}')") +endif() + +# Vulkan headers only (never the Android C library headers on the host). +set(_ndk_include "${AXRB_NDK}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include") +file(COPY "${_ndk_include}/vulkan" "${_ndk_include}/vk_video" DESTINATION "${CMAKE_BINARY_DIR}/vulkan-headers") + +add_subdirectory(guest) +enable_testing() +add_executable(axrb-time-math-test tests/time_math.cpp) +target_include_directories(axrb-time-math-test PRIVATE src) +add_test(NAME time-math COMMAND axrb-time-math-test) +add_subdirectory(digitalis) +option(AXRB_BUILD_LEGACY_BOOT "Build the former experimental managed interpreter boot DEX" OFF) +add_subdirectory(vm) + +add_executable(axrb-direct + src/main.cpp + src/log.cpp + src/memory.cpp + src/cpu.cpp + src/cpu_engine.cpp + src/fallback.cpp + src/kernel.cpp + src/vfs.cpp + src/files.cpp + src/binder.cpp + src/threads.cpp + src/signals.cpp + src/net.cpp + src/exec.cpp + src/sysroot.cpp + src/props.cpp + src/app.cpp + src/jni.cpp + src/java_vfs.cpp + src/natives.cpp + src/ndk_android.cpp + src/audio.cpp + src/vk_host.cpp + src/vk_texture.cpp + src/media.cpp + src/media_codec.cpp + src/profiler.cpp + src/window.cpp +) +target_include_directories(axrb-direct PRIVATE src "${CMAKE_BINARY_DIR}/vulkan-headers" "${AXRB_ROOT}/protocol") +# The emulator layer's texture transcoding (runtime/vulkan/texture), built for the +# host with Android header shims. bc7e links the prebuilt Windows objects ISPC +# makes from bc7e.ispc (scripts/build/build_bc7e.py), so no ISPC is needed here. +set_source_files_properties(src/vk_texture.cpp PROPERTIES INCLUDE_DIRECTORIES + "${CMAKE_CURRENT_LIST_DIR}/src/compat;${AXRB_ROOT}/runtime/vulkan") +set(_bc7e_dir "${AXRB_ROOT}/runtime/vulkan/texture/bc7e/win64") +target_sources(axrb-direct PRIVATE "${_bc7e_dir}/bc7e.obj" "${_bc7e_dir}/bc7e_sse4.obj" "${_bc7e_dir}/bc7e_avx2.obj") +target_compile_definitions(axrb-direct PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS UNICODE _UNICODE) +target_compile_options(axrb-direct PRIVATE /W3 /utf-8 /EHa /Zi /MP) +target_link_options(axrb-direct PRIVATE /DEBUG /OPT:REF /OPT:ICF /STACK:16777216) +target_link_libraries(axrb-direct PRIVATE arun_framework_host arun_managed arun_resources arun_manifest arun_package arun_vfs arun_diagnostics dynarmic axrb-digitalis ws2_32 bcrypt ntdll synchronization winmm dbghelp onecore psapi shell32 ole32 mfplat mfreadwrite mfuuid shlwapi) +add_dependencies(axrb-direct axrb_guest) +if(AXRB_BUILD_LEGACY_BOOT) + add_dependencies(axrb-direct arun_boot_dex) +endif() + +# AXRB's OpenXR runtime (arm64, built with the NDK) and the host GPU-share layer, +# installed beside the runner when they have been built. +set(AXRB_OPENXR_RUNTIME_SO "${AXRB_ROOT}/build/runtime-arm64/runtime/src/libopenxr_runtime.so" CACHE FILEPATH + "arm64 libopenxr_runtime.so for the guest") +set(AXRB_GPU_LAYER_DLL "${AXRB_ROOT}/build/gpu/Release/axrb_gpu_layer.dll" CACHE FILEPATH + "Host GPU-share Vulkan layer") +if(EXISTS "${AXRB_OPENXR_RUNTIME_SO}") + add_custom_command(TARGET axrb-direct POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${AXRB_OPENXR_RUNTIME_SO}" "${CMAKE_BINARY_DIR}/guest/lib64/libopenxr_runtime.so") +endif() +if(EXISTS "${AXRB_GPU_LAYER_DLL}") + file(WRITE "${CMAKE_BINARY_DIR}/axrb_gpu_layer.json" + "{\"file_format_version\":\"1.2.0\",\"layer\":{\"name\":\"VK_LAYER_AXRB_gpu_share\",\"type\":\"GLOBAL\",\"library_path\":\".\\\\axrb_gpu_layer.dll\",\"api_version\":\"1.3.0\",\"implementation_version\":\"1\",\"description\":\"AXRB Windows shared eye textures\"}} +") + add_custom_command(TARGET axrb-direct POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/gpu" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${AXRB_GPU_LAYER_DLL}" "$/gpu/axrb_gpu_layer.dll" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/axrb_gpu_layer.json" "$/gpu/axrb_gpu_layer.json") +endif() + +# Developer-only embedding of the proven ARM userspace for native x86 ART. +# Kept out of normal launcher/runtime builds until mixed-ISA game tests pass. +option(AXRB_BUILD_ARM_SERVICE "Build experimental embedded ARM service" OFF) +if(AXRB_BUILD_ARM_SERVICE) + get_target_property(_arm_sources axrb-direct SOURCES) + list(REMOVE_ITEM _arm_sources src/main.cpp) + add_library(axrb-arm-service SHARED ${_arm_sources} x64/arm_service.cpp) + get_target_property(_arm_includes axrb-direct INCLUDE_DIRECTORIES) + get_target_property(_arm_libraries axrb-direct LINK_LIBRARIES) + target_include_directories(axrb-arm-service PRIVATE ${_arm_includes}) + target_compile_definitions(axrb-arm-service PRIVATE AXRB_ARM_SERVICE_EMBEDDED WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS UNICODE _UNICODE) + target_compile_options(axrb-arm-service PRIVATE /W3 /utf-8 /EHa /Zi /MP) + target_link_options(axrb-arm-service PRIVATE /DEBUG /OPT:REF /OPT:ICF) + target_link_libraries(axrb-arm-service PRIVATE ${_arm_libraries}) + add_executable(arm-service-test x64/tests/arm_service_test.cpp) + target_link_libraries(arm-service-test PRIVATE axrb-arm-service) + target_link_options(arm-service-test PRIVATE /STACK:16777216) + add_custom_command(OUTPUT "${CMAKE_BINARY_DIR}/native-arm-helper" + COMMAND "${NDK_BIN}/clang.exe" --target=aarch64-linux-android29 -O2 -Wall -Werror + "${CMAKE_CURRENT_SOURCE_DIR}/x64/arm_service_guest.c" -ldl -o "${CMAKE_BINARY_DIR}/native-arm-helper" + DEPENDS x64/arm_service_guest.c VERBATIM) + add_custom_target(native-arm-helper ALL DEPENDS "${CMAKE_BINARY_DIR}/native-arm-helper") +endif() diff --git a/direct/art/art_binder.cpp b/direct/art/art_binder.cpp new file mode 100644 index 0000000..0955ac2 --- /dev/null +++ b/direct/art/art_binder.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include +#include +#include + +// AOSP sp is one pointer, with a non-trivial destructor (indirect AAPCS64 +// return). These two process-lifetime references deliberately stay retained. +// No private object fields or vtable offsets are accessed. +struct RetainedStrong { + void* pointer; + ~RetainedStrong() {} +}; + +static JavaVM* java_vm; +static jobject java_registry; +static jclass parcel_class; +static jmethodID parcel_obtain, parcel_recycle, binder_transact; +static void* (*view_parcel)(const AParcel*); + +static void* binder_symbol(const char* name) { + if(void* symbol=dlsym(RTLD_DEFAULT,name)) return symbol; + // app_process loads these in the platform namespace. A bridge loaded by + // ART's class-loader namespace may need explicit handles to see them. + static void* binder=dlopen("libbinder.so",RTLD_NOW); + static void* runtime=dlopen("libandroid_runtime.so",RTLD_NOW); + static void* ndk=dlopen("libbinder_ndk.so",RTLD_NOW); + for(void* handle:{binder,runtime,ndk}) + if(handle) if(void* symbol=dlsym(handle,name)) return symbol; + return nullptr; +} + +static binder_status_t registry_transaction(AIBinder*, transaction_code_t code, + const AParcel* input, AParcel* output) { + JNIEnv* env = nullptr; + bool attached = java_vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK; + if (attached && java_vm->AttachCurrentThreadAsDaemon(&env, nullptr) != JNI_OK) return STATUS_FAILED_TRANSACTION; + jobject in = env->CallStaticObjectMethod(parcel_class, parcel_obtain, reinterpret_cast(view_parcel(input))); + jobject out = env->CallStaticObjectMethod(parcel_class, parcel_obtain, reinterpret_cast(view_parcel(output))); + jboolean handled = env->CallBooleanMethod(java_registry, binder_transact, code, in, out, 0); + binder_status_t result = handled ? STATUS_OK : STATUS_UNKNOWN_TRANSACTION; + if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); result = STATUS_FAILED_TRANSACTION; } + env->CallVoidMethod(in, parcel_recycle); + env->CallVoidMethod(out, parcel_recycle); + env->DeleteLocalRef(in); + env->DeleteLocalRef(out); + if (attached) java_vm->DetachCurrentThread(); + return result; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_com_axrb_runtime_NativeRegistry_installNative( + JNIEnv* env, jclass, jobject registry) { + auto convert = reinterpret_cast( + binder_symbol("_ZN7android20ibinderForJavaObjectEP7_JNIEnvP8_jobject")); + auto process = reinterpret_cast( + binder_symbol("_ZN7android12ProcessState4selfEv")); + auto thread = reinterpret_cast( + binder_symbol("_ZN7android14IPCThreadState4selfEv")); + auto context = reinterpret_cast( + binder_symbol("_ZN7android14IPCThreadState19setTheContextObjectERKNS_2spINS_7BBinderEEE")); + auto become = reinterpret_cast( + binder_symbol("_ZN7android12ProcessState20becomeContextManagerEv")); + if (!convert || !process || !thread || !context || !become) { + fprintf(stderr,"AXRB Binder registry: missing API convert=%p process=%p thread=%p context=%p become=%p\n",reinterpret_cast(convert),reinterpret_cast(process),reinterpret_cast(thread),reinterpret_cast(context),reinterpret_cast(become)); + return JNI_FALSE; + } + env->GetJavaVM(&java_vm); + java_registry = env->NewGlobalRef(registry); + parcel_class = static_cast(env->NewGlobalRef(env->FindClass("android/os/Parcel"))); + parcel_obtain = env->GetStaticMethodID(parcel_class, "obtain", "(J)Landroid/os/Parcel;"); + parcel_recycle = env->GetMethodID(parcel_class, "recycle", "()V"); + binder_transact = env->GetMethodID(env->GetObjectClass(registry), "transact", "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z"); + view_parcel = reinterpret_cast(binder_symbol("_Z26AParcel_viewPlatformParcelPK7AParcel")); + if (!parcel_obtain || !parcel_recycle || !binder_transact || !view_parcel) return JNI_FALSE; + static AIBinder_Class* type = AIBinder_Class_define("android.os.IServiceManager", + [](void* data) { return data; }, [](void*) {}, registry_transaction); + static AIBinder* native = AIBinder_new(type, nullptr); + jobject wrapped = AIBinder_toJavaBinder(env, native); + static RetainedStrong binder = convert(env, wrapped); + env->DeleteLocalRef(wrapped); + static RetainedStrong state = process(); + if (!binder.pointer || !state.pointer) return JNI_FALSE; + context(thread(), binder); + bool installed=become(state.pointer); + if(!installed) fprintf(stderr,"AXRB Binder registry: becomeContextManager failed\n"); + return installed ? JNI_TRUE : JNI_FALSE; +} diff --git a/direct/art/art_bridge.c b/direct/art/art_bridge.c new file mode 100644 index 0000000..7f3db9a --- /dev/null +++ b/direct/art/art_bridge.c @@ -0,0 +1,225 @@ +#include +#include +#include +#include +#include "../guest/axrb_guest.h" + +extern long axrb_window_register(ANativeWindow*, int, int); +extern int register_art_media(JNIEnv*); +AXRB_HOST_FUNCTION(axrb_window_register, "art.window_register"); +extern long axrb_stop_requested(void); +AXRB_HOST_FUNCTION(axrb_stop_requested, "art.stop_requested"); +JNIEXPORT jboolean JNICALL Java_com_axrb_runtime_AppProcess_stopRequested(JNIEnv* env, jclass type) { + (void)env; (void)type; + return axrb_stop_requested() != 0; +} + +static JavaVM* frame_vm; +static jclass frame_class; +static jmethodID frame_create, frame_schedule, frame_latest, frame_dispose; +static jlong display_create(JNIEnv* env, jclass type, jobject receiver, jobject event, jobject queue, jint source, jint flags, jlong layer) { + (void)type; (void)queue; (void)source; (void)flags; (void)layer; + return (*env)->CallStaticLongMethod(env, frame_class, frame_create, receiver, event); +} +static void display_schedule(JNIEnv* env, jclass type, jlong receiver) { + (void)type; (*env)->CallStaticVoidMethod(env, frame_class, frame_schedule, receiver); +} +static jobject display_latest(JNIEnv* env, jclass type, jlong receiver) { + (void)type; return (*env)->CallStaticObjectMethod(env, frame_class, frame_latest, receiver); +} +static void display_dispose(void* receiver) { + JNIEnv* env = 0; + int attached = (*frame_vm)->GetEnv(frame_vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK; + if (attached && (*frame_vm)->AttachCurrentThreadAsDaemon(frame_vm, &env, 0) != JNI_OK) return; + (*env)->CallStaticVoidMethod(env, frame_class, frame_dispose, (jlong)(uintptr_t)receiver); + if (attached) (*frame_vm)->DetachCurrentThread(frame_vm); +} +static jlong display_finalizer(JNIEnv* env, jclass type) { (void)env; (void)type; return (jlong)(uintptr_t)display_dispose; } + +// The Direct WASAPI backend uses 48 kHz output and 256-frame bursts. +static jint audio_rate(JNIEnv* env, jobject type, ...) { (void)env; (void)type; return 48000; } +static jint audio_frames(JNIEnv* env, jobject type) { (void)env; (void)type; return 256; } +static jint audio_latency(JNIEnv* env, jobject type, jint stream) { (void)env; (void)type; (void)stream; return 20; } +static jint audio_connection(JNIEnv* env, jobject type, jint device, jstring address) { + (void)env; (void)type; (void)address; return device == 2 || device == 1 ? 1 : 0; +} +static jint audio_new_id(JNIEnv* env, jobject type) { + (void)env; (void)type; static atomic_int next = 1000; return atomic_fetch_add(&next, 1); +} +static jboolean audio_inactive(JNIEnv* env, jobject type, ...) { (void)env; (void)type; return JNI_FALSE; } +static jstring audio_parameters(JNIEnv* env, jobject type, jstring keys) { (void)type; (void)keys; return (*env)->NewStringUTF(env, ""); } +static jint audio_devices(JNIEnv* env, jobject type, jobject attributes, jobjectArray devices, jboolean volume) { + (void)env; (void)type; (void)attributes; (void)devices; (void)volume; return 0; +} +static jint audio_patches(JNIEnv* env, jobject type, jobject list, jintArray generation) { + (void)type; + if (!list || !generation || (*env)->GetArrayLength(env, generation) != 1) return -2; + jclass list_type = (*env)->GetObjectClass(env, list); + jmethodID clear = (*env)->GetMethodID(env, list_type, "clear", "()V"); + (*env)->CallVoidMethod(env, list, clear); + (*env)->DeleteLocalRef(env, list_type); + jint value = 1; (*env)->SetIntArrayRegion(env, generation, 0, 1, &value); + return 0; +} +static jclass audio_host; +static jmethodID audio_list_ports; +static jint audio_ports(JNIEnv* env, jobject type, jobject list, jintArray generation) { + (void)type; + return (*env)->CallStaticIntMethod(env, audio_host, audio_list_ports, list, generation); +} +// The virtual default route is stable for the lifetime of a Direct session. +// AOSP delivers initial device callbacks itself. Do not attach its native +// AudioPolicyService watcher: Direct routes PCM through WASAPI instead. +static void audio_events_setup(JNIEnv* env, jobject type, jobject weak) { (void)env; (void)type; (void)weak; } +static void audio_events_finalize(JNIEnv* env, jobject type) { (void)env; (void)type; } +static jint audio_min_buffer(JNIEnv* env, jobject type, jint rate, jint channels, jint format) { + (void)env; (void)type; + int bytes = format == 2 ? 2 : format == 3 ? 1 : format == 4 ? 4 : 0; + if (rate <= 0 || channels <= 0 || !bytes) return -2; + return ((rate / 50 + 255) / 256) * 256 * channels * bytes; +} + +// Handheld Android sensors are absent on the Windows host. Headset tracking +// remains provided by OpenXR. Keep AOSP's SensorManager and report an empty +// device list instead of blocking on an unavailable Android sensor daemon. +static jlong sensor_create(JNIEnv* env, jclass type, jstring package) { (void)env; (void)type; (void)package; return 1; } +static jboolean sensor_at(JNIEnv* env, jclass type, jlong manager, jobject sensor, jint index) { + (void)env; (void)type; (void)manager; (void)sensor; (void)index; return JNI_FALSE; +} +static void sensor_dynamic(JNIEnv* env, jclass type, jlong manager, jobject list) { + (void)env; (void)type; (void)manager; (void)list; +} +static jboolean sensor_injection(JNIEnv* env, jclass type, jlong manager) { + (void)env; (void)type; (void)manager; return JNI_FALSE; +} +static void sensor_runtime(JNIEnv* env, jclass type, jlong manager, jint device, jobject list) { + (void)env; (void)type; (void)manager; (void)device; (void)list; +} +static jint sensor_unsupported(JNIEnv* env, jclass type, ...) { (void)env; (void)type; return -19; } +static void sensor_destroy_channel(JNIEnv* env, jclass type, jlong manager, jint channel) { + (void)env; (void)type; (void)manager; (void)channel; +} + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + (void)reserved; + JNIEnv* env = 0; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; +#if defined(__x86_64__) + // app_process initializes NativeBridge before registering framework JNI, + // so getAppEnv cannot initialize Build at that point. Apply the translated + // application's ABI view here, once platform JNI registration is complete. + jclass build = (*env)->FindClass(env, "android/os/Build"); + jclass string = (*env)->FindClass(env, "java/lang/String"); + if (!build || !string) return JNI_ERR; + jstring abi = (*env)->NewStringUTF(env, "arm64-v8a"); + jobjectArray abis = (*env)->NewObjectArray(env, 1, string, abi); + (*env)->SetStaticObjectField(env, build, (*env)->GetStaticFieldID(env, build, "CPU_ABI", "Ljava/lang/String;"), abi); + (*env)->SetStaticObjectField(env, build, (*env)->GetStaticFieldID(env, build, "SUPPORTED_ABIS", "[Ljava/lang/String;"), abis); + (*env)->SetStaticObjectField(env, build, (*env)->GetStaticFieldID(env, build, "SUPPORTED_64_BIT_ABIS", "[Ljava/lang/String;"), abis); + jclass system = (*env)->FindClass(env, "java/lang/System"); + jmethodID set_property = (*env)->GetStaticMethodID(env, system, "setProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); + jstring key = (*env)->NewStringUTF(env, "os.arch"), arch = (*env)->NewStringUTF(env, "aarch64"); + jobject previous = (*env)->CallStaticObjectMethod(env, system, set_property, key, arch); + (*env)->DeleteLocalRef(env, previous); (*env)->DeleteLocalRef(env, key); (*env)->DeleteLocalRef(env, arch); + (*env)->DeleteLocalRef(env, system); (*env)->DeleteLocalRef(env, abis); (*env)->DeleteLocalRef(env, abi); + (*env)->DeleteLocalRef(env, string); (*env)->DeleteLocalRef(env, build); + if ((*env)->ExceptionCheck(env)) return JNI_ERR; +#endif + jclass sensors = (*env)->FindClass(env, "android/hardware/SystemSensorManager"); + if (!sensors) return JNI_ERR; + JNINativeMethod methods[] = { + {"nativeCreate", "(Ljava/lang/String;)J", (void*)sensor_create}, + {"nativeGetSensorAtIndex", "(JLandroid/hardware/Sensor;I)Z", (void*)sensor_at}, + {"nativeGetDefaultDeviceSensorAtIndex", "(JLandroid/hardware/Sensor;I)Z", (void*)sensor_at}, + {"nativeGetDynamicSensors", "(JLjava/util/List;)V", (void*)sensor_dynamic}, + {"nativeGetRuntimeSensors", "(JILjava/util/List;)V", (void*)sensor_runtime}, + {"nativeIsDataInjectionEnabled", "(J)Z", (void*)sensor_injection}, + {"nativeIsReplayDataInjectionEnabled", "(J)Z", (void*)sensor_injection}, + {"nativeIsHalBypassReplayDataInjectionEnabled", "(J)Z", (void*)sensor_injection}, + {"nativeCreateDirectChannel", "(JIJIILandroid/hardware/HardwareBuffer;)I", (void*)sensor_unsupported}, + {"nativeDestroyDirectChannel", "(JI)V", (void*)sensor_destroy_channel}, + {"nativeConfigDirectChannel", "(JIII)I", (void*)sensor_unsupported}, + {"nativeSetOperationParameter", "(JII[F[I)I", (void*)sensor_unsupported}, + }; + if ((*env)->RegisterNatives(env, sensors, methods, sizeof methods / sizeof methods[0]) != 0) return JNI_ERR; + frame_vm = vm; + frame_class = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/axrb/runtime/FrameScheduler")); + if (!frame_class) return JNI_ERR; + frame_create = (*env)->GetStaticMethodID(env, frame_class, "create", "(Ljava/lang/Object;Ljava/lang/Object;)J"); + frame_schedule = (*env)->GetStaticMethodID(env, frame_class, "schedule", "(J)V"); + frame_latest = (*env)->GetStaticMethodID(env, frame_class, "latest", "(J)Ljava/lang/Object;"); + frame_dispose = (*env)->GetStaticMethodID(env, frame_class, "dispose", "(J)V"); + jclass display = (*env)->FindClass(env, "android/view/DisplayEventReceiver"); + if (!display || !frame_create || !frame_schedule || !frame_latest || !frame_dispose) return JNI_ERR; + const JNINativeMethod displays[] = { + {"nativeInit", "(Ljava/lang/ref/WeakReference;Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;IIJ)J", (void*)display_create}, + {"nativeScheduleVsync", "(J)V", (void*)display_schedule}, + {"nativeGetLatestVsyncEventData", "(J)Landroid/view/DisplayEventReceiver$VsyncEventData;", (void*)display_latest}, + {"nativeGetDisplayEventReceiverFinalizer", "()J", (void*)display_finalizer}, + }; + if ((*env)->RegisterNatives(env, display, displays, sizeof displays / sizeof displays[0]) != 0) return JNI_ERR; + jmethodID install = (*env)->GetStaticMethodID(env, frame_class, "installFinalizer", "(J)V"); + if (!install) return JNI_ERR; + (*env)->CallStaticVoidMethod(env, frame_class, install, (jlong)(uintptr_t)display_dispose); + if ((*env)->ExceptionCheck(env)) return JNI_ERR; + jclass audio = (*env)->FindClass(env, "android/media/AudioSystem"); + audio_host = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/axrb/runtime/HostAudio")); + if (!audio_host) return JNI_ERR; + audio_list_ports = (*env)->GetStaticMethodID(env, audio_host, "listPorts", "(Ljava/util/ArrayList;[I)I"); + if (!audio_list_ports) return JNI_ERR; + const JNINativeMethod audio_methods[] = { + {"getPrimaryOutputSamplingRate", "()I", (void*)audio_rate}, + {"getPrimaryOutputFrameCount", "()I", (void*)audio_frames}, + {"getOutputLatency", "(I)I", (void*)audio_latency}, + {"getDeviceConnectionState", "(ILjava/lang/String;)I", (void*)audio_connection}, + {"newAudioSessionId", "()I", (void*)audio_new_id}, + {"newAudioPlayerId", "()I", (void*)audio_new_id}, + {"newAudioRecorderId", "()I", (void*)audio_new_id}, + {"isStreamActive", "(II)Z", (void*)audio_inactive}, + {"isStreamActiveRemotely", "(II)Z", (void*)audio_inactive}, + {"getParameters", "(Ljava/lang/String;)Ljava/lang/String;", (void*)audio_parameters}, + {"getDevicesForAttributes", "(Landroid/media/AudioAttributes;[Landroid/media/AudioDeviceAttributes;Z)I", (void*)audio_devices}, + {"listAudioPorts", "(Ljava/util/ArrayList;[I)I", (void*)audio_ports}, + {"listAudioPatches", "(Ljava/util/ArrayList;[I)I", (void*)audio_patches}, + }; + if (!audio || (*env)->RegisterNatives(env, audio, audio_methods, sizeof audio_methods / sizeof audio_methods[0]) != 0) return JNI_ERR; + jclass audio_events = (*env)->FindClass(env, "android/media/AudioPortEventHandler"); + const JNINativeMethod event_methods[] = { + {"native_setup", "(Ljava/lang/Object;)V", (void*)audio_events_setup}, + {"native_finalize", "()V", (void*)audio_events_finalize}, + }; + if (!audio_events || (*env)->RegisterNatives(env, audio_events, event_methods, sizeof event_methods / sizeof event_methods[0]) != 0) return JNI_ERR; + jclass track = (*env)->FindClass(env, "android/media/AudioTrack"); + const JNINativeMethod track_queries[] = { + {"native_get_output_sample_rate", "(I)I", (void*)audio_rate}, + {"native_get_min_buff_size", "(III)I", (void*)audio_min_buffer}, + }; + if (!track || (*env)->RegisterNatives(env, track, track_queries, sizeof track_queries / sizeof track_queries[0]) != 0) return JNI_ERR; + if (register_art_media(env) != 0) return JNI_ERR; + return JNI_VERSION_1_6; +} + +JNIEXPORT jlong JNICALL Java_com_axrb_runtime_HostWindow_registerSurface( + JNIEnv* env, jclass type, jobject surface, jint width, jint height) { + (void)type; + ANativeWindow* window = ANativeWindow_fromSurface(env, surface); + if (!window) return 0; + if (ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888) != 0 || + axrb_window_register(window, width, height) != 0) { + ANativeWindow_release(window); + return 0; + } + // The Java host window holds this reference until its surface is destroyed. + return (jlong)(uintptr_t)window; +} + +JNIEXPORT void JNICALL Java_com_axrb_runtime_HostWindow_releaseSurface(JNIEnv* env, jclass type, jlong window) { + (void)env; (void)type; + if (window) ANativeWindow_release((ANativeWindow*)(uintptr_t)window); +} + +JNIEXPORT jobject JNICALL Java_com_axrb_runtime_HostSurface_bufferAt(JNIEnv* env, jclass type, jlong address, jint bytes) { + (void)type; + if (!address || bytes <= 0) return 0; + return (*env)->NewDirectByteBuffer(env, (void*)(uintptr_t)address, bytes); +} diff --git a/direct/art/art_media.c b/direct/art/art_media.c new file mode 100644 index 0000000..0dcf6ce --- /dev/null +++ b/direct/art/art_media.c @@ -0,0 +1,213 @@ +#include "../guest/axrb_guest.h" +#include +#include + +extern long axrb_media(int op, long id, long a, long b, long c); +AXRB_HOST_FUNCTION(axrb_media, "art.media"); +extern long axrb_media_frame(long surface, long address, int width, int height, + long previous); +AXRB_HOST_FUNCTION(axrb_media_frame, "art.media_frame"); +static jclass backend; +static jmethodID control, setup, surface, invoke; +static jlong call(JNIEnv *e, jobject p, int op, jlong a, jlong b, jlong c) { + return (*e)->CallStaticLongMethod(e, backend, control, p, op, a, b, c); +} +static void player_setup(JNIEnv *e, jobject p, jobject weak, + jobject attribution, jint session) { + (void)attribution; + (*e)->CallStaticVoidMethod(e, backend, setup, p, weak, session); +} +static void player_source(JNIEnv *e, jobject p, jobject fd, jlong offset, + jlong length) { + jclass cls = (*e)->GetObjectClass(e, fd); + jfieldID descriptor = (*e)->GetFieldID(e, cls, "descriptor", "I"); + if (descriptor) + call(e, p, 1, (*e)->GetIntField(e, fd, descriptor), offset, length); +} +static void player_surface(JNIEnv *e, jobject p, jobject value) { + (*e)->CallStaticVoidMethod(e, backend, surface, p, value); +} +static jint player_prepare(JNIEnv *e, jobject p, jobject parcel) { + (void)parcel; + return (jint)call(e, p, 3, 0, 0, 0); +} +static jint player_prepare_async(JNIEnv *e, jobject p, jobject parcel) { + (void)parcel; + return (jint)call(e, p, 103, 0, 0, 0); +} +#define VOID_CONTROL(name, op) \ + static void name(JNIEnv *e, jobject p) { call(e, p, op, 0, 0, 0); } +#define INT_CONTROL(name, op) \ + static jint name(JNIEnv *e, jobject p) { \ + return (jint)call(e, p, op, 0, 0, 0); \ + } +VOID_CONTROL(player_start, 4) +VOID_CONTROL(player_pause, 5) +VOID_CONTROL(player_stop, 6) +VOID_CONTROL(player_release, 12) +VOID_CONTROL(player_reset, 104) +INT_CONTROL(player_position, 10) +INT_CONTROL(player_duration, 11) +INT_CONTROL(player_width, 13) +INT_CONTROL(player_height, 14) +INT_CONTROL(player_session, 100) +static jboolean player_playing(JNIEnv *e, jobject p) { + return call(e, p, 9, 0, 0, 0) != 0; +} +static jboolean player_looping(JNIEnv *e, jobject p) { + return call(e, p, 15, 0, 0, 0) != 0; +} +static void player_loop(JNIEnv *e, jobject p, jboolean v) { + call(e, p, 8, v, 0, 0); +} +static void player_seek(JNIEnv *e, jobject p, jlong ms, jint mode) { + (void)mode; + call(e, p, 7, ms, 0, 0); +} +static void player_set_session(JNIEnv *e, jobject p, jint v) { + call(e, p, 101, v, 0, 0); +} +static void player_noop(JNIEnv *e, jobject p, ...) { + (void)e; + (void)p; +} +static jint player_stream(JNIEnv *e, jobject p) { + (void)e; + (void)p; + return 3; +} +static jboolean player_parameter(JNIEnv *e, jobject p, jint key, + jobject parcel) { + (void)e; + (void)p; + (void)key; + (void)parcel; + return JNI_TRUE; +} +static jint player_invoke(JNIEnv *e, jobject p, jobject request, + jobject reply) { + return (*e)->CallStaticIntMethod(e, backend, invoke, p, request, reply); +} +static void player_volume(JNIEnv *e, jobject p, jfloat left, jfloat right) { + (void)e; + (void)p; + (void)left; + (void)right; +} +static jlong player_unsupported(JNIEnv *e, jobject p, ...) { + (void)p; + jclass error = (*e)->FindClass(e, "java/lang/UnsupportedOperationException"); + if (error) + (*e)->ThrowNew(e, error, "This MediaPlayer operation is not supported by the host media backend"); + return 0; +} +static void player_finalize(JNIEnv *e, jobject p) { + jclass cls = (*e)->GetObjectClass(e, p); + jfieldID context = (*e)->GetFieldID(e, cls, "mNativeContext", "J"); + if (context && (*e)->GetLongField(e, p, context)) + call(e, p, 12, 0, 0, 0); +} +JNIEXPORT jlong JNICALL Java_com_axrb_runtime_HostMedia_command( + JNIEnv *e, jclass cls, jint op, jlong id, jlong a, jlong b, jlong c) { + (void)e; + (void)cls; + return axrb_media(op, id, a, b, c); +} +JNIEXPORT jlong JNICALL Java_com_axrb_runtime_HostMedia_readFrame( + JNIEnv *e, jclass cls, jlong id, jlong dst, jint w, jint h, + jlong previous) { + (void)e; + (void)cls; + return axrb_media_frame(id, dst, w, h, previous); +} +JNIEXPORT void JNICALL Java_com_axrb_runtime_HostMedia_releaseSurface( + JNIEnv *e, jclass cls, jlong id) { + (void)e; + (void)cls; + axrb_media(16, id, 0, 0, 0); +} +int register_art_media(JNIEnv *e) { + backend = + (*e)->NewGlobalRef(e, (*e)->FindClass(e, "com/axrb/runtime/HostMedia")); + if (!backend) + return -1; + control = (*e)->GetStaticMethodID(e, backend, "control", + "(Landroid/media/MediaPlayer;IJJJ)J"); + setup = (*e)->GetStaticMethodID( + e, backend, "setup", "(Landroid/media/MediaPlayer;Ljava/lang/Object;I)V"); + surface = (*e)->GetStaticMethodID( + e, backend, "surface", + "(Landroid/media/MediaPlayer;Landroid/view/Surface;)V"); + invoke = (*e)->GetStaticMethodID( + e, backend, "invoke", + "(Landroid/media/MediaPlayer;Landroid/os/Parcel;Landroid/os/Parcel;)I"); + if (!control || !setup || !surface || !invoke) + return -1; + jclass cls = (*e)->FindClass(e, "android/media/MediaPlayer"); + if (!cls) + return -1; + // Initialize libmedia_jni before replacing its daemon-dependent methods. + // Otherwise MediaPlayer's static initializer re-registers the originals. + if (!(*e)->GetStaticMethodID(e, cls, "native_init", "()V")) + return -1; + const JNINativeMethod methods[] = { + {"native_setup", "(Ljava/lang/Object;Landroid/os/Parcel;I)V", + (void *)player_setup}, + {"_setDataSource", "(Ljava/io/FileDescriptor;JJ)V", + (void *)player_source}, + {"_setVideoSurface", "(Landroid/view/Surface;)V", (void *)player_surface}, + {"_prepare", "(Landroid/os/Parcel;)I", (void *)player_prepare}, + {"_prepareAsync", "(Landroid/os/Parcel;)I", (void *)player_prepare_async}, + {"_start", "()V", (void *)player_start}, + {"_pause", "()V", (void *)player_pause}, + {"_stop", "()V", (void *)player_stop}, + {"_release", "()V", (void *)player_release}, + {"_reset", "()V", (void *)player_reset}, + {"native_finalize", "()V", (void *)player_finalize}, + {"isPlaying", "()Z", (void *)player_playing}, + {"isLooping", "()Z", (void *)player_looping}, + {"getCurrentPosition", "()I", (void *)player_position}, + {"getDuration", "()I", (void *)player_duration}, + {"getVideoWidth", "()I", (void *)player_width}, + {"getVideoHeight", "()I", (void *)player_height}, + {"getAudioSessionId", "()I", (void *)player_session}, + {"native_setAudioSessionId", "(I)V", (void *)player_set_session}, + {"setLooping", "(Z)V", (void *)player_loop}, + {"_seekTo", "(JI)V", (void *)player_seek}, + {"_setVolume", "(FF)V", (void *)player_volume}, + {"_setAudioStreamType", "(I)V", (void *)player_noop}, + {"_getAudioStreamType", "()I", (void *)player_stream}, + {"setParameter", "(ILandroid/os/Parcel;)Z", (void *)player_parameter}, + {"native_invoke", "(Landroid/os/Parcel;Landroid/os/Parcel;)I", + (void *)player_invoke}, + {"native_enableDeviceCallback", "(Z)V", (void *)player_noop}, + // mNativeContext is a host handle, not an android::MediaPlayer pointer. + // Override every remaining instance native boundary in the pinned AOSP + // framework so an unsupported operation raises Java rather than letting + // libmedia_jni dereference the host handle as an Android object. +#define UNSUPPORTED(name, signature) {name, signature, (void *)player_unsupported} + UNSUPPORTED("nativeSetDataSource", "(Landroid/os/IBinder;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V"), + UNSUPPORTED("_setDataSource", "(Landroid/media/MediaDataSource;)V"), + UNSUPPORTED("native_getMetrics", "()Landroid/os/PersistableBundle;"), + UNSUPPORTED("setPlaybackParams", "(Landroid/media/PlaybackParams;)V"), + UNSUPPORTED("getPlaybackParams", "()Landroid/media/PlaybackParams;"), + UNSUPPORTED("setSyncParams", "(Landroid/media/SyncParams;)V"), + UNSUPPORTED("getSyncParams", "()Landroid/media/SyncParams;"), + UNSUPPORTED("_notifyAt", "(J)V"), + UNSUPPORTED("native_setMetadataFilter", "(Landroid/os/Parcel;)I"), + UNSUPPORTED("native_getMetadata", "(ZZLandroid/os/Parcel;)Z"), + UNSUPPORTED("_setAuxEffectSendLevel", "(F)V"), + UNSUPPORTED("attachAuxEffect", "(I)V"), + UNSUPPORTED("native_setRetransmitEndpoint", "(Ljava/lang/String;I)I"), + UNSUPPORTED("setNextMediaPlayer", "(Landroid/media/MediaPlayer;)V"), + UNSUPPORTED("native_applyVolumeShaper", "(Landroid/media/VolumeShaper$Configuration;Landroid/media/VolumeShaper$Operation;)I"), + UNSUPPORTED("native_getVolumeShaperState", "(I)Landroid/media/VolumeShaper$State;"), + UNSUPPORTED("_prepareDrm", "([B[B)V"), + UNSUPPORTED("_releaseDrm", "()V"), + UNSUPPORTED("native_setOutputDevice", "(I)Z"), + UNSUPPORTED("native_getRoutedDeviceIds", "()[I"), +#undef UNSUPPORTED + }; + return (*e)->RegisterNatives(e, cls, methods, + sizeof methods / sizeof methods[0]); +} diff --git a/direct/art/java/com/axrb/runtime/AppProcess.java b/direct/art/java/com/axrb/runtime/AppProcess.java new file mode 100644 index 0000000..97c4de7 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/AppProcess.java @@ -0,0 +1,264 @@ +package com.axrb.runtime; + +import android.os.Looper; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.Intent; +import android.content.ComponentName; +import android.content.res.Configuration; +import android.os.Binder; +import android.os.Process; +import java.io.File; + +/** Real AOSP framework entry point for Direct-mode applications. */ +public final class AppProcess { + private static native boolean stopRequested(); + private static HostWindow window; + private static Object activityThread; + private static PackageInfo application; + private static final java.util.ArrayList activityStack = new java.util.ArrayList<>(); + private static final java.util.Map activityResults = new java.util.HashMap<>(); + private static final java.util.Map activityWindows = new java.util.HashMap<>(); + + static boolean finishActivity(android.os.IBinder token, int resultCode, Intent result) { + new android.os.Handler(Looper.getMainLooper()).post(() -> { + try { + Object finished = null; + for (Object record : activityStack) if (Reflect.field(record, "token") == token) finished = record; + if (finished == null) return; + Object activity = Reflect.field(finished, "activity"); + if ((Integer) Reflect.call(finished, "getLifecycleState") == 3) Reflect.call(activity, "performPause"); + Reflect.call(activity, "performStop", false, "AXRB finish"); + Reflect.call(activity, "performDestroy"); + activityStack.remove(finished); + Object[] returnTo = activityResults.remove(token); + if (returnTo != null && returnTo[0] != null) for (Object record : activityStack) + if (Reflect.field(record, "token") == returnTo[0]) + Reflect.call(Reflect.field(record, "activity"), "dispatchActivityResult", returnTo[1], returnTo[2], resultCode, result, "AXRB activity result"); + if (!activityStack.isEmpty()) { + Object previous = activityStack.get(activityStack.size() - 1); + Reflect.call(activityThread, "performResumeActivity", previous, true, "AXRB return"); + window = activityWindows.get(previous); + window.attach((android.app.Activity) Reflect.field(previous, "activity")); + } + } catch (Exception error) { throw new RuntimeException("Activity finish failed", error); } + }); + return true; + } + + static int requestPermissions(Intent intent, android.os.IBinder caller, String who, + int requestCode, HostPackageManager packages) { + String[] requested = intent.getStringArrayExtra("android.content.pm.extra.REQUEST_PERMISSIONS_NAMES"); + if (requested == null || caller == null || requestCode < 0) return -92; + String[] names = requested.clone(); + int[] results = new int[names.length]; + for (int i = 0; i < names.length; i++) + results[i] = names[i] == null ? PackageManager.PERMISSION_DENIED : packages.permission(names[i]); + Intent result = new Intent() + .putExtra("android.content.pm.extra.REQUEST_PERMISSIONS_NAMES", names) + .putExtra("android.content.pm.extra.REQUEST_PERMISSIONS_RESULTS", results); + // Preserve AOSP Activity/Fragment callback routing and asynchronous + // completion. Unsupported permissions stay denied by the same policy + // used by checkSelfPermission; no camera/microphone access is granted. + new android.os.Handler(Looper.getMainLooper()).post(() -> { + try { + for (Object record : activityStack) if (Reflect.field(record, "token") == caller) { + Reflect.call(Reflect.field(record, "activity"), "dispatchActivityResult", who, + requestCode, android.app.Activity.RESULT_OK, result, "AXRB permission result"); + System.out.println("AXRB_PERMISSION_RESULT " + java.util.Arrays.toString(names) + + " " + java.util.Arrays.toString(results)); + break; + } + } catch (Exception error) { throw new RuntimeException("Permission result delivery failed", error); } + }); + return 0; + } + + static int startActivity(Intent intent, android.os.IBinder caller, String who, int requestCode) { + ComponentName component = intent.getComponent(); + if (component == null || application == null || !application.packageName.equals(component.getPackageName())) return -92; + ActivityInfo target = null; + for (ActivityInfo candidate : application.activities) if (candidate.name.equals(component.getClassName())) target = candidate; + if (target == null) return -92; + ActivityInfo selected = target; + new android.os.Handler(Looper.getMainLooper()).post(() -> { + try { + if (!activityStack.isEmpty()) { + Object previous = activityStack.get(activityStack.size() - 1); + Reflect.call(Reflect.field(previous, "activity"), "performPause"); + Reflect.call(previous, "setState", 4); + } + launchActivity(activityThread, application, selected, intent, caller, who, requestCode); + } catch (Exception error) { throw new RuntimeException("Activity launch failed", error); } + }); + return 0; + } + public static void main(String[] args) { + try { + System.out.println("AXRB_AOSP_BOOTSTRAP"); + System.load("/system/lib64/libaxrb_art_bridge.so"); + Reflect.call(Class.forName("android.graphics.Typeface"), "loadPreinstalledSystemFontMap"); + if (java.util.Arrays.asList(args).contains("--diagnostics")) { + System.setProperty("axrb.inspectViews", "true"); + Thread watchdog = new Thread(() -> { + for (int snapshot = 0; snapshot < 5; ++snapshot) { + try { Thread.sleep(15000); } catch (InterruptedException ignored) { return; } + for (java.util.Map.Entry entry : Thread.getAllStackTraces().entrySet()) { + System.err.println("AXRB_THREAD " + entry.getKey().getName()); + for (StackTraceElement frame : entry.getValue()) System.err.println(" at " + frame); + } + } + }, "AXRB startup diagnostics"); + watchdog.setDaemon(true); + watchdog.start(); + } + Reflect.set(Class.forName("android.view.ThreadedRenderer"), "sRendererEnabled", false); + HostServices services = new HostServices(); + services.install(); + Looper.prepareMainLooper(); + Object thread = Reflect.call(Class.forName("android.app.ActivityThread"), "systemMain"); + Object context = Reflect.call(thread, "getSystemContext"); + services.attach((Context) context); + String legacyDriver = (String) Reflect.call(Class.forName("android.os.SystemProperties"), "get", "ro.axrb.legacy_oculus_driver"); + if (!legacyDriver.isEmpty()) { + services.packages.offerLegacyDriver((Context) context, legacyDriver); + System.out.println("AXRB_AOSP_LEGACY_DRIVER " + legacyDriver); + } + Class properties = Class.forName("android.os.SystemProperties"); + String countText = (String) Reflect.call(properties, "get", "ro.axrb.companion_count"); + int companionCount = countText.isEmpty() ? 0 : Integer.parseInt(countText); + if (companionCount < 0 || companionCount > 8) throw new IllegalArgumentException("Invalid companion package count"); + for (int i = 0; i < companionCount; ++i) { + String path = (String) Reflect.call(properties, "get", "ro.axrb.companion_apk." + i); + services.packages.offerCompanion((Context) context, path); + System.out.println("AXRB_AOSP_COMPANION " + path); + } + Object configuration = Reflect.field(thread, "mConfigurationController"); + Configuration initial = new Configuration(((Context) context).getResources().getConfiguration()); + initial.densityDpi = 320; + initial.screenWidthDp = 960; + initial.screenHeightDp = 540; + initial.smallestScreenWidthDp = 540; + initial.orientation = Configuration.ORIENTATION_LANDSCAPE; + Reflect.call(configuration, "setConfiguration", initial); + Reflect.call(configuration, "setCompatConfiguration", initial); + System.out.println("AXRB_AOSP_CONTEXT " + context); + if (args.length > 0) { + PackageInfo info = ((Context) context).getPackageManager().getPackageArchiveInfo(args[0], + PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | + PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA | PackageManager.GET_PERMISSIONS); + if (info == null) throw new IllegalArgumentException("Cannot parse APK " + args[0]); + services.packages.installGame(info); + if (args.length > 1) services.packages.launcherActivity = args[1]; + System.out.println("AXRB_AOSP_PACKAGE " + info.packageName); + if (args.length > 1) launch(thread, info, args); + } + } catch (Throwable error) { + error.printStackTrace(); + System.exit(1); + } + } + + private static void launch(Object thread, PackageInfo info, String[] args) throws Exception { + ApplicationInfo app = info.applicationInfo; + app.sourceDir = app.publicSourceDir = args[0]; + app.dataDir = "/data/data/" + info.packageName; + app.deviceProtectedDataDir = app.dataDir; + Reflect.set(app, "credentialProtectedDataDir", app.dataDir); + app.nativeLibraryDir = new File(args[0]).getParent() + "/lib/arm64"; + app.uid = Process.myUid(); + Reflect.set(app, "primaryCpuAbi", "arm64-v8a"); + for (String dir : new String[]{"files", "cache", "code_cache", "shared_prefs", "databases"}) + new File(app.dataDir, dir).mkdirs(); + ActivityInfo activity = null; + for (ActivityInfo candidate : info.activities) if (candidate.name.equals(args[1])) activity = candidate; + if (activity == null) throw new IllegalArgumentException("Activity not in manifest: " + args[1]); + activity.applicationInfo = app; + Object loaded = Reflect.call(thread, "getPackageInfoNoCheck", app); + activityThread = thread; + application = info; + Object bound = Reflect.create("android.app.ActivityThread$AppBindData"); + Reflect.set(bound, "appInfo", app); + Reflect.set(bound, "info", loaded); + Reflect.set(bound, "processName", app.processName == null ? app.packageName : app.processName); + Reflect.set(thread, "mBoundApplication", bound); + Object appInstance = Reflect.call(loaded, "makeApplicationInner", false, Reflect.field(thread, "mInstrumentation")); + Reflect.set(thread, "mInitialApplication", appInstance); + launchActivity(thread, info, activity, new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(new ComponentName(info.packageName, activity.name)), null, null, -1); + android.os.Handler handler = new android.os.Handler(Looper.getMainLooper()); + handler.postDelayed(new Runnable() { + public void run() { + if (!stopRequested()) { handler.postDelayed(this, 250); return; } + try { + for (int i = activityStack.size() - 1; i >= 0; --i) { + Object record = activityStack.get(i); + Object current = Reflect.field(record, "activity"); + int state = (Integer)Reflect.call(record, "getLifecycleState"); + if (state == 3) Reflect.call(current, "performPause"); + if (state < 5) Reflect.call(current, "performStop", false, "AXRB shutdown"); + } + // ActivityThread normally drains QueuedWork after stopping + // activities. In this host-driven lifecycle we must do it + // explicitly: SharedPreferences.apply() can still be queued + // when onStop returns, and System.exit would lose that save. + Reflect.call(Class.forName("android.app.QueuedWork"), "waitToFinish"); + // Some engines intentionally terminate their process in + // onDestroy. Record completion of the save lifecycle first. + System.out.println("AXRB_AOSP_ACTIVITIES_STOPPED"); + for (int i = activityStack.size() - 1; i >= 0; --i) { + Object current = Reflect.field(activityStack.get(i), "activity"); + Reflect.call(current, "performDestroy"); + } + // Also finish asynchronous writes requested in onDestroy. + Reflect.call(Class.forName("android.app.QueuedWork"), "waitToFinish"); + System.out.println("AXRB_AOSP_SESSION_CLOSED"); + System.exit(0); + } catch (Throwable error) { + error.printStackTrace(); + System.exit(1); + } + } + }, 250); + Looper.loop(); + } + + private static void launchActivity(Object thread, PackageInfo info, ActivityInfo activity, Intent intent, + android.os.IBinder caller, String who, int requestCode) throws Exception { + activity.applicationInfo = info.applicationInfo; + Object loaded = Reflect.call(thread, "getPackageInfoNoCheck", info.applicationInfo); + Object record = Reflect.create("android.app.ActivityThread$ActivityClientRecord"); + Binder token = new Binder(); + activityResults.put(token, new Object[]{caller, who, requestCode}); + HostActivities.orientations.put(token, activity.screenOrientation); + Reflect.set(record, "token", token); + Reflect.set(record, "assistToken", new Binder()); + Reflect.set(record, "shareableActivityToken", new Binder()); + Reflect.set(record, "activityInfo", activity); + Reflect.set(record, "packageInfo", loaded); + Reflect.set(record, "intent", intent); + activityStack.add(record); + Reflect.set(record, "overrideConfig", new Configuration()); + System.out.println("AXRB_AOSP_LAUNCH " + activity.name); + Object launched = Reflect.call(thread, "performLaunchActivity", record, null); + if (launched == null) throw new IllegalStateException("Activity creation returned null"); + System.out.println("AXRB_AOSP_ACTIVITY_CREATED " + activity.name); + window = new HostWindow(); + HostWindow createdWindow = window; + activityWindows.put(record, createdWindow); + new android.os.Handler(Looper.getMainLooper()).postDelayed(() -> { + try { + Reflect.call(thread, "handleStartActivity", record, null, null); + Object resumed = Reflect.call(thread, "performResumeActivity", record, true, "AXRB launch"); + System.out.println("AXRB_AOSP_LIFECYCLE resumed=" + resumed + " state=" + Reflect.call(record, "getLifecycleState") + + " finished=" + ((android.app.Activity) launched).isFinishing()); + createdWindow.attach((android.app.Activity) launched); + } catch (Exception error) { + throw new RuntimeException("Host window attachment failed", error); + } + }, 100); + } +} diff --git a/direct/art/java/com/axrb/runtime/FrameScheduler.java b/direct/art/java/com/axrb/runtime/FrameScheduler.java new file mode 100644 index 0000000..6d69679 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/FrameScheduler.java @@ -0,0 +1,69 @@ +package com.axrb.runtime; + +import android.os.Handler; +import android.os.Looper; +import java.lang.ref.WeakReference; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** Supplies real AOSP Choreographer with the host's monotonic frame timeline. */ +public final class FrameScheduler { + private static final long PERIOD = 16_666_667L; + private static final AtomicLong ids = new AtomicLong(); + private static final ConcurrentHashMap receivers = new ConcurrentHashMap<>(); + private final WeakReference receiver, event; + private final Handler handler; + private boolean pending; + private int frame; + + private FrameScheduler(WeakReference receiver, WeakReference event) { + this.receiver = receiver; + this.event = event; + handler = new Handler(Looper.myLooper()); + } + public static long create(Object receiver, Object event) { + long id = ids.incrementAndGet(); + receivers.put(id, new FrameScheduler((WeakReference) receiver, (WeakReference) event)); + return id; + } + public static void dispose(long id) { receivers.remove(id); } + public static void schedule(long id) { + FrameScheduler scheduler = receivers.get(id); + if (scheduler == null) return; + synchronized (scheduler) { + if (scheduler.pending) return; + scheduler.pending = true; + } + scheduler.handler.postDelayed(() -> { + synchronized (scheduler) { scheduler.pending = false; } + Object receiver = scheduler.receiver.get(); + if (receiver == null || !receivers.containsKey(id)) return; + try { + long now = System.nanoTime(); + scheduler.update(now); + Reflect.call(receiver, "dispatchVsync", now, 0L, ++scheduler.frame); + } catch (Exception error) { throw new RuntimeException("Host vsync dispatch failed", error); } + }, 16); + } + private Object update(long now) throws Exception { + Object data = event.get(); + if (data == null) return null; + Reflect.set(data, "frameInterval", PERIOD); + Reflect.set(data, "frameTimelinesLength", 1); + Reflect.set(data, "preferredFrameTimelineIndex", 0); + Object timeline = java.lang.reflect.Array.get(Reflect.field(data, "frameTimelines"), 0); + Reflect.set(timeline, "vsyncId", (long) frame + 1); + Reflect.set(timeline, "expectedPresentationTime", now + PERIOD); + Reflect.set(timeline, "deadline", now + PERIOD); + return data; + } + public static Object latest(long id) throws Exception { + FrameScheduler scheduler = receivers.get(id); + return scheduler == null ? null : scheduler.update(System.nanoTime()); + } + public static void installFinalizer(long finalizer) throws Exception { + Object registry = Reflect.call(Class.forName("libcore.util.NativeAllocationRegistry"), + "createMalloced", FrameScheduler.class.getClassLoader(), finalizer); + Reflect.set(Class.forName("android.view.DisplayEventReceiver"), "sNativeAllocationRegistry", registry); + } +} diff --git a/direct/art/java/com/axrb/runtime/HostActivities.java b/direct/art/java/com/axrb/runtime/HostActivities.java new file mode 100644 index 0000000..28bd0ff --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostActivities.java @@ -0,0 +1,117 @@ +package com.axrb.runtime; + +import android.os.IBinder; +import android.os.Process; +import android.content.Intent; + +/** Single foreground application lifecycle exposed through AOSP service APIs. */ +final class HostActivities { + static final java.util.Map orientations = new java.util.concurrent.ConcurrentHashMap<>(); + static void install(HostServices services) throws Exception { + IBinder client = services.register("axrb.activity_client", "android.app.IActivityClientController", (method, args) -> { + switch (method.getName()) { + case "getDisplayId": return 0; + case "finishActivity": return AppProcess.finishActivity((IBinder) args[0], (Integer) args[1], (Intent) args[2]); + case "getTaskForActivity": return 1; + case "getRequestedOrientation": return orientations.getOrDefault((IBinder) args[0], -1); + case "setRequestedOrientation": orientations.put((IBinder) args[0], (Integer) args[1]); return null; + case "getLaunchedFromUid": return Process.myUid(); + case "getLaunchedFromPackage": return "com.axrb.launcher"; + case "getCallingPackage": case "getCallingActivity": return null; + case "isTopOfTask": case "isRequestedToLaunchInTaskFragment": return false; + case "activityIdle": case "activityResumed": case "activityPaused": + case "activityStopped": case "activityDestroyed": case "reportActivityFullyDrawn": + case "setTaskDescription": case "setShowWhenLocked": + case "setTurnScreenOn": case "setInheritShowWhenLocked": case "setShouldDockBigOverlays": + case "overridePendingTransition": case "overrideActivityTransition": case "clearOverrideActivityTransition": + return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + services.register("activity_task", "android.app.IActivityTaskManager", (method, args) -> { + switch (method.getName()) { + case "getActivityClientController": return client.queryLocalInterface("android.app.IActivityClientController"); + case "startActivity": + // IActivityTaskManager: caller, callingPackage, featureId, + // intent, resolvedType, resultTo, resultWho, requestCode... + Intent intent = (Intent) args[3]; + if ("android.content.pm.action.REQUEST_PERMISSIONS".equals(intent.getAction()) + && "com.axrb.permissioncontroller".equals(intent.getPackage())) + return AppProcess.requestPermissions(intent, (IBinder) args[5], (String) args[6], + (Integer) args[7], services.packages); + return AppProcess.startActivity((Intent) args[3], (IBinder) args[5], (String) args[6], (Integer) args[7]); + case "getTaskForActivity": return 1; + case "getDeviceConfigurationInfo": return new android.content.pm.ConfigurationInfo(); + default: throw new UnsupportedOperationException(method.getName()); + } + }); + services.register("activity", "android.app.IActivityManager", (method, args) -> { + switch (method.getName()) { + case "registerReceiverWithFeature": case "registerReceiver": return HostBroadcasts.register(args); + case "unregisterReceiver": HostBroadcasts.unregister(args[0]); return null; + case "broadcastIntentWithFeature": case "broadcastIntent": + for (Object arg : args) if (arg instanceof Intent) { HostBroadcasts.send((Intent)arg); break; } + return 0; + case "bindServiceInstance": { + Intent intent = null; + Object connection = null; + Class connectionType = Class.forName("android.app.IServiceConnection"); + for (Object arg : args) { + if (arg instanceof Intent) intent = (Intent) arg; + if (arg != null && connectionType.isInstance(arg)) connection = arg; + } + return HostAppServices.bind(services, intent, connection); + } + case "unbindService": return HostAppServices.unbind(args[0]); + case "getContentProvider": + for (Object arg : args) if (services.providers.containsKey(arg)) return services.providers.get(arg); + return null; + case "refContentProvider": return true; + case "removeContentProvider": return null; + case "getCurrentUserId": return 0; + case "getRunningAppProcesses": { + android.content.pm.PackageInfo app = services.packages.installed; + if (app == null) return java.util.Collections.emptyList(); + android.app.ActivityManager.RunningAppProcessInfo info = + new android.app.ActivityManager.RunningAppProcessInfo( + app.applicationInfo.processName, Process.myPid(), new String[]{app.packageName}); + info.uid = Process.myUid(); + info.importance = android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + return java.util.Collections.singletonList(info); + } + case "getMemoryInfo": { + android.app.ActivityManager.MemoryInfo memory = (android.app.ActivityManager.MemoryInfo) args[0]; + try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader("/proc/meminfo"))) { + String line; + while ((line = reader.readLine()) != null) { + String[] words = line.trim().split("\\s+"); + if (words[0].equals("MemTotal:")) memory.totalMem = Long.parseLong(words[1]) * 1024; + if (words[0].equals("MemAvailable:")) memory.availMem = Long.parseLong(words[1]) * 1024; + } + } + memory.threshold = Math.min(memory.totalMem / 10, 256L * 1024 * 1024); + memory.lowMemory = memory.availMem < memory.threshold; + return null; + } + case "getProcessMemoryInfo": { + int[] pids = (int[]) args[0]; + android.os.Debug.MemoryInfo[] result = new android.os.Debug.MemoryInfo[pids.length]; + for (int i = 0; i < pids.length; ++i) { + result[i] = new android.os.Debug.MemoryInfo(); + if (pids[i] == Process.myPid()) android.os.Debug.getMemoryInfo(result[i]); + } + return result; + } + case "getUidProcessState": return 2; + case "isUserAMonkey": case "isBackgroundRestricted": return false; + case "isUserRunning": return true; + case "checkPermission": case "checkPermissionForDevice": return services.packages.permission((String) args[0]); + case "finishReceiver": case "attachApplication": case "setProcessImportant": return null; + // Loading another package's code (com.oculus.systemdriver) records a + // dependency for the system to restart this process if it changes. + case "addPackageDependency": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + } +} diff --git a/direct/art/java/com/axrb/runtime/HostAppServices.java b/direct/art/java/com/axrb/runtime/HostAppServices.java new file mode 100644 index 0000000..6f33469 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostAppServices.java @@ -0,0 +1,48 @@ +package com.axrb.runtime; + +import android.app.Service; +import android.content.*; +import android.content.pm.ServiceInfo; +import android.os.*; +import java.util.*; + +/** Runs APK-declared Android Services and delivers standard binding callbacks. */ +final class HostAppServices { + private static final Map running = new HashMap<>(); + private static final Map connections = new HashMap<>(); + + static int bind(HostServices platform, Intent intent, Object connection) { + ServiceInfo info = resolve(platform, intent); + if (info == null) return 0; + new Handler(Looper.getMainLooper()).post(() -> { + try { + Service service = create(platform, info); + IBinder binder = service.onBind(intent); + connections.put(connection, service); + Reflect.call(connection, "connected", new ComponentName(info.packageName, info.name), binder, false); + } catch (Exception error) { throw new RuntimeException("Service binding failed: " + info.name, error); } + }); + return 1; + } + static boolean unbind(Object connection) { return connections.remove(connection) != null; } + static ServiceInfo resolve(HostServices platform, Intent intent) { + if (intent.getComponent() == null || platform.packages.installed.services == null) return null; + for (ServiceInfo info : platform.packages.installed.services) + if (info.name.equals(intent.getComponent().getClassName()) && info.packageName.equals(intent.getComponent().getPackageName())) return info; + return null; + } + private static Service create(HostServices platform, ServiceInfo info) throws Exception { + ComponentName name = new ComponentName(info.packageName, info.name); + Service service = running.get(name); + if (service != null) return service; + Object thread = Reflect.call(Class.forName("android.app.ActivityThread"), "currentActivityThread"); + android.app.Application application = (android.app.Application) Reflect.field(thread, "mInitialApplication"); + Context context = application.createPackageContext(info.packageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); + service = (Service) context.getClassLoader().loadClass(info.name).getDeclaredConstructor().newInstance(); + Reflect.call(service, "attach", context, thread, info.name, new Binder(), application, + Reflect.call(Class.forName("android.app.ActivityManager"), "getService")); + running.put(name, service); + service.onCreate(); + return service; + } +} diff --git a/direct/art/java/com/axrb/runtime/HostAudio.java b/direct/art/java/com/axrb/runtime/HostAudio.java new file mode 100644 index 0000000..ed525e2 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostAudio.java @@ -0,0 +1,23 @@ +package com.axrb.runtime; + +import java.lang.reflect.Array; +import java.util.ArrayList; + +/** The stable output route used by Direct's AAudio/OpenSL ES WASAPI backend. */ +public final class HostAudio { + private HostAudio() {} + + public static int listPorts(ArrayList ports, int[] generation) throws Exception { + if (ports == null || generation == null || generation.length != 1) return -2; + Object gains = Array.newInstance(Class.forName("android.media.AudioGain"), 0); + Object port = Reflect.create("android.media.AudioDevicePort", + // Match AAudioStream_getDeviceId in the guest output backend. + Reflect.create("android.media.AudioHandle", 2), "AXRB default output", + new int[]{48000}, new int[]{12}, new int[0], new int[]{2, 4}, gains, + 2, "axrb:default", new int[0], new int[0]); + ports.clear(); + ports.add(port); + generation[0] = 1; + return 0; + } +} diff --git a/direct/art/java/com/axrb/runtime/HostBroadcasts.java b/direct/art/java/com/axrb/runtime/HostBroadcasts.java new file mode 100644 index 0000000..e731181 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostBroadcasts.java @@ -0,0 +1,40 @@ +package com.axrb.runtime; + +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.CopyOnWriteArrayList; + +/** Process-local Android broadcast delivery through the real receiver dispatcher. */ +final class HostBroadcasts { + private static final class Registration { + final Object receiver; + final IntentFilter filter; + Registration(Object receiver, IntentFilter filter) { this.receiver = receiver; this.filter = new IntentFilter(filter); } + } + private static final CopyOnWriteArrayList registrations = new CopyOnWriteArrayList<>(); + static Intent register(Object[] args) throws Exception { + Object receiver = null; + IntentFilter filter = null; + Class type = Class.forName("android.content.IIntentReceiver"); + for (Object arg : args) { + if (arg != null && type.isInstance(arg)) receiver = arg; + if (arg instanceof IntentFilter) filter = (IntentFilter)arg; + } + if (filter == null) throw new IllegalArgumentException("Missing receiver filter"); + if (receiver != null) registrations.add(new Registration(receiver, filter)); + return null; + } + static void unregister(Object receiver) { registrations.removeIf(r -> r.receiver == receiver); } + static void send(Intent intent) { + for (Registration registration : registrations) { + if (registration.filter.match(intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(), intent.getCategories(), "AXRB") < 0) continue; + new Handler(Looper.getMainLooper()).post(() -> { + if (!registrations.contains(registration)) return; + try { Reflect.call(registration.receiver, "performReceive", new Intent(intent), 0, null, null, false, false, 0); } + catch (Exception e) { throw new IllegalStateException("Broadcast delivery failed", e); } + }); + } + } +} diff --git a/direct/art/java/com/axrb/runtime/HostMedia.java b/direct/art/java/com/axrb/runtime/HostMedia.java new file mode 100644 index 0000000..0017cd9 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostMedia.java @@ -0,0 +1,110 @@ +package com.axrb.runtime; + +import android.media.MediaPlayer; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.view.Surface; +import java.lang.ref.WeakReference; +import java.util.concurrent.ConcurrentHashMap; + +/** Routes the AOSP MediaPlayer native boundary to the Windows media backend. */ +public final class HostMedia { + // Keep the backend state until release/finalize. A WeakHashMap can discard + // the entry before MediaPlayer's finalizer has closed the native decoder. + private static final ConcurrentHashMap players = new ConcurrentHashMap<>(); + private static final class State { + volatile long id; + volatile int session; + WeakReference owner; + volatile boolean started, released; + } + static native long command(int operation, long id, long a, long b, long c); + static native long readFrame(long surface, long destination, int width, int height, long previous); + static native void releaseSurface(long surface); + private static State state(MediaPlayer player) { + try { + State state = players.get((Long)Reflect.field(player, "mNativeContext")); + if (state == null || state.released) throw new IllegalStateException("MediaPlayer released"); + return state; + } catch (Exception error) { throw new IllegalStateException(error); } + } + public static void setup(MediaPlayer player, Object reference, int session) throws Exception { + State state = new State(); + state.id = command(0, 0, 0, 0, 0); + state.session = session > 0 ? session : (int)state.id; + state.owner = new WeakReference<>(player); + players.put(state.id, state); + Reflect.set(player, "mNativeContext", state.id); + } + private static void event(State state, int what, int a, int b) { + if (state.released) return; + try { Reflect.call(MediaPlayer.class, "postEventFromNative", state.owner, what, a, b, null); } + catch (Exception error) { throw new IllegalStateException(error); } + } + public static long control(MediaPlayer player, int op, long a, long b, long c) throws Exception { + if (op == 12 && (Long)Reflect.field(player, "mNativeContext") == 0L) return 0; + State state = state(player); + if (op == 100) return state.session; + if (op == 101) { state.session = (int)a; return 0; } + if (op == 12) { + state.released = true; + players.remove(state.id); + Reflect.set(player, "mNativeContext", 0L); + } + if (op == 103) { + new Thread(() -> { + long result = command(3, state.id, 0, 0, 0); + if (result == 0) { + event(state, 5, (int)command(13, state.id, 0, 0, 0), (int)command(14, state.id, 0, 0, 0)); + event(state, 1, 0, 0); + } else event(state, 100, 1, -1004); + }, "AXRB media prepare").start(); + return 0; + } + if (op == 104) { + command(12, state.id, 0, 0, 0); + players.remove(state.id); + state.id = command(0, 0, 0, 0, 0); + players.put(state.id, state); + state.started = false; + Reflect.set(player, "mNativeContext", state.id); + return 0; + } + long result = command(op, state.id, a, b, c); + if (result < 0) throw new java.io.IOException("Media operation " + op + " failed"); + if (op == 3) event(state, 5, (int)command(13, state.id, 0, 0, 0), (int)command(14, state.id, 0, 0, 0)); + if (op == 7) event(state, 4, 0, 0); + if (op == 5 || op == 6) state.started = false; + if (op == 4 && !state.started) { + state.started = true; + Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed(new Runnable() { + public void run() { + if (!state.started || state.released || state.owner.get() == null) return; + if (command(9, state.id, 0, 0, 0) == 0) { state.started = false; event(state, 2, 0, 0); } + else handler.postDelayed(this, 30); + } + }, 30); + } + return result; + } + public static void surface(MediaPlayer player, Surface surface) { + if (surface != null && !(surface instanceof HostSurface)) + throw new UnsupportedOperationException("Media output requires an AXRB host surface"); + command(2, state(player).id, surface == null ? 0 : ((HostSurface)surface).mediaHandle, 0, 0); + } + public static int invoke(MediaPlayer player, Parcel request, Parcel reply) { + state(player); + request.setDataPosition(0); + request.enforceInterface("android.media.IMediaPlayer"); + int operation = request.readInt(); + if (reply != null) { + reply.writeInt(0); + if (operation == 1) reply.writeInt(0); // No subtitle or timed-text tracks in the host decoder. + if (operation == 7) reply.writeInt(-1); + reply.setDataPosition(0); + } + return operation == 1 || operation == 6 || operation == 7 || operation == 8 ? 0 : -38; + } +} diff --git a/direct/art/java/com/axrb/runtime/HostPackageManager.java b/direct/art/java/com/axrb/runtime/HostPackageManager.java new file mode 100644 index 0000000..fa67ec0 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostPackageManager.java @@ -0,0 +1,154 @@ +package com.axrb.runtime; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Process; +import java.lang.reflect.Method; +import java.util.Collections; + +/** Package metadata for the APK installed into this isolated Direct session. */ +final class HostPackageManager implements HostServices.Call { + PackageInfo installed; + /** com.oculus.systemdriver for older Oculus loaders, when the session offers it. */ + PackageInfo legacyDriver; + private final java.util.Map companions = new java.util.LinkedHashMap<>(); + String launcherActivity; + final java.util.Map providers = new java.util.HashMap<>(); + private final java.util.List features = new java.util.ArrayList<>(); + HostPackageManager() { + feature("android.hardware.vr.headtracking", 1); + feature("android.hardware.vulkan.level", 1); + feature("android.hardware.vulkan.version", 0x401000); + } + private void feature(String name, int version) { + android.content.pm.FeatureInfo info = new android.content.pm.FeatureInfo(); + info.name = name; + info.version = version; + features.add(info); + } + int permission(String permission) { + // Only capabilities exposed by the Direct session can be granted. + if (!(permission.equals("android.permission.INTERNET") || + permission.equals("android.permission.ACCESS_NETWORK_STATE") || + permission.equals("android.permission.ACCESS_WIFI_STATE") || + permission.equals("android.permission.WAKE_LOCK") || + permission.equals("android.permission.READ_EXTERNAL_STORAGE") || + permission.equals("android.permission.WRITE_EXTERNAL_STORAGE") || + permission.equals("android.permission.MODIFY_AUDIO_SETTINGS") || + permission.equals("android.permission.VIBRATE"))) return PackageManager.PERMISSION_DENIED; + if (installed != null && installed.requestedPermissions != null) + for (String declared : installed.requestedPermissions) if (permission.equals(declared)) return PackageManager.PERMISSION_GRANTED; + return PackageManager.PERMISSION_DENIED; + } + private boolean known(Object value) { return installed != null && installed.packageName.equals(value); } + private PackageInfo lookup(Object value) { + if (known(value)) return installed; + if (legacyDriver != null && legacyDriver.packageName.equals(value)) return legacyDriver; + return companions.get(value); + } + private java.util.List all() { + java.util.List result = new java.util.ArrayList<>(); + if (installed != null) result.add(installed); + if (legacyDriver != null) result.add(legacyDriver); + result.addAll(companions.values()); + return result; + } + /** Registers the adapter APK mounted at path as an installed system package. */ + void offerLegacyDriver(android.content.Context context, String path) { + legacyDriver = parsePackage(context, path); + } + void offerCompanion(android.content.Context context, String path) { + PackageInfo info = parsePackage(context, path); + if (lookup(info.packageName) != null) throw new IllegalArgumentException("Duplicate companion package " + info.packageName); + companions.put(info.packageName, info); + } + void installGame(PackageInfo info) { + if (lookup(info.packageName) != null) throw new IllegalArgumentException("Companion package conflicts with game " + info.packageName); + installed = info; + } + private PackageInfo parsePackage(android.content.Context context, String path) { + PackageInfo info = context.getPackageManager().getPackageArchiveInfo(path, + PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS); + if (info == null) throw new IllegalArgumentException("Cannot parse supplied package " + path); + ApplicationInfo app = info.applicationInfo; + app.packageName = info.packageName; + app.sourceDir = app.publicSourceDir = path; + app.nativeLibraryDir = new java.io.File(path).getParent() + "/lib/arm64"; + app.dataDir = "/data/data/" + info.packageName; + app.deviceProtectedDataDir = app.dataDir; + app.uid = Process.myUid(); + app.flags |= ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_INSTALLED | ApplicationInfo.FLAG_HAS_CODE; + app.enabled = true; + try { + Reflect.set(app, "credentialProtectedDataDir", app.dataDir); + Reflect.set(app, "primaryCpuAbi", "arm64-v8a"); + } catch (Exception error) { throw new IllegalStateException(error); } + return info; + } + public Object invoke(Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "resolveContentProvider": return providers.get(args[0]); + case "queryIntentActivities": { + android.content.Intent intent = (android.content.Intent) args[0]; + java.util.List matches = new java.util.ArrayList<>(); + for (PackageInfo info : all()) if (info.activities != null) for (android.content.pm.ActivityInfo activity : info.activities) { + boolean explicit = intent.getComponent() != null && intent.getComponent().getPackageName().equals(info.packageName) + && intent.getComponent().getClassName().equals(activity.name); + boolean launcher = android.content.Intent.ACTION_MAIN.equals(intent.getAction()) && (info == installed ? activity.name.equals(launcherActivity) : activity.exported) + && (intent.getPackage() == null || intent.getPackage().equals(info.packageName)); + if (explicit || launcher) { android.content.pm.ResolveInfo result = new android.content.pm.ResolveInfo(); result.activityInfo = activity; matches.add(result); } + } + return Reflect.create("android.content.pm.ParceledListSlice", matches); + } + case "getActivityInfo": case "getReceiverInfo": { + android.content.ComponentName component = (android.content.ComponentName) args[0]; + PackageInfo info = lookup(component.getPackageName()); + android.content.pm.ActivityInfo[] entries = info == null ? null : method.getName().equals("getActivityInfo") ? info.activities : info.receivers; + if (entries != null) for (android.content.pm.ActivityInfo entry : entries) + if (entry.packageName.equals(component.getPackageName()) && entry.name.equals(component.getClassName())) return entry; + return null; + } + case "getServiceInfo": { + android.content.ComponentName component = (android.content.ComponentName) args[0]; + PackageInfo info = lookup(component.getPackageName()); + if (info != null && info.services != null) for (android.content.pm.ServiceInfo entry : info.services) + if (entry.packageName.equals(component.getPackageName()) && entry.name.equals(component.getClassName())) return entry; + return null; + } + case "getPackageInfo": case "getPackageInfoVersioned": return lookup(args[0]); + case "getApplicationInfo": { PackageInfo info = lookup(args[0]); return info == null ? null : info.applicationInfo; } + case "getInstalledApplications": { + java.util.List apps = new java.util.ArrayList<>(); + for (PackageInfo info : all()) apps.add(info.applicationInfo); + return Reflect.create("android.content.pm.ParceledListSlice", apps); + } + case "getInstalledPackages": return Reflect.create("android.content.pm.ParceledListSlice", all()); + case "getPackageUid": return lookup(args[0]) != null ? Process.myUid() : -1; + case "getPackagesForUid": return installed == null ? new String[0] : new String[]{installed.packageName}; + case "getNameForUid": return installed == null ? "android" : installed.packageName; + case "isPackageAvailable": return lookup(args[0]) != null; + case "hasSystemFeature": + for (android.content.pm.FeatureInfo feature : features) + if (feature.name.equals(args[0]) && feature.version >= (Integer) args[1]) return true; + return false; + case "getSystemAvailableFeatures": return Reflect.create("android.content.pm.ParceledListSlice", features); + case "getSystemSharedLibraryNames": return new String[0]; + case "getSharedLibraries": return Reflect.create("android.content.pm.ParceledListSlice", Collections.emptyList()); + case "getTargetSdkVersion": return installed == null ? 36 : installed.applicationInfo.targetSdkVersion; + case "getApplicationEnabledSetting": case "getComponentEnabledSetting": return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + case "getInstallerPackageName": return null; + case "getInstallSourceInfo": + if (!known(args[0])) return null; + // A local APK has no Android installer package or signing identity. + return Reflect.create("android.content.pm.InstallSourceInfo", null, null, null, null, + null, android.content.pm.PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE); + case "checkPermission": return permission((String) args[0]); + case "getPermissionControllerPackageName": return "com.axrb.permissioncontroller"; + case "getSdkSandboxPackageName": return "com.android.sdksandbox"; + case "isInstantApp": case "isAppArchivingEnabled": return false; + case "notifyPackageUse": case "notifyDexLoad": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + } +} diff --git a/direct/art/java/com/axrb/runtime/HostPlatformServices.java b/direct/art/java/com/axrb/runtime/HostPlatformServices.java new file mode 100644 index 0000000..01cac03 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostPlatformServices.java @@ -0,0 +1,211 @@ +package com.axrb.runtime; + +import android.os.Bundle; +import java.util.Collections; + +/** Session-level platform state that has no separate Android system process. */ +final class HostPlatformServices { + static void install(HostServices services) throws Exception { + android.net.Network network = (android.net.Network) Reflect.create("android.net.Network", 100); + android.net.NetworkCapabilities networkCapabilities = new android.net.NetworkCapabilities(); + Reflect.call(networkCapabilities, "addTransportType", android.net.NetworkCapabilities.TRANSPORT_ETHERNET); + for (int capability : new int[]{12, 16, 11}) Reflect.call(networkCapabilities, "addCapability", capability); + android.net.LinkProperties networkLinks = new android.net.LinkProperties(); + networkLinks.setInterfaceName("axrb0"); + android.net.NetworkInfo networkInfo = new android.net.NetworkInfo(9, 0, "ETHERNET", ""); + networkInfo.setDetailedState(android.net.NetworkInfo.DetailedState.CONNECTED, null, "AXRB host"); + java.util.concurrent.atomic.AtomicInteger nextNetworkRequest = new java.util.concurrent.atomic.AtomicInteger(1); + java.util.Set networkRequests = java.util.concurrent.ConcurrentHashMap.newKeySet(); + services.register("connectivity", "android.net.IConnectivityManager", (method, args) -> { + switch (method.getName()) { + case "getEnabledConnectivityManagerFeatures": return 0L; + case "listenForNetwork": case "requestNetwork": { + boolean listen = method.getName().equals("listenForNetwork"); + android.net.NetworkCapabilities requested = (android.net.NetworkCapabilities) args[listen ? 0 : 1]; + android.os.Messenger messenger = (android.os.Messenger) args[listen ? 1 : 3]; + Class requestTypes = Class.forName("android.net.NetworkRequest$Type"); + Object type = listen ? Reflect.field(requestTypes, "LISTEN") : requestTypes.getEnumConstants()[(Integer) args[2]]; + android.net.NetworkRequest request = (android.net.NetworkRequest) Reflect.create( + "android.net.NetworkRequest", requested == null ? networkCapabilities : requested, + listen ? -1 : (Integer) args[6], nextNetworkRequest.getAndIncrement(), type); + networkRequests.add(request); + // AOSP registers the returned request before dispatching this Messenger event. + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + if (!networkRequests.contains(request)) return; + try { + if (requested != null && !(Boolean) Reflect.call(requested, "satisfiedByNetworkCapabilities", networkCapabilities)) return; + android.os.Message message = android.os.Message.obtain(); + message.what = (Integer) Reflect.field(android.net.ConnectivityManager.class, "CALLBACK_AVAILABLE"); + Bundle data = new Bundle(); + data.putParcelable("NetworkRequest", request); + data.putParcelable("Network", network); + data.putParcelable("NetworkCapabilities", new android.net.NetworkCapabilities(networkCapabilities)); + data.putParcelable("LinkProperties", networkLinks); + message.setData(data); + messenger.send(message); + } catch (Exception e) { throw new IllegalStateException("Network callback delivery failed", e); } + }); + return request; + } + case "getActiveNetwork": case "getActiveNetworkForUid": return network; + case "getAllNetworks": return new android.net.Network[]{network}; + case "getActiveNetworkInfo": case "getActiveNetworkInfoForUid": case "getNetworkInfo": case "getNetworkInfoForUid": return networkInfo; + case "getAllNetworkInfo": return new android.net.NetworkInfo[]{networkInfo}; + case "getNetworkCapabilities": return new android.net.NetworkCapabilities(networkCapabilities); + case "getLinkProperties": case "getActiveLinkProperties": return Reflect.create("android.net.LinkProperties", networkLinks); + case "isActiveNetworkMetered": return false; + case "getRestrictBackgroundStatus": return 1; + case "getDefaultProxy": case "getProxyForNetwork": return null; + case "releaseNetworkRequest": networkRequests.remove(args[0]); return null; + case "reportNetworkConnectivity": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + java.util.Set wakeLocks = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + services.register("power", "android.os.IPowerManager", (method, args) -> { + switch (method.getName()) { + case "isInteractive": case "isWakeLockLevelSupported": return true; + case "isPowerSaveMode": case "isDeviceIdleMode": case "isLightDeviceIdleMode": return false; + case "acquireWakeLock": case "acquireWakeLockAsync": wakeLocks.add(args[0]); return null; + case "releaseWakeLock": case "releaseWakeLockAsync": wakeLocks.remove(args[0]); return null; + case "updateWakeLockUids": case "updateWakeLockWorkSource": case "userActivity": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + java.util.Set thermalListeners = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + services.register("thermalservice", "android.os.IThermalService", (method, args) -> { + switch (method.getName()) { + case "registerThermalStatusListener": thermalListeners.add(args[0]); + Reflect.call(args[0], "onStatusChange", 0); return true; + case "unregisterThermalStatusListener": return thermalListeners.remove(args[0]); + case "getCurrentThermalStatus": return 0; + case "getThermalHeadroom": return Float.NaN; + case "getCurrentTemperatures": case "getCurrentCoolingDevices": + return java.lang.reflect.Array.newInstance(method.getReturnType().getComponentType(), 0); + default: throw new UnsupportedOperationException(method.getName()); + } + }); + Object volume = Reflect.create("android.os.storage.StorageVolume", "emulated;0", + new java.io.File("/storage/emulated/0"), new java.io.File("/storage/emulated/0"), "AXRB storage", + true, false, true, false, false, 0L, Reflect.create("android.os.UserHandle", 0), null, null, "mounted"); + Object storageVolumes = java.lang.reflect.Array.newInstance(volume.getClass(), 1); + java.lang.reflect.Array.set(storageVolumes, 0, volume); + services.register("mount", "android.os.storage.IStorageManager", (method, args) -> { + switch (method.getName()) { + case "getVolumeList": return storageVolumes; + case "isUserKeyUnlocked": case "isExternalStorageEmulated": return true; + case "isExternalStorageManager": return false; + case "registerListener": case "unregisterListener": return null; + case "getAllocatableBytes": return new java.io.File("/storage/emulated/0").getUsableSpace(); + case "fixupAppDir": + // The session's emulated storage already uses the app UID; + // there is no separate FUSE daemon to adjust ownership. + String appPath = (String) args[0]; + if (!new java.io.File(appPath).getCanonicalPath().startsWith("/storage/emulated/0/Android/")) + throw new SecurityException(appPath); + if (!new java.io.File(appPath).isDirectory() && !new java.io.File(appPath).mkdirs()) + throw new java.io.IOException("Cannot create app storage " + appPath); + return null; + case "mkdirs": + String path = (String) args[args.length - 1]; + if (!path.startsWith("/storage/emulated/0/Android/")) throw new SecurityException(path); + new java.io.File(path).mkdirs(); return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + java.util.Set inputClients = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + services.register("input_method", "com.android.internal.view.IInputMethodManager", (method, args) -> { + switch (method.getName()) { + case "addClient": inputClients.add(args[0]); return null; + case "getCurrentInputMethodInfoAsUser": case "getLastInputMethodSubtype": return null; + case "getInputMethodList": case "getEnabledInputMethodList": + if (java.util.List.class.isAssignableFrom(method.getReturnType())) return Collections.emptyList(); + return Reflect.call(method.getReturnType(), "create", Collections.emptyList()); + case "getEnabledInputMethodSubtypeList": return Collections.emptyList(); + case "isInputMethodPickerShownForTest": case "isStylusHandwritingAvailableAsUser": + case "showSoftInput": case "hideSoftInput": return false; + // Requests to show or hide the keyboard are tracked by this service + // first; without one, Android uses a local token (no IME is installed). + case "getImeTrackerService": return null; + case "startInputOrWindowGainedFocus": + return Reflect.field(Class.forName("com.android.internal.inputmethod.InputBindResult"), "NO_IME"); + case "reportPerceptibleAsync": case "removeImeSurface": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + services.register("input", "android.hardware.input.IInputManager", (method, args) -> { + switch (method.getName()) { + case "getInputDeviceIds": return new int[0]; + case "getInputDevice": return null; + case "getInputDevices": return new android.view.InputDevice[0]; + case "getVelocityTrackerStrategy": return "default"; + case "registerInputDevicesChangedListener": case "registerTabletModeChangedListener": return null; + case "isInTabletMode": return 0; + case "isMicMuted": return 1; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + java.util.Map volumes = new java.util.concurrent.ConcurrentHashMap<>(); + java.util.concurrent.atomic.AtomicInteger nextPlayer = new java.util.concurrent.atomic.AtomicInteger(1); + java.util.Map players = new java.util.concurrent.ConcurrentHashMap<>(); + services.register("audio", "android.media.IAudioService", (method, args) -> { + switch (method.getName()) { + case "trackPlayer": { + int id = nextPlayer.getAndIncrement(); + players.put(id, args[0]); + return id; + } + case "releasePlayer": players.remove(args[0]); return null; + case "playerAttributes": case "playerEvent": case "playerSessionId": return null; + case "getStreamVolume": case "getLastAudibleStreamVolume": return volumes.getOrDefault((Integer) args[0], 10); + case "getStreamMaxVolume": return 15; + case "isMusicActive": return HostMedia.command(17, 0, 0, 0, 0) != 0; + case "getStreamMinVolume": return 0; + case "setStreamVolume": { + int stream = (Integer)args[0], before = volumes.getOrDefault(stream, 10); + int value = Math.max(0, Math.min(15, (Integer)args[1])); + volumes.put(stream, value); + if (before != value) HostBroadcasts.send(new android.content.Intent("android.media.VOLUME_CHANGED_ACTION") + .putExtra("android.media.EXTRA_VOLUME_STREAM_TYPE", stream) + .putExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", value) + .putExtra("android.media.EXTRA_PREV_VOLUME_STREAM_VALUE", before)); + return null; + } + case "isStreamMute": return volumes.getOrDefault((Integer) args[0], 10) == 0; + case "isMasterMute": case "isMicrophoneMuted": case "isBluetoothScoOn": case "isBluetoothA2dpOn": case "isVolumeFixed": return false; + case "getMode": return 0; + case "getRingerModeExternal": case "getRingerModeInternal": return 2; + case "requestAudioFocus": case "abandonAudioFocus": return 1; + case "registerAudioFocusDispatcher": case "unregisterAudioFocusClient": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + java.util.Set observers = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); + services.register("content", "android.content.IContentService", (method, args) -> { + switch (method.getName()) { + case "registerContentObserver": observers.add(args[2]); return null; + case "unregisterContentObserver": observers.remove(args[0]); return null; + case "getMasterSyncAutomatically": case "isSyncActive": case "isSyncPending": return false; + case "getCurrentSyncs": return Collections.emptyList(); + default: throw new UnsupportedOperationException(method.getName()); + } + }); + Object owner = Reflect.create("android.content.pm.UserInfo", 0, "Owner", 0x13); + services.register("user", "android.os.IUserManager", (method, args) -> { + switch (method.getName()) { + case "isUserUnlocked": case "isUserUnlockingOrUnlocked": case "isUserRunning": return (Integer) args[0] == 0; + case "isManagedProfile": case "isQuietModeEnabled": case "hasUserRestriction": + case "hasUserRestrictionOnAnyUser": case "isDemoUser": case "isPreCreated": return false; + case "getUserInfo": return (Integer) args[0] == 0 ? owner : null; + case "getUsers": case "getProfiles": return Collections.singletonList(owner); + case "getProfileIds": return new int[]{0}; + case "getUserRestrictions": return new Bundle(); + case "getProfileParent": return null; + case "getUserSerialNumber": case "getUserHandle": return (Integer) args[0] == 0 ? 0 : -1; + case "getCredentialOwnerProfile": case "getProfileParentId": return 0; + case "getUserName": return "Owner"; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + } +} diff --git a/direct/art/java/com/axrb/runtime/HostServices.java b/direct/art/java/com/axrb/runtime/HostServices.java new file mode 100644 index 0000000..45ccac8 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostServices.java @@ -0,0 +1,135 @@ +package com.axrb.runtime; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import java.lang.reflect.*; +import java.util.*; + +/** Android service interfaces backed by the Direct host, shared by all apps. */ +final class HostServices { + private final Map services = new HashMap<>(); + private NativeRegistry nativeRegistry; + final HostPackageManager packages = new HostPackageManager(); + final Map providers = new HashMap<>(); + void attach(android.content.Context context) throws Exception { + provider(context, "settings", new HostSettings()); + provider(context, "org.khronos.openxr.system_runtime_broker", new OpenXrProvider()); + provider(context, "org.khronos.openxr.runtime_broker", new OpenXrProvider()); + } + private void provider(android.content.Context context, String authority, android.content.ContentProvider provider) throws Exception { + android.content.pm.ProviderInfo info = new android.content.pm.ProviderInfo(); + info.authority = authority; + info.name = provider.getClass().getName(); + info.packageName = "android"; + info.applicationInfo = context.getApplicationInfo(); + provider.attachInfo(context, info); + Object holder = Reflect.create("android.app.ContentProviderHolder", info); + Reflect.set(holder, "provider", Reflect.call(provider, "getIContentProvider")); + Reflect.set(holder, "noReleaseNeeded", true); + providers.put(authority, holder); + packages.providers.put(authority, info); + } + interface Call { Object invoke(Method method, Object[] args) throws Throwable; } + + IBinder register(String name, String descriptor, Call call) throws Exception { + Class type = Class.forName(descriptor); + Binder binder = new Binder(); + IInterface implementation = (IInterface) Proxy.newProxyInstance(type.getClassLoader(), + new Class[]{type}, (proxy, method, args) -> { + if (method.getName().equals("asBinder")) return binder; + if (method.getName().equals("toString")) return "AXRB " + descriptor; + if (method.getName().equals("hashCode")) return System.identityHashCode(proxy); + if (method.getName().equals("equals")) return proxy == args[0]; + try { return call.invoke(method, args == null ? new Object[0] : args); } + catch (UnsupportedOperationException e) { + System.err.println("AXRB_SERVICE_UNIMPLEMENTED " + descriptor + "." + method.getName()); + throw e; + } + }); + binder.attachInterface(implementation, descriptor); + services.put(name, binder); + return binder; + } + + void install() throws Exception { + IBinder registry = register("manager", "android.os.IServiceManager", (method, args) -> { + switch (method.getName()) { + case "getService": case "checkService": return services.get((String) args[0]); + case "getService2": case "checkService2": { + Object metadata = Reflect.create("android.os.ServiceWithMetadata"); + Reflect.set(metadata, "service", services.get((String) args[0])); + return Reflect.call(method.getReturnType(), "serviceWithMetadata", metadata); + } + case "addService": services.put((String) args[0], (IBinder) args[1]); return null; + case "listServices": return services.keySet().toArray(new String[0]); + case "isDeclared": return services.containsKey((String) args[0]); + case "getDeclaredInstances": return new String[0]; + case "registerForNotifications": case "unregisterForNotifications": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + Reflect.set(Class.forName("android.os.ServiceManager"), "sServiceManager", + registry.queryLocalInterface("android.os.IServiceManager")); + installDisplay(); + register("package", "android.content.pm.IPackageManager", packages); + register("permissionmgr", "android.permission.IPermissionManager", (method, args) -> { + switch (method.getName()) { + // Direct has no user-toggleable Android permissions. Its + // capability policy permanently denies unsupported access. + case "shouldShowRequestPermissionRationale": case "isPermissionRevokedByPolicy": return false; + case "checkPermission": case "checkUidPermission": return packages.permission((String) args[0]); + case "getPermissionFlags": return 0; + case "getSplitPermissions": return Collections.emptyList(); + default: throw new UnsupportedOperationException(method.getName()); + } + }); + HostActivities.install(this); + register("notification", "android.app.INotificationManager", (method, args) -> { + switch (method.getName()) { + case "enqueueTextToast": + // There is no Android SystemUI in Direct mode. Preserve the + // diagnostic text and report that no toast was displayed. + System.out.println("AXRB_ANDROID_TOAST " + args[2]); + return false; + case "areNotificationsEnabled": case "areNotificationsEnabledForPackage": return false; + case "cancelToast": case "finishToken": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + HostPlatformServices.install(this); + HostWindows.install(this); + nativeRegistry = new NativeRegistry(services); + } + + private void installDisplay() throws Exception { + Object info = Reflect.create("android.view.DisplayInfo"); + for (String field : new String[]{"logicalWidth", "appWidth", "smallestNominalAppWidth", "largestNominalAppWidth"}) Reflect.set(info, field, 1920); + for (String field : new String[]{"logicalHeight", "appHeight", "smallestNominalAppHeight", "largestNominalAppHeight"}) Reflect.set(info, field, 1080); + Reflect.set(info, "name", "AXRB Host Display"); + Reflect.set(info, "uniqueId", "axrb:0"); + Reflect.set(info, "displayId", 0); + Reflect.set(info, "logicalDensityDpi", 320); + Reflect.set(info, "physicalXDpi", 320f); + Reflect.set(info, "physicalYDpi", 320f); + Reflect.set(info, "state", 2); + Reflect.set(info, "type", 1); + Reflect.set(info, "modeId", 1); + Reflect.set(info, "defaultModeId", 1); + Object mode = Reflect.create("android.view.Display$Mode", 1, 1920, 1080, 60f); + Object modes = Array.newInstance(mode.getClass(), 1); + Array.set(modes, 0, mode); + Reflect.set(info, "supportedModes", modes); + register("display", "android.hardware.display.IDisplayManager", (method, args) -> { + switch (method.getName()) { + case "getDisplayInfo": return ((Integer) args[0]) == 0 ? info : null; + case "getDisplayIds": return new int[]{0}; + case "getWifiDisplayStatus": return Reflect.create("android.hardware.display.WifiDisplayStatus"); + case "registerCallback": case "registerCallbackWithEventMask": return null; + case "getPreferredWideGamutColorSpaceId": return 0; + case "getOverlaySupport": return Reflect.create("android.hardware.OverlayProperties", 0L); + default: throw new UnsupportedOperationException(method.getName()); + } + }); + } +} diff --git a/direct/art/java/com/axrb/runtime/HostSettings.java b/direct/art/java/com/axrb/runtime/HostSettings.java new file mode 100644 index 0000000..547fb25 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostSettings.java @@ -0,0 +1,77 @@ +package com.axrb.runtime; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Bundle; +import java.util.Properties; +import java.io.*; + +/** Settings storage for the isolated Direct Android environment. */ +public final class HostSettings extends ContentProvider { + private final Properties values = new Properties(); + private final File storage = new File("/data/system/axrb-settings.properties"); + public boolean onCreate() { + if (storage.isFile()) try (InputStream input = new FileInputStream(storage)) { values.load(input); } + catch (IOException error) { throw new IllegalStateException("Cannot read settings", error); } + return true; + } + private synchronized String get(String table, String name) { return values.getProperty(table + ":" + name); } + private synchronized void put(String table, String name, String value) { + String key = table + ":" + name; + if (value == null) values.remove(key); else values.setProperty(key, value); + storage.getParentFile().mkdirs(); + try (OutputStream output = new FileOutputStream(storage)) { values.store(output, "AXRB Android settings"); } + catch (IOException error) { throw new IllegalStateException("Cannot write settings", error); } + } + public synchronized Bundle call(String method, String name, Bundle extras) { + if (method.startsWith("LIST_")) { + String table = method.substring(5) + ":"; + String prefix = extras == null ? "" : extras.getString("_prefix", ""); + java.util.HashMap entries = new java.util.HashMap<>(); + for (String key : values.stringPropertyNames()) if (key.startsWith(table + prefix)) + entries.put(key.substring(table.length()), values.getProperty(key)); + Bundle result = new Bundle(); + result.putSerializable("value", entries); + return result; + } + if (method.startsWith("GET_")) { + Bundle result = new Bundle(); + result.putString("value", get(method.substring(4), name)); + return result; + } + if (method.startsWith("PUT_")) { + put(method.substring(4), name, extras == null ? null : extras.getString("value")); + return null; + } + throw new UnsupportedOperationException("Settings call " + method); + } + public Cursor query(Uri uri, String[] projection, String selection, String[] args, String order) { + String[] columns = projection == null ? new String[]{"name", "value"} : projection; + MatrixCursor cursor = new MatrixCursor(columns); + String name = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : args != null && args.length > 0 ? args[0] : null; + if (name != null) { + String value = get(uri.getPathSegments().get(0), name); + if (value != null) { + Object[] row = new Object[columns.length]; + for (int i = 0; i < columns.length; ++i) row[i] = columns[i].equals("name") ? name : value; + cursor.addRow(row); + } + } + return cursor; + } + public String getType(Uri uri) { return "vnd.android.cursor.item/setting"; } + public Uri insert(Uri uri, ContentValues values) { + String name = values.getAsString("name"); + put(uri.getPathSegments().get(0), name, values.getAsString("value")); + return Uri.withAppendedPath(uri, name); + } + public int update(Uri uri, ContentValues values, String selection, String[] args) { insert(uri, values); return 1; } + public int delete(Uri uri, String selection, String[] args) { + String name = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : args[0]; + put(uri.getPathSegments().get(0), name, null); + return 1; + } +} diff --git a/direct/art/java/com/axrb/runtime/HostSurface.java b/direct/art/java/com/axrb/runtime/HostSurface.java new file mode 100644 index 0000000..c09aac0 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostSurface.java @@ -0,0 +1,58 @@ +package com.axrb.runtime; + +import android.graphics.*; +import android.view.Surface; +import java.nio.ByteBuffer; +import java.util.concurrent.locks.ReentrantLock; + +/** AOSP software Canvas producer for OpenXR Android-surface overlay layers. */ +public final class HostSurface extends Surface { + private final SurfaceTexture texture; + private final Bitmap bitmap; + private final ReentrantLock pixels = new ReentrantLock(); + private Canvas canvas; + private long serial = 1; + private boolean released; + private static final java.util.concurrent.atomic.AtomicLong nextMediaHandle = new java.util.concurrent.atomic.AtomicLong(1); + final long mediaHandle = nextMediaHandle.getAndIncrement(); + private static native ByteBuffer bufferAt(long address, int bytes); + + public HostSurface(int width, int height) { this(new SurfaceTexture(false), width, height); } + private HostSurface(SurfaceTexture texture, int width, int height) { + super(texture); + this.texture = texture; + texture.setDefaultBufferSize(width, height); + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + } + @Override public Canvas lockCanvas(Rect dirty) { + pixels.lock(); + if (released || canvas != null) { pixels.unlock(); throw new IllegalStateException("Surface unavailable"); } + canvas = new Canvas(bitmap); + return canvas; + } + @Override public Canvas lockHardwareCanvas() { return lockCanvas(null); } + @Override public void unlockCanvasAndPost(Canvas drawing) { + if (!pixels.isHeldByCurrentThread() || drawing != canvas) throw new IllegalArgumentException("Canvas was not locked here"); + canvas = null; + ++serial; + pixels.unlock(); + } + public long readFrame(long address, int width, int height, long previous) { + pixels.lock(); + try { + long mediaSerial = HostMedia.readFrame(mediaHandle, address, width, height, previous & ~(1L << 60)); + if (mediaSerial != 0) return mediaSerial | (1L << 60); + if (released || serial == previous) return previous; + Bitmap frame = width == bitmap.getWidth() && height == bitmap.getHeight() ? bitmap : Bitmap.createScaledBitmap(bitmap, width, height, true); + try { frame.copyPixelsToBuffer(bufferAt(address, Math.multiplyExact(Math.multiplyExact(width, height), 4))); } + finally { if (frame != bitmap) frame.recycle(); } + return serial; + } finally { pixels.unlock(); } + } + @Override public void release() { + pixels.lock(); + try { + if (!released) { released = true; HostMedia.releaseSurface(mediaHandle); bitmap.recycle(); texture.release(); super.release(); } + } finally { pixels.unlock(); } + } +} diff --git a/direct/art/java/com/axrb/runtime/HostWindow.java b/direct/art/java/com/axrb/runtime/HostWindow.java new file mode 100644 index 0000000..35067f8 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostWindow.java @@ -0,0 +1,76 @@ +package com.axrb.runtime; + +import android.app.Activity; +import android.graphics.SurfaceTexture; +import android.graphics.PixelFormat; +import android.view.*; +import java.util.*; + +/** Adapts standard Android game surfaces to the existing Windows renderer. */ +final class HostWindow { + static { System.load("/system/lib64/libaxrb_art_bridge.so"); } + private static native long registerSurface(Surface surface, int width, int height); + private static native void releaseSurface(long window); + private final List textures = new ArrayList<>(); + private final List windows = new ArrayList<>(); + private final Set attached = Collections.newSetFromMap(new IdentityHashMap<>()); + + void attach(Activity activity) throws Exception { + View decor = activity.getWindow().getDecorView(); + decor.measure(View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY)); + decor.layout(0, 0, 1920, 1080); + attachSurfaces(decor); + decor.getViewTreeObserver().dispatchOnGlobalLayout(); + activity.onWindowFocusChanged(true); + decor.dispatchWindowFocusChanged(true); + System.out.println("AXRB_AOSP_HOST_SURFACES " + windows.size()); + if (Boolean.getBoolean("axrb.inspectViews")) inspect(decor); + } + + private static void inspect(View view) throws Exception { + Class type = view.getClass(); + if (!type.getName().startsWith("android.") && !type.getName().startsWith("com.android.")) { + for (java.lang.reflect.Field field : type.getDeclaredFields()) { + field.setAccessible(true); + Object value = field.get(view); + System.out.println("AXRB_VIEW " + type.getName() + "." + field.getName() + "=" + value); + if (value != null && value.getClass().getPackage().equals(type.getPackage())) + for (java.lang.reflect.Field inner : value.getClass().getDeclaredFields()) if (inner.getType().isPrimitive()) { + inner.setAccessible(true); + System.out.println("AXRB_VIEW_STATE " + field.getName() + "." + inner.getName() + "=" + inner.get(value)); + } + } + } + if (view instanceof ViewGroup) for (int i = 0; i < ((ViewGroup) view).getChildCount(); ++i) + inspect(((ViewGroup) view).getChildAt(i)); + } + + private void attachSurfaces(View view) throws Exception { + if (view instanceof SurfaceView && attached.add(view)) { + SurfaceView surfaceView = (SurfaceView) view; + SurfaceHolder holder = surfaceView.getHolder(); + SurfaceTexture texture = new SurfaceTexture(false); + texture.setDefaultBufferSize(1920, 1080); + textures.add(texture); + Surface surface = new Surface(texture); + Reflect.call(holder.getSurface(), "transferFrom", surface); + holder.getSurfaceFrame().set(0, 0, 1920, 1080); + long window = registerSurface(holder.getSurface(), 1920, 1080); + if (window == 0) throw new IllegalStateException("Cannot bridge Android surface"); + windows.add(window); + @SuppressWarnings("unchecked") + List callbacks = new ArrayList<>((List) Reflect.field(surfaceView, "mCallbacks")); + for (SurfaceHolder.Callback callback : callbacks) { + callback.surfaceCreated(holder); + callback.surfaceChanged(holder, PixelFormat.RGBA_8888, 1920, 1080); + } + for (SurfaceHolder.Callback callback : callbacks) if (callback instanceof SurfaceHolder.Callback2) + ((SurfaceHolder.Callback2) callback).surfaceRedrawNeededAsync(holder, () -> {}); + } + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); ++i) attachSurfaces(group.getChildAt(i)); + } + } +} diff --git a/direct/art/java/com/axrb/runtime/HostWindows.java b/direct/art/java/com/axrb/runtime/HostWindows.java new file mode 100644 index 0000000..4ff83d2 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/HostWindows.java @@ -0,0 +1,55 @@ +package com.axrb.runtime; + +import android.graphics.Point; +import android.graphics.Rect; +import android.os.IBinder; + +/** Window/display policy for the Direct host's single application window. */ +final class HostWindows { + static void install(HostServices services) throws Exception { + IBinder session = services.register("axrb.window_session", "android.view.IWindowSession", (method, args) -> { + switch (method.getName()) { + case "addToDisplayAsUser": + Object root = ((java.lang.ref.WeakReference) Reflect.field(args[0], "mViewAncestor")).get(); + logText((android.view.View) Reflect.field(root, "mView")); + throw new UnsupportedOperationException(method.getName()); + case "getInTouchMode": return false; + case "setInTouchMode": case "updateRequestedVisibleTypes": case "setInsets": + case "reportSystemGestureExclusionChanged": case "reportKeepClearAreasChanged": + return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + services.register("window", "android.view.IWindowManager", (method, args) -> { + switch (method.getName()) { + case "hasNavigationBar": case "isKeyguardLocked": case "isKeyguardSecure": + case "isSafeModeEnabled": case "isWindowTraceEnabled": case "isLayerTracing": return false; + case "getCurrentAnimatorScale": case "getAnimationScale": return 1f; + case "getAnimationScales": return new float[]{1f, 1f, 1f}; + case "getInitialDisplaySize": case "getBaseDisplaySize": + ((Point) args[1]).set(1920, 1080); return null; + case "getInitialDisplayDensity": case "getBaseDisplayDensity": return 320; + case "getDefaultDisplayRotation": return 0; + case "getWindowingMode": return 1; + case "openSession": return session.queryLocalInterface("android.view.IWindowSession"); + case "getWindowInsets": + for (Object arg : args) if (arg != null && arg.getClass().getName().equals("android.view.InsetsState")) + ((Rect) Reflect.call(arg, "getDisplayFrame")).set(0, 0, 1920, 1080); + return false; + case "registerCrossWindowBlurEnabledListener": return false; + case "registerDisplayWindowListener": return new int[]{0}; + case "registerTaskFpsCallback": case "unregisterTaskFpsCallback": return null; + default: throw new UnsupportedOperationException(method.getName()); + } + }); + } + + private static void logText(android.view.View view) { + if (view instanceof android.widget.TextView) + System.err.println("AXRB_WINDOW_TEXT " + ((android.widget.TextView) view).getText()); + if (view instanceof android.view.ViewGroup) { + android.view.ViewGroup group = (android.view.ViewGroup) view; + for (int i = 0; i < group.getChildCount(); ++i) logText(group.getChildAt(i)); + } + } +} diff --git a/direct/art/java/com/axrb/runtime/NativeRegistry.java b/direct/art/java/com/axrb/runtime/NativeRegistry.java new file mode 100644 index 0000000..7671892 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/NativeRegistry.java @@ -0,0 +1,50 @@ +package com.axrb.runtime; + +import android.os.*; +import java.util.*; + +/** The native libbinder and Java service managers share one session registry. */ +final class NativeRegistry extends Binder { + private static final String DESCRIPTOR = "android.os.IServiceManager"; + private final Map services; + private final Map transactions = new HashMap<>(); + private static native boolean installNative(IBinder registry); + + NativeRegistry(Map services) throws Exception { + this.services = services; + for (java.lang.reflect.Field field : Class.forName(DESCRIPTOR + "$Stub").getDeclaredFields()) + if (field.getName().startsWith("TRANSACTION_")) { + field.setAccessible(true); + transactions.put(field.getInt(null), field.getName().substring(12)); + } + if (!installNative(this)) throw new IllegalStateException("Cannot install native Binder service registry"); + } + + @Override protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { + if (code == INTERFACE_TRANSACTION) { reply.writeString(DESCRIPTOR); return true; } + String method = transactions.get(code); + if (method == null) return super.onTransact(code, data, reply, flags); + data.enforceInterface(DESCRIPTOR); + try { + switch (method) { + case "getService": case "checkService": case "getService2": case "checkService2": { + String name = data.readString(); + IBinder binder = services.get(name); + System.out.println("AXRB_NATIVE_SERVICE " + method + " " + name + " present=" + (binder != null)); + reply.writeNoException(); + if (method.endsWith("2")) { + Object metadata = Reflect.create("android.os.ServiceWithMetadata"); + Reflect.set(metadata, "service", binder); + Parcelable result = (Parcelable) Reflect.call(Class.forName("android.os.Service"), "serviceWithMetadata", metadata); + reply.writeTypedObject(result, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else reply.writeStrongBinder(binder); + return true; + } + case "isDeclared": reply.writeNoException(); reply.writeBoolean(services.containsKey(data.readString())); return true; + case "listServices": reply.writeNoException(); reply.writeStringArray(services.keySet().toArray(new String[0])); return true; + case "getDeclaredInstances": reply.writeNoException(); reply.writeStringArray(new String[0]); return true; + default: System.err.println("AXRB_NATIVE_REGISTRY_UNIMPLEMENTED " + method); return false; + } + } catch (Exception error) { reply.writeException(error); return true; } + } +} diff --git a/direct/art/java/com/axrb/runtime/OpenXrProvider.java b/direct/art/java/com/axrb/runtime/OpenXrProvider.java new file mode 100644 index 0000000..e31ad42 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/OpenXrProvider.java @@ -0,0 +1,27 @@ +package com.axrb.runtime; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import java.util.List; + +/** Khronos runtime discovery backed by AXRB's existing OpenXR runtime. */ +public final class OpenXrProvider extends ContentProvider { + public boolean onCreate() { return true; } + public Cursor query(Uri uri, String[] projection, String selection, String[] args, String order) { + List parts = uri.getPathSegments(); + MatrixCursor result = new MatrixCursor(new String[]{"package_name", "native_lib_dir", "so_filename", "has_functions"}); + if (parts.size() < 6 || !parts.get(0).equals("openxr") || !parts.get(2).equals("abi") || + !parts.get(4).equals("runtimes") || !parts.get(5).equals("active")) return result; + if (parts.size() > 6) return new MatrixCursor(new String[]{"function_name", "symbol_name"}); + if (parts.get(1).equals("1") && parts.get(3).equals("arm64-v8a")) + result.addRow(new Object[]{"com.axrb.openxrruntime", "/data/app/axrb/lib/arm64", "libopenxr_runtime.so", 0}); + return result; + } + public String getType(Uri uri) { return "vnd.android.cursor.dir/vnd.org.khronos.openxr.runtime"; } + public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException("Runtime broker is read-only"); } + public int update(Uri uri, ContentValues values, String selection, String[] args) { throw new UnsupportedOperationException("Runtime broker is read-only"); } + public int delete(Uri uri, String selection, String[] args) { throw new UnsupportedOperationException("Runtime broker is read-only"); } +} diff --git a/direct/art/java/com/axrb/runtime/Reflect.java b/direct/art/java/com/axrb/runtime/Reflect.java new file mode 100644 index 0000000..f1b2da5 --- /dev/null +++ b/direct/art/java/com/axrb/runtime/Reflect.java @@ -0,0 +1,60 @@ +package com.axrb.runtime; + +import java.lang.reflect.*; + +final class Reflect { + static Object field(Object target, String name) throws Exception { + Field field = findField(target instanceof Class ? (Class) target : target.getClass(), name); + return field.get(target instanceof Class ? null : target); + } + static void set(Object target, String name, Object value) throws Exception { + Field field = findField(target instanceof Class ? (Class) target : target.getClass(), name); + field.set(target instanceof Class ? null : target, value); + } + static Field findField(Class type, String name) throws Exception { + for (Class c = type; c != null; c = c.getSuperclass()) { + try { Field f = c.getDeclaredField(name); f.setAccessible(true); return f; } + catch (NoSuchFieldException ignored) {} + } + throw new NoSuchFieldException(type.getName() + "." + name); + } + static Class boxed(Class type) { + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == boolean.class) return Boolean.class; + if (type == float.class) return Float.class; + return type; + } + static boolean matches(Class[] types, Object[] args) { + if (types.length != args.length) return false; + for (int i = 0; i < types.length; ++i) + if (args[i] == null ? types[i].isPrimitive() : !boxed(types[i]).isInstance(args[i])) return false; + return true; + } + static Object call(Object target, String name, Object... args) throws Exception { + Class type = target instanceof Class ? (Class) target : target.getClass(); + for (Class c = type; c != null; c = c.getSuperclass()) { + for (Method m : c.getDeclaredMethods()) { + if (!m.getName().equals(name) || !matches(m.getParameterTypes(), args)) continue; + m.setAccessible(true); + try { return m.invoke(target instanceof Class ? null : target, args); } + catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) throw (Exception) cause; + if (cause instanceof Error) throw (Error) cause; + throw e; + } + } + } + throw new NoSuchMethodException(type.getName() + "." + name); + } + static Object create(String name, Object... args) throws Exception { + Class type = Class.forName(name); + for (Constructor ctor : type.getDeclaredConstructors()) { + if (!matches(ctor.getParameterTypes(), args)) continue; + ctor.setAccessible(true); + return ctor.newInstance(args); + } + throw new NoSuchMethodException(name + " constructor"); + } +} diff --git a/direct/art/ndk_host.cpp b/direct/art/ndk_host.cpp new file mode 100644 index 0000000..79031c1 --- /dev/null +++ b/direct/art/ndk_host.cpp @@ -0,0 +1,149 @@ +#include "../guest/axrb_guest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Native frame callbacks run on the requesting Android looper, driven by a +// monotonic timer. The renderer continues to pace actual presentation. +static constexpr int64_t period = 16666667; +static int64_t now_ns() { timespec t{}; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec * 1000000000LL + t.tv_nsec; } +struct AChoreographerFrameCallbackData { int64_t time, id; }; +struct FrameRequest { + int64_t due; + AChoreographer_frameCallback64 frame; + AChoreographer_vsyncCallback vsync; + AChoreographer_refreshRateCallback refresh; + void* data; +}; +struct AChoreographer { + ALooper* looper; + int timer; + int64_t serial = 0; + std::mutex lock; + std::vector requests; + void arm() { + itimerspec value{}; + if (!requests.empty()) { + int64_t due = std::min_element(requests.begin(), requests.end(), [](auto& a, auto& b) { return a.due < b.due; })->due; + value.it_value.tv_sec = due / 1000000000LL; + value.it_value.tv_nsec = due % 1000000000LL; + } + timerfd_settime(timer, TFD_TIMER_ABSTIME, &value, nullptr); + } + void post(FrameRequest request) { std::lock_guard guard(lock); requests.push_back(request); arm(); } + ~AChoreographer() { ALooper_removeFd(looper, timer); close(timer); ALooper_release(looper); } +}; +static int dispatch_frames(int fd, int, void* pointer) { + auto* c = static_cast(pointer); + uint64_t count; (void)read(fd, &count, sizeof count); + std::vector ready; + int64_t time = now_ns(); + { + std::lock_guard guard(c->lock); + for (auto it = c->requests.begin(); it != c->requests.end();) { + if (it->due <= time) { ready.push_back(*it); it = c->requests.erase(it); } + else ++it; + } + c->arm(); + } + AChoreographerFrameCallbackData frame{time, ++c->serial}; + for (auto& request : ready) { + if (request.frame) request.frame(time, request.data); + if (request.vsync) request.vsync(&frame, request.data); + if (request.refresh) request.refresh(period, request.data); + } + return 1; +} +extern "C" { +AChoreographer* AChoreographer_getInstance() { + struct Owner { AChoreographer* value = nullptr; ~Owner() { delete value; } }; + thread_local Owner owner; + if (owner.value) return owner.value; + ALooper* looper = ALooper_forThread(); + if (!looper) return nullptr; + int timer = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (timer < 0) return nullptr; + ALooper_acquire(looper); + auto* c = new AChoreographer{looper, timer}; + if (ALooper_addFd(looper, timer, ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, dispatch_frames, c) != 1) { delete c; return nullptr; } + return owner.value = c; +} +void AChoreographer_postFrameCallbackDelayed64(AChoreographer* c, AChoreographer_frameCallback64 callback, void* data, uint32_t delay) { + if (c && callback) c->post({now_ns() + std::max(period, int64_t(delay) * 1000000), callback, nullptr, nullptr, data}); +} +void AChoreographer_postFrameCallback64(AChoreographer* c, AChoreographer_frameCallback64 callback, void* data) { AChoreographer_postFrameCallbackDelayed64(c, callback, data, 0); } +void AChoreographer_postFrameCallback(AChoreographer* c, AChoreographer_frameCallback callback, void* data) { AChoreographer_postFrameCallback64(c, callback, data); } +void AChoreographer_postFrameCallbackDelayed(AChoreographer* c, AChoreographer_frameCallback callback, void* data, long delay) { AChoreographer_postFrameCallbackDelayed64(c, callback, data, std::max(delay, 0)); } +void AChoreographer_postVsyncCallback(AChoreographer* c, AChoreographer_vsyncCallback callback, void* data) { if (c && callback) c->post({now_ns() + period, nullptr, callback, nullptr, data}); } +void AChoreographer_registerRefreshRateCallback(AChoreographer* c, AChoreographer_refreshRateCallback callback, void* data) { if (c && callback) c->post({now_ns() + 1, nullptr, nullptr, callback, data}); } +void AChoreographer_unregisterRefreshRateCallback(AChoreographer* c, AChoreographer_refreshRateCallback callback, void* data) { + if (!c) return; + std::lock_guard guard(c->lock); + c->requests.erase(std::remove_if(c->requests.begin(), c->requests.end(), [&](auto& r) { return r.refresh == callback && r.data == data; }), c->requests.end()); c->arm(); +} +int64_t AChoreographerFrameCallbackData_getFrameTimeNanos(const AChoreographerFrameCallbackData* data) { return data->time; } +size_t AChoreographerFrameCallbackData_getFrameTimelinesLength(const AChoreographerFrameCallbackData*) { return 1; } +size_t AChoreographerFrameCallbackData_getPreferredFrameTimelineIndex(const AChoreographerFrameCallbackData*) { return 0; } +AVsyncId AChoreographerFrameCallbackData_getFrameTimelineVsyncId(const AChoreographerFrameCallbackData* data, size_t) { return data->id; } +int64_t AChoreographerFrameCallbackData_getFrameTimelineExpectedPresentationTimeNanos(const AChoreographerFrameCallbackData* data, size_t) { return data->time + period; } +int64_t AChoreographerFrameCallbackData_getFrameTimelineDeadlineNanos(const AChoreographerFrameCallbackData* data, size_t) { return data->time + period; } +} + +// There are no handheld Android sensors. Headset/controller poses use OpenXR. +struct ASensorManager {}; +struct ASensorEventQueue {}; +extern "C" { +ASensorManager* ASensorManager_getInstance() { static ASensorManager manager; return &manager; } +ASensorManager* ASensorManager_getInstanceForPackage(const char*) { static ASensorManager manager; return &manager; } +int ASensorManager_getSensorList(ASensorManager*, ASensorList* list) { if (list) *list = nullptr; return 0; } +const ASensor* ASensorManager_getDefaultSensor(ASensorManager*, int) { return nullptr; } +const ASensor* ASensorManager_getDefaultSensorEx(ASensorManager*, int, bool) { return nullptr; } +ASensorEventQueue* ASensorManager_createEventQueue(ASensorManager*, ALooper*, int, ALooper_callbackFunc, void*) { return new ASensorEventQueue; } +int ASensorManager_destroyEventQueue(ASensorManager*, ASensorEventQueue* queue) { delete queue; return 0; } +int ASensorManager_createSharedMemoryDirectChannel(ASensorManager*, int, size_t) { return -ENODEV; } +int ASensorManager_createHardwareBufferDirectChannel(ASensorManager*, const AHardwareBuffer*, size_t) { return -ENODEV; } +void ASensorManager_destroyDirectChannel(ASensorManager*, int) {} +int ASensorManager_configureDirectReport(ASensorManager*, const ASensor*, int, int) { return -ENODEV; } +int ASensorEventQueue_registerSensor(ASensorEventQueue*, const ASensor*, int32_t, int64_t) { return -ENODEV; } +int ASensorEventQueue_enableSensor(ASensorEventQueue*, const ASensor*) { return -ENODEV; } +int ASensorEventQueue_disableSensor(ASensorEventQueue*, const ASensor*) { return -ENODEV; } +int ASensorEventQueue_setEventRate(ASensorEventQueue*, const ASensor*, int32_t) { return -ENODEV; } +int ASensorEventQueue_hasEvents(ASensorEventQueue*) { return 0; } +ssize_t ASensorEventQueue_getEvents(ASensorEventQueue*, ASensorEvent*, size_t) { return 0; } +int ASensorEventQueue_requestAdditionalInfoEvents(ASensorEventQueue*, bool) { return 0; } +} + +// Windows of AXRB host surfaces (OpenXR Android-surface swapchains) are +// registered with the host, so AMediaCodec can show decoded frames on them. +extern "C" long axrb_mediandk(long op, long id, long a, long b, long c, long d); +AXRB_HOST_FUNCTION(axrb_mediandk, "art.mediandk"); +extern "C" ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface) { + using From = ANativeWindow* (*)(JNIEnv*, jobject); + static const From original = reinterpret_cast(dlsym(dlopen("/system/lib64/libaxrbndk.so", RTLD_NOW | RTLD_NOLOAD), "ANativeWindow_fromSurface")); + ANativeWindow* window = original ? original(env, surface) : nullptr; + if (!window || !env || !surface) return window; + jclass type = env->GetObjectClass(surface); + jclass classClass = type ? env->FindClass("java/lang/Class") : nullptr; + jmethodID getName = classClass ? env->GetMethodID(classClass, "getName", "()Ljava/lang/String;") : nullptr; + auto name = getName ? static_cast(env->CallObjectMethod(type, getName)) : nullptr; + const char* chars = name ? env->GetStringUTFChars(name, nullptr) : nullptr; + if (chars && strcmp(chars, "com.axrb.runtime.HostSurface") == 0) { + jfieldID field = env->GetFieldID(type, "mediaHandle", "J"); + if (field) axrb_mediandk(41, 0, reinterpret_cast(window), static_cast(env->GetLongField(surface, field)), 0, 0); + } + if (chars) env->ReleaseStringUTFChars(name, chars); + if (env->ExceptionCheck()) env->ExceptionClear(); + if (name) env->DeleteLocalRef(name); + if (classClass) env->DeleteLocalRef(classClass); + if (type) env->DeleteLocalRef(type); + return window; +} diff --git a/direct/art/ndk_media.cpp b/direct/art/ndk_media.cpp new file mode 100644 index 0000000..afaff83 --- /dev/null +++ b/direct/art/ndk_media.cpp @@ -0,0 +1,602 @@ +// AMediaExtractor and AMediaCodec for Direct mode. AOSP's versions need the +// media.extractor and media.codec services, which Direct mode does not run +// ("extractor service not running"). These call the host's Media Foundation +// backend (direct/src/media_codec.cpp) instead. Every other libmediandk export, +// AMediaFormat included, is forwarded to the matched AOSP library. +#include "../guest/axrb_guest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" long axrb_mediandk(long op, long id, long a, long b, long c, long d); +AXRB_HOST_FUNCTION(axrb_mediandk, "art.mediandk"); + +#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "AXRB.Media", __VA_ARGS__) + +namespace { +// Shared with direct/src/media_codec.cpp. +enum Op : long { + kExtractorNew = 0, kExtractorSourceFd, kExtractorTrackCount, kExtractorTrack, kExtractorCsd, + kExtractorSelect, kExtractorSeek, kExtractorSample, kExtractorRead, kExtractorAdvance, + kExtractorDelete, kExtractorFileFormat, + kCodecNew = 20, kCodecConfigure, kCodecStart, kCodecStop, kCodecFlush, kCodecDelete, + kCodecInput, kCodecOutput, kCodecRelease, kCodecFormat, kCodecSurface, + kWindowHandle = 40, +}; +struct TrackInfo { + char mime[64]; + int64_t duration_us; + int32_t width, height, sample_rate, channels, max_input_size, frame_rate, bit_rate, is_adts; + int32_t csd_count, reserved; +}; +struct SampleInfo { + int64_t time_us; + int32_t track, flags, size, reserved; +}; +struct CodecConfig { + int32_t width, height, sample_rate, channels, is_adts, reserved; + uint64_t csd[2]; + int32_t csd_size[2]; +}; +struct OutputInfo { + int64_t time_us; + uint64_t sequence; + int32_t size, flags; +}; +struct OutputFormat { + char mime[64]; + int32_t width, height, stride, slice_height, color_format; + int32_t crop_left, crop_top, crop_right, crop_bottom; + int32_t sample_rate, channels, pcm_encoding; +}; + +long host(long op, long id = 0, long a = 0, long b = 0, long c = 0, long d = 0) { + return axrb_mediandk(op, id, a, b, c, d); +} +media_status_t status(long value) { return value < 0 ? static_cast(value) : AMEDIA_OK; } +} // namespace + +struct AMediaExtractor { + long id; +}; + +struct AMediaCodec { + long id; + std::string mime; + std::mutex lock; + struct Input { + std::vector data; + bool client = false; // dequeued and not yet queued back + }; + struct Output { + std::vector data; + OutputInfo info{}; + bool client = false; + }; + std::vector inputs; + std::vector outputs; + size_t input_size = 0; + long window = 0; + bool started = false; + bool input_ended = false; // end of stream queued: no more input until flush + // Asynchronous mode (AMediaCodec_setAsyncNotifyCallback). + AMediaCodecOnAsyncNotifyCallback callback{}; + void* userdata = nullptr; + std::thread worker; + std::atomic running{false}; + std::vector offered; +}; + +extern "C" { + +// --- AMediaExtractor ------------------------------------------------------------------------- + +AMediaExtractor* AMediaExtractor_new() { + const long id = host(kExtractorNew); + return id > 0 ? new AMediaExtractor{id} : nullptr; +} + +media_status_t AMediaExtractor_delete(AMediaExtractor* x) { + if (!x) return AMEDIA_ERROR_INVALID_OBJECT; + host(kExtractorDelete, x->id); + delete x; + return AMEDIA_OK; +} + +media_status_t AMediaExtractor_setDataSourceFd(AMediaExtractor* x, int fd, off64_t offset, off64_t length) { + if (!x) return AMEDIA_ERROR_INVALID_OBJECT; + return status(host(kExtractorSourceFd, x->id, fd, offset, length)); +} + +media_status_t AMediaExtractor_setDataSource(AMediaExtractor* x, const char* location) { + if (!x || !location) return AMEDIA_ERROR_INVALID_PARAMETER; + const char* path = strncmp(location, "file://", 7) == 0 ? location + 7 : location; + if (path[0] != '/') { + LOG("AMediaExtractor: only local files are supported, not %.64s", location); + return AMEDIA_ERROR_UNSUPPORTED; + } + const int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) return AMEDIA_ERROR_IO; + const media_status_t result = AMediaExtractor_setDataSourceFd(x, fd, 0, -1); + close(fd); + return result; +} + +media_status_t AMediaExtractor_setDataSourceCustom(AMediaExtractor*, AMediaDataSource*) { + LOG("AMediaExtractor: custom data sources are not supported"); + return AMEDIA_ERROR_UNSUPPORTED; +} + +size_t AMediaExtractor_getTrackCount(AMediaExtractor* x) { + const long count = x ? host(kExtractorTrackCount, x->id) : 0; + return count > 0 ? static_cast(count) : 0; +} + +AMediaFormat* AMediaExtractor_getTrackFormat(AMediaExtractor* x, size_t index) { + TrackInfo info{}; + if (!x || host(kExtractorTrack, x->id, static_cast(index), reinterpret_cast(&info)) < 0) return nullptr; + AMediaFormat* format = AMediaFormat_new(); + AMediaFormat_setString(format, "mime", info.mime); + if (info.duration_us > 0) AMediaFormat_setInt64(format, "durationUs", info.duration_us); + if (info.width > 0) AMediaFormat_setInt32(format, "width", info.width); + if (info.height > 0) AMediaFormat_setInt32(format, "height", info.height); + if (info.frame_rate > 0) AMediaFormat_setInt32(format, "frame-rate", info.frame_rate); + if (info.sample_rate > 0) AMediaFormat_setInt32(format, "sample-rate", info.sample_rate); + if (info.channels > 0) AMediaFormat_setInt32(format, "channel-count", info.channels); + if (info.max_input_size > 0) AMediaFormat_setInt32(format, "max-input-size", info.max_input_size); + if (info.bit_rate > 0) AMediaFormat_setInt32(format, "bitrate", info.bit_rate); + if (info.is_adts) AMediaFormat_setInt32(format, "is-adts", 1); + for (int i = 0; i < info.csd_count; ++i) { + const long size = host(kExtractorCsd, x->id, static_cast(index), i, 0, 0); + if (size <= 0) continue; + std::vector csd(static_cast(size)); + host(kExtractorCsd, x->id, static_cast(index), i, reinterpret_cast(csd.data()), size); + const char key[] = {'c', 's', 'd', '-', static_cast('0' + i), 0}; + AMediaFormat_setBuffer(format, key, csd.data(), csd.size()); + } + return format; +} + +media_status_t AMediaExtractor_selectTrack(AMediaExtractor* x, size_t index) { + return x ? status(host(kExtractorSelect, x->id, static_cast(index), 1)) : AMEDIA_ERROR_INVALID_OBJECT; +} +media_status_t AMediaExtractor_unselectTrack(AMediaExtractor* x, size_t index) { + return x ? status(host(kExtractorSelect, x->id, static_cast(index), 0)) : AMEDIA_ERROR_INVALID_OBJECT; +} + +ssize_t AMediaExtractor_readSampleData(AMediaExtractor* x, uint8_t* buffer, size_t capacity) { + if (!x || !buffer) return -1; + const long n = host(kExtractorRead, x->id, reinterpret_cast(buffer), static_cast(capacity)); + return n < 0 ? -1 : n; +} + +static bool sample(AMediaExtractor* x, SampleInfo& info) { + return x && host(kExtractorSample, x->id, reinterpret_cast(&info)) == 0; +} +uint32_t AMediaExtractor_getSampleFlags(AMediaExtractor* x) { + SampleInfo info{}; + return sample(x, info) ? static_cast(info.flags) : 0; +} +int AMediaExtractor_getSampleTrackIndex(AMediaExtractor* x) { + SampleInfo info{}; + return sample(x, info) ? info.track : -1; +} +int64_t AMediaExtractor_getSampleTime(AMediaExtractor* x) { + SampleInfo info{}; + return sample(x, info) ? info.time_us : -1; +} +ssize_t AMediaExtractor_getSampleSize(AMediaExtractor* x) { + SampleInfo info{}; + return sample(x, info) ? info.size : -1; +} +bool AMediaExtractor_advance(AMediaExtractor* x) { return x && host(kExtractorAdvance, x->id) > 0; } + +media_status_t AMediaExtractor_seekTo(AMediaExtractor* x, int64_t time_us, SeekMode) { + return x ? status(host(kExtractorSeek, x->id, static_cast(time_us))) : AMEDIA_ERROR_INVALID_OBJECT; +} + +AMediaFormat* AMediaExtractor_getFileFormat(AMediaExtractor* x) { + char mime[64]{}; + if (!x || host(kExtractorFileFormat, x->id, reinterpret_cast(mime), sizeof mime) < 0) return nullptr; + AMediaFormat* format = AMediaFormat_new(); + AMediaFormat_setString(format, "mime", mime); + return format; +} + +media_status_t AMediaExtractor_getSampleFormat(AMediaExtractor* x, AMediaFormat* format) { + SampleInfo info{}; + if (!format) return AMEDIA_ERROR_INVALID_PARAMETER; + if (!sample(x, info)) return AMEDIA_ERROR_END_OF_STREAM; + AMediaFormat_clear(format); + AMediaFormat_setInt64(format, "time-us", info.time_us); + if (info.flags & AMEDIAEXTRACTOR_SAMPLE_FLAG_SYNC) AMediaFormat_setInt32(format, "is-sync-frame", 1); + return AMEDIA_OK; +} + +int64_t AMediaExtractor_getCachedDuration(AMediaExtractor*) { return -1; } +PsshInfo* AMediaExtractor_getPsshInfo(AMediaExtractor*) { return nullptr; } +AMediaCodecCryptoInfo* AMediaExtractor_getSampleCryptoInfo(AMediaExtractor*) { return nullptr; } + +// --- AMediaCodec --------------------------------------------------------------------------- + +static const char* mime_for_name(const char* name) { + static const struct { const char* part; const char* mime; } names[] = { + {"avc", "video/avc"}, {"h264", "video/avc"}, {"hevc", "video/hevc"}, {"h265", "video/hevc"}, + {"vp9", "video/x-vnd.on2.vp9"}, {"vp8", "video/x-vnd.on2.vp8"}, {"av1", "video/av01"}, + {"mpeg4", "video/mp4v-es"}, {"aac", "audio/mp4a-latm"}, {"mp3", "audio/mpeg"}, + {"flac", "audio/flac"}, {"opus", "audio/opus"}, + }; + std::string lower(name ? name : ""); + for (char& c : lower) c = static_cast(tolower(static_cast(c))); + if (lower.find("encoder") != std::string::npos) return nullptr; + for (auto& n : names) + if (lower.find(n.part) != std::string::npos) return n.mime; + return nullptr; +} + +AMediaCodec* AMediaCodec_createDecoderByType(const char* mime) { + if (!mime) return nullptr; + const long id = host(kCodecNew, 0, reinterpret_cast(mime)); + if (id <= 0) return nullptr; + auto* codec = new AMediaCodec; + codec->id = id; + codec->mime = mime; + return codec; +} +AMediaCodec* AMediaCodec_createCodecByName(const char* name) { + const char* mime = mime_for_name(name); + return mime ? AMediaCodec_createDecoderByType(mime) : nullptr; +} +AMediaCodec* AMediaCodec_createDecoderByTypeForClient(const char* mime, pid_t, uid_t) { return AMediaCodec_createDecoderByType(mime); } +AMediaCodec* AMediaCodec_createCodecByNameForClient(const char* name, pid_t, uid_t) { return AMediaCodec_createCodecByName(name); } +AMediaCodec* AMediaCodec_createEncoderByType(const char* mime) { + LOG("AMediaCodec: encoders are not supported (%s)", mime ? mime : "?"); + return nullptr; +} +AMediaCodec* AMediaCodec_createEncoderByTypeForClient(const char* mime, pid_t, uid_t) { return AMediaCodec_createEncoderByType(mime); } + +static void stop_worker(AMediaCodec* codec) { + codec->running = false; + if (codec->worker.joinable() && codec->worker.get_id() != std::this_thread::get_id()) codec->worker.join(); + else if (codec->worker.joinable()) codec->worker.detach(); +} + +media_status_t AMediaCodec_delete(AMediaCodec* codec) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + stop_worker(codec); + host(kCodecDelete, codec->id); + delete codec; + return AMEDIA_OK; +} + +media_status_t AMediaCodec_configure(AMediaCodec* codec, const AMediaFormat* format, ANativeWindow* window, + AMediaCrypto* crypto, uint32_t flags) { + if (!codec || !format) return AMEDIA_ERROR_INVALID_PARAMETER; + if (flags & AMEDIACODEC_CONFIGURE_FLAG_ENCODE) return AMEDIA_ERROR_UNSUPPORTED; + if (crypto) { + LOG("AMediaCodec: encrypted media is not supported"); + return AMEDIA_ERROR_UNSUPPORTED; + } + auto* f = const_cast(format); + CodecConfig config{}; + AMediaFormat_getInt32(f, "width", &config.width); + AMediaFormat_getInt32(f, "height", &config.height); + AMediaFormat_getInt32(f, "sample-rate", &config.sample_rate); + AMediaFormat_getInt32(f, "channel-count", &config.channels); + AMediaFormat_getInt32(f, "is-adts", &config.is_adts); + for (int i = 0; i < 2; ++i) { + void* data = nullptr; + size_t size = 0; + const char key[] = {'c', 's', 'd', '-', static_cast('0' + i), 0}; + if (AMediaFormat_getBuffer(f, key, &data, &size) && data && size) { + config.csd[i] = reinterpret_cast(data); + config.csd_size[i] = static_cast(size); + } + } + int32_t max_input = 0; + AMediaFormat_getInt32(f, "max-input-size", &max_input); + const bool video = codec->mime.compare(0, 6, "video/") == 0; + const size_t fallback = video ? std::max(size_t(config.width) * config.height * 3 / 2, 1 << 20) : 64 * 1024; + codec->input_size = std::max(max_input > 0 ? size_t(max_input) : 0, fallback); + codec->window = window ? host(kWindowHandle, 0, reinterpret_cast(window)) : 0; + if (window && codec->window <= 0) { + LOG("AMediaCodec: output Surface is not an AXRB host surface; frames are decoded but not shown"); + codec->window = 0; + } + std::lock_guard guard(codec->lock); + codec->inputs.assign(4, {}); + codec->outputs.assign(video ? 8 : 4, {}); + return status(host(kCodecConfigure, codec->id, reinterpret_cast(&config), codec->window > 0 ? codec->window : 0)); +} + +media_status_t AMediaCodec_setOutputSurface(AMediaCodec* codec, ANativeWindow* window) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + const long handle = window ? host(kWindowHandle, 0, reinterpret_cast(window)) : 0; + codec->window = handle > 0 ? handle : 0; + return status(host(kCodecSurface, codec->id, codec->window)); +} + +static void reset_buffers(AMediaCodec* codec) { + std::lock_guard guard(codec->lock); + for (auto& in : codec->inputs) in.client = false; + for (auto& out : codec->outputs) out.client = false; + codec->offered.assign(codec->inputs.size(), false); + codec->input_ended = false; +} + +static void async_loop(AMediaCodec* codec); + +media_status_t AMediaCodec_start(AMediaCodec* codec) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + const media_status_t result = status(host(kCodecStart, codec->id)); + if (result != AMEDIA_OK) return result; + reset_buffers(codec); + codec->started = true; + if (codec->callback.onAsyncInputAvailable && !codec->running) { + stop_worker(codec); + codec->running = true; + codec->worker = std::thread(async_loop, codec); + } + return AMEDIA_OK; +} + +media_status_t AMediaCodec_stop(AMediaCodec* codec) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + stop_worker(codec); + codec->started = false; + host(kCodecStop, codec->id); + reset_buffers(codec); + return AMEDIA_OK; +} + +media_status_t AMediaCodec_flush(AMediaCodec* codec) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + // In asynchronous mode, a flushed codec resumes at the next start(). + stop_worker(codec); + host(kCodecFlush, codec->id); + reset_buffers(codec); + return AMEDIA_OK; +} + +ssize_t AMediaCodec_dequeueInputBuffer(AMediaCodec* codec, int64_t) { + if (!codec || !codec->started) return AMEDIACODEC_INFO_TRY_AGAIN_LATER; + std::lock_guard guard(codec->lock); + for (size_t i = 0; i < codec->inputs.size(); ++i) { + if (codec->inputs[i].client) continue; + codec->inputs[i].client = true; + if (codec->inputs[i].data.size() < codec->input_size) codec->inputs[i].data.resize(codec->input_size); + return static_cast(i); + } + return AMEDIACODEC_INFO_TRY_AGAIN_LATER; +} + +uint8_t* AMediaCodec_getInputBuffer(AMediaCodec* codec, size_t index, size_t* size) { + if (!codec) return nullptr; + std::lock_guard guard(codec->lock); + if (index >= codec->inputs.size()) return nullptr; + auto& in = codec->inputs[index]; + if (in.data.size() < codec->input_size) in.data.resize(codec->input_size); + if (size) *size = in.data.size(); + return in.data.data(); +} + +media_status_t AMediaCodec_queueInputBuffer(AMediaCodec* codec, size_t index, off_t offset, size_t size, uint64_t time, + uint32_t flags) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + uint8_t* data; + { + std::lock_guard guard(codec->lock); + if (index >= codec->inputs.size() || offset < 0 || size_t(offset) + size > codec->inputs[index].data.size()) + return AMEDIA_ERROR_INVALID_PARAMETER; + data = codec->inputs[index].data.data() + offset; + } + const media_status_t result = status(host(kCodecInput, codec->id, reinterpret_cast(data), static_cast(size), + static_cast(time), static_cast(flags))); + std::lock_guard guard(codec->lock); + codec->inputs[index].client = false; + if (index < codec->offered.size()) codec->offered[index] = false; + if (flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) codec->input_ended = true; + return result; +} + +media_status_t AMediaCodec_queueSecureInputBuffer(AMediaCodec*, size_t, off_t, AMediaCodecCryptoInfo*, uint64_t, uint32_t) { + return AMEDIA_ERROR_UNSUPPORTED; +} + +// One output step: a buffer index, OUTPUT_FORMAT_CHANGED or TRY_AGAIN_LATER. +static ssize_t next_output(AMediaCodec* codec, AMediaCodecBufferInfo* info, int64_t timeout_us) { + if (!codec || !codec->started) return AMEDIACODEC_INFO_TRY_AGAIN_LATER; + size_t index = 0; + { + std::lock_guard guard(codec->lock); + while (index < codec->outputs.size() && codec->outputs[index].client) ++index; + if (index == codec->outputs.size()) return AMEDIACODEC_INFO_TRY_AGAIN_LATER; + codec->outputs[index].client = true; // reserved while the host fills it + } + auto& out = codec->outputs[index]; + long result; + for (;;) { + OutputInfo got{}; + const bool surface = codec->window > 0; + result = host(kCodecOutput, codec->id, reinterpret_cast(&got), surface ? 0 : reinterpret_cast(out.data.data()), + surface ? 0 : static_cast(out.data.size()), static_cast(timeout_us)); + if (result == -3) { + out.data.resize(static_cast(got.size)); + continue; + } + out.info = got; + break; + } + if (result != 0) { + std::lock_guard guard(codec->lock); + out.client = false; + if (result == 1) return AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED; + return result == -1 ? AMEDIACODEC_INFO_TRY_AGAIN_LATER : result; + } + if (info) { + info->offset = 0; + info->size = out.info.size; + info->presentationTimeUs = out.info.time_us; + info->flags = static_cast(out.info.flags); + } + return static_cast(index); +} + +ssize_t AMediaCodec_dequeueOutputBuffer(AMediaCodec* codec, AMediaCodecBufferInfo* info, int64_t timeout_us) { + return next_output(codec, info, timeout_us); +} + +uint8_t* AMediaCodec_getOutputBuffer(AMediaCodec* codec, size_t index, size_t* size) { + if (!codec) return nullptr; + std::lock_guard guard(codec->lock); + if (index >= codec->outputs.size() || codec->window > 0) return nullptr; + auto& out = codec->outputs[index]; + if (size) *size = out.data.size(); + return out.data.data(); +} + +media_status_t AMediaCodec_releaseOutputBuffer(AMediaCodec* codec, size_t index, bool render) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + uint64_t sequence; + { + std::lock_guard guard(codec->lock); + if (index >= codec->outputs.size() || !codec->outputs[index].client) return AMEDIA_ERROR_INVALID_PARAMETER; + sequence = codec->outputs[index].info.sequence; + codec->outputs[index].client = false; + } + host(kCodecRelease, codec->id, static_cast(sequence), render ? 1 : 0); + return AMEDIA_OK; +} + +media_status_t AMediaCodec_releaseOutputBufferAtTime(AMediaCodec* codec, size_t index, int64_t) { + return AMediaCodec_releaseOutputBuffer(codec, index, true); +} + +AMediaFormat* AMediaCodec_getOutputFormat(AMediaCodec* codec) { + OutputFormat out{}; + if (!codec || host(kCodecFormat, codec->id, reinterpret_cast(&out)) < 0) return nullptr; + AMediaFormat* format = AMediaFormat_new(); + AMediaFormat_setString(format, "mime", out.mime); + if (out.width > 0) { + AMediaFormat_setInt32(format, "width", out.width); + AMediaFormat_setInt32(format, "height", out.height); + AMediaFormat_setInt32(format, "stride", out.stride); + AMediaFormat_setInt32(format, "slice-height", out.slice_height); + AMediaFormat_setInt32(format, "color-format", out.color_format); + AMediaFormat_setInt32(format, "crop-left", out.crop_left); + AMediaFormat_setInt32(format, "crop-top", out.crop_top); + AMediaFormat_setInt32(format, "crop-right", out.crop_right); + AMediaFormat_setInt32(format, "crop-bottom", out.crop_bottom); + } + if (out.sample_rate > 0) { + AMediaFormat_setInt32(format, "sample-rate", out.sample_rate); + AMediaFormat_setInt32(format, "channel-count", out.channels); + AMediaFormat_setInt32(format, "pcm-encoding", out.pcm_encoding); + } + return format; +} + +AMediaFormat* AMediaCodec_getBufferFormat(AMediaCodec* codec, size_t) { return AMediaCodec_getOutputFormat(codec); } + +AMediaFormat* AMediaCodec_getInputFormat(AMediaCodec* codec) { + if (!codec) return nullptr; + AMediaFormat* format = AMediaFormat_new(); + AMediaFormat_setString(format, "mime", codec->mime.c_str()); + return format; +} + +media_status_t AMediaCodec_getName(AMediaCodec* codec, char** name) { + if (!codec || !name) return AMEDIA_ERROR_INVALID_PARAMETER; + std::string value = "c2.axrb." + codec->mime.substr(codec->mime.find('/') + 1) + ".decoder"; + *name = strdup(value.c_str()); + return AMEDIA_OK; +} +void AMediaCodec_releaseName(AMediaCodec*, char* name) { free(name); } + +media_status_t AMediaCodec_setParameters(AMediaCodec* codec, const AMediaFormat*) { + return codec ? AMEDIA_OK : AMEDIA_ERROR_INVALID_OBJECT; +} +media_status_t AMediaCodec_releaseCrypto(AMediaCodec* codec) { return codec ? AMEDIA_OK : AMEDIA_ERROR_INVALID_OBJECT; } +media_status_t AMediaCodec_signalEndOfInputStream(AMediaCodec*) { return AMEDIA_ERROR_UNSUPPORTED; } +media_status_t AMediaCodec_createInputSurface(AMediaCodec*, ANativeWindow**) { return AMEDIA_ERROR_UNSUPPORTED; } +media_status_t AMediaCodec_createPersistentInputSurface(ANativeWindow**) { return AMEDIA_ERROR_UNSUPPORTED; } +media_status_t AMediaCodec_setInputSurface(AMediaCodec*, ANativeWindow*) { return AMEDIA_ERROR_UNSUPPORTED; } +media_status_t AMediaCodec_setOnFrameRenderedCallback(AMediaCodec* codec, AMediaCodecOnFrameRendered, void*) { + return codec ? AMEDIA_OK : AMEDIA_ERROR_INVALID_OBJECT; +} + +media_status_t AMediaCodec_setAsyncNotifyCallback(AMediaCodec* codec, AMediaCodecOnAsyncNotifyCallback callback, void* userdata) { + if (!codec) return AMEDIA_ERROR_INVALID_OBJECT; + if (codec->started) return AMEDIA_ERROR_INVALID_OPERATION; + codec->callback = callback; + codec->userdata = userdata; + return AMEDIA_OK; +} + +} // extern "C" + +// Asynchronous mode: offers free input buffers and delivers decoded output on +// a codec thread, like MediaCodec's callback looper. +static void async_loop(AMediaCodec* codec) { + pthread_setname_np(pthread_self(), "AXRB codec"); + while (codec->running) { + bool busy = false; + // Each free input buffer is offered once per round, and none after + // the end of stream, so output keeps flowing between offers. + for (size_t next = 0;; ++next) { + size_t index; + { + std::lock_guard guard(codec->lock); + if (codec->input_ended) break; + index = next; + while (index < codec->inputs.size() && (codec->inputs[index].client || codec->offered[index])) ++index; + if (index == codec->inputs.size()) break; + next = index; + codec->offered[index] = true; + codec->inputs[index].client = true; + if (codec->inputs[index].data.size() < codec->input_size) codec->inputs[index].data.resize(codec->input_size); + } + codec->callback.onAsyncInputAvailable(codec, codec->userdata, static_cast(index)); + busy = true; + if (!codec->running) return; + } + AMediaCodecBufferInfo info{}; + const ssize_t index = next_output(codec, &info, 0); + if (index == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { + AMediaFormat* format = AMediaCodec_getOutputFormat(codec); + if (codec->callback.onAsyncFormatChanged) codec->callback.onAsyncFormatChanged(codec, codec->userdata, format); + AMediaFormat_delete(format); + busy = true; + } else if (index >= 0) { + if (codec->callback.onAsyncOutputAvailable) + codec->callback.onAsyncOutputAvailable(codec, codec->userdata, static_cast(index), &info); + else + AMediaCodec_releaseOutputBuffer(codec, static_cast(index), false); + busy = true; + } else if (index != AMEDIACODEC_INFO_TRY_AGAIN_LATER) { + if (codec->callback.onAsyncError) + codec->callback.onAsyncError(codec, codec->userdata, static_cast(index), 0, "decode failed"); + return; + } + if (!busy) { + timespec pause{0, 2000000}; + nanosleep(&pause, nullptr); + } + } +} diff --git a/direct/digitalis/CMakeLists.txt b/direct/digitalis/CMakeLists.txt new file mode 100644 index 0000000..2c237b5 --- /dev/null +++ b/direct/digitalis/CMakeLists.txt @@ -0,0 +1,23 @@ +find_package(Python3 REQUIRED COMPONENTS Interpreter) +set(DIGITALIS_SOURCE "${AXRB_ROOT}/third_party/digitalis") +if(NOT EXISTS "${DIGITALIS_SOURCE}/interpreter/arm64/interpreter.h") + message(FATAL_ERROR "Digitalis sources missing. Run scripts/build/direct_deps.ps1.") +endif() +set(_digitalis_out "${CMAKE_CURRENT_BINARY_DIR}/$") +file(GLOB_RECURSE _digitalis_dependencies CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/*.h" "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/*.asm" "${DIGITALIS_SOURCE}/*.h" + "${DIGITALIS_SOURCE}/*.inc" "${DIGITALIS_SOURCE}/*.cc" + "${DIGITALIS_SOURCE}/*.py" "${DIGITALIS_SOURCE}/*.json") +add_custom_command(OUTPUT "${_digitalis_out}/digitalis.lib" "${_digitalis_out}/digitalis-selftest.exe" + "${_digitalis_out}/digitalis-differential.exe" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/build.py" + --heavy --ndk "${AXRB_NDK}" --linker "${CMAKE_LINKER}" --out "${_digitalis_out}" --config "$" + DEPENDS ${_digitalis_dependencies} "${CMAKE_CURRENT_SOURCE_DIR}/build.py" "${CMAKE_CURRENT_SOURCE_DIR}/build_heavy.py" + COMMENT "Building Digitalis Windows optimizing JIT, lite JIT and interpreter" VERBATIM) +add_custom_target(axrb-digitalis-build DEPENDS "${_digitalis_out}/digitalis.lib") +add_library(axrb-digitalis INTERFACE) +add_dependencies(axrb-digitalis axrb-digitalis-build) +target_link_libraries(axrb-digitalis INTERFACE "${_digitalis_out}/digitalis.lib") +add_test(NAME axrb_digitalis_windows COMMAND "${_digitalis_out}/digitalis-selftest.exe") +add_test(NAME axrb_digitalis_differential COMMAND "${_digitalis_out}/digitalis-differential.exe") diff --git a/direct/digitalis/README.md b/direct/digitalis/README.md new file mode 100644 index 0000000..884895a --- /dev/null +++ b/direct/digitalis/README.md @@ -0,0 +1,110 @@ +# Digitalis Windows adapter + +AXRB builds the public Digitalis ARM64 optimizing translator, lite translator and interpreter at +`f5f1c90b17d0c1df9e9be41534c653a3e74bfc64` from +https://github.com/DigitalisX64/platform_frameworks_libs_binary_translation, +with AXRB's patches from `direct/patches`. +Run `scripts/build/direct_deps.ps1` before configuring Direct. + +This is a native Windows adapter, not Android NativeBridge. The optimizing tier +promotes hot regions from the lite JIT, with register allocation and internal +loop linking. Promotion runs on two background threads: the guest thread keeps +running the lite translation and installs the optimized code the next time it +runs, unless an invalidation or cache reset happened meanwhile. Unsupported lite-JIT instructions use Digitalis's interpreter; +they are not silently delegated to Dynarmic. The existing AXRB process, syscall, +memory, Android framework and graphics implementations are shared by both CPUs. +Dynarmic remains the default. Select Digitalis in the launcher settings, or set +`AXRB_DIRECT_TRANSLATOR=digitalis` when invoking axrb-direct.exe directly. + +The build uses NDK 29.0.14206865 Clang targeting the MSVC ABI, Visual Studio's +x64 tools and static CRT. No compiler, Android SDK or extra runtime DLL is +required on the player's machine. CMake runs the native self-test when building +the adapter, including runs with AVX and block chaining disabled, forced +optimization, optimization on the guest thread, and the lite tier alone. A +differential test compares randomized arithmetic, flags, memory loops, a loop +optimized in the background and FP min/max lanes against the interpreter. AVX/FMA/F16C +are enabled only when supported by both the CPU and Windows XSTATE support. +AVX2 uses the same OS-state check. Integer extensions are selected from CPUID, +including upstream's avoidance of slow PDEP/PEXT on pre-Zen-3 AMD processors. + +The compatibility headers replace Android logging/allocation/platform glue. +`entry.asm` preserves the Windows nonvolatile registers around generated code. +The per-thread cache uses upstream memory-fault recovery maps; unmapped guest +accesses return through AXRB's signal handling. Cache invalidations are queued +by AXRB and applied between execution batches to affected guest pages. The cache +commits memory as needed, per thread, up to 1/32 of the machine's RAM (at least +Dynarmic's 256 MiB, at most 1 GiB); reaching the limit discards the thread's +translations. +Each thread's code lives in one reserved range that is writable and executable, +as Dynarmic's cache is, so installing a block needs no protection changes. The +range lies within one 4 GiB block of the address space (branch predictors keep +only the low half of a target), first-tier code grows up from its bottom and +optimized code down from its top, which keeps the code a thread spends its time +in together. +A bounded assembly dispatcher chains cached blocks without repeatedly crossing +the Windows ABI boundary; it checks interrupts and faults between blocks. +Internal optimized loops also consume a bounded execution budget. Syscall sites +are cached with the same invalidation rules as translated instructions, avoiding +repeated Windows memory queries on hot graphics and Android service calls. +Translated syscall exits return their already decoded immediate directly. +The per-thread branch lookup tables have 262,144 entries (address space of +which only the pages a thread's code reaches use memory). A game engine's main +thread translates a few hundred thousand blocks; with 65,536 entries its +returns and virtual calls kept evicting each other, and every miss left +generated code for the C++ dispatcher. Blocks that no longer count toward +optimization are in a table of their own with 16-byte entries, the only one +generated code reads. +Translated blocks jump to each other directly. A direct branch is a rel32 jump +that the owning thread points at the target's code when the target is compiled, +promoted or invalidated, and at a dispatcher stub at the start of the range +otherwise. Indirect branches probe the lookup table inline. Both forms check +the interrupt flag first. Interrupting a thread also expires the loop budget. +ARM64 ignores the top byte of an address. Clearing it costs two shifts in the +path of every address, so loads and stores that have fault recovery start +without them: a tagged pointer faults there as a non-canonical address, the +instruction joins a process-wide set and its page is translated again with the +shifts (bionic's allocator tags the address of its chunk headers, so every +process has a few such instructions). Past 2,048 of them all code is translated +with the shifts, as `AXRB_DIGITALIS_MASK_ADDRESSES=1` does from the start. +`svc #0` first tries AXRB's in-process fast syscalls (`clock_gettime`, +`gettimeofday`, futex wake) before leaving generated code. +`direct/patches/digitalis-*.patch` are applied by `direct_deps.ps1`. They add lite +JIT lowering for pairwise FMAXP/FMINP/FMAXNMP/FMINNMP and mask stale upper lanes +in 64-bit element-wise FMAX/FMIN (the differential test covers both), make the +top-byte mask conditional as described above, and let optimized conditional +branches test the stored NZCV bits directly instead of rebuilding host flags. + +`build_heavy.py` generates compatibility copies outside the pinned upstream tree. +These adapt operand evaluation order to clang-cl, preserve observable ARM FP +exception flags during dead-code elimination, and allocate the upstream constant +pool below 2 GiB for signed 32-bit references. The generated code makes no SysV +native calls; the Windows ABI boundary is owned by `entry.asm`. + +Developer diagnostics: `AXRB_DIGITALIS_PROFILE=1` logs per-thread compilation, +dispatch and interpreter totals. `AXRB_DIGITALIS_DISABLE_AVX=1` exercises the +portable instruction path, and `AXRB_DIGITALIS_NO_CHAIN=1` disables block chaining +for comparisons. `AXRB_DIGITALIS_HEAVY=0` disables hot-region optimization; +`force` exercises the optimizer immediately in CPU tests. The normal mode +promotes after 512 hits and accepts short successful regions as well as loops. +`AXRB_DIGITALIS_SYNC_HEAVY=1` optimizes on the guest thread instead of in the +background. +`AXRB_DIGITALIS_DISABLE_EXTENDED_CPU=1` disables the additional CPUID extensions. +`AXRB_DIGITALIS_CACHE_BITS=16` selects the earlier 65,536-entry lookup table for +comparisons (supported range 12–20). `AXRB_DIGITALIS_HEAVY_MIN_INSNS=20` restores +the older region-size heuristic; the default is 1. +`AXRB_DIGITALIS_DUMP=` writes every region's host code there as +`--.bin`, and with `AXRB_DIRECT_PROFILE` set, +`AXRB_DIRECT_PROFILE_JIT=1` adds `.jit` and `.code`: every sample +in translated code with its region, and those regions' host code, for an +instruction-level profile. +These switches are not set by the launcher. The deterministic +ARM64 `direct/tests/art/translator_benchmark.c` times one construct per line +(calls, indirect branches, memory copies, atomics, floating point), separately +from game FPS. + +The vendored CPU-state declaration in `compat/native_bridge_support` comes from +AOSP frameworks/libs/native_bridge_support, main branch, downloaded 2026-10-06: +https://android.googlesource.com/platform/frameworks/libs/native_bridge_support/+/refs/heads/main/guest_state/include/native_bridge_support/arm64/guest_state/guest_state_cpu_state.h +It retains its upstream Apache-2.0 header. Digitalis/Berberis sources retain +their original Apache-2.0 copyright headers. See the launcher's third-party +notices and `licenses/Apache-2.0.txt`. diff --git a/direct/digitalis/build.py b/direct/digitalis/build.py new file mode 100644 index 0000000..365cf60 --- /dev/null +++ b/direct/digitalis/build.py @@ -0,0 +1,95 @@ +"""Build Digitalis's Windows adapter with the NDK's bundled Clang and MSVC CRT. + +Clang is needed for the upstream interpreter's 128-bit integers and GNU inline +assembly. The result is a static library: players need no compiler or new DLL. +""" +import argparse +import os +from pathlib import Path +import subprocess +import sys + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--ndk', type=Path, required=True) +parser.add_argument('--linker', type=Path, required=True) +parser.add_argument('--out', type=Path, required=True) +parser.add_argument('--config', default='Release') +parser.add_argument('--heavy', action='store_true') +args = parser.parse_args() +here = Path(__file__).resolve().parent +root = here.parents[1] +source = root / 'third_party/digitalis' +work = args.out.resolve() +work.mkdir(parents=True, exist_ok=True) +linker = args.linker.resolve() +vcvars = linker.parents[6] / 'Auxiliary/Build/vcvars64.bat' +environment = work / 'environment.cmd' +environment.write_text(f'@call "{vcvars}" >nul\n@set\n', encoding='utf-8') +output = subprocess.run(['cmd', '/d', '/c', str(environment)], cwd=work, + capture_output=True, text=True, check=True) +env = {k.upper(): v for k, v in os.environ.items()} +for line in output.stdout.splitlines(): + if '=' in line and not line.startswith('='): + key, value = line.split('=', 1) + env[key.upper()] = value +if 'INCLUDE' not in env or 'LIB' not in env: + raise RuntimeError('MSVC x64 development environment could not be initialized') + +def run(command): + subprocess.run([str(x) for x in command], env=env, check=True, cwd=work) + +generated = work / 'generated/berberis/assembler' +generated.mkdir(parents=True, exist_ok=True) +run([sys.executable, '-X', 'utf8', source / 'assembler/gen_asm.py', '--binary-assembler', + generated / 'gen_assembler_x86_32_or_x86_64-inl.h', generated / 'gen_assembler_x86_64-inl.h', + source / 'assembler/instructions/insn_def_x86.json', source / 'assembler/instructions/insn_def_x86_64.json']) +clang = args.ndk.resolve() / 'toolchains/llvm/prebuilt/windows-x86_64/bin/clang-cl.exe' +debug = args.config == 'Debug' +flags = [clang, '--target=x86_64-pc-windows-msvc', '/nologo', '/std:c++20', '/EHsc', + '/MTd' if debug else '/MT', '/Od' if debug else '/O2', '/c', '/utf-8', '/DNOMINMAX', '/D_CRT_SECURE_NO_WARNINGS'] +includes = [here / 'compat', source, work / 'generated'] + [source / name for name in ( + 'base/include', 'guest_state/include', 'guest_state/arm64/include', 'interpreter/include', + 'decoder/include', 'kernel_api/include', 'runtime_primitives/include', 'assembler/include', + 'lite_translator/include', 'lite_translator', 'code_gen_lib/include')] +if args.heavy: + flags += ['/DAXRB_DIGITALIS_HEAVY'] + includes.append(source / 'heavy_optimizer/arm64/include') +flags += ['/I' + str(path) for path in includes] +objects = [] +for name, file in [ + ('engine', here / 'engine.cpp'), ('builtins', here / 'builtins.cpp'), + ('region', source / 'lite_translator/arm64_to_x86_64/lite_translate_region.cc'), + ('translator', source / 'lite_translator/arm64_to_x86_64/lite_translator.cc'), + ('machine', source / 'assembler/machine_code.cc')]: + obj = work / (name + '.obj') + run(flags + [file, '/Fo' + str(obj)]) + objects.append(obj) +entry = work / 'entry.obj' +run([linker.parent / 'ml64.exe', '/nologo', '/c', '/Fo' + str(entry), here / 'entry.asm']) +objects.append(entry) +if args.heavy: + from build_heavy import build + objects += build(source, work, flags, run, env) +run([linker.parent / 'lib.exe', '/nologo', '/OUT:' + str(work / 'digitalis.lib'), *objects]) +run(flags + [here / 'selftest.cpp', '/Fo' + str(work / 'selftest.obj')]) +run([linker, '/nologo', '/OUT:' + str(work / 'digitalis-selftest.exe'), + work / 'selftest.obj', work / 'digitalis.lib']) +run([work / 'digitalis-selftest.exe']) +for option in ('AXRB_DIGITALIS_DISABLE_AVX', 'AXRB_DIGITALIS_NO_CHAIN', 'AXRB_DIGITALIS_DISABLE_EXTENDED_CPU'): + test_env = dict(env, **{option: '1'}) + subprocess.run([str(work / 'digitalis-selftest.exe')], env=test_env, check=True, cwd=work) +if args.heavy: + for disable_avx in (False, True): + test_env = dict(env, AXRB_DIGITALIS_HEAVY='force') + if disable_avx: + test_env['AXRB_DIGITALIS_DISABLE_AVX'] = '1' + subprocess.run([str(work / 'digitalis-selftest.exe')], env=test_env, check=True, cwd=work) + run(flags + [here / 'differential.cpp', '/Fo' + str(work / 'differential.obj')]) + run([linker, '/nologo', '/OUT:' + str(work / 'digitalis-differential.exe'), + work / 'differential.obj', work / 'digitalis.lib']) + run([work / 'digitalis-differential.exe']) + subprocess.run([str(work / 'digitalis-selftest.exe')], + env=dict(env, AXRB_DIGITALIS_HEAVY='0'), check=True, cwd=work) + # Hot regions optimized on the guest thread instead of in the background. + subprocess.run([str(work / 'digitalis-selftest.exe')], + env=dict(env, AXRB_DIGITALIS_SYNC_HEAVY='1'), check=True, cwd=work) diff --git a/direct/digitalis/build_heavy.py b/direct/digitalis/build_heavy.py new file mode 100644 index 0000000..1e5e182 --- /dev/null +++ b/direct/digitalis/build_heavy.py @@ -0,0 +1,198 @@ +"""Build the upstream optimizing frontend and MachineIR backend on Windows.""" +import os +import re +import sys +import hashlib +from pathlib import Path + + +def patch(text, old, new, count=1): + """Fail visibly if the pinned upstream compatibility anchor changes.""" + actual = text.count(old) + if actual != count: + raise RuntimeError(f'Digitalis compatibility anchor count {actual}, expected {count}: {old[:100]!r}') + return text.replace(old, new) + + +def build(source, work, flags, run, env): + generated = work / 'heavy-generated' + generated.mkdir(parents=True, exist_ok=True) + env['PYTHONPATH'] = os.pathsep.join(str(source / p) for p in + ('assembler', 'intrinsics', 'backend', 'device_arch_info')) + env['PYTHONUTF8'] = '1' + env['PYTHONHASHSEED'] = '0' + asm = [source / 'assembler/instructions' / name for name in + ('insn_def_x86.json', 'insn_def_x86_64.json')] + (generated / 'berberis/assembler').mkdir(parents=True, exist_ok=True) + for name, definition in zip(('x86_common', 'x86_64'), asm): + run([sys.executable, source / 'assembler/gen_asm.py', '--using', + generated / f'berberis/assembler/gen_assembler_{name}-using-inl.h', definition]) + macros = [source / 'intrinsics' / p / 'macro_def.json' for p in + ('all_to_x86_32_or_x86_64', 'all_to_x86_64', 'riscv64_to_x86_64')] + regdir = generated / 'berberis/device_arch_info/x86_64' + regdir.mkdir(parents=True, exist_ok=True) + run([sys.executable, source / 'device_arch_info/gen_reg_class.py', + regdir / 'machine_reg_class-inl.h', source / 'device_arch_info/x86_64/reg_class_def.json']) + text = (source / 'device_arch_info/include/berberis/device_arch_info/x86_64/device_arch_info.h').read_text() + (regdir / 'device_arch_info.h').write_text(text.replace('defined(__LP64__)', + '(defined(__LP64__) || defined(_WIN64))')) + device_outputs = [generated / 'berberis/device_arch_info' / p / 'device_insn_info-inl.h' + for p in ('x86_32_or_x86_64', 'x86_64', 'all_to_x86_32_or_x86_64', 'all_to_x86_64')] + for p in device_outputs: + p.parent.mkdir(parents=True, exist_ok=True) + run([sys.executable, source / 'device_arch_info/gen_device_insn_info.py', + *device_outputs, *asm, *macros[:2]]) + lir_inputs = [source / 'backend/x86_64/lir_instructions.json', + source / 'intrinsics/arm64_to_x86_64/machine_ir_intrinsic_binding.json', + *asm, *macros[:2]] + # The current template emitter uses headers; the legacy generated .cc + # emitter has a different interface and must not be compiled alongside it. + names = ('insn-inl_x86_64.h', 'machine_info_x86_64-inl.h', + 'machine_opcode_x86_64-inl.h', 'gen_machine_ir_x86_64-inl.h') + run([sys.executable, source / 'backend/gen_lir.py', '--headers', + *(generated / name for name in names), *lir_inputs]) + outputs = [generated / 'berberis/intrinsics' / p.parent.name / 'macro_assembler_interface-inl.h' + for p in macros] + for p in outputs: + p.parent.mkdir(parents=True, exist_ok=True) + run([sys.executable, source / 'assembler/gen_asm.py', '--binary-assembler', *outputs, *macros]) + names = ('intrinsics-inl.h', 'intrinsics_process_bindings-inl.h', + 'interpreter_intrinsics_hooks-inl.h', 'translator_intrinsics_hooks-inl.h', + 'demultiplexer_intrinsics_hooks-inl.h', 'mock_semantics_listener_intrinsics_hooks-inl.h') + run([sys.executable, source / 'intrinsics/gen_intrinsics.py', '--public_headers', + *(generated / 'berberis/intrinsics' / n for n in names), + source / 'intrinsics/riscv64_to_all/intrinsic_def.json', + source / 'intrinsics/riscv64_to_x86_64/machine_ir_intrinsic_binding.json', *asm, *macros]) + (generated / 'berberis/intrinsics/intrinsics.h').write_text( + '#pragma once\n#include \n' + '#include "berberis/intrinsics/common/intrinsics.h"\n' + '#include "berberis/intrinsics/intrinsics_float.h"\n' + '#include "berberis/intrinsics/simd_register.h"\n' + 'namespace berberis::intrinsics {\n' + '#include "berberis/intrinsics/intrinsics-inl.h"\n}\n') + # clang-cl implements GNU vectors, but intentionally does not define GCC's + # identity macro. Keep this narrow adaptation out of the pinned sources. + for name in ('simd_register.h', 'type_traits.h'): + text = (source / 'intrinsics/include/berberis/intrinsics' / name).read_text(encoding='utf-8') + text = re.sub(r'#(?:ifdef __GNUC__|if defined\(__GNUC__\))', + '#if defined(__GNUC__) || defined(__clang__)', text) + (generated / 'berberis/intrinsics' / name).write_text(text, encoding='utf-8') + # Function arguments are evaluated right-to-left by clang-cl. Upstream's + # emitter increments a shared operand cursor inside a tuple_cat argument + # pack, which then reads EFLAGS as a GPR. Braced tuple construction gives + # the cursor an explicitly sequenced, architecture-independent order. + ir_header = source / 'backend/include/berberis/backend/x86_64/machine_ir.h' + text = ir_header.read_text(encoding='utf-8') + text = patch(text, ''' std::apply( + kEmitInsnFunc, + std::tuple_cat(std::tuple{*as}, [®_idx, &mem_idx, this] {''', + ''' auto get_operand = [®_idx, &mem_idx, this] {''') + text = patch(text, ''' }.template operator()()...));''', ''' }; + auto operands = std::tuple())...>{ + get_operand.template operator()()...}; + std::apply([&](auto&&... pieces) { + std::apply(kEmitInsnFunc, std::tuple_cat(std::tuple{*as}, pieces...)); + }, operands);''') + start = text.index('auto get_operand =') + end = text.index('auto operands =', start) + text = text[:start] + text[start:end].replace('', '') + text[end:] + # ARM FP exception flags remain observable even if an instruction's value + # is overwritten. Preserve the host arithmetic that updates MXCSR. + text = '#include \n' + text + text = patch(text, ''' if constexpr (kSideEffects) {''', ''' constexpr std::string_view name(kMnemo); + constexpr bool fp_status = name.ends_with("SS") || name.ends_with("SD") || + name.ends_with("PS") || name.ends_with("PD") || name.find("CVT") != name.npos; + if constexpr (kSideEffects || fp_status) {''') + output = generated / 'berberis/backend/x86_64/machine_ir.h' + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(text, encoding='utf-8') + includes = ('backend/include', 'heavy_optimizer/arm64/include', 'intrinsics/include', + 'intrinsics/riscv64_to_x86_64/include', 'intrinsics/riscv64_to_all/include', + 'intrinsics/all_to_x86_32_or_x86_64/include', 'intrinsics/all_to_x86_64/include', + 'device_arch_info/include') + flags = [flags[0], '/I' + str(generated), *flags[1:]] + ['/I' + str(source / p) for p in includes] + flags = ['/clang:-std=c++23' if flag == '/std:c++20' else flag for flag in flags] + flags += ['/clang:-Wno-deprecated-anon-enum-enum-conversion', '/clang:-ferror-limit=5'] + flags += ['/D_SILENCE_CXX23_DENORM_DEPRECATION_WARNING'] + bp = (source / 'backend/Android.bp').read_text() + backend = re.search(r'name: "berberis_backend_srcs",\s*srcs: \[(.*?)\]', bp, re.S).group(1) + files = [source / 'backend' / p for p in re.findall(r'"([^"]+\.cc)"', backend)] + files += [source / 'heavy_optimizer/arm64' / p for p in ('frontend.cc', 'heavy_optimize_region.cc')] + files.append(source / 'intrinsics/riscv64_to_x86_64/constants_pool.cc') + flags += ['/I' + str(source / 'heavy_optimizer/arm64')] + # Content-based stamps avoid recompiling the large frontend when only the + # adapter changes, while still noticing every upstream/compatibility header. + headers = hashlib.sha256(repr(flags).encode()) + for root in (source, generated, Path(__file__).parent / 'compat'): + for header in sorted(root.rglob('*.h')): + headers.update(str(header).encode()) + headers.update(header.read_bytes()) + header_digest = headers.digest() + objects = [] + for file in files: + relative = file.relative_to(source) if file.is_relative_to(source) else file.relative_to(generated) + obj = work / ('heavy_' + '_'.join(relative.with_suffix('').parts) + '.obj') + if file.name == 'code_emit.cc': + text = patch(file.read_text(encoding='utf-8'), + 'as->Pmov(GetXReg(dst), GetXReg(src));', 'as->Movdqa(GetXReg(dst), GetXReg(src));') + file = generated / file.name + file.write_text(text, encoding='utf-8') + if file.name == 'code_gen.cc': + text = file.read_text(encoding='utf-8') + text = '#include \n#include \n' + text + text = patch(text, 'machine_ir->Emit(&emitter);', + 'static const bool dump_ir = std::getenv("AXRB_DIGITALIS_DUMP_IR") != nullptr;\n' + ' if (dump_ir) ' + 'std::fprintf(stderr, "%s\\n", machine_ir->GetDebugString().c_str());\n' + ' machine_ir->Emit(&emitter);') + file = generated / file.name + file.write_text(text, encoding='utf-8') + if file.name == 'frontend.cc': + text = patch(file.read_text(encoding='utf-8'), + 'GuestAddr target = jump->target();', + 'static const bool no_loops = std::getenv("AXRB_DIGITALIS_NO_LOOPS") != nullptr;\n' + ' if (no_loops && ' + 'jump->kind() == PseudoJump::Kind::kJumpWithPendingSignalsCheck) continue;\n' + ' GuestAddr target = jump->target();') + text = patch(text, '''const size_t offset = offsetof(ThreadState, pending_signals_status); + auto* cmpb = ir->NewInsn({.base = x86_64::kMachineRegRBP, .disp = offset}, + kPendingSignalsPresent, + GetFlagsRegister());''', '''const size_t offset = sizeof(ThreadState); + auto* cmpb = ir->NewInsn({.base = x86_64::kMachineRegRBP, .disp = offset}, + 1, GetFlagsRegister());''') + file = generated / file.name + file.write_text(text, encoding='utf-8') + if file.name == 'constants_pool.cc': + text = file.read_text(encoding='utf-8') + text = text.replace('#include ', '#include \n#include ') + text = text.replace('#include "berberis/base/mmap.h"', '') + start = text.index('int32_t GetConstants() {') + end = text.index('\n}\n', start) + 2 + text = text[:start] + '''int32_t GetConstants() { + static const int32_t address = [] { + // Upstream emits absolute signed-32-bit references to this immutable pool. + // Reserve a free Windows allocation granule below 2 GiB, without replacing + // any existing host or guest mapping. + for (uintptr_t candidate = 0x10000000; candidate < 0x70000000; candidate += 65536) { + void* memory = VirtualAlloc(reinterpret_cast(candidate), sizeof(MacroAssemblerConstants), + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (!memory) continue; + new (memory) MacroAssemblerConstants(kBerberisMacroAssemblerConstants); + DWORD old; + if (!VirtualProtect(memory, sizeof(MacroAssemblerConstants), PAGE_READONLY, &old)) + throw std::runtime_error("Digitalis constant-pool protection failed"); + return static_cast(candidate); + } + throw std::runtime_error("Digitalis could not allocate its low-address constant pool"); + }(); + return address; +}''' + text[end:] + file = generated / file.name + file.write_text(text, encoding='utf-8') + digest = hashlib.sha256(header_digest + file.read_bytes()).hexdigest() + stamp = obj.with_suffix('.sha256') + if not obj.exists() or not stamp.exists() or stamp.read_text() != digest: + run([*flags, file, '/Fo' + str(obj)]) + stamp.write_text(digest) + objects.append(obj) + return objects diff --git a/direct/digitalis/builtins.cpp b/direct/digitalis/builtins.cpp new file mode 100644 index 0000000..eb7918c --- /dev/null +++ b/direct/digitalis/builtins.cpp @@ -0,0 +1,48 @@ +// Small compiler support routines needed by clang's Windows build of Digitalis. +// No compiler runtime DLL is required on the player's machine. +#include +#include +#include +extern "C" __uint128_t __udivti3(__uint128_t numerator, __uint128_t denominator) { + if (!denominator) std::abort(); + __uint128_t quotient = 0, remainder = 0; + for (int bit = 127; bit >= 0; --bit) { + const bool carry = (remainder >> 127) != 0; + remainder = (remainder << 1) | ((numerator >> bit) & 1); + if (carry || remainder >= denominator) { remainder -= denominator; quotient |= __uint128_t{1} << bit; } + } + return quotient; +} +extern "C" float __extendhfsf2(_Float16 value) { + uint16_t half; std::memcpy(&half, &value, sizeof half); + uint32_t sign = uint32_t(half & 0x8000) << 16; + uint32_t exponent = (half >> 10) & 31, mantissa = half & 1023, bits; + if (!exponent) { + if (!mantissa) bits = sign; + else { + int shift = 0; + while (!(mantissa & 1024)) { mantissa <<= 1; ++shift; } + bits = sign | (uint32_t(113 - shift) << 23) | ((mantissa & 1023) << 13); + } + } else if (exponent == 31) bits = sign | 0x7f800000 | (mantissa << 13); + else bits = sign | ((exponent + 112) << 23) | (mantissa << 13); + float result; std::memcpy(&result, &bits, sizeof result); return result; +} +extern "C" _Float16 __truncsfhf2(float value) { + uint32_t bits; std::memcpy(&bits, &value, sizeof bits); + uint16_t result = (bits >> 16) & 0x8000; + uint32_t exponent = (bits >> 23) & 255, mantissa = bits & 0x7fffff; + if (exponent == 255) result |= 0x7c00 | (mantissa ? ((mantissa >> 13) | 0x200) : 0); + else if (exponent >= 143) result |= 0x7c00; + else if (exponent >= 102) { + uint32_t significand = mantissa | 0x800000; + unsigned shift = exponent < 113 ? 126 - exponent : 13; + uint32_t rounded = significand >> shift; + const uint32_t remainder = significand & ((1u << shift) - 1); + const uint32_t halfway = 1u << (shift - 1); + if (remainder > halfway || (remainder == halfway && (rounded & 1))) ++rounded; + if (exponent >= 113) rounded += (exponent - 113) << 10; + result |= static_cast(rounded); + } + _Float16 half; std::memcpy(&half, &result, sizeof half); return half; +} diff --git a/direct/digitalis/compat/berberis/base/arena_alloc.h b/direct/digitalis/compat/berberis/base/arena_alloc.h new file mode 100644 index 0000000..fdb40e9 --- /dev/null +++ b/direct/digitalis/compat/berberis/base/arena_alloc.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include +namespace berberis { +class Arena { + public: + void* Alloc(size_t size, size_t alignment) { return memory_.allocate(size ? size : 1, alignment); } + private: + std::pmr::monotonic_buffer_resource memory_; +}; +template T* NewInArena(Arena* arena, Args&&... args) { + return new (arena->Alloc(sizeof(T), alignof(T))) T(std::forward(args)...); +} +template T* NewArrayInArena(Arena* arena, size_t size) { + return new (arena->Alloc(sizeof(T) * size, alignof(T))) T[size]; +} +template class ArenaAllocator { + public: + using value_type = T; + ArenaAllocator(Arena* arena) : arena_(arena) {} + template ArenaAllocator(const ArenaAllocator& other) : arena_(other.arena()) {} + T* allocate(size_t n) { return static_cast(arena_->Alloc(n * sizeof(T), alignof(T))); } + void deallocate(T*, size_t) {} + Arena* arena() const { return arena_; } + template bool operator==(const ArenaAllocator& other) const { return arena_ == other.arena(); } + private: + Arena* arena_; +}; +} diff --git a/direct/digitalis/compat/berberis/base/forever_map.h b/direct/digitalis/compat/berberis/base/forever_map.h new file mode 100644 index 0000000..a6e1197 --- /dev/null +++ b/direct/digitalis/compat/berberis/base/forever_map.h @@ -0,0 +1,5 @@ +#pragma once +#include +namespace berberis { +template> using ForeverMap = std::map; +} diff --git a/direct/digitalis/compat/berberis/base/logging.h b/direct/digitalis/compat/berberis/base/logging.h new file mode 100644 index 0000000..691d456 --- /dev/null +++ b/direct/digitalis/compat/berberis/base/logging.h @@ -0,0 +1,10 @@ +#pragma once +#include +#include +#define LOG_ALWAYS_FATAL(...) do { char message[1024]; std::snprintf(message, sizeof(message), __VA_ARGS__); throw std::runtime_error(message); } while (0) +#define LOG_ALWAYS_FATAL_IF(condition, ...) do { if (condition) LOG_ALWAYS_FATAL(__VA_ARGS__); } while (0) +#define ALOGE(...) do { std::fprintf(stderr, __VA_ARGS__); std::fputc('\n', stderr); } while (0) +#define ALOGW(...) ALOGE(__VA_ARGS__) +#define ALOGI(...) ALOGE(__VA_ARGS__) +#define ALOGD(...) ((void)0) +#define ALOGV(...) ((void)0) diff --git a/direct/digitalis/compat/berberis/base/macros.h b/direct/digitalis/compat/berberis/base/macros.h new file mode 100644 index 0000000..ae89ebf --- /dev/null +++ b/direct/digitalis/compat/berberis/base/macros.h @@ -0,0 +1,5 @@ +#pragma once +template constexpr void UNUSED(const T&...) {} +#define DISALLOW_COPY_AND_ASSIGN(Type) Type(const Type&) = delete; Type& operator=(const Type&) = delete +#define DISALLOW_IMPLICIT_CONSTRUCTORS(Type) Type() = delete; DISALLOW_COPY_AND_ASSIGN(Type) +#define DISALLOW_ALLOCATION() void* operator new(size_t) = delete; void operator delete(void*) = delete diff --git a/direct/digitalis/compat/berberis/base/page_size.h b/direct/digitalis/compat/berberis/base/page_size.h new file mode 100644 index 0000000..67d15f1 --- /dev/null +++ b/direct/digitalis/compat/berberis/base/page_size.h @@ -0,0 +1,6 @@ +#pragma once +#include +namespace berberis { +inline constexpr size_t kPageSize = 4096; +inline constexpr size_t kMaxPageSize = 65536; +} diff --git a/direct/digitalis/compat/berberis/base/stringprintf.h b/direct/digitalis/compat/berberis/base/stringprintf.h new file mode 100644 index 0000000..c227e9d --- /dev/null +++ b/direct/digitalis/compat/berberis/base/stringprintf.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include +#include +namespace berberis { +inline void StringAppendV(std::string* out, const char* format, va_list args) { + va_list copy; + va_copy(copy, args); + const int count = std::vsnprintf(nullptr, 0, format, copy); + va_end(copy); + if (count < 0) return; + std::vector buffer(size_t(count) + 1); + std::vsnprintf(buffer.data(), buffer.size(), format, args); + out->append(buffer.data(), size_t(count)); +} +inline void StringAppendF(std::string* out, const char* format, ...) { + va_list args; va_start(args, format); StringAppendV(out, format, args); va_end(args); +} +inline std::string StringPrintf(const char* format, ...) { + std::string out; + va_list args; va_start(args, format); StringAppendV(&out, format, args); va_end(args); + return out; +} +} diff --git a/direct/digitalis/compat/berberis/base/tracing.h b/direct/digitalis/compat/berberis/base/tracing.h new file mode 100644 index 0000000..b0a5272 --- /dev/null +++ b/direct/digitalis/compat/berberis/base/tracing.h @@ -0,0 +1,15 @@ +#pragma once +#include +#include +namespace berberis { +class Tracing { + public: + static bool IsOn() { return false; } + static void Init() {} + static void TraceF(const char* format, ...) { + va_list args; va_start(args, format); std::vfprintf(stderr, format, args); va_end(args); + } +}; +} +#define TRACE(...) do { if (::berberis::Tracing::IsOn()) ::berberis::Tracing::TraceF(__VA_ARGS__); } while (false) +#define TRACE_AND_ALOGD(...) TRACE(__VA_ARGS__) diff --git a/direct/digitalis/compat/berberis/runtime_primitives/runtime_library.h b/direct/digitalis/compat/berberis/runtime_primitives/runtime_library.h new file mode 100644 index 0000000..87dc317 --- /dev/null +++ b/direct/digitalis/compat/berberis/runtime_primitives/runtime_library.h @@ -0,0 +1,6 @@ +#pragma once +#include "berberis/guest_state/guest_state.h" +namespace berberis { +extern "C" void berberis_entry_HandleLiteCounterThresholdReached(); +void InvalidateGuestRange(GuestAddr start, GuestAddr end); +} diff --git a/direct/digitalis/compat/native_bridge_support/arm64/guest_state/guest_state_cpu_state.h b/direct/digitalis/compat/native_bridge_support/arm64/guest_state/guest_state_cpu_state.h new file mode 100644 index 0000000..91917a4 --- /dev/null +++ b/direct/digitalis/compat/native_bridge_support/arm64/guest_state/guest_state_cpu_state.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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. + */ + +#ifndef NATIVE_BRIDGE_SUPPORT_ARM64_GUEST_STATE_GUEST_STATE_CPU_STATE_H_ +#define NATIVE_BRIDGE_SUPPORT_ARM64_GUEST_STATE_GUEST_STATE_CPU_STATE_H_ + +#include + +namespace berberis { + +using GuestAddr = uintptr_t; +using Reservation = __uint128_t; + +struct CPUState { + // General registers. + uint64_t x[31]; + + // Flags + // clang-format off +#if defined(__x86_64__) + enum FlagMask { + kFlagNegative = 1 << 15, + kFlagZero = 1 << 14, + kFlagCarry = 1 << 8, + kFlagOverflow = 1, + }; +#else + enum FlagMask { + kFlagNegative = 1 << 3, + kFlagZero = 1 << 2, + kFlagCarry = 1 << 1, + kFlagOverflow = 1, + }; +#endif + + static constexpr uint32_t kFpsrQcBit = 1U << 27; + + // clang-format on + uint16_t flags; + + // Caches last-written FPCR, to minimize reads of host register + uint32_t cached_fpcr; + + // Stores the FPSR flags whose functionality we emulate: currently only IDC. (later IXC) + uint32_t emulated_fpsr; + + // Stack pointer. + uint64_t sp; + + // SIMD & FP registers. + alignas(16) __uint128_t v[32]; + + // Current insn address. + uint64_t insn_addr; + + GuestAddr reservation_address; + Reservation reservation_value; +}; + +} +#endif // NATIVE_BRIDGE_SUPPORT_BRIDGE_SUPPORT_ARM64_GUEST_STATE_GUEST_STATE_CPU_STATE_H_ + diff --git a/direct/digitalis/differential.cpp b/direct/digitalis/differential.cpp new file mode 100644 index 0000000..c2cc0b8 --- /dev/null +++ b/direct/digitalis/differential.cpp @@ -0,0 +1,233 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include "digitalis.h" +using namespace axrb::digitalis; +static bool read(void*, uint64_t address, void* out, size_t size) { + std::memcpy(out, reinterpret_cast(address), size); return true; +} +static bool write(void*, uint64_t address, const void* value, size_t size) { + std::memcpy(reinterpret_cast(address), value, size); return true; +} +int main() try { + SetEnvironmentVariableA("AXRB_DIGITALIS_HEAVY", "force"); + auto* optimized = create({nullptr, read, write}); + SetEnvironmentVariableA("AXRB_DIGITALIS_HEAVY", nullptr); + SetEnvironmentVariableA("AXRB_DIGITALIS_INTERPRET_ONLY", "1"); + auto* reference = create({nullptr, read, write}); + SetEnvironmentVariableA("AXRB_DIGITALIS_INTERPRET_ONLY", nullptr); + constexpr uint32_t instructions[] = { + 0x8b010000, 0xcb020021, 0x9b017c02, 0xca010000, 0xaa020023, + 0xab010003, 0xeb020024, 0x9a030005, 0x8a020026, + 0xdac01003, 0xdac00003, 0xd3475004, 0x9ac12404, 0x9ac12c04, + 0x1e222820, 0x4e22cc20, 0x1f010803, + }; + alignas(4096) std::array code{}; + uint64_t seed = 0x825416351921a975; + auto random = [&] { seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; return seed; }; + for (unsigned trial = 0; trial != 1000; ++trial) { + clear_cache(optimized); clear_cache(reference); + code.fill(0xd4000041); + for (unsigned i = 0; i != 24; ++i) code[i] = instructions[random() % std::size(instructions)]; + Registers expected{}; + for (auto& x : expected.x) x = random(); + for (auto& v : expected.v) { + for (auto& word : v) { + const uint32_t lo = 0x3e800000 | (random() & 0x7fffff); + const uint32_t hi = 0x3e800000 | (random() & 0x7fffff); + word = (uint64_t(hi) << 32) | lo; + } + } + expected.pc = reinterpret_cast(code.data()); + expected.pstate = uint32_t(random()) & 0xf0000000; + Registers actual = expected; + const auto a = run(reference, &expected, 1000); + const auto b = run(optimized, &actual, 1000); + if (a.reason != Stop::Svc || b.reason != a.reason || std::memcmp(&expected, &actual, sizeof actual)) { + std::fprintf(stderr, "DIGITALIS_DIFFERENTIAL_FAIL trial=%u stop=%u/%u flags=%08x/%08x fpsr=%08x/%08x\n", + trial, unsigned(a.reason), unsigned(b.reason), expected.pstate, actual.pstate, expected.fpsr, actual.fpsr); + for (auto word : code) { if (word == 0xd4000041) break; std::fprintf(stderr, "%08x ", word); } + std::fputc('\n', stderr); + return 1; + } + } + for (unsigned trial = 0; trial != 100; ++trial) { + clear_cache(optimized); clear_cache(reference); + alignas(16) std::array memory{}, initial{}; + for (auto& value : initial) value = random(); + const unsigned iterations = 1 + random() % memory.size(); + code.fill(0xd4000041); + const uint32_t loop[] = { + 0xd2800000 | (iterations << 5), // mov x0,iterations + 0xf9400122, // ldr x2,[x9] + 0x8b010042, // add x2,x2,x1 + 0xf9000122, // str x2,[x9] + 0x91002129, // add x9,x9,8 + 0xf1000400, // subs x0,x0,1 + 0x54ffff61, // b.ne to ldr (backedge) + }; + std::copy(std::begin(loop), std::end(loop), code.begin()); + Registers expected{}; + expected.pc = reinterpret_cast(code.data()); + expected.x[1] = random(); + expected.x[9] = reinterpret_cast(memory.data()); + Registers actual = expected; + memory = initial; + const auto a = run(reference, &expected, 1000); + const auto expected_memory = memory; + memory = initial; + const auto b = run(optimized, &actual, 1000); + if (a.reason != Stop::Svc || b.reason != a.reason || + std::memcmp(&expected, &actual, sizeof actual) || memory != expected_memory) { + std::fprintf(stderr, "DIGITALIS_LOOP_DIFFERENTIAL_FAIL trial=%u iterations=%u\n", trial, iterations); + return 1; + } + } + // A loop made hot normally: optimized in the background and installed by + // a later run, then compared with the interpreter. + unsigned long long promoted_count = 0; + { + auto* promoted = create({nullptr, read, write}); + alignas(16) std::array memory{}, initial{}; + code.fill(0xd4000041); + const uint32_t loop[] = { + 0xf9400122, // ldr x2,[x9] + 0x8b010042, // add x2,x2,x1 + 0xf9000122, // str x2,[x9] + 0x91002129, // add x9,x9,8 + 0xf1000400, // subs x0,x0,1 + 0x54ffff61, // b.ne to ldr (backedge) + }; + std::copy(std::begin(loop), std::end(loop), code.begin()); + clear_cache(reference); + auto start = [&](unsigned iterations) { + Registers registers{}; + registers.pc = reinterpret_cast(code.data()); + registers.x[0] = iterations; + registers.x[1] = random(); + registers.x[9] = reinterpret_cast(memory.data()); + return registers; + }; + const auto deadline = GetTickCount64() + 10000; + while (!optimized_blocks(promoted) && GetTickCount64() < deadline) { + Registers registers = start(32); + run(promoted, ®isters, 1000); + Sleep(0); + } + if (!optimized_blocks(promoted)) { + std::fprintf(stderr, "DIGITALIS_PROMOTION_FAIL: no optimized code was installed\n"); + return 1; + } + for (unsigned trial = 0; trial != 200; ++trial) { + for (auto& value : initial) value = random(); + Registers expected = start(1 + random() % memory.size()); + Registers actual = expected; + memory = initial; + const auto a = run(reference, &expected, 1000); + const auto expected_memory = memory; + memory = initial; + const auto b = run(promoted, &actual, 1000); + if (a.reason != Stop::Svc || b.reason != a.reason || + std::memcmp(&expected, &actual, sizeof actual) || memory != expected_memory) { + std::fprintf(stderr, "DIGITALIS_PROMOTION_DIFFERENTIAL_FAIL trial=%u stop=%u/%u pc=%llx/%llx x0=%llx/%llx " + "x2=%llx/%llx x9=%llx/%llx nzcv=%08x/%08x memory=%d\n", trial, unsigned(a.reason), unsigned(b.reason), + (unsigned long long)expected.pc, (unsigned long long)actual.pc, + (unsigned long long)expected.x[0], (unsigned long long)actual.x[0], + (unsigned long long)expected.x[2], (unsigned long long)actual.x[2], + (unsigned long long)expected.x[9], (unsigned long long)actual.x[9], expected.pstate, actual.pstate, + memory == expected_memory); + return 1; + } + } + promoted_count = optimized_blocks(promoted); + destroy(promoted); + } + // Pairwise FP max/min (FMAXP/FMINP/FMAXNMP/FMINNMP .2S/.4S/.2D) in the + // lite tier, including NaN, signed-zero and infinite lanes. + SetEnvironmentVariableA("AXRB_DIGITALIS_HEAVY", "0"); + auto* lite = create({nullptr, read, write}); + SetEnvironmentVariableA("AXRB_DIGITALIS_HEAVY", nullptr); + constexpr uint32_t pairwise[] = { + 0x2e22f420, 0x6e22f420, 0x6e62f420, 0x2ea2f420, 0x6ea2f420, 0x6ee2f420, + 0x2e22c420, 0x6e22c420, 0x6e62c420, 0x2ea2c420, 0x6ea2c420, 0x6ee2c420, + // The element-wise forms share the lowering. + 0x0e22f420, 0x4e22f420, 0x4e62f420, 0x0ea2f420, 0x4ea2f420, 0x4ee2f420, + 0x0e22c420, 0x4e22c420, 0x4e62c420, 0x0ea2c420, 0x4ea2c420, 0x4ee2c420, + }; + constexpr uint32_t singles[] = {0, 0x80000000, 0x3f800000, 0xbf800000, 0x7f800000, 0xff800000, + 0x7fc00000, 0x7fa00001, 0xffc00123, 0x40490fdb}; + constexpr uint64_t doubles[] = {0, 0x8000000000000000, 0x3ff0000000000000, 0xbff0000000000000, + 0x7ff0000000000000, 0xfff0000000000000, 0x7ff8000000000000, + 0x7ff4000000000001, 0x400921fb54442d18}; + for (unsigned trial = 0; trial != 2000; ++trial) { + clear_cache(lite); clear_cache(reference); + code.fill(0xd4000041); + code[0] = pairwise[trial % std::size(pairwise)]; + Registers expected{}; + const bool is_double = (code[0] >> 22) & 1; + for (int reg : {1, 2}) { + for (auto& word : expected.v[reg]) { + if (is_double) { + word = random() % 4 ? doubles[random() % std::size(doubles)] : random(); + } else { + const uint32_t lo = random() % 4 ? singles[random() % std::size(singles)] : uint32_t(random()); + const uint32_t hi = random() % 4 ? singles[random() % std::size(singles)] : uint32_t(random()); + word = (uint64_t(hi) << 32) | lo; + } + } + } + expected.v[0][0] = random(); expected.v[0][1] = random(); + expected.pc = reinterpret_cast(code.data()); + Registers actual = expected; + const auto a = run(reference, &expected, 1000); + const auto b = run(lite, &actual, 1000); + // Like the element-wise FMAX/FMIN lowering, a NaN result is a valid + // NaN whose payload may differ from the interpreter's default NaN. + bool lanes_match = true, nan_result = false; + for (int half = 0; half != 2; ++half) { + const uint64_t want = expected.v[0][half], got = actual.v[0][half]; + if (is_double) { + const auto nan = [](uint64_t x) { return (x & 0x7ff0000000000000) == 0x7ff0000000000000 && (x << 12); }; + lanes_match = lanes_match && (want == got || (nan(want) && nan(got))); + nan_result = nan_result || nan(want); + } else { + for (int shift : {0, 32}) { + const uint32_t w = uint32_t(want >> shift), g = uint32_t(got >> shift); + const auto nan = [](uint32_t x) { return (x & 0x7f800000) == 0x7f800000 && (x & 0x7fffff); }; + lanes_match = lanes_match && (w == g || (nan(w) && nan(g))); + nan_result = nan_result || nan(w); + } + } + } + Registers compared = actual; + compared.v[0][0] = expected.v[0][0]; compared.v[0][1] = expected.v[0][1]; + // x86 MINPS/MAXPS also signal invalid for quiet NaNs; ARM only for + // signaling NaNs. Only IOC may differ, and only for NaN results. + if (nan_result) compared.fpsr = (compared.fpsr & ~1u) | (expected.fpsr & 1u); + if (a.reason != Stop::Svc || b.reason != a.reason || !lanes_match || + std::memcmp(&expected, &compared, sizeof compared)) { + std::fprintf(stderr, "DIGITALIS_PAIRWISE_FAIL trial=%u insn=%08x v1=%016llx:%016llx v2=%016llx:%016llx " + "want=%016llx:%016llx got=%016llx:%016llx fpsr=%08x/%08x\n", trial, code[0], + (unsigned long long)actual.v[1][1], (unsigned long long)actual.v[1][0], + (unsigned long long)actual.v[2][1], (unsigned long long)actual.v[2][0], + (unsigned long long)expected.v[0][1], (unsigned long long)expected.v[0][0], + (unsigned long long)actual.v[0][1], (unsigned long long)actual.v[0][0], expected.fpsr, actual.fpsr); + return 1; + } + } + if (interpreted_instructions(lite)) { + std::fprintf(stderr, "DIGITALIS_PAIRWISE_FAIL: lite tier interpreted %llu instructions\n", + static_cast(interpreted_instructions(lite))); + return 1; + } + destroy(lite); + if (!optimized_blocks(optimized)) return 2; + std::printf("DIGITALIS_DIFFERENTIAL_PASS arithmetic=1000 memory_loops=100 pairwise=2000 promoted=200 " + "optimized=%llu/%llu\n", static_cast(optimized_blocks(optimized)), promoted_count); + destroy(reference); destroy(optimized); +} catch (const std::exception& error) { + std::fprintf(stderr, "DIGITALIS_DIFFERENTIAL_ERROR: %s\n", error.what()); return 1; +} diff --git a/direct/digitalis/digitalis.h b/direct/digitalis/digitalis.h new file mode 100644 index 0000000..a4b5a9b --- /dev/null +++ b/direct/digitalis/digitalis.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include + +namespace axrb::digitalis { +struct Registers { + uint64_t x[31]{}; + uint64_t sp{}, pc{}, tls{}; + uint32_t pstate{}, fpcr{}, fpsr{}; + alignas(16) uint64_t v[32][2]{}; +}; +struct Callbacks { + void* opaque{}; + bool (*read)(void*, uint64_t, void*, size_t){}; + bool (*write)(void*, uint64_t, const void*, size_t){}; + bool (*fault)(void*, uint64_t, bool){}; + void (*invalidate)(void*, uint64_t, uint64_t){}; + bool (*fetch)(void*, uint64_t, void*, size_t){}; + // Optional: completes a non-blocking Linux syscall (number in x[8]) from + // translated code without leaving it; false selects the regular SVC exit. + bool (*syscall)(void*, uint64_t* x){}; +}; +enum class Stop : uint32_t { Yield, Svc, MemoryFault, Undefined, Breakpoint }; +struct Result { Stop reason; uint32_t immediate{}; uint64_t address{}; }; +struct Engine; +Engine* create(Callbacks callbacks); +void destroy(Engine* engine); +Result run(Engine* engine, Registers* registers, uint32_t block_budget); +void clear_cache(Engine* engine); +void invalidate_cache(Engine* engine, uint64_t address, uint64_t size); +void interrupt(Engine* engine); +// Drops an interrupt requested by the owning thread itself (a handled halt). +void clear_interrupt(Engine* engine); +uint64_t translated_blocks(const Engine* engine); +uint64_t interpreted_instructions(const Engine* engine); +uint64_t optimized_blocks(const Engine* engine); +// For a profiler on another thread: the guest address of the translated +// region containing host code (~0 if none), and whether the optimizing tier +// made it. The owner may be running. +uint64_t guest_pc_for_host_code(Engine* engine, const void* code, bool* optimized = nullptr, + uint64_t* region_start = nullptr, uint32_t* region_bytes = nullptr); +} diff --git a/direct/digitalis/engine.cpp b/direct/digitalis/engine.cpp new file mode 100644 index 0000000..31937f4 --- /dev/null +++ b/direct/digitalis/engine.cpp @@ -0,0 +1,1063 @@ +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "digitalis.h" +#include "interpreter/arm64/interpreter.h" +#include "berberis/lite_translator/lite_translate_region.h" +#include "berberis/code_gen_lib/code_gen_lib.h" +#include "berberis/runtime_primitives/platform.h" +#ifdef AXRB_DIGITALIS_HEAVY +#include "berberis/heavy_optimizer/arm64/heavy_optimize_region.h" +#include "berberis/base/config_globals.h" +#endif + +extern "C" void digitalis_enter(berberis::ThreadState*, const void*, void*); +extern "C" void digitalis_leave(); +extern "C" void digitalis_dispatch(); + +namespace axrb::digitalis { +struct Block { + void* code{}; + size_t bytes{}; + uint32_t countdown = UINT32_MAX; + uint32_t syscall = UINT32_MAX; +}; +// Two branch lookup tables, indexed by the same bits of the guest address. +// FinalEntry holds blocks that no longer count toward optimization: generated +// code's inline indirect-branch lookup reads only that table, four entries to +// a cache line. CacheEntry holds the blocks still counting. +struct FinalEntry { uint64_t pc{}; void* code{}; }; +struct CacheEntry { uint64_t pc{}; void* code{}; uint32_t* countdown{}; uint64_t unused{}; }; +struct DispatchContext { + CacheEntry* entries; + std::atomic* interrupted; + Result* result; + uint32_t remaining, used; + uint32_t* loop_budget; + uint32_t index_mask; // the table index of a guest address, times four + FinalEntry* finals; +}; +static_assert(sizeof(CacheEntry) == 32 && sizeof(FinalEntry) == 16 && offsetof(DispatchContext, remaining) == 24); +static_assert(offsetof(DispatchContext, loop_budget) == 32); +static_assert(offsetof(DispatchContext, index_mask) == 40 && offsetof(DispatchContext, finals) == 48); +static_assert(offsetof(berberis::ThreadState, cpu.insn_addr) == 784); +static_assert(sizeof(std::atomic) == 1 && uint32_t(Stop::Yield) == 0); +// A per-thread branch lookup table. Its pages are zero until written, so +// a thread pays only for the part its code reaches, and clearing hands the +// pages back instead of writing megabytes of zeros. +template +struct DispatchCache { + Entry* entries{}; + size_t bytes{}; + void allocate(size_t count) { + bytes = count * sizeof(Entry); + entries = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (!entries) throw std::bad_alloc(); + entries[0].pc = 1; + } + void clear() { + if (!entries) return; + // Generated code holds the table's address: the same range, new zero pages. + if (!VirtualFree(entries, bytes, MEM_DECOMMIT) || !VirtualAlloc(entries, bytes, MEM_COMMIT, PAGE_READWRITE)) + std::memset(entries, 0, bytes); + // A zero entry must not match a branch to address zero. + entries[0].pc = 1; + } + Entry* data() { return entries; } + Entry& operator[](size_t index) { return entries[index]; } + ~DispatchCache() { if (entries) VirtualFree(entries, 0, MEM_RELEASE); } +}; +// One contiguous range for all of an engine's generated code, so regions +// reach each other and the dispatcher stub at its start with a rel32 jump. +// First-tier code grows up from the bottom and optimized code down from the +// top: the code a thread spends its time in stays together instead of being +// scattered among the regions that ran a few times. +struct CodeArena { + static constexpr size_t kStub = 16, kCommit = size_t{1} << 20; + uint8_t* base{}; + size_t size{}, low{}, high{}, low_committed{}, high_committed{}; + void reserve(size_t bytes) { + size = bytes; + // Within one 4 GiB block of the address space: branch predictors keep + // only the low half of a target and take the rest from the branch's + // own address, so a branch across such a boundary is never predicted. + // Found by reserving a block more than needed and taking its aligned part. + constexpr uint64_t kBlock = uint64_t{1} << 32; + for (int attempt = 0; attempt != 16 && !base && size <= kBlock; ++attempt) { + void* probe = VirtualAlloc(nullptr, size + kBlock, MEM_RESERVE, PAGE_NOACCESS); + if (!probe) break; + const uint64_t aligned = (reinterpret_cast(probe) + kBlock - 1) & ~(kBlock - 1); + VirtualFree(probe, 0, MEM_RELEASE); + // Another thread may have taken the range in between. + base = static_cast(VirtualAlloc(reinterpret_cast(aligned), size, MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + } + if (!base) base = static_cast(VirtualAlloc(nullptr, size, MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + if (!base) throw std::bad_alloc(); + low_committed = 0; + high_committed = size; + start(); + } + // Writable and executable, like Dynarmic's code cache: switching + // protection per block cost two system calls on every compile. + void commit(size_t from, size_t to) { + if (!VirtualAlloc(base + from, to - from, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) throw std::bad_alloc(); + } + void start() { + low = kStub; + high = size; + // jmp [rip+0]; the dispatcher's address. + const uint8_t jump[6] = {0xff, 0x25, 0, 0, 0, 0}; + const uint64_t target = reinterpret_cast(&digitalis_dispatch); + ensure_low(kStub); + std::memcpy(base, jump, sizeof jump); + std::memcpy(base + sizeof jump, &target, sizeof target); + } + void ensure_low(size_t end) { + if (end <= low_committed) return; + const size_t to = std::min(size, (end + kCommit - 1) & ~(kCommit - 1)); + commit(low_committed, to); + low_committed = to; + } + uint8_t* allocate(size_t bytes, bool optimized) { + if (bytes > high - low) return nullptr; + if (!optimized) { + ensure_low(low + bytes); + low += bytes; + return base + low - bytes; + } + high -= bytes; + if (high < high_committed) { + const size_t from = high & ~(kCommit - 1); + commit(from, high_committed); + high_committed = from; + } + return base + high; + } + void reset() { + VirtualFree(base, size, MEM_DECOMMIT); + low_committed = 0; + high_committed = size; + start(); + } + ~CodeArena() { if (base) VirtualFree(base, 0, MEM_RELEASE); } +}; +// A direct jump to another region: where its rel32 field is in the code just +// generated, and the guest address it leads to. +struct JumpSite { uint32_t offset; uint64_t pc; }; +struct Optimized { + uint64_t pc{}, generation{}; + std::unique_ptr code; // null: nothing to install + std::vector sites; +}; +// Every installed region, in installation order, for guest_pc_for_host_code. +// The owner appends; a profiler thread reads up to the published count. +struct RegionLog { + struct Region { uint64_t host, pc; uint32_t bytes; bool optimized; }; + static constexpr size_t kChunk = 4096, kChunks = 8192; + std::unique_ptr[]> chunks{new std::atomic[kChunks]{}}; + std::atomic count{0}; + std::atomic resets{0}; + // The reader's sorted copy. + std::mutex index_lock; + std::vector index; + size_t indexed{}; + uint64_t index_resets{}; + void add(uint64_t host, uint32_t bytes, uint64_t pc, bool optimized) { + const size_t n = count.load(std::memory_order_relaxed); + if (n == kChunk * kChunks) return; + Region* chunk = chunks[n / kChunk].load(std::memory_order_relaxed); + if (!chunk) chunks[n / kChunk].store(chunk = new Region[kChunk], std::memory_order_release); + chunk[n % kChunk] = {host, pc, bytes, optimized}; + count.store(n + 1, std::memory_order_release); + } + // Host addresses are reused after a cache reset. + void reset() { + count.store(0, std::memory_order_release); + resets.fetch_add(1, std::memory_order_release); + } + ~RegionLog() { for (size_t i = 0; i < kChunks; ++i) delete[] chunks[i].load(); } +}; +struct Engine { + Callbacks callbacks; + berberis::ThreadState state{}; + uint32_t loop_budget{}; + std::unordered_map blocks; + std::unordered_map> page_blocks; + DispatchCache dispatch_cache; + DispatchCache final_cache; + uint32_t cache_mask = 262143; + bool chain = !GetEnvironmentVariableA("AXRB_DIGITALIS_NO_CHAIN", nullptr, 0); + // Whether all of this engine's code clears the top byte of the addresses + // it uses (see TaggedSites). The background optimizer reads it too. + std::atomic mask_addresses{false}; + std::unordered_set retranslated_sites; // instructions this engine retranslated for a tagged pointer + bool heavy = false, force_heavy = false; + uint32_t heavy_min_instructions = 1; + bool interpret_only = GetEnvironmentVariableA("AXRB_DIGITALIS_INTERPRET_ONLY", nullptr, 0) != 0; + CodeArena arena; + berberis::RecoveryMap recovery; + std::atomic interrupted{false}; + Result result{Stop::Yield}; + uint64_t compiled{}, interpreted{}; + uint64_t heavy_attempts{}, heavy_compiled{}; + size_t cache_bytes{}; + bool profile = GetEnvironmentVariableA("AXRB_DIGITALIS_PROFILE", nullptr, 0) != 0; + uint64_t jit_calls{}, clears{}, compile_ns{}, interpret_ns{}, run_ns{}, reports{}; + uint64_t runs{}, enters{}, svc_exits{}; + std::unordered_map fallback_words; + // Direct-branch chaining: a region jumps straight to its target's final + // host code, or to the arena's dispatcher stub until the target is + // translated (or while it still counts toward optimization). The owner + // patches the rel32 fields listed here, as arena offsets by target; no + // other thread runs or writes this engine's code. + std::unordered_map> sites_by_pc; + bool async_heavy = !GetEnvironmentVariableA("AXRB_DIGITALIS_SYNC_HEAVY", nullptr, 0); + uint64_t generation{}; // invalidations and cache resets; stale optimizations are dropped + std::mutex optimized_lock; + std::vector optimized; // finished in the background, installed by the owner + std::atomic optimizer_done{false}; + std::chrono::steady_clock::time_point next_report = std::chrono::steady_clock::now() + std::chrono::seconds(5); + RegionLog regions; +}; +static_assert(offsetof(Engine, loop_budget) - offsetof(Engine, state) == sizeof(berberis::ThreadState)); +using Clock = std::chrono::steady_clock; +uint64_t elapsed_ns(Clock::time_point start) { + return std::chrono::duration_cast(Clock::now() - start).count(); +} +// A thread's translated code is discarded and translated again from nothing +// when it reaches this size, which the player sees as a stall. An engine's main +// thread reaches 150 MiB in a start scene, so machines with the memory for it +// get more room than Dynarmic's fixed 256 MiB: 1/32 of RAM, up to 1 GiB. +size_t cache_limit() { + static const size_t limit = [] { + MEMORYSTATUSEX status{sizeof status}; + const uint64_t ram = GlobalMemoryStatusEx(&status) ? status.ullTotalPhys : 0; + return size_t(std::clamp(ram / 32, 256ull << 20, 1024ull << 20)); + }(); + return limit; +} +// ARM64 ignores the top byte of an address, and a guest may keep a tag there. +// The host does not, so translated code would have to clear the byte before +// every load and store: two shifts in the path of each address. Few +// instructions ever see a tagged pointer (AXRB reports no tagged-address ABI +// to bionic; its allocator still tags the address of its chunk headers), so +// code starts out without the shifts. A tagged pointer then faults as a +// non-canonical address, and the instruction it faulted in joins this +// process-wide set of those translated with the shifts. A guest that tags +// pointers all over gets the shifts everywhere: `everywhere`, which +// AXRB_DIGITALIS_MASK_ADDRESSES=1 starts with. +struct TaggedSites { + static constexpr size_t kMost = 2048; + std::shared_mutex lock; + std::unordered_set sites; + std::atomic count{0}; + std::atomic everywhere{GetEnvironmentVariableA("AXRB_DIGITALIS_MASK_ADDRESSES", nullptr, 0) != 0}; + bool contains(uint64_t pc) { + if (everywhere.load(std::memory_order_relaxed)) return true; + if (!count.load(std::memory_order_relaxed)) return false; + std::shared_lock held(lock); + return sites.contains(pc); + } + void add(uint64_t pc) { + std::unique_lock held(lock); + sites.insert(pc); + count.store(sites.size(), std::memory_order_relaxed); + if (sites.size() > kMost && !everywhere.exchange(true)) + std::fprintf(stderr, "Digitalis: tagged pointers at over %zu instructions, masking every address from now on\n", kMost); + } +} tagged_sites; +thread_local Engine* active = nullptr; +thread_local bool optimizing = false; // a background optimizer thread +thread_local std::vector emitted_sites; // by the region being generated +// The guest code a region is being generated from, for return_addresses(). +struct GuestWindow { uint64_t begin{}, end{}; const uint8_t* bytes{}; bool lite{}; }; +thread_local GuestWindow guest_window; +// Generated code is per engine, so its interrupt flag sits at a fixed offset +// from the ThreadState that RBP points to. +int32_t interrupted_offset() { + return int32_t(offsetof(Engine, interrupted)) - int32_t(offsetof(Engine, state)); +} +bool chainable(const Block& block) { return block.code && block.countdown == UINT32_MAX && block.syscall == UINT32_MAX; } +// Points the jump whose rel32 field is at this arena offset at code, or at the dispatcher stub. +void point(Engine* engine, uint32_t field, const void* code) { + uint8_t* at = engine->arena.base + field; + const int32_t relative = int32_t((code ? static_cast(code) : engine->arena.base) - (at + 4)); + std::memcpy(at, &relative, sizeof relative); +} +void point_all(Engine* engine, uint64_t pc, const void* code) { + auto found = engine->sites_by_pc.find(pc); + if (found == engine->sites_by_pc.end()) return; + for (const uint32_t field : found->second) point(engine, field, code); +} +// Called from generated code (Windows x64 ABI) with every guest register +// already stored to the ThreadState. +bool fast_syscall(berberis::ThreadState* state) { + Engine* engine = active; + return engine->callbacks.syscall(engine->callbacks.opaque, state->cpu.x); +} +void publish(Engine* engine, uint64_t pc, const Block& block) { + point_all(engine, pc, chainable(block) ? block.code : nullptr); +} +bool fetch(Engine* engine, uint64_t address, void* out, size_t bytes) { + const auto callback = engine->callbacks.fetch ? engine->callbacks.fetch : engine->callbacks.read; + return callback(engine->callbacks.opaque, address, out, bytes); +} + +LONG recover(EXCEPTION_POINTERS* exception) { + if (!active || exception->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) + return EXCEPTION_CONTINUE_SEARCH; + const auto address = exception->ExceptionRecord->ExceptionInformation[1]; + if (active->callbacks.fault && active->callbacks.fault(active->callbacks.opaque, address, + exception->ExceptionRecord->ExceptionInformation[0] == 1)) return EXCEPTION_CONTINUE_EXECUTION; + if (optimizing) return EXCEPTION_CONTINUE_SEARCH; // the optimizer abandons the region + auto found = active->recovery.find(exception->ContextRecord->Rip); + if (found == active->recovery.end()) return EXCEPTION_CONTINUE_SEARCH; + active->result = {Stop::MemoryFault, 0, address}; + exception->ContextRecord->Rip = found->second; + return EXCEPTION_CONTINUE_EXECUTION; +} + +void interpret_one(Engine* engine) { + berberis::Interpreter interpreter(&engine->state); + berberis::SemanticsPlayer player(&interpreter); + berberis::Decoder decoder(&player); + auto length = decoder.Decode(berberis::ToHostAddr(engine->state.cpu.insn_addr)); + interpreter.FinalizeInsn(length); +} +int interpreter_fault(EXCEPTION_POINTERS* exception, Engine* engine) { + if (exception->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) return EXCEPTION_CONTINUE_SEARCH; + engine->result = {Stop::MemoryFault, 0, exception->ExceptionRecord->ExceptionInformation[1]}; + return EXCEPTION_EXECUTE_HANDLER; +} +void safe_interpret(Engine* engine) { + __try { interpret_one(engine); } + __except(interpreter_fault(GetExceptionInformation(), engine)) {} +} + +void clear_cache(Engine* engine) { + ++engine->clears; + ++engine->generation; + engine->arena.reset(); + engine->sites_by_pc.clear(); + engine->retranslated_sites.clear(); + engine->blocks.clear(); + engine->page_blocks.clear(); + engine->dispatch_cache.clear(); + engine->final_cache.clear(); + engine->recovery.clear(); + engine->cache_bytes = 0; + engine->regions.reset(); +} +Engine* create(Callbacks callbacks) { + static std::once_flag installed; + std::call_once(installed, [] { if (!AddVectoredExceptionHandler(1, recover)) throw std::runtime_error("Digitalis fault handler registration failed"); }); + auto engine = std::make_unique(); + engine->callbacks = callbacks; + char cache_bits[16]{}; + if (GetEnvironmentVariableA("AXRB_DIGITALIS_CACHE_BITS", cache_bits, sizeof cache_bits)) { + const auto bits = std::clamp(std::strtoul(cache_bits, nullptr, 10), 12, 20); + engine->cache_mask = (1u << bits) - 1; + } + engine->dispatch_cache.allocate(size_t{engine->cache_mask} + 1); + engine->final_cache.allocate(size_t{engine->cache_mask} + 1); + // The cache is reset at cache_limit(); the margin holds what is installed before the next check. + engine->arena.reserve(cache_limit() + (size_t{4} << 20)); +#ifdef AXRB_DIGITALIS_HEAVY + char mode[16]{}; + GetEnvironmentVariableA("AXRB_DIGITALIS_HEAVY", mode, sizeof mode); + engine->heavy = std::strcmp(mode, "0") != 0; + engine->force_heavy = std::strcmp(mode, "force") == 0; + char minimum[16]{}; + if (GetEnvironmentVariableA("AXRB_DIGITALIS_HEAVY_MIN_INSNS", minimum, sizeof minimum)) + engine->heavy_min_instructions = std::clamp(std::strtoul(minimum, nullptr, 10), 1, 200); +#endif + return engine.release(); +} +void invalidate_cache(Engine* engine, uint64_t address, uint64_t size) { + if (!size) return; + const uint64_t first = address & ~uint64_t{4095}; + const uint64_t last = size - 1 > UINT64_MAX - address ? UINT64_MAX : (address + size - 1) | uint64_t{4095}; + bool erased = false; + // Compilation never crosses a guest page, so page-granular invalidation + // covers blocks beginning before a modified instruction without flushing + // unrelated game code. Retired code stays allocated until the next cache + // reset; no executing thread shares this engine's code. + auto erase_page = [&](auto page) { + for (const auto pc : page->second) { + engine->blocks.erase(pc); + point_all(engine, pc, nullptr); + } + erased = true; + return engine->page_blocks.erase(page); + }; + const auto last_page = last & ~uint64_t{4095}; + const auto pages = ((last_page - first) >> 12) + 1; + // Most invalidations are tiny. Index by guest page instead of scanning + // hundreds of thousands of blocks on every mmap/protection change. + if (pages <= engine->page_blocks.size()) { + for (uint64_t page = first;; page += 4096) { + auto found = engine->page_blocks.find(page); + if (found != engine->page_blocks.end()) erase_page(found); + if (page == last_page) break; + } + } else { + for (auto it = engine->page_blocks.begin(); it != engine->page_blocks.end();) { + if (it->first >= first && it->first <= last_page) it = erase_page(it); + else ++it; + } + } + if (erased) { + ++engine->generation; + engine->dispatch_cache.clear(); + engine->final_cache.clear(); + if (engine->blocks.empty()) clear_cache(engine); + } +} +void start_masking_addresses(Engine* engine) { + engine->mask_addresses.store(true, std::memory_order_relaxed); + clear_cache(engine); // optimizations in progress belong to the old generation and are dropped +} +void forget_optimizations(Engine* engine); +void destroy(Engine* engine) { + if (!engine) return; + forget_optimizations(engine); + clear_cache(engine); + delete engine; +} +void interrupt(Engine* engine) { + engine->interrupted.store(true, std::memory_order_release); + // Optimized loops count this down and exit at zero; a lost race with + // their decrement only delays the exit to the regular budget. + reinterpret_cast(engine->loop_budget) = 1; +} +void clear_interrupt(Engine* engine) { engine->interrupted.store(false, std::memory_order_release); } +uint64_t translated_blocks(const Engine* engine) { return engine->compiled; } +uint64_t interpreted_instructions(const Engine* engine) { return engine->interpreted; } +uint64_t optimized_blocks(const Engine* engine) { return engine->heavy_compiled; } +uint64_t guest_pc_for_host_code(Engine* engine, const void* code, bool* optimized, uint64_t* region_start, + uint32_t* region_bytes) { + auto& log = engine->regions; + std::lock_guard held(log.index_lock); + const uint64_t resets = log.resets.load(std::memory_order_acquire); + if (resets != log.index_resets) { + log.index.clear(); + log.indexed = 0; + log.index_resets = resets; + } + const size_t count = log.count.load(std::memory_order_acquire); + if (count > log.indexed) { + for (size_t i = log.indexed; i < count; ++i) + log.index.push_back(log.chunks[i / RegionLog::kChunk].load(std::memory_order_acquire)[i % RegionLog::kChunk]); + log.indexed = count; + std::sort(log.index.begin(), log.index.end(), [](const auto& a, const auto& b) { return a.host < b.host; }); + } + const uint64_t ip = reinterpret_cast(code); + auto after = std::upper_bound(log.index.begin(), log.index.end(), ip, [](uint64_t value, const auto& region) { return value < region.host; }); + if (after == log.index.begin()) return ~uint64_t{0}; + const auto& region = *std::prev(after); + if (ip - region.host >= region.bytes) return ~uint64_t{0}; + if (optimized) *optimized = region.optimized; + if (region_start) *region_start = region.host; + if (region_bytes) *region_bytes = region.bytes; + return region.pc; +} + +// Places generated code in the engine's arena and connects its direct jumps. +// Only the owning thread does; no other thread executes or modifies the code. +// No code in the result: the arena is full until the next cache reset. +Block install(Engine* engine, berberis::MachineCode& code, uint64_t pc, bool optimized, const std::vector& sites) { + const size_t bytes = (code.install_size() + 15) & ~size_t{15}; + uint8_t* memory = engine->arena.allocate(bytes, optimized); + if (!memory) return {}; + code.InstallUnsafe(memory, &engine->recovery); + for (const auto& site : sites) { + if (site.offset < 1 || site.offset + 4 > code.install_size() || memory[site.offset - 1] != 0xe9) + throw std::runtime_error("Digitalis lost track of a direct jump"); + const uint32_t field = uint32_t(memory + site.offset - engine->arena.base); + engine->sites_by_pc[site.pc].push_back(field); + auto target = engine->blocks.find(site.pc); + if (target != engine->blocks.end() && chainable(target->second)) point(engine, field, target->second.code); + } + ++engine->compiled; + engine->cache_bytes += bytes; + engine->regions.add(reinterpret_cast(memory), uint32_t(bytes), pc, optimized); + // AXRB_DIGITALIS_DUMP=: every region's host code, for a disassembler + // (llvm-objdump -D -b binary -m i386:x86-64), named --. + static const std::string dump = [] { + char directory[MAX_PATH]{}; + GetEnvironmentVariableA("AXRB_DIGITALIS_DUMP", directory, sizeof directory); + return std::string(directory); + }(); + if (!dump.empty()) { + char name[MAX_PATH + 64]; + std::snprintf(name, sizeof name, "%s\\%llx-%s-%llx.bin", dump.c_str(), static_cast(pc), + optimized ? "opt" : "lite", static_cast(reinterpret_cast(memory))); + if (FILE* file = std::fopen(name, "wb")) { + std::fwrite(memory, 1, code.install_size(), file); + std::fclose(file); + } + } + return {memory, bytes}; +} + +Block compile(Engine* engine, uint64_t pc, bool optimize = false) { + if (engine->interpret_only) return {}; + // Compiler arithmetic must neither inherit guest rounding nor contribute + // exceptions to the guest's FPSR when returning to generated code. + struct CompilerFpEnvironment { + unsigned saved = _mm_getcsr(); + CompilerFpEnvironment() { _mm_setcsr(0x1f80); } + ~CompilerFpEnvironment() { _mm_setcsr(saved); } + } compiler_fp_environment; + // Never decode beyond the readable guest page. Failed prefixes are retried + // up to the unsupported instruction so the interpreter handles only that op. + uint64_t end = (pc & ~uint64_t{4095}) + 4096; + end = std::min(end, pc + (optimize || engine->force_heavy ? 800 : 256)); + uint8_t readable[800]; + const bool fetched = fetch(engine, pc, readable, end - pc); + if (!fetched) end = pc + 4; + struct Window { + ~Window() { guest_window = {}; } + } window; + for (int attempt = 0; attempt != 2; ++attempt) { + berberis::MachineCode code; + berberis::MachineCode* selected_code = &code; + berberis::LiteTranslateParams params; + params.end_pc = end; + params.allow_dispatch = engine->chain; + bool ok = false; + uint64_t next = pc; +#ifdef AXRB_DIGITALIS_HEAVY + berberis::MachineCode heavy_code; + emitted_sites.clear(); + guest_window = fetched ? GuestWindow{pc, end, readable, false} : GuestWindow{}; + if (optimize || engine->force_heavy) { + ++engine->heavy_attempts; + berberis::HeavyOptimizeParams heavy_params; + heavy_params.end_pc = end; + bool loop = false; + auto [stop, success, instructions] = berberis::HeavyOptimizeRegion(pc, &heavy_code, heavy_params, &loop); + // Even an unsupported final instruction leaves a valid translated + // prefix. No progress falls through to the lite tier. + ok = instructions > 0 && stop != pc; + if (optimize && ((!success && !loop) || (success && instructions < engine->heavy_min_instructions))) + ok = false; + next = stop; + if (ok) { selected_code = &heavy_code; ++engine->heavy_compiled; } + else if (optimize) return {}; + } +#endif + if (!ok) { + emitted_sites.clear(); + guest_window.lite = true; + std::tie(ok, next) = berberis::TryLiteTranslateRegion(pc, &code, params); + } + if (!ok) { + if (next == pc) return {}; + end = next; + continue; + } + Block block = install(engine, *selected_code, pc, selected_code != &code, emitted_sites); + if (!block.code) return {}; + block.countdown = engine->heavy && !optimize && !engine->force_heavy ? 512u : UINT32_MAX; + return block; + } + return {}; +} + +#ifdef AXRB_DIGITALIS_HEAVY +// Optimizes the hot region at pc on a background thread. False if the region +// does not qualify or became unreadable (a fault abandons it). +bool optimize_region(Engine* engine, uint64_t pc, berberis::MachineCode* code) { + const uint64_t end = std::min((pc & ~uint64_t{4095}) + 4096, pc + 800); + uint8_t readable[800]; + if (!fetch(engine, pc, readable, end - pc)) return false; + berberis::HeavyOptimizeParams params; + params.end_pc = end; + bool loop = false; + emitted_sites.clear(); + guest_window = {pc, end, readable, false}; + auto [stop, success, instructions] = berberis::HeavyOptimizeRegion(pc, code, params, &loop); + guest_window = {}; + return instructions > 0 && stop != pc && (success || loop) && + !(success && instructions < engine->heavy_min_instructions); +} +struct OptimizeCall { Engine* engine; uint64_t pc; berberis::MachineCode* code; bool ok; }; +void optimize_unguarded(OptimizeCall* call) { + try { call->ok = optimize_region(call->engine, call->pc, call->code); } catch (...) { call->ok = false; } +} +bool optimize_guarded(OptimizeCall* call) { + __try { optimize_unguarded(call); return call->ok; } + __except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + return false; + } +} + +// Hot regions are optimized by background threads, so the guest thread that +// made them hot keeps running their lite translation instead of stalling for +// the optimizer (about 0.1 ms per region; thousands while a game loads). The +// owner installs the code the next time it runs, unless an invalidation or a +// cache reset happened since the region was queued. +class Optimizer { +public: + static Optimizer& instance() { + static Optimizer optimizer; + return optimizer; + } + bool post(Engine* engine, uint64_t pc) { + std::call_once(started_, [this] { + for (int i = 0; i < kWorkers; ++i) std::thread([this, i] { work(i); }).detach(); + }); + std::lock_guard held(lock_); + if (jobs_.size() >= 4096) return false; + jobs_.push_back({engine, pc, engine->generation}); + ready_.notify_one(); + return true; + } + // Before an engine goes away: drops its jobs and waits for one in progress. + void forget(Engine* engine) { + std::unique_lock held(lock_); + std::erase_if(jobs_, [&](const Job& job) { return job.engine == engine; }); + idle_.wait(held, [&] { return std::find(std::begin(busy_), std::end(busy_), engine) == std::end(busy_); }); + } + +private: + static constexpr int kWorkers = 2; + struct Job { Engine* engine; uint64_t pc, generation; }; + void work(int index) { + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); + optimizing = true; + for (;;) { + Job job; + { + std::unique_lock held(lock_); + ready_.wait(held, [&] { return !jobs_.empty(); }); + job = jobs_.front(); + jobs_.pop_front(); + busy_[index] = job.engine; + } + active = job.engine; + auto code = std::make_unique(); + OptimizeCall call{job.engine, job.pc, code.get(), false}; + const bool ok = optimize_guarded(&call); + active = nullptr; + { + std::lock_guard held(job.engine->optimized_lock); + if (ok) job.engine->optimized.push_back({job.pc, job.generation, std::move(code), std::move(emitted_sites)}); + emitted_sites.clear(); + job.engine->optimizer_done.store(true, std::memory_order_release); + } + std::lock_guard held(lock_); + busy_[index] = nullptr; + idle_.notify_all(); + } + } + std::once_flag started_; + std::mutex lock_; + std::condition_variable ready_, idle_; + std::deque jobs_; + Engine* busy_[kWorkers]{}; +}; + +void forget_optimizations(Engine* engine) { Optimizer::instance().forget(engine); } + +// Owner thread, before running translated code. +void install_optimized(Engine* engine) { + if (!engine->optimizer_done.load(std::memory_order_acquire)) return; + std::vector ready; + { + std::lock_guard held(engine->optimized_lock); + ready.swap(engine->optimized); + engine->optimizer_done.store(false, std::memory_order_relaxed); + } + for (auto& result : ready) { + auto found = engine->blocks.find(result.pc); + if (!result.code || result.generation != engine->generation || found == engine->blocks.end() || + !found->second.code) continue; + const Block optimized = install(engine, *result.code, result.pc, true, result.sites); + if (!optimized.code) continue; // no room: the first-tier code stays + found->second = optimized; + ++engine->heavy_compiled; + publish(engine, result.pc, found->second); + const size_t index = (result.pc >> 2) & engine->cache_mask; + if (engine->dispatch_cache[index].pc == result.pc) engine->dispatch_cache[index] = {}; + engine->final_cache[index] = {result.pc, optimized.code}; + } +} +#else +void forget_optimizations(Engine*) {} +#endif + +Result run(Engine* engine, Registers* registers, uint32_t budget) { + const auto run_started = engine->profile ? Clock::now() : Clock::time_point{}; + auto& cpu = engine->state.cpu; + std::memcpy(cpu.x, registers->x, sizeof cpu.x); + std::memcpy(cpu.v, registers->v, sizeof cpu.v); + cpu.sp = registers->sp; + cpu.insn_addr = registers->pc; + cpu.cached_fpcr = registers->fpcr; + cpu.emulated_fpsr = registers->fpsr; + cpu.flags = ((registers->pstate >> 16) & 0xc000) | ((registers->pstate >> 21) & 0x100) | ((registers->pstate >> 28) & 1); + engine->state.tls = registers->tls; + engine->result = {Stop::Yield}; + engine->loop_budget = std::min(budget, 4096u); + const unsigned saved_mxcsr = _mm_getcsr(); + constexpr unsigned rounding[] = {0, 2, 1, 3}; + unsigned mxcsr = 0x1f80 | (rounding[(registers->fpcr >> 22) & 3] << 13); + if (registers->fpcr & (1u << 24)) mxcsr |= 0x8040; + _mm_setcsr(mxcsr); + struct Restore { + Engine* previous; unsigned mxcsr; + ~Restore() { active = previous; _mm_setcsr(mxcsr); } + } restore{active, saved_mxcsr}; + active = engine; + if (tagged_sites.everywhere.load(std::memory_order_relaxed) && !engine->mask_addresses.load(std::memory_order_relaxed)) + start_masking_addresses(engine); +#ifdef AXRB_DIGITALIS_HEAVY + install_optimized(engine); +#endif + // The host reports a non-canonical address as -1: a tagged pointer in an + // instruction this engine translated without masking. The fault left the + // guest at that instruction, which runs again once its page is translated + // anew. A second fault there is the guest's own. + auto tagged_pointer_fault = [&] { + if (engine->result.reason != Stop::MemoryFault || engine->result.address != ~uint64_t{0} || + engine->mask_addresses.load(std::memory_order_relaxed) || + !engine->retranslated_sites.insert(cpu.insn_addr).second) return false; + tagged_sites.add(cpu.insn_addr); + if (tagged_sites.everywhere.load(std::memory_order_relaxed)) start_masking_addresses(engine); + else invalidate_cache(engine, cpu.insn_addr, 4); + engine->result = {Stop::Yield}; + return true; + }; + auto execute = [&](uint64_t pc, Block& block, uint32_t remaining) { + if (block.countdown != UINT32_MAX) { + if (block.countdown) --block.countdown; + if (!block.countdown) { +#ifdef AXRB_DIGITALIS_HEAVY + if (engine->async_heavy) { + // Chained as it is until the optimized code arrives; + // with the queue full, try again later. + if (Optimizer::instance().post(engine, pc)) { + ++engine->heavy_attempts; + block.countdown = UINT32_MAX; + publish(engine, pc, block); + } else { + block.countdown = 4096; + } + } else +#endif + { + const auto start = engine->profile ? Clock::now() : Clock::time_point{}; + auto optimized = compile(engine, pc, true); + if (optimized.code) block = optimized; + block.countdown = UINT32_MAX; + publish(engine, pc, block); + if (engine->profile) engine->compile_ns += elapsed_ns(start); + } + } + } + const size_t index = (pc >> 2) & engine->cache_mask; + if (chainable(block)) engine->final_cache[index] = {pc, block.code}; + else engine->dispatch_cache[index] = {pc, block.code, &block.countdown}; + DispatchContext context{engine->dispatch_cache.data(), &engine->interrupted, &engine->result, + engine->chain ? remaining : 1, 0, &engine->loop_budget, engine->cache_mask << 2, engine->final_cache.data()}; + digitalis_enter(&engine->state, block.code, &context); + if (engine->profile) { engine->jit_calls += context.used; ++engine->enters; } + return context.used; + }; + for (uint32_t i = 0; i < budget && !engine->interrupted.exchange(false, std::memory_order_acq_rel); ++i) { + const uint64_t pc = cpu.insn_addr; + if (!engine->loop_budget) engine->loop_budget = 4096; + // The arena is committed only as code is compiled. + if (engine->cache_bytes >= cache_limit()) clear_cache(engine); + auto found = engine->blocks.find(pc); + if (found != engine->blocks.end() && found->second.syscall != UINT32_MAX) { + cpu.insn_addr += 4; + engine->result = {Stop::Svc, found->second.syscall}; + break; + } + if (found != engine->blocks.end() && found->second.code) { + i += execute(pc, found->second, budget - i) - 1; + if (engine->result.reason != Stop::Yield && !tagged_pointer_fault()) break; + continue; + } + if (found != engine->blocks.end() && !engine->profile) { + // A known untranslatable instruction (not an SVC, which returned + // above): interpret it without fetching and decoding it again. + ++engine->interpreted; + safe_interpret(engine); + if (engine->result.reason != Stop::Yield) break; + continue; + } + uint32_t word{}; + if ((pc & 3) || !fetch(engine, pc, &word, sizeof word)) { + engine->result = {Stop::MemoryFault, 0, pc}; break; + } + if ((word & 0xffe0001f) == 0xd4000001) { + // SVC sites are code too. Re-fetching every invocation repeatedly + // entered the Windows virtual-memory query and population locks. + // They share native blocks' page index and invalidation lifetime. + if (found == engine->blocks.end()) { + engine->blocks.emplace(pc, Block{nullptr, 0, UINT32_MAX, (word >> 5) & 0xffff}); + engine->page_blocks[pc & ~uint64_t{4095}].push_back(pc); + } + cpu.insn_addr += 4; + engine->result = {Stop::Svc, (word >> 5) & 0xffff}; break; + } + if (found == engine->blocks.end()) { + const auto start = engine->profile ? Clock::now() : Clock::time_point{}; + found = engine->blocks.emplace(pc, compile(engine, pc)).first; + engine->page_blocks[pc & ~uint64_t{4095}].push_back(pc); + publish(engine, pc, found->second); + if (engine->profile) engine->compile_ns += elapsed_ns(start); + } + if (found->second.code) { + i += execute(pc, found->second, budget - i) - 1; + if (tagged_pointer_fault()) continue; + } else { + ++engine->interpreted; + const auto start = engine->profile ? Clock::now() : Clock::time_point{}; + safe_interpret(engine); + if (engine->profile) { engine->interpret_ns += elapsed_ns(start); ++engine->fallback_words[word]; } + } + if (engine->result.reason != Stop::Yield) break; + } + std::memcpy(registers->x, cpu.x, sizeof cpu.x); + std::memcpy(registers->v, cpu.v, sizeof cpu.v); + registers->sp = cpu.sp; + registers->pc = cpu.insn_addr; + registers->tls = engine->state.tls; + registers->pstate = (uint32_t(cpu.flags & 0xc000) << 16) | (uint32_t(cpu.flags & 0x100) << 21) | (uint32_t(cpu.flags & 1) << 28); + registers->fpcr = cpu.cached_fpcr; + // Preserve cumulative FP exceptions across syscall/yield boundaries before + // restoring the Windows thread's MXCSR. Digitalis mirrors these lazily on MRS. + const unsigned exceptions = _mm_getcsr() & 63; + registers->fpsr = cpu.emulated_fpsr | (exceptions & 1) | + ((exceptions & 2) << 6) | ((exceptions & 60) >> 1); + if (engine->profile) { + ++engine->runs; + if (engine->result.reason == Stop::Svc) ++engine->svc_exits; + engine->run_ns += elapsed_ns(run_started); + if (Clock::now() >= engine->next_report) { + std::fprintf(stderr, "Digitalis.Profile tid=%lu report=%llu jit=%llu interpreted=%llu compiled=%llu clears=%llu cache=%zu run_ms=%.1f compile_ms=%.1f interpret_ms=%.1f\n", + GetCurrentThreadId(), ++engine->reports, engine->jit_calls, engine->interpreted, engine->compiled, + engine->clears, engine->cache_bytes, engine->run_ns / 1e6, engine->compile_ns / 1e6, engine->interpret_ns / 1e6); + std::fprintf(stderr, "Digitalis.Heavy tid=%lu attempts=%llu compiled=%llu runs=%llu enters=%llu svc=%llu\n", + GetCurrentThreadId(), engine->heavy_attempts, engine->heavy_compiled, engine->runs, engine->enters, engine->svc_exits); + std::vector> words; + for (auto [word, count] : engine->fallback_words) words.push_back({count, word}); + std::sort(words.rbegin(), words.rend()); + for (size_t i = 0; i < std::min(words.size(), 5); ++i) + std::fprintf(stderr, "Digitalis.Fallback tid=%lu word=%08x count=%llu\n", GetCurrentThreadId(), words[i].second, words[i].first); + engine->next_report = Clock::now() + std::chrono::seconds(5); + } + } + return engine->result; +} +} + +namespace berberis { +using namespace axrb::digitalis; +#ifdef AXRB_DIGITALIS_HEAVY +ConfigStr::ConfigStr(const char* env_name, const char*) : value_(std::getenv(env_name)) {} +bool IsConfigFlagSet(ConfigFlag) { return false; } +size_t GetThreadStateSimdRegOffset(int reg) { return offsetof(ThreadState, cpu.v) + size_t(reg) * 16; } +bool IsSimdOffset(size_t offset) { + return offset >= offsetof(ThreadState, cpu.v) && + offset - offsetof(ThreadState, cpu.v) < sizeof(ThreadState::cpu.v); +} +#endif +GuestAddr GetGuestAddrRangeEnd() { return UINT64_MAX; } +bool MaskAddressTopByte(GuestAddr insn_addr) { + return active->mask_addresses.load(std::memory_order_relaxed) || tagged_sites.contains(insn_addr); +} +extern "C" void berberis_entry_HandleLiteCounterThresholdReached() {} +void UndefinedInsn(GuestAddr) { active->result = {Stop::Undefined}; } +void BreakpointInsn(GuestAddr) { active->result = {Stop::Breakpoint}; } +void RunGuestSyscall(ThreadState*) { active->result = {Stop::Undefined}; } +void InvalidateGuestRange(GuestAddr start, GuestAddr end) { + if (active->callbacks.invalidate) active->callbacks.invalidate(active->callbacks.opaque, start, end - start); +} +FaultyLoadResult FaultyLoad(const void* address, uint8_t bytes) { + uint64_t value{}; + if (!active->callbacks.read(active->callbacks.opaque, reinterpret_cast(address), &value, bytes)) { + active->result = {Stop::MemoryFault, 0, reinterpret_cast(address)}; + return {0, 1}; + } + return {value, 0}; +} +bool FaultyStore(void* address, uint8_t bytes, uint64_t value) { + if (active->callbacks.write(active->callbacks.opaque, reinterpret_cast(address), &value, bytes)) return false; + active->result = {Stop::MemoryFault, 0, reinterpret_cast(address)}; + return true; +} +void EmitExitGeneratedCode(x86_64::Assembler* as, x86_64::Assembler::Register target) { + as->Movq({.base = as->rbp, .disp = int32_t(offsetof(ThreadState, cpu.insn_addr))}, target); + as->Movq(as->rax, reinterpret_cast(&digitalis_dispatch)); + as->Jmp(as->rax); +} +void EmitSyscall(x86_64::Assembler* as, GuestAddr pc) { + uint32_t word{}; + if (!fetch(active, pc, &word, sizeof word)) + throw std::runtime_error("Digitalis syscall became unreadable during compilation"); + // A translated SVC already knows its immediate and return PC. Return it + // directly instead of dispatching to the SVC address for a second lookup. + if (((word >> 5) & 0xffff) == 0 && active->callbacks.syscall) { + // Non-blocking syscalls complete in a direct call and continue at the + // next instruction. The stack stays 16-byte aligned with 32 bytes of + // shadow space, below the dispatch context at [rsp+8]. + auto* slow = as->MakeLabel(); + as->Movq(as->rcx, as->rbp); + as->Movq(as->rax, reinterpret_cast(&fast_syscall)); + as->Subq(as->rsp, int32_t{48}); + as->Call(as->rax); + as->Addq(as->rsp, int32_t{48}); + as->Testb(as->rax, as->rax); + as->Jcc(x86_64::Assembler::Condition::kZero, *slow); + EmitDirectDispatch(as, pc + 4, true); + as->Bind(slow); + } + as->Movq(as->rax, reinterpret_cast(&active->result)); + as->Movl({.base = as->rax, .disp = int32_t(offsetof(Result, reason))}, uint32_t(Stop::Svc)); + as->Movl({.base = as->rax, .disp = int32_t(offsetof(Result, immediate))}, (word >> 5) & 0xffff); + as->Movq(as->rax, pc + 4); + EmitExitGeneratedCode(as, as->rax); +} +// A guest return is an indirect branch: a lookup in the table of final +// blocks, whose entry has usually left the cache by the time the callee +// returns. The call knows where the return will land, so it fetches that +// entry ahead. Which exit of a region is a call is read off the guest code: +// the returns of the calls that this exit can be, at most two. A wrong guess +// costs a prefetch. +int return_addresses(GuestAddr target, bool indirect, GuestAddr out[2]) { + const GuestWindow& window = guest_window; + int count = 0; + for (uint64_t at = window.begin; at + 4 <= window.end && count != 2; at += 4) { + uint32_t word; + std::memcpy(&word, window.bytes + (at - window.begin), sizeof word); + const bool call = indirect ? (word & 0xfffffc1f) == 0xd63f0000 // BLR + : (word & 0xfc000000) == 0x94000000 && at + (int64_t(int32_t(word << 6)) >> 4) == target; // BL + if (call) out[count++] = at + 4; + // An optimized region has several exits; a return or a jump through a register among them + // would fetch for calls it is not. + else if (indirect && !window.lite && (word & 0xff9ffc1f) == 0xd61f0000) return 0; + // A first-tier region ends at its first branch that is not conditional. + if (window.lite && ((word & 0x7c000000) == 0x14000000 || (word & 0xff9ffc1f) == 0xd61f0000)) break; + } + return count; +} +void emit_return_prefetch(x86_64::Assembler* as, GuestAddr target, bool indirect) { + GuestAddr returns[2]; + const int count = return_addresses(target, indirect, returns); + for (int i = 0; i != count; ++i) { + as->Movq(as->rdx, reinterpret_cast(&active->final_cache[(returns[i] >> 2) & active->cache_mask])); + as->Emit8(0x0f); // prefetcht0 [rdx] + as->Emit8(0x18); + as->Emit8(0x0a); + } +} +void EmitDirectDispatch(x86_64::Assembler* as, GuestAddr pc, bool check_interrupt) { + as->Movq(as->rax, pc); + if (!active->chain) { EmitExitGeneratedCode(as, as->rax); return; } + as->Movq({.base = as->rbp, .disp = int32_t(offsetof(ThreadState, cpu.insn_addr))}, as->rax); + // Every cycle of chained regions contains a jump that checks for an + // interrupt (upstream marks loop back edges), so halts stay prompt. + if (check_interrupt) { + as->Cmpb({.base = as->rbp, .disp = interrupted_offset()}, int8_t{0}); + as->Jcc32(x86_64::Assembler::Condition::kNotEqual, reinterpret_cast(active->arena.base)); + } + emit_return_prefetch(as, pc, false); + // To the dispatcher stub at the start of the arena; install() and + // publish() point the jump at the target's code. + as->Jmp32(reinterpret_cast(active->arena.base)); + emitted_sites.push_back({as->pc() - 4, pc}); +} +void EmitIndirectDispatch(x86_64::Assembler* as, x86_64::Assembler::Register target) { + if (!active->chain) { EmitExitGeneratedCode(as, target); return; } + // Inline lookup in this engine's direct-mapped table of final blocks; + // misses, blocks still counting toward optimization and interrupts take + // the full dispatcher. RCX and RDX are free at region exits. + if (target != as->rax) as->Movq(as->rax, target); + as->Movq({.base = as->rbp, .disp = int32_t(offsetof(ThreadState, cpu.insn_addr))}, as->rax); + as->Cmpb({.base = as->rbp, .disp = interrupted_offset()}, int8_t{0}); + as->Jcc32(x86_64::Assembler::Condition::kNotEqual, reinterpret_cast(active->arena.base)); + emit_return_prefetch(as, 0, true); + // The index times sizeof(FinalEntry) is the address's index bits times four. + as->Movl(as->rcx, as->rax); + as->Andl(as->rcx, int32_t(active->cache_mask << 2)); + as->Movq(as->rdx, reinterpret_cast(active->final_cache.data())); + as->Cmpq(as->rax, {.base = as->rdx, .index = as->rcx, .scale = x86_64::Assembler::kTimesFour}); + as->Jcc32(x86_64::Assembler::Condition::kNotEqual, reinterpret_cast(active->arena.base)); + as->Jmpq({.base = as->rdx, .index = as->rcx, .scale = x86_64::Assembler::kTimesFour, + .disp = int32_t(offsetof(FinalEntry, code))}); +} +void EmitAllocStackFrame(x86_64::Assembler* as, uint32_t bytes) { as->Subq(as->rsp, int32_t(bytes)); } +void EmitFreeStackFrame(x86_64::Assembler* as, uint32_t bytes) { as->Addq(as->rsp, int32_t(bytes)); } +namespace host_platform { +const PlatformCapabilities kPlatformCapabilities = [] { + PlatformCapabilities caps{}; + int leaf[4]{}; + __cpuid(leaf, 0); + const unsigned max_basic = unsigned(leaf[0]); + caps.kIsAuthenticAMD = + (leaf[1] == 0x68747541 && leaf[3] == 0x69746e65 && leaf[2] == 0x444d4163) || + (leaf[1] == 0x6f677948 && leaf[3] == 0x6e65476e && leaf[2] == 0x656e6975); + __cpuid(leaf, 1); + unsigned family = (unsigned(leaf[0]) >> 8) & 15; + if (family == 15) family += (unsigned(leaf[0]) >> 20) & 255; + caps.kHasAES = (leaf[2] & (1 << 25)) != 0; + caps.kHasCLMUL = (leaf[2] & 2) != 0; + caps.kHasSSE3 = (leaf[2] & 1) != 0; + caps.kHasSSSE3 = (leaf[2] & (1 << 9)) != 0; + caps.kHasSSE4_1 = (leaf[2] & (1 << 19)) != 0; + caps.kHasSSE4_2 = (leaf[2] & (1 << 20)) != 0; + caps.kHasPOPCNT = (leaf[2] & (1 << 23)) != 0; + // VEX instructions require both CPU support and Windows saving the AVX + // register state. Upper YMM halves are volatile under the Windows ABI; + // entry.asm preserves the nonvolatile lower XMM halves. + const bool avx = (leaf[2] & (1 << 27)) && (leaf[2] & (1 << 28)) && + (GetEnabledXStateFeatures() & XSTATE_MASK_AVX) && + !GetEnvironmentVariableA("AXRB_DIGITALIS_DISABLE_AVX", nullptr, 0); + caps.kHasAVX = avx; + caps.kHasFMA = avx && (leaf[2] & (1 << 12)); + caps.kHasF16C = avx && (leaf[2] & (1 << 29)); + if (!GetEnvironmentVariableA("AXRB_DIGITALIS_DISABLE_EXTENDED_CPU", nullptr, 0)) { + if (max_basic >= 7) { + __cpuidex(leaf, 7, 0); + caps.kHasAVX2 = avx && (leaf[1] & (1 << 5)); + caps.kHasBMI = (leaf[1] & (1 << 3)) != 0; + caps.kHasBMI2 = (leaf[1] & (1 << 8)) != 0; + // Match upstream's tuning: early Zen implements PDEP/PEXT slowly. + caps.kHasPDEP = caps.kHasBMI2 && (!caps.kIsAuthenticAMD || family >= 0x19); + caps.kHasSHA = (leaf[1] & (1 << 29)) != 0; + } + __cpuid(leaf, int(0x80000000u)); + if (unsigned(leaf[0]) >= 0x80000001u) { + __cpuid(leaf, int(0x80000001u)); + caps.kHasLZCNT = (leaf[2] & (1 << 5)) != 0; + caps.kHasSSE4a = (leaf[2] & (1 << 6)) != 0; + caps.kHasFMA4 = avx && (leaf[2] & (1 << 16)); + } + } + return caps; +}(); +} +} diff --git a/direct/digitalis/entry.asm b/direct/digitalis/entry.asm new file mode 100644 index 0000000..6d0d0f2 --- /dev/null +++ b/direct/digitalis/entry.asm @@ -0,0 +1,119 @@ +; Windows x64 ABI boundary for Digitalis's private generated-code convention. +; RBP points at ThreadState; [RSP] is the translator's eight-byte scratch area. +; Generated regions contain no native calls and jump to digitalis_leave. +PUBLIC digitalis_enter +PUBLIC digitalis_leave +PUBLIC digitalis_dispatch +.code +digitalis_enter PROC FRAME + push rbx + .pushreg rbx + push rbp + .pushreg rbp + push rdi + .pushreg rdi + push rsi + .pushreg rsi + push r12 + .pushreg r12 + push r13 + .pushreg r13 + push r14 + .pushreg r14 + push r15 + .pushreg r15 + sub rsp, 184 + .allocstack 184 + movdqu [rsp+16], xmm6 + .savexmm128 xmm6, 16 + movdqu [rsp+32], xmm7 + .savexmm128 xmm7, 32 + movdqu [rsp+48], xmm8 + .savexmm128 xmm8, 48 + movdqu [rsp+64], xmm9 + .savexmm128 xmm9, 64 + movdqu [rsp+80], xmm10 + .savexmm128 xmm10, 80 + movdqu [rsp+96], xmm11 + .savexmm128 xmm11, 96 + movdqu [rsp+112], xmm12 + .savexmm128 xmm12, 112 + movdqu [rsp+128], xmm13 + .savexmm128 xmm13, 128 + movdqu [rsp+144], xmm14 + .savexmm128 xmm14, 144 + movdqu [rsp+160], xmm15 + .savexmm128 xmm15, 160 + .endprolog + mov [rsp+8], r8 + mov rbp, rcx + jmp rdx +; Chain cached regions without saving/restoring the Windows ABI registers or +; returning through a C++ hash lookup for every guest branch. All guest +; registers are already spilled by the region exit. The shared stack scratch +; is [rsp+0..7]; the dispatch context lives in the separate [rsp+8..15] slot. +digitalis_dispatch LABEL NEAR + mov r10, [rsp+8] + inc dword ptr [r10+28] + dec dword ptr [r10+24] + jz digitalis_leave + ; Optimized loops exit here when their budget runs out; an interrupt + ; sets it to one, so refill it and let the interrupt check decide. + mov rcx, [r10+32] + cmp dword ptr [rcx], 0 + jne digitalis_budget_ok + mov dword ptr [rcx], 4096 +digitalis_budget_ok LABEL NEAR + mov rcx, [r10+16] + cmp dword ptr [rcx], 0 + jne digitalis_leave + mov rcx, [r10+8] + cmp byte ptr [rcx], 0 + jne digitalis_leave + mov rax, [rbp+784] + ; Final blocks first (16-byte entries), then those still counting (32-byte). + mov ecx, eax + and ecx, dword ptr [r10+40] + mov rdx, [r10+48] + cmp [rdx+rcx*4], rax + jne digitalis_counting + jmp qword ptr [rdx+rcx*4+8] +digitalis_counting LABEL NEAR + shl rcx, 3 + add rcx, [r10] + cmp [rcx], rax + jne digitalis_leave + mov rdx, [rcx+8] + test rdx, rdx + jz digitalis_leave + mov r8, [rcx+16] + cmp dword ptr [r8], -1 + je digitalis_chain + cmp dword ptr [r8], 0 + je digitalis_leave + dec dword ptr [r8] +digitalis_chain LABEL NEAR + jmp rdx +digitalis_leave LABEL NEAR + movdqu xmm6, [rsp+16] + movdqu xmm7, [rsp+32] + movdqu xmm8, [rsp+48] + movdqu xmm9, [rsp+64] + movdqu xmm10, [rsp+80] + movdqu xmm11, [rsp+96] + movdqu xmm12, [rsp+112] + movdqu xmm13, [rsp+128] + movdqu xmm14, [rsp+144] + movdqu xmm15, [rsp+160] + add rsp, 184 + pop r15 + pop r14 + pop r13 + pop r12 + pop rsi + pop rdi + pop rbp + pop rbx + ret +digitalis_enter ENDP +END diff --git a/direct/digitalis/selftest.cpp b/direct/digitalis/selftest.cpp new file mode 100644 index 0000000..ace9970 --- /dev/null +++ b/direct/digitalis/selftest.cpp @@ -0,0 +1,234 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include "digitalis.h" +using namespace axrb::digitalis; +extern "C" float __extendhfsf2(_Float16); +extern "C" _Float16 __truncsfhf2(float); +extern "C" __uint128_t __udivti3(__uint128_t, __uint128_t); +static uint64_t reads = 0; +static bool read(void*, uint64_t address, void* out, size_t bytes) { + ++reads; + __try { std::memcpy(out, reinterpret_cast(address), bytes); return true; } + __except(EXCEPTION_EXECUTE_HANDLER) { return false; } +} +static bool write(void*, uint64_t address, const void* data, size_t bytes) { + __try { std::memcpy(reinterpret_cast(address), data, bytes); return true; } + __except(EXCEPTION_EXECUTE_HANDLER) { return false; } +} +int main() try { + for (unsigned bits = 0; bits < 65536; ++bits) { + uint16_t original = static_cast(bits), converted; + _Float16 half; std::memcpy(&half, &original, 2); + half = __truncsfhf2(__extendhfsf2(half)); + std::memcpy(&converted, &half, 2); + const bool nan = (original & 0x7c00) == 0x7c00 && (original & 1023); + assert(converted == (nan ? (original | 0x200) : original)); + } + const __uint128_t high = __uint128_t{1} << 127; + assert(__udivti3(high + 123, high) == 1); + assert(__udivti3(~__uint128_t{0}, 3) == (~__uint128_t{0} / uint64_t{3})); + assert(__udivti3(high, 2) == (high >> 1)); + auto* engine = create({nullptr, read, write}); + alignas(4096) std::array code{}; + Registers registers; + auto execute = [&](std::initializer_list instructions) { + if (GetEnvironmentVariableA("AXRB_DIGITALIS_DUMP_IR", nullptr, 0)) + std::fprintf(stderr, "SELFTEST first=%08x\n", *instructions.begin()); + clear_cache(engine); + code.fill(0xd4000041); // svc #2 + std::copy(instructions.begin(), instructions.end(), code.begin()); + registers.pc = reinterpret_cast(code.data()); + return run(engine, ®isters, 100); + }; + auto result = execute({0xd2800500, 0x91000800}); // mov x0,40; add x0,x0,2 + assert(result.reason == Stop::Svc && result.immediate == 2 && registers.x[0] == 42); + assert(translated_blocks(engine)); + registers.x[0] = UINT64_MAX; + registers.x[1] = 1; + result = execute({0xab010002}); // adds x2,x0,x1 + assert(result.reason == Stop::Svc && registers.x[2] == 0 && registers.pstate == 0x60000000); + registers.v[1][0] = 10; registers.v[1][1] = 20; + registers.v[2][0] = 3; registers.v[2][1] = 7; + result = execute({0x4ee28420}); // add v0.2d,v1.2d,v2.2d + assert(result.reason == Stop::Svc && registers.v[0][0] == 13 && registers.v[0][1] == 27); + registers.fpsr = 0; + registers.v[1][0] = 0x3f800000; registers.v[2][0] = 0; + result = execute({0x1e221820}); // fdiv s0,s1,s2 (1 / 0) + assert(result.reason == Stop::Svc && (registers.fpsr & 2)); + result = execute({0xd53b4420}); // mrs x0,fpsr, in a separate execution batch + assert(result.reason == Stop::Svc && (registers.x[0] & 2)); + // Fused arithmetic must retain single-rounding semantics and ARM signs. + registers.v[1][0] = 0x3f800001; registers.v[2][0] = 0x3f7ffffe; + registers.v[3][0] = 0xbf800000; + result = execute({0x1f020c20}); // fmadd s0,s1,s2,s3 + assert(result.reason == Stop::Svc && registers.v[0][0] == 0xa8800000); + registers.v[1][0] = 0x40000000; registers.v[2][0] = 0x40400000; + registers.v[3][0] = 0x40800000; + for (auto [instruction, expected] : std::array, 4>{{ + {0x1f020c20, 0x41200000}, {0x1f028c20, 0xc0000000}, + {0x1f220c20, 0xc1200000}, {0x1f228c20, 0x40000000}}}) { + result = execute({instruction}); + assert(result.reason == Stop::Svc && registers.v[0][0] == expected); + assert(registers.v[0][1] == 0); + } + registers.v[0][0] = registers.v[0][1] = 0x4008000000000000; // 3.0 + registers.v[1][0] = registers.v[1][1] = 0x3ff8000000000000; // 1.5 + registers.v[2][0] = registers.v[2][1] = 0x4000000000000000; // 2.0 + result = execute({0x4e62cc20}); // fmla v0.2d,v1.2d,v2.2d + assert(result.reason == Stop::Svc && registers.v[0][0] == 0x4018000000000000); + assert(registers.v[0][1] == 0x4018000000000000); + registers.v[1][0] = 0x4000; registers.v[2][0] = 0x4200; registers.v[3][0] = 0x4400; + result = execute({0x1fc20c20}); // fmadd h0,h1,h2,h3: 2*3+4 = 10 + assert(result.reason == Stop::Svc && registers.v[0][0] == 0x4900 && !registers.v[0][1]); + registers.x[0] = 0x1234; + registers.x[1] = 0x9988; + result = execute({0xf9400001}); // ldr x1,[x0] + assert(result.reason == Stop::MemoryFault && result.address == 0x1234); + assert(registers.pc == reinterpret_cast(code.data())); + assert(registers.x[1] == 0x9988); + result = execute({0xffffffff}); + assert(result.reason == Stop::Undefined); + result = execute({0xd4200000}); + assert(result.reason == Stop::Breakpoint); + { + // b . : chained code runs until interrupted (unchained code also + // yields at the execution budget). + std::thread stopper([&] { std::this_thread::sleep_for(std::chrono::milliseconds(5)); interrupt(engine); }); + result = execute({0x14000000}); + stopper.join(); + assert(result.reason == Stop::Yield && registers.pc == reinterpret_cast(code.data())); + } + interrupt(engine); + assert(run(engine, ®isters, 100).reason == Stop::Yield); + const auto started = std::chrono::steady_clock::now(); + std::thread kicker([&] { std::this_thread::sleep_for(std::chrono::milliseconds(5)); interrupt(engine); }); + result = run(engine, ®isters, UINT32_MAX); + kicker.join(); + assert(result.reason == Stop::Yield); + assert(std::chrono::steady_clock::now() - started < std::chrono::seconds(2)); + // Drop any interrupt still pending before starting the next program. + clear_interrupt(engine); + // Reusing the same guest address after invalidation must execute fresh code. + result = execute({0xd28000e0}); // mov x0,7 + assert(result.reason == Stop::Svc && registers.x[0] == 7); + alignas(4096) std::array other{}; + other[0] = 0xd28000e0; other[1] = 0xd4000041; + registers.pc = reinterpret_cast(other.data()); + assert(run(engine, ®isters, 100).reason == Stop::Svc); + const auto compiled = translated_blocks(engine); + code[0] = 0xd2800120; // mov x0,9 + invalidate_cache(engine, reinterpret_cast(code.data()) + 2, 1); + registers.pc = reinterpret_cast(code.data()); + assert(run(engine, ®isters, 100).reason == Stop::Svc && registers.x[0] == 9); + registers.pc = reinterpret_cast(other.data()); + assert(run(engine, ®isters, 100).reason == Stop::Svc && registers.x[0] == 7); + assert(translated_blocks(engine) == compiled + 1); // unrelated cached block survives + // A linked caller survives invalidation of its target on another page. + // Both cold dispatch and a previously cached target must refresh. + const auto target = reinterpret_cast(other.data()); + const auto caller = reinterpret_cast(code.data()); + code[0] = 0x14000000 | (uint32_t((int64_t(target) - int64_t(caller)) / 4) & 0x03ffffff); + invalidate_cache(engine, caller, 4); + for (uint32_t value : {11u, 13u}) { + other[0] = 0xd2800000 | (value << 5); // mov x0,value + invalidate_cache(engine, target, 4); + for (unsigned repeat = 0; repeat != 3; ++repeat) { + registers.pc = caller; + result = run(engine, ®isters, 100); + assert(result.reason == Stop::Svc && registers.x[0] == value); + } + } + result = execute({0xd4000041}); // svc #2 + assert(result.reason == Stop::Svc && result.immediate == 2); + const auto fetched = reads; + registers.pc = reinterpret_cast(code.data()); + result = run(engine, ®isters, 100); + assert(result.reason == Stop::Svc && result.immediate == 2 && reads == fetched); + code[0] = 0xd40000e1; // svc #7 after explicit instruction-cache invalidation + invalidate_cache(engine, reinterpret_cast(code.data()), 4); + registers.pc = reinterpret_cast(code.data()); + result = run(engine, ®isters, 100); + assert(result.reason == Stop::Svc && result.immediate == 7 && reads > fetched); + result = execute({0xd503201f, 0xd4000041}); // nop; svc #2 translated together + assert(result.reason == Stop::Svc && result.immediate == 2); + code[1] = 0xd40000e1; + invalidate_cache(engine, reinterpret_cast(code.data()) + 4, 4); + registers.pc = reinterpret_cast(code.data()); + result = run(engine, ®isters, 100); + assert(result.reason == Stop::Svc && result.immediate == 7); + { + // Fast syscalls complete inside translated code; declined ones exit + // with the SVC as before. Indirect branches reuse cached targets. + Callbacks callbacks{nullptr, read, write}; + callbacks.syscall = [](void*, uint64_t* x) { + if (x[8] != 999) return false; + x[0] = 123; + return true; + }; + auto* fast = create(callbacks); + alignas(4096) std::array program{}; + program.fill(0xd4000041); // svc #2 + program[0] = 0xd2807ce8; // mov x8,#999 + program[1] = 0xd4000001; // svc #0 + program[2] = 0xd4000041; // svc #2 + Registers state{}; + for (int repeat = 0; repeat != 3; ++repeat) { + state = {}; + state.pc = reinterpret_cast(program.data()); + result = run(fast, &state, 100); + assert(result.reason == Stop::Svc && result.immediate == 2 && state.x[0] == 123); + } + program[0] = 0xd2807d08; // mov x8,#1000: declined + invalidate_cache(fast, reinterpret_cast(program.data()), 4); + state = {}; + state.pc = reinterpret_cast(program.data()); + result = run(fast, &state, 100); + assert(result.reason == Stop::Svc && result.immediate == 0 && state.x[0] == 0); + assert(state.pc == reinterpret_cast(program.data() + 2)); + program[0] = 0xd61f0020; // br x1 + program[64] = 0xd28000a0; // mov x0,#5 + program[65] = 0xd4000041; // svc #2 + invalidate_cache(fast, reinterpret_cast(program.data()), 4096); + for (int repeat = 0; repeat != 3; ++repeat) { + state = {}; + state.pc = reinterpret_cast(program.data()); + state.x[1] = reinterpret_cast(program.data() + 64); + result = run(fast, &state, 100); + assert(result.reason == Stop::Svc && result.immediate == 2 && state.x[0] == 5); + } + destroy(fast); + } + // Exercise many independent call targets, then invalidate one code page + // while the other targets remain cached. + clear_cache(engine); + auto* targets = static_cast(VirtualAlloc(nullptr, 65536, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + assert(targets); + for (uint32_t index = 0; index < 1200; ++index) { + targets[index * 4] = 0xd2800000 | (index << 5); + targets[index * 4 + 1] = 0xd4000041; + } + for (unsigned pass = 0; pass != 2; ++pass) { + for (uint32_t index = 0; index < 1200; ++index) { + registers.pc = reinterpret_cast(targets + index * 4); + result = run(engine, ®isters, 100); + assert(result.reason == Stop::Svc && registers.x[0] == index); + } + targets[0] = 0xd2800000; + invalidate_cache(engine, reinterpret_cast(targets), 4); + } + clear_cache(engine); + VirtualFree(targets, 0, MEM_RELEASE); + std::printf("DIGITALIS_WINDOWS_SELFTEST_PASS jit=%llu interpreted=%llu\n", + (unsigned long long)translated_blocks(engine), (unsigned long long)interpreted_instructions(engine)); + destroy(engine); +} catch (const std::exception& error) { + std::fprintf(stderr, "DIGITALIS_WINDOWS_SELFTEST_ERROR: %s\n", error.what()); + return 1; +} diff --git a/direct/guest/CMakeLists.txt b/direct/guest/CMakeLists.txt new file mode 100644 index 0000000..4d937e3 --- /dev/null +++ b/direct/guest/CMakeLists.txt @@ -0,0 +1,51 @@ +# ARM64 Android binaries that run inside the guest. They are built with the +# NDK's clang and placed next to axrb-direct.exe under guest/. + +set(GUEST_OUT "${CMAKE_BINARY_DIR}/guest") +set(GUEST_CLANG "${NDK_BIN}/clang.exe") +set(GUEST_CLANGXX "${NDK_BIN}/clang++.exe") +set(GUEST_TARGET --target=aarch64-linux-android29) +file(MAKE_DIRECTORY "${GUEST_OUT}") + +set(_guest_outputs) +# axrb_guest( KIND exe|lib SOURCES ... [FLAGS ...]) +function(axrb_guest name) + cmake_parse_arguments(G "" "KIND" "SOURCES;FLAGS;DEPENDS" ${ARGN}) + set(out "${GUEST_OUT}/${name}") + if(G_KIND STREQUAL "lib") + get_filename_component(soname "${name}" NAME) + set(kind_flags -fPIC -shared -Wl,-soname,${soname}) + else() + set(kind_flags -fPIE -pie) + endif() + set(compiler "${GUEST_CLANGXX}") + list(GET G_SOURCES 0 first_source) + if(first_source MATCHES "\.c$") + set(compiler "${GUEST_CLANG}") + endif() + add_custom_command(OUTPUT "${out}" + COMMAND "${compiler}" ${GUEST_TARGET} -O2 -Wall ${kind_flags} ${G_SOURCES} ${G_FLAGS} -o "${out}" + DEPENDS ${G_SOURCES} ${G_DEPENDS} "${CMAKE_CURRENT_SOURCE_DIR}/axrb_guest.h" VERBATIM) + set(_guest_outputs ${_guest_outputs} "${out}" PARENT_SCOPE) +endfunction() + +axrb_guest(hello KIND exe SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/hello.c" FLAGS -static-libstdc++) +axrb_guest(selftest KIND exe SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/selftest.cpp" FLAGS -static-libstdc++) + +axrb_guest(axrb_app KIND exe SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/axrb_app.c") + +# NDK libraries provided in place of the system ones (guest /apex/com.axrb.direct/lib64). +file(MAKE_DIRECTORY "${GUEST_OUT}/lib64") +set(P "${CMAKE_CURRENT_SOURCE_DIR}/proxy") +axrb_guest(lib64/libandroid.so KIND lib SOURCES "${P}/libandroid.c" "${P}/native_activity.c") +axrb_guest(lib64/libEGL.so KIND lib SOURCES "${P}/libEGL.c") +axrb_guest(lib64/libGLESv1_CM.so KIND lib SOURCES "${P}/libGLES.c") +axrb_guest(lib64/libGLESv2.so KIND lib SOURCES "${P}/libGLES.c") +axrb_guest(lib64/libGLESv3.so KIND lib SOURCES "${P}/libGLES.c") +axrb_guest(lib64/libOpenSLES.so KIND lib SOURCES "${P}/libOpenSLES.c" + DEPENDS "${P}/opensles_output.inc" FLAGS -laaudio -lm) +axrb_guest(lib64/libaaudio.so KIND lib SOURCES "${P}/libaaudio.c") +axrb_guest(lib64/libvulkan.so KIND lib SOURCES "${P}/libvulkan.cpp" + FLAGS -std=c++17 -fno-exceptions -fno-rtti -nostdlib++ -fvisibility=default) + +add_custom_target(axrb_guest ALL DEPENDS ${_guest_outputs}) diff --git a/direct/guest/axrb_app.c b/direct/guest/axrb_app.c new file mode 100644 index 0000000..97a559e --- /dev/null +++ b/direct/guest/axrb_app.c @@ -0,0 +1,77 @@ +// The guest-side launcher of an Android application under AXRB Direct. +// +// It is the process's main program (in place of app_process): the Android +// linker and bionic start it normally, then it hands the host the few guest +// facilities only guest code can provide -- the dynamic linker's dlopen family +// and pthread_create -- and calls into the host, which runs the Java side of +// the app on this thread (the app's main thread) and never returns. +#include +#include +#include +#include +#include +#include + +#include "axrb_guest.h" + +struct axrb_guest_api { + uint32_t version; + uint32_t size; + void* (*dlopen)(const char*, int); + void* (*dlsym)(void*, const char*); + char* (*dlerror)(void); + int (*dlclose)(void*); + void* (*android_dlopen_ext)(const char*, int, const android_dlextinfo*); + int (*spawn_thread)(void* cookie, size_t stack_size); + void* (*malloc)(size_t); + void (*free)(void*); + int (*dladdr)(const void*, Dl_info*); +}; + +// Host side (direct/src/app.cpp). +extern int axrb_app_main(const struct axrb_guest_api* api, int argc, char** argv); +extern void axrb_thread_main(void* cookie); +AXRB_HOST_FUNCTION(axrb_app_main, "axrb.app_main"); +AXRB_HOST_FUNCTION(axrb_thread_main, "axrb.thread_main"); + +static void* thread_start(void* cookie) { + axrb_thread_main(cookie); + return NULL; +} + +// Java threads: a detached pthread whose body is the host's thread entry. +static int spawn_thread(void* cookie, size_t stack_size) { + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_attr_setstacksize(&attr, stack_size ? stack_size : 1024 * 1024); + pthread_t thread; + int r = pthread_create(&thread, &attr, thread_start, cookie); + pthread_attr_destroy(&attr); + return r; +} + +// dlopen/dlsym resolve the caller's linker namespace from the return address, +// so these wrappers make the launcher (default namespace) the caller. +static void* app_dlopen(const char* path, int flags) { return dlopen(path, flags); } +static void* app_dlsym(void* handle, const char* name) { return dlsym(handle, name); } +static void* app_dlopen_ext(const char* path, int flags, const android_dlextinfo* info) { + return android_dlopen_ext(path, flags, info); +} + +int main(int argc, char** argv) { + static const struct axrb_guest_api api = { + .version = 1, + .size = sizeof(struct axrb_guest_api), + .dlopen = app_dlopen, + .dlsym = app_dlsym, + .dlerror = dlerror, + .dlclose = dlclose, + .android_dlopen_ext = app_dlopen_ext, + .spawn_thread = spawn_thread, + .malloc = malloc, + .free = free, + .dladdr = dladdr, + }; + return axrb_app_main(&api, argc, argv); +} diff --git a/direct/guest/axrb_guest.h b/direct/guest/axrb_guest.h new file mode 100644 index 0000000..21721f7 --- /dev/null +++ b/direct/guest/axrb_guest.h @@ -0,0 +1,32 @@ +// Calling AXRB Direct's host from guest code. +// +// AXRB_HOST_FUNCTION(symbol, "host.name") defines a guest function `symbol` +// that traps to the host function registered under "host.name" (svc #3 with +// the name in x17). Arguments and results follow AAPCS64 unchanged: the host +// reads x0-x7/v0-v7/the stack and writes x0/v0, so a stub can stand in for any +// C function signature, variadic ones included. +#pragma once + +#define AXRB_STR2(x) #x +#define AXRB_STR(x) AXRB_STR2(x) + +#if defined(__x86_64__) +// Experimental ART platform bridge only: at most five integer/pointer args. +// General NDK signatures need typed wrappers; this is not a universal ABI shim. +#define AXRB_HOST_FUNCTION(symbol, host_name) \ + __asm__(".text\n.globl " #symbol "\n.type " #symbol ", @function\n" #symbol ":\n" \ + "mov %rcx, %r10\nlea 1f(%rip), %r9\nmov $0x41585244, %eax\nsyscall\nret\n" \ + ".size " #symbol ", . - " #symbol "\n.pushsection .rodata\n1: .asciz \"" host_name "\"\n.popsection\n") +#else +#define AXRB_HOST_FUNCTION(symbol, host_name) \ + __asm__(".text\n" \ + ".balign 4\n" \ + ".globl " #symbol "\n" \ + ".type " #symbol ", %function\n" #symbol ":\n" \ + " adr x17, 1f\n" \ + " svc #3\n" \ + " ret\n" \ + "1: .asciz \"" host_name "\"\n" \ + " .balign 4\n" \ + ".size " #symbol ", . - " #symbol "\n") +#endif diff --git a/direct/guest/hello.c b/direct/guest/hello.c new file mode 100644 index 0000000..af557d2 --- /dev/null +++ b/direct/guest/hello.c @@ -0,0 +1,15 @@ +// The smallest end-to-end check: the kernel-style exec of linker64, bionic's +// own startup, stdio and the exit path. +#include +#include +#include +#include + +int main(int argc, char** argv) { + printf("hello from arm64 bionic, pid %d, argc %d, argv[0] %s\n", getpid(), argc, argv[0]); + char* p = (char*)malloc(1 << 20); + memset(p, 0x5a, 1 << 20); + printf("malloc ok: %p %d\n", (void*)p, p[12345]); + free(p); + return 42; +} diff --git a/direct/guest/proxy/gles_stubs.inc b/direct/guest/proxy/gles_stubs.inc new file mode 100644 index 0000000..f5bfe08 --- /dev/null +++ b/direct/guest/proxy/gles_stubs.inc @@ -0,0 +1,121 @@ +// The rest of the GLES 1.x/2.0/3.2 entry points (the exports of the NDK +// libGLESv1_CM/v2/v3 stubs, API 29) as no-ops; see libGLES.c. +NOP(glActiveShaderProgram) NOP(glAlphaFunc) NOP(glAlphaFuncx) NOP(glAlphaFuncxOES) NOP(glBeginPerfMonitorAMD) +NOP(glBeginQuery) NOP(glBeginTransformFeedback) NOP(glBindAttribLocation) NOP(glBindBufferBase) NOP(glBindBufferRange) +NOP(glBindFramebufferOES) NOP(glBindImageTexture) NOP(glBindProgramPipeline) NOP(glBindRenderbuffer) NOP(glBindRenderbufferOES) +NOP(glBindSampler) NOP(glBindTransformFeedback) NOP(glBindVertexArrayOES) NOP(glBindVertexBuffer) NOP(glBlendBarrier) +NOP(glBlendBarrierKHR) NOP(glBlendColor) NOP(glBlendEquation) NOP(glBlendEquationOES) NOP(glBlendEquationSeparate) +NOP(glBlendEquationSeparateOES) NOP(glBlendEquationSeparatei) NOP(glBlendEquationSeparateiEXT) NOP(glBlendEquationi) +NOP(glBlendEquationiEXT) NOP(glBlendFunc) NOP(glBlendFuncSeparate) NOP(glBlendFuncSeparateOES) NOP(glBlendFuncSeparatei) +NOP(glBlendFuncSeparateiEXT) NOP(glBlendFunci) NOP(glBlendFunciEXT) NOP(glBufferStorageEXT) NOP(glBufferStorageExternalEXT) +NOP(glBufferSubData) NOP(glCheckFramebufferStatusOES) NOP(glClearBufferfi) NOP(glClearBufferfv) NOP(glClearBufferiv) +NOP(glClearBufferuiv) NOP(glClearColorx) NOP(glClearColorxOES) NOP(glClearDepthf) NOP(glClearDepthfOES) +NOP(glClearDepthx) NOP(glClearDepthxOES) NOP(glClearStencil) NOP(glClientActiveTexture) NOP(glClipPlanef) +NOP(glClipPlanefIMG) NOP(glClipPlanefOES) NOP(glClipPlanex) NOP(glClipPlanexIMG) NOP(glClipPlanexOES) +NOP(glColor4f) NOP(glColor4ub) NOP(glColor4x) NOP(glColor4xOES) NOP(glColorMaski) NOP(glColorMaskiEXT) +NOP(glColorPointer) NOP(glColorPointerBounds) NOP(glCompressedTexImage2D) NOP(glCompressedTexImage3D) +NOP(glCompressedTexImage3DOES) NOP(glCompressedTexSubImage2D) NOP(glCompressedTexSubImage3D) NOP(glCompressedTexSubImage3DOES) +NOP(glCopyBufferSubData) NOP(glCopyImageSubData) NOP(glCopyImageSubDataEXT) NOP(glCopyTexImage2D) NOP(glCopyTexSubImage2D) +NOP(glCopyTexSubImage3D) NOP(glCopyTexSubImage3DOES) NOP(glCoverageMaskNV) NOP(glCoverageOperationNV) +NOP(glCreateShaderProgramv) NOP(glCullFace) NOP(glCurrentPaletteMatrixOES) NOP(glDebugMessageCallback) +NOP(glDebugMessageCallbackKHR) NOP(glDebugMessageControl) NOP(glDebugMessageControlKHR) NOP(glDebugMessageInsert) +NOP(glDebugMessageInsertKHR) NOP(glDeleteFencesNV) NOP(glDeleteFramebuffersOES) NOP(glDeletePerfMonitorsAMD) +NOP(glDeleteProgramPipelines) NOP(glDeleteQueries) NOP(glDeleteRenderbuffers) NOP(glDeleteRenderbuffersOES) +NOP(glDeleteSamplers) NOP(glDeleteTransformFeedbacks) NOP(glDeleteVertexArraysOES) NOP(glDepthFunc) NOP(glDepthRangef) +NOP(glDepthRangefOES) NOP(glDepthRangex) NOP(glDepthRangexOES) NOP(glDetachShader) NOP(glDisableClientState) +NOP(glDisableDriverControlQCOM) NOP(glDisablei) NOP(glDisableiEXT) NOP(glDiscardFramebufferEXT) NOP(glDispatchCompute) +NOP(glDispatchComputeIndirect) NOP(glDrawArraysIndirect) NOP(glDrawArraysInstanced) NOP(glDrawElementsBaseVertex) +NOP(glDrawElementsIndirect) NOP(glDrawElementsInstanced) NOP(glDrawElementsInstancedBaseVertex) NOP(glDrawRangeElements) +NOP(glDrawRangeElementsBaseVertex) NOP(glDrawTexfOES) NOP(glDrawTexfvOES) NOP(glDrawTexiOES) NOP(glDrawTexivOES) +NOP(glDrawTexsOES) NOP(glDrawTexsvOES) NOP(glDrawTexxOES) NOP(glDrawTexxvOES) NOP(glEGLImageTargetRenderbufferStorageOES) +NOP(glEGLImageTargetTexStorageEXT) NOP(glEGLImageTargetTexture2DOES) NOP(glEnableClientState) NOP(glEnableDriverControlQCOM) +NOP(glEnablei) NOP(glEnableiEXT) NOP(glEndPerfMonitorAMD) NOP(glEndQuery) NOP(glEndTilingQCOM) NOP(glEndTransformFeedback) +NOP(glExtGetBufferPointervQCOM) NOP(glExtGetBuffersQCOM) NOP(glExtGetFramebuffersQCOM) NOP(glExtGetProgramBinarySourceQCOM) +NOP(glExtGetProgramsQCOM) NOP(glExtGetRenderbuffersQCOM) NOP(glExtGetShadersQCOM) NOP(glExtGetTexLevelParameterivQCOM) +NOP(glExtGetTexSubImageQCOM) NOP(glExtGetTexturesQCOM) NOP(glExtIsProgramBinaryQCOM) NOP(glExtTexObjectStateOverrideiQCOM) +NOP(glFinishFenceNV) NOP(glFlushMappedBufferRange) NOP(glFogf) NOP(glFogfv) NOP(glFogx) NOP(glFogxOES) +NOP(glFogxv) NOP(glFogxvOES) NOP(glFramebufferParameteri) NOP(glFramebufferRenderbuffer) NOP(glFramebufferRenderbufferOES) +NOP(glFramebufferTexture) NOP(glFramebufferTexture2DMultisampleEXT) NOP(glFramebufferTexture2DMultisampleIMG) +NOP(glFramebufferTexture2DOES) NOP(glFramebufferTexture3DOES) NOP(glFramebufferTextureEXT) NOP(glFramebufferTextureMultisampleMultiviewOVR) +NOP(glFramebufferTextureMultiviewOVR) NOP(glFrontFace) NOP(glFrustumf) NOP(glFrustumfOES) NOP(glFrustumx) +NOP(glFrustumxOES) NOP(glGenFencesNV) NOP(glGenFramebuffersOES) NOP(glGenPerfMonitorsAMD) NOP(glGenProgramPipelines) +NOP(glGenQueries) NOP(glGenRenderbuffers) NOP(glGenRenderbuffersOES) NOP(glGenSamplers) NOP(glGenTransformFeedbacks) +NOP(glGenVertexArraysOES) NOP(glGenerateMipmap) NOP(glGenerateMipmapOES) NOP(glGetActiveAttrib) NOP(glGetActiveUniform) +NOP(glGetActiveUniformBlockName) NOP(glGetActiveUniformBlockiv) NOP(glGetActiveUniformsiv) NOP(glGetAttachedShaders) +NOP(glGetBooleani_v) NOP(glGetBooleanv) NOP(glGetBufferParameteri64v) NOP(glGetBufferParameteriv) NOP(glGetBufferPointerv) +NOP(glGetBufferPointervOES) NOP(glGetClipPlanef) NOP(glGetClipPlanefOES) NOP(glGetClipPlanex) NOP(glGetClipPlanexOES) +NOP(glGetDebugMessageLog) NOP(glGetDebugMessageLogKHR) NOP(glGetDriverControlStringQCOM) NOP(glGetDriverControlsQCOM) +NOP(glGetFenceivNV) NOP(glGetFixedv) NOP(glGetFixedvOES) NOP(glGetFloatv) NOP(glGetFragDataLocation) NOP(glGetFramebufferAttachmentParameteriv) +NOP(glGetFramebufferAttachmentParameterivOES) NOP(glGetFramebufferParameteriv) NOP(glGetGraphicsResetStatus) +NOP(glGetInteger64i_v) NOP(glGetInteger64v) NOP(glGetIntegeri_v) NOP(glGetInternalformativ) NOP(glGetLightfv) +NOP(glGetLightxv) NOP(glGetLightxvOES) NOP(glGetMaterialfv) NOP(glGetMaterialxv) NOP(glGetMaterialxvOES) +NOP(glGetMultisamplefv) NOP(glGetObjectLabel) NOP(glGetObjectLabelKHR) NOP(glGetObjectPtrLabel) NOP(glGetObjectPtrLabelKHR) +NOP(glGetPerfMonitorCounterDataAMD) NOP(glGetPerfMonitorCounterInfoAMD) NOP(glGetPerfMonitorCounterStringAMD) +NOP(glGetPerfMonitorCountersAMD) NOP(glGetPerfMonitorGroupStringAMD) NOP(glGetPerfMonitorGroupsAMD) NOP(glGetPointerv) +NOP(glGetPointervKHR) NOP(glGetProgramBinary) NOP(glGetProgramBinaryOES) NOP(glGetProgramInterfaceiv) +NOP(glGetProgramPipelineInfoLog) NOP(glGetProgramPipelineiv) NOP(glGetProgramResourceIndex) NOP(glGetProgramResourceLocation) +NOP(glGetProgramResourceName) NOP(glGetProgramResourceiv) NOP(glGetQueryObjectuiv) NOP(glGetQueryiv) NOP(glGetRenderbufferParameteriv) +NOP(glGetRenderbufferParameterivOES) NOP(glGetSamplerParameterIiv) NOP(glGetSamplerParameterIivEXT) NOP(glGetSamplerParameterIuiv) +NOP(glGetSamplerParameterIuivEXT) NOP(glGetSamplerParameterfv) NOP(glGetSamplerParameteriv) NOP(glGetShaderPrecisionFormat) +NOP(glGetShaderSource) NOP(glGetSynciv) NOP(glGetTexEnvfv) NOP(glGetTexEnviv) NOP(glGetTexEnvxv) NOP(glGetTexEnvxvOES) +NOP(glGetTexGenfvOES) NOP(glGetTexGenivOES) NOP(glGetTexGenxvOES) NOP(glGetTexLevelParameterfv) NOP(glGetTexLevelParameteriv) +NOP(glGetTexParameterIiv) NOP(glGetTexParameterIivEXT) NOP(glGetTexParameterIuiv) NOP(glGetTexParameterIuivEXT) +NOP(glGetTexParameterfv) NOP(glGetTexParameteriv) NOP(glGetTexParameterxv) NOP(glGetTexParameterxvOES) +NOP(glGetTransformFeedbackVarying) NOP(glGetUniformBlockIndex) NOP(glGetUniformIndices) NOP(glGetUniformfv) +NOP(glGetUniformiv) NOP(glGetUniformuiv) NOP(glGetVertexAttribIiv) NOP(glGetVertexAttribIuiv) NOP(glGetVertexAttribPointerv) +NOP(glGetVertexAttribfv) NOP(glGetVertexAttribiv) NOP(glGetnUniformfv) NOP(glGetnUniformiv) NOP(glGetnUniformuiv) +NOP(glHint) NOP(glInvalidateSubFramebuffer) NOP(glIsBuffer) NOP(glIsEnabled) NOP(glIsEnabledi) NOP(glIsEnablediEXT) +NOP(glIsFenceNV) NOP(glIsFramebuffer) NOP(glIsFramebufferOES) NOP(glIsProgram) NOP(glIsProgramPipeline) +NOP(glIsQuery) NOP(glIsRenderbuffer) NOP(glIsRenderbufferOES) NOP(glIsSampler) NOP(glIsShader) NOP(glIsSync) +NOP(glIsTexture) NOP(glIsTransformFeedback) NOP(glIsVertexArray) NOP(glIsVertexArrayOES) NOP(glLightModelf) +NOP(glLightModelfv) NOP(glLightModelx) NOP(glLightModelxOES) NOP(glLightModelxv) NOP(glLightModelxvOES) +NOP(glLightf) NOP(glLightfv) NOP(glLightx) NOP(glLightxOES) NOP(glLightxv) NOP(glLightxvOES) NOP(glLineWidth) +NOP(glLineWidthx) NOP(glLineWidthxOES) NOP(glLoadIdentity) NOP(glLoadMatrixf) NOP(glLoadMatrixx) NOP(glLoadMatrixxOES) +NOP(glLoadPaletteFromModelViewMatrixOES) NOP(glLogicOp) NOP(glMapBufferOES) NOP(glMapBufferRange) NOP(glMaterialf) +NOP(glMaterialfv) NOP(glMaterialx) NOP(glMaterialxOES) NOP(glMaterialxv) NOP(glMaterialxvOES) NOP(glMatrixIndexPointerOES) +NOP(glMatrixIndexPointerOESBounds) NOP(glMatrixMode) NOP(glMemoryBarrier) NOP(glMemoryBarrierByRegion) +NOP(glMinSampleShading) NOP(glMinSampleShadingOES) NOP(glMultMatrixf) NOP(glMultMatrixx) NOP(glMultMatrixxOES) +NOP(glMultiDrawArraysEXT) NOP(glMultiDrawElementsEXT) NOP(glMultiTexCoord4f) NOP(glMultiTexCoord4x) NOP(glMultiTexCoord4xOES) +NOP(glNormal3f) NOP(glNormal3x) NOP(glNormal3xOES) NOP(glNormalPointer) NOP(glNormalPointerBounds) NOP(glObjectLabel) +NOP(glObjectLabelKHR) NOP(glObjectPtrLabel) NOP(glObjectPtrLabelKHR) NOP(glOrthof) NOP(glOrthofOES) NOP(glOrthox) +NOP(glOrthoxOES) NOP(glPatchParameteri) NOP(glPatchParameteriEXT) NOP(glPauseTransformFeedback) NOP(glPointParameterf) +NOP(glPointParameterfv) NOP(glPointParameterx) NOP(glPointParameterxOES) NOP(glPointParameterxv) NOP(glPointParameterxvOES) +NOP(glPointSize) NOP(glPointSizePointerOES) NOP(glPointSizePointerOESBounds) NOP(glPointSizex) NOP(glPointSizexOES) +NOP(glPolygonOffset) NOP(glPolygonOffsetx) NOP(glPolygonOffsetxOES) NOP(glPopDebugGroup) NOP(glPopDebugGroupKHR) +NOP(glPopMatrix) NOP(glPrimitiveBoundingBox) NOP(glPrimitiveBoundingBoxEXT) NOP(glProgramBinary) NOP(glProgramBinaryOES) +NOP(glProgramParameteri) NOP(glProgramUniform1f) NOP(glProgramUniform1fv) NOP(glProgramUniform1i) NOP(glProgramUniform1iv) +NOP(glProgramUniform1ui) NOP(glProgramUniform1uiv) NOP(glProgramUniform2f) NOP(glProgramUniform2fv) NOP(glProgramUniform2i) +NOP(glProgramUniform2iv) NOP(glProgramUniform2ui) NOP(glProgramUniform2uiv) NOP(glProgramUniform3f) NOP(glProgramUniform3fv) +NOP(glProgramUniform3i) NOP(glProgramUniform3iv) NOP(glProgramUniform3ui) NOP(glProgramUniform3uiv) NOP(glProgramUniform4f) +NOP(glProgramUniform4fv) NOP(glProgramUniform4i) NOP(glProgramUniform4iv) NOP(glProgramUniform4ui) NOP(glProgramUniform4uiv) +NOP(glProgramUniformMatrix2fv) NOP(glProgramUniformMatrix2x3fv) NOP(glProgramUniformMatrix2x4fv) NOP(glProgramUniformMatrix3fv) +NOP(glProgramUniformMatrix3x2fv) NOP(glProgramUniformMatrix3x4fv) NOP(glProgramUniformMatrix4fv) NOP(glProgramUniformMatrix4x2fv) +NOP(glProgramUniformMatrix4x3fv) NOP(glPushDebugGroup) NOP(glPushDebugGroupKHR) NOP(glPushMatrix) NOP(glQueryMatrixxOES) +NOP(glReadnPixels) NOP(glReleaseShaderCompiler) NOP(glRenderbufferStorage) NOP(glRenderbufferStorageMultisample) +NOP(glRenderbufferStorageMultisampleEXT) NOP(glRenderbufferStorageMultisampleIMG) NOP(glRenderbufferStorageOES) +NOP(glResumeTransformFeedback) NOP(glRotatef) NOP(glRotatex) NOP(glRotatexOES) NOP(glSampleCoverage) NOP(glSampleCoveragex) +NOP(glSampleCoveragexOES) NOP(glSampleMaski) NOP(glSamplerParameterIiv) NOP(glSamplerParameterIivEXT) +NOP(glSamplerParameterIuiv) NOP(glSamplerParameterIuivEXT) NOP(glSamplerParameterf) NOP(glSamplerParameterfv) +NOP(glSamplerParameteri) NOP(glSamplerParameteriv) NOP(glScalef) NOP(glScalex) NOP(glScalexOES) NOP(glSelectPerfMonitorCountersAMD) +NOP(glSetFenceNV) NOP(glShadeModel) NOP(glShaderBinary) NOP(glStartTilingQCOM) NOP(glStencilFunc) NOP(glStencilFuncSeparate) +NOP(glStencilMask) NOP(glStencilMaskSeparate) NOP(glStencilOp) NOP(glStencilOpSeparate) NOP(glTestFenceNV) +NOP(glTexBuffer) NOP(glTexBufferEXT) NOP(glTexBufferRange) NOP(glTexBufferRangeEXT) NOP(glTexCoordPointer) +NOP(glTexCoordPointerBounds) NOP(glTexEnvf) NOP(glTexEnvfv) NOP(glTexEnvi) NOP(glTexEnviv) NOP(glTexEnvx) +NOP(glTexEnvxOES) NOP(glTexEnvxv) NOP(glTexEnvxvOES) NOP(glTexGenfOES) NOP(glTexGenfvOES) NOP(glTexGeniOES) +NOP(glTexGenivOES) NOP(glTexGenxOES) NOP(glTexGenxvOES) NOP(glTexImage3D) NOP(glTexImage3DOES) NOP(glTexParameterIiv) +NOP(glTexParameterIivEXT) NOP(glTexParameterIuiv) NOP(glTexParameterIuivEXT) NOP(glTexParameterf) NOP(glTexParameterfv) +NOP(glTexParameteriv) NOP(glTexParameterx) NOP(glTexParameterxOES) NOP(glTexParameterxv) NOP(glTexParameterxvOES) +NOP(glTexStorage2DMultisample) NOP(glTexStorage3DMultisample) NOP(glTexStorage3DMultisampleOES) NOP(glTexSubImage2D) +NOP(glTexSubImage3D) NOP(glTexSubImage3DOES) NOP(glTransformFeedbackVaryings) NOP(glTranslatef) NOP(glTranslatex) +NOP(glTranslatexOES) NOP(glUniform1fv) NOP(glUniform1iv) NOP(glUniform1ui) NOP(glUniform1uiv) NOP(glUniform2fv) +NOP(glUniform2i) NOP(glUniform2iv) NOP(glUniform2ui) NOP(glUniform2uiv) NOP(glUniform3f) NOP(glUniform3fv) +NOP(glUniform3i) NOP(glUniform3iv) NOP(glUniform3ui) NOP(glUniform3uiv) NOP(glUniform4i) NOP(glUniform4iv) +NOP(glUniform4ui) NOP(glUniform4uiv) NOP(glUniformBlockBinding) NOP(glUniformMatrix2fv) NOP(glUniformMatrix2x3fv) +NOP(glUniformMatrix2x4fv) NOP(glUniformMatrix3fv) NOP(glUniformMatrix3x2fv) NOP(glUniformMatrix3x4fv) +NOP(glUniformMatrix4x2fv) NOP(glUniformMatrix4x3fv) NOP(glUnmapBuffer) NOP(glUnmapBufferOES) NOP(glUseProgramStages) +NOP(glValidateProgram) NOP(glValidateProgramPipeline) NOP(glVertexAttrib1f) NOP(glVertexAttrib1fv) NOP(glVertexAttrib2f) +NOP(glVertexAttrib2fv) NOP(glVertexAttrib3f) NOP(glVertexAttrib3fv) NOP(glVertexAttrib4f) NOP(glVertexAttrib4fv) +NOP(glVertexAttribBinding) NOP(glVertexAttribDivisor) NOP(glVertexAttribFormat) NOP(glVertexAttribI4i) +NOP(glVertexAttribI4iv) NOP(glVertexAttribI4ui) NOP(glVertexAttribI4uiv) NOP(glVertexAttribIFormat) NOP(glVertexAttribIPointer) +NOP(glVertexBindingDivisor) NOP(glVertexPointer) NOP(glVertexPointerBounds) NOP(glWaitSync) NOP(glWeightPointerOES) +NOP(glWeightPointerOESBounds) diff --git a/direct/guest/proxy/libEGL.c b/direct/guest/proxy/libEGL.c new file mode 100644 index 0000000..e698899 --- /dev/null +++ b/direct/guest/proxy/libEGL.c @@ -0,0 +1,101 @@ +// libEGL.so for AXRB Direct: there is no OpenGL ES implementation, so EGL has +// no display. Applications that offer Vulkan (Unity, Unreal) use that instead; +// libraries that merely probe EGL (eglGetCurrentContext) see no context. +#include +#include + +typedef void* EGLDisplay; +typedef void* EGLConfig; +typedef void* EGLContext; +typedef void* EGLSurface; +typedef void* EGLNativeWindowType; +typedef void* EGLClientBuffer; +typedef int32_t EGLint; +typedef unsigned int EGLBoolean; +typedef unsigned int EGLenum; +typedef void (*__eglMustCastToProperFunctionPointerType)(void); + +#define EGL_FALSE 0 +#define EGL_NOT_INITIALIZED 0x3001 +#define EGL_BAD_DISPLAY 0x3008 + +static __thread EGLint g_error = 0x3000; // EGL_SUCCESS + +static EGLBoolean fail(EGLint error) { + g_error = error; + return EGL_FALSE; +} + +EGLint eglGetError(void) { + EGLint e = g_error; + g_error = 0x3000; + return e; +} + +EGLDisplay eglGetDisplay(void* display_id) { return NULL; } +EGLDisplay eglGetPlatformDisplay(EGLenum platform, void* native_display, const intptr_t* attrib_list) { return NULL; } +EGLBoolean eglInitialize(EGLDisplay dpy, EGLint* major, EGLint* minor) { return fail(EGL_BAD_DISPLAY); } +EGLBoolean eglTerminate(EGLDisplay dpy) { return fail(EGL_BAD_DISPLAY); } +const char* eglQueryString(EGLDisplay dpy, EGLint name) { + g_error = EGL_BAD_DISPLAY; + return NULL; +} +__eglMustCastToProperFunctionPointerType eglGetProcAddress(const char* procname) { return NULL; } +EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig* configs, EGLint config_size, EGLint* num_config) { + return fail(EGL_NOT_INITIALIZED); +} +EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint* attrib_list, EGLConfig* configs, EGLint config_size, + EGLint* num_config) { + if (num_config) *num_config = 0; + return fail(EGL_NOT_INITIALIZED); +} +EGLBoolean eglGetConfigAttrib(EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint* value) { + return fail(EGL_NOT_INITIALIZED); +} +EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, + const EGLint* attrib_list) { + g_error = EGL_NOT_INITIALIZED; + return NULL; +} +EGLSurface eglCreatePbufferSurface(EGLDisplay dpy, EGLConfig config, const EGLint* attrib_list) { + g_error = EGL_NOT_INITIALIZED; + return NULL; +} +EGLBoolean eglDestroySurface(EGLDisplay dpy, EGLSurface surface) { return fail(EGL_NOT_INITIALIZED); } +EGLBoolean eglQuerySurface(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint* value) { + return fail(EGL_NOT_INITIALIZED); +} +EGLBoolean eglSurfaceAttrib(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value) { + return fail(EGL_NOT_INITIALIZED); +} +EGLBoolean eglBindAPI(EGLenum api) { return 1; } +EGLenum eglQueryAPI(void) { return 0x30A0; } // EGL_OPENGL_ES_API +EGLBoolean eglWaitClient(void) { return 1; } +EGLBoolean eglReleaseThread(void) { return 1; } +EGLContext eglCreateContext(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint* attrib_list) { + g_error = EGL_NOT_INITIALIZED; + return NULL; +} +EGLBoolean eglDestroyContext(EGLDisplay dpy, EGLContext ctx) { return fail(EGL_NOT_INITIALIZED); } +EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { + if (!draw && !read && !ctx) return 1; + return fail(EGL_NOT_INITIALIZED); +} +EGLContext eglGetCurrentContext(void) { return NULL; } +EGLSurface eglGetCurrentSurface(EGLint readdraw) { return NULL; } +EGLDisplay eglGetCurrentDisplay(void) { return NULL; } +EGLBoolean eglQueryContext(EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint* value) { + return fail(EGL_NOT_INITIALIZED); +} +EGLBoolean eglWaitGL(void) { return 1; } +EGLBoolean eglWaitNative(EGLint engine) { return 1; } +EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface) { return fail(EGL_NOT_INITIALIZED); } +EGLBoolean eglSwapInterval(EGLDisplay dpy, EGLint interval) { return fail(EGL_NOT_INITIALIZED); } +EGLBoolean eglCopyBuffers(EGLDisplay dpy, EGLSurface surface, void* target) { return fail(EGL_NOT_INITIALIZED); } +EGLBoolean eglBindTexImage(EGLDisplay dpy, EGLSurface surface, EGLint buffer) { return fail(EGL_NOT_INITIALIZED); } +EGLBoolean eglReleaseTexImage(EGLDisplay dpy, EGLSurface surface, EGLint buffer) { return fail(EGL_NOT_INITIALIZED); } +EGLSurface eglCreatePbufferFromClientBuffer(EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, + const EGLint* attrib_list) { + g_error = EGL_NOT_INITIALIZED; + return NULL; +} diff --git a/direct/guest/proxy/libGLES.c b/direct/guest/proxy/libGLES.c new file mode 100644 index 0000000..7861e71 --- /dev/null +++ b/direct/guest/proxy/libGLES.c @@ -0,0 +1,35 @@ +// libGLESv2.so / libGLESv3.so / libGLESv1_CM.so for AXRB Direct: there is +// never a current context, so queries return nothing (as GL does without one). +#include +#include + +typedef unsigned int GLenum; +typedef int GLint; +typedef unsigned char GLubyte; +typedef unsigned int GLuint; + +GLenum glGetError(void) { return 0; } +const GLubyte* glGetString(GLenum name) { return NULL; } +const GLubyte* glGetStringi(GLenum name, GLuint index) { return NULL; } +void glGetIntegerv(GLenum pname, GLint* data) {} +void glFlush(void) {} +void glFinish(void) {} + +// Everything else is a no-op returning 0 (no object, no status): with no current +// context GL calls have no effect. AAPCS64 lets a stub ignore any arguments. +#define NOP(name) uint64_t name(void) { return 0; } +NOP(glActiveTexture) NOP(glAttachShader) NOP(glBindFramebuffer) NOP(glBindTexture) NOP(glBlitFramebuffer) +NOP(glCheckFramebufferStatus) NOP(glClear) NOP(glClearColor) NOP(glCompileShader) NOP(glCreateProgram) +NOP(glCreateShader) NOP(glDeleteFramebuffers) NOP(glDeleteProgram) NOP(glDeleteShader) NOP(glDeleteTextures) +NOP(glDrawArrays) NOP(glFramebufferTexture2D) NOP(glFramebufferTextureLayer) NOP(glGenFramebuffers) +NOP(glGenTextures) NOP(glGetProgramiv) NOP(glGetShaderInfoLog) NOP(glGetShaderiv) NOP(glGetUniformLocation) +NOP(glLinkProgram) NOP(glPixelStorei) NOP(glReadPixels) NOP(glShaderSource) NOP(glTexImage2D) +NOP(glTexParameteri) NOP(glTexStorage2D) NOP(glTexStorage3D) NOP(glUniform1i) NOP(glUniformMatrix4fv) +NOP(glUseProgram) NOP(glViewport) NOP(glEnable) NOP(glDisable) NOP(glBindBuffer) NOP(glGenBuffers) +NOP(glDeleteBuffers) NOP(glBufferData) NOP(glScissor) NOP(glDrawElements) NOP(glBindVertexArray) +NOP(glGenVertexArrays) NOP(glDeleteVertexArrays) NOP(glVertexAttribPointer) NOP(glEnableVertexAttribArray) +NOP(glDisableVertexAttribArray) NOP(glGetAttribLocation) NOP(glUniform1f) NOP(glUniform2f) NOP(glUniform4f) +NOP(glUniform4fv) NOP(glGetProgramInfoLog) NOP(glFenceSync) NOP(glClientWaitSync) NOP(glDeleteSync) +NOP(glReadBuffer) NOP(glDrawBuffers) NOP(glInvalidateFramebuffer) NOP(glColorMask) NOP(glDepthMask) + +#include "gles_stubs.inc" diff --git a/direct/guest/proxy/libOpenSLES.c b/direct/guest/proxy/libOpenSLES.c new file mode 100644 index 0000000..02bd1a2 --- /dev/null +++ b/direct/guest/proxy/libOpenSLES.c @@ -0,0 +1,33 @@ +// OpenSL ES PCM output on Direct AAudio/WASAPI. +#include +#include +#define IID(name, a, b, c, d, n0, n1, n2, n3, n4, n5) \ + static const struct SLInterfaceID_ name##_data = {a, b, c, d, {n0, n1, n2, n3, n4, n5}}; \ + const SLInterfaceID name = &name##_data; + +IID(SL_IID_NULL, 0xec7178ec, 0xe5e1, 0x4432, 0xa3f4, 0x46, 0x57, 0xe6, 0x79, 0x52, 0x10) +IID(SL_IID_OBJECT, 0x79216360, 0xddd7, 0x11db, 0xac16, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_ENGINE, 0x8d97c260, 0xddd4, 0x11db, 0x958f, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_ENGINECAPABILITIES, 0x8320d0a0, 0xddd5, 0x11db, 0xa1b1, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_PLAY, 0xef0bd9c0, 0xddd7, 0x11db, 0xbf49, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_BUFFERQUEUE, 0x2bc99cc0, 0xddd4, 0x11db, 0x8d99, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_ANDROIDSIMPLEBUFFERQUEUE, 0x198e4940, 0xc5d7, 0x11df, 0xa2a6, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_VOLUME, 0x09e8ede0, 0xddde, 0x11db, 0xb4f6, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_RECORD, 0xc5657aa0, 0xdddb, 0x11db, 0x82f7, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_OUTPUTMIX, 0x97750f60, 0xddd7, 0x11db, 0x92b1, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_ANDROIDCONFIGURATION, 0x89f6a7e0, 0xbeac, 0x11df, 0x8b5c, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_ANDROIDEFFECT, 0xae12da60, 0x7ac0, 0x11e0, 0xb8b2, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_EFFECTSEND, 0x56e7d200, 0xddd4, 0x11db, 0xaefb, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_SEEK, 0xd43135a0, 0xdddc, 0x11db, 0xb458, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_PLAYBACKRATE, 0x2e3b2a40, 0xddda, 0x11db, 0xa349, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_PREFETCHSTATUS, 0x2a41ee80, 0xddd8, 0x11db, 0x8ff0, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_METADATAEXTRACTION, 0xaa5b1f80, 0xddd6, 0x11db, 0xac8e, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_ENVIRONMENTALREVERB, 0xc2e5d5f0, 0x94bd, 0x4763, 0x9cac, 0x4e, 0x23, 0x4d, 0x06, 0x83, 0x9e) +IID(SL_IID_PRESETREVERB, 0x47382d60, 0xddd8, 0x11db, 0xbf3a, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_EQUALIZER, 0x0bed4300, 0xddd6, 0x11db, 0x8f34, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_MUTESOLO, 0x5a28ebe0, 0xddd8, 0x11db, 0xb2d1, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_DYNAMICINTERFACEMANAGEMENT, 0x63936540, 0xf775, 0x11db, 0x9cc4, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_AUDIOIODEVICECAPABILITIES, 0xb2564dc0, 0xddd3, 0x11db, 0xbd62, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b) +IID(SL_IID_ANDROIDBUFFERQUEUESOURCE, 0x7fc1a460, 0xeec1, 0x4b8d, 0xa6b8, 0x4c, 0x91, 0x9b, 0xe8, 0x72, 0x7b) + +#include "opensles_output.inc" diff --git a/direct/guest/proxy/libaaudio.c b/direct/guest/proxy/libaaudio.c new file mode 100644 index 0000000..3f22263 --- /dev/null +++ b/direct/guest/proxy/libaaudio.c @@ -0,0 +1,461 @@ +// libaaudio.so for AXRB Direct. +// +// The API state lives here; the samples go to the host's default output device +// (WASAPI, direct/src/audio.cpp). Data callbacks run on a guest thread of this +// library that waits for room in the host buffer, so applications see the +// AAudio threading they expect. There is no input device. +#include +#include +#include +#include +#include +#include + +#include "../axrb_guest.h" + +typedef int32_t aaudio_result_t; +typedef int32_t aaudio_stream_state_t; +typedef int32_t aaudio_format_t; +typedef int32_t aaudio_direction_t; +typedef int32_t aaudio_sharing_mode_t; +typedef int32_t aaudio_performance_mode_t; +typedef int32_t aaudio_data_callback_result_t; + +enum { + AAUDIO_OK = 0, + AAUDIO_ERROR_BASE = -900, + AAUDIO_ERROR_DISCONNECTED = -899, + AAUDIO_ERROR_ILLEGAL_ARGUMENT = -898, + AAUDIO_ERROR_INTERNAL = -896, + AAUDIO_ERROR_INVALID_STATE = -895, + AAUDIO_ERROR_INVALID_HANDLE = -892, + AAUDIO_ERROR_UNIMPLEMENTED = -890, + AAUDIO_ERROR_UNAVAILABLE = -889, + AAUDIO_ERROR_NO_FREE_HANDLES = -888, + AAUDIO_ERROR_NO_MEMORY = -887, + AAUDIO_ERROR_NULL = -886, + AAUDIO_ERROR_TIMEOUT = -885, + AAUDIO_ERROR_WOULD_BLOCK = -884, + AAUDIO_ERROR_INVALID_FORMAT = -883, + AAUDIO_ERROR_OUT_OF_RANGE = -882, + AAUDIO_ERROR_NO_SERVICE = -881, + AAUDIO_ERROR_INVALID_RATE = -880, +}; + +enum { + AAUDIO_STREAM_STATE_UNINITIALIZED = 0, + AAUDIO_STREAM_STATE_UNKNOWN, + AAUDIO_STREAM_STATE_OPEN, + AAUDIO_STREAM_STATE_STARTING, + AAUDIO_STREAM_STATE_STARTED, + AAUDIO_STREAM_STATE_PAUSING, + AAUDIO_STREAM_STATE_PAUSED, + AAUDIO_STREAM_STATE_FLUSHING, + AAUDIO_STREAM_STATE_FLUSHED, + AAUDIO_STREAM_STATE_STOPPING, + AAUDIO_STREAM_STATE_STOPPED, + AAUDIO_STREAM_STATE_CLOSING, + AAUDIO_STREAM_STATE_CLOSED, + AAUDIO_STREAM_STATE_DISCONNECTED, +}; + +enum { AAUDIO_DIRECTION_OUTPUT = 0, AAUDIO_DIRECTION_INPUT = 1 }; +enum { + AAUDIO_FORMAT_INVALID = -1, + AAUDIO_FORMAT_UNSPECIFIED = 0, + AAUDIO_FORMAT_PCM_I16 = 1, + AAUDIO_FORMAT_PCM_FLOAT = 2, + AAUDIO_FORMAT_PCM_I24_PACKED = 3, + AAUDIO_FORMAT_PCM_I32 = 4, +}; +enum { AAUDIO_SHARING_MODE_EXCLUSIVE = 0, AAUDIO_SHARING_MODE_SHARED = 1 }; +enum { AAUDIO_PERFORMANCE_MODE_NONE = 10, AAUDIO_PERFORMANCE_MODE_POWER_SAVING = 11, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY = 12 }; +enum { AAUDIO_CALLBACK_RESULT_CONTINUE = 0, AAUDIO_CALLBACK_RESULT_STOP = 1 }; +#define AAUDIO_UNSPECIFIED 0 + +typedef struct AAudioStreamStruct AAudioStream; +typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(AAudioStream* stream, void* userData, + void* audioData, int32_t numFrames); +typedef void (*AAudioStream_errorCallback)(AAudioStream* stream, void* userData, aaudio_result_t error); + +typedef struct AAudioStreamBuilderStruct { + int32_t device_id, sample_rate, channel_count, buffer_capacity, frames_per_callback, session_id; + aaudio_format_t format; + aaudio_direction_t direction; + aaudio_sharing_mode_t sharing_mode; + aaudio_performance_mode_t performance_mode; + int32_t usage, content_type, input_preset, capture_policy, privacy_sensitive; + uint32_t channel_mask; + AAudioStream_dataCallback data_callback; + void* data_user; + AAudioStream_errorCallback error_callback; + void* error_user; +} AAudioStreamBuilder; + +struct AAudioStreamStruct { + AAudioStreamBuilder params; + int64_t host; // host stream handle + int32_t burst; + int32_t buffer_size; + int32_t frame_bytes; + volatile aaudio_stream_state_t state; + pthread_mutex_t lock; + pthread_cond_t changed; + pthread_t thread; + int thread_running; + volatile int stop_thread; + int64_t frames_written; + void* callback_buffer; + int32_t callback_capacity; +}; + +// Host side (direct/src/audio.cpp). +extern int64_t axrb_audio_open(int32_t sample_rate, int32_t channels, int32_t format, int32_t* out_rate, + int32_t* out_burst, int32_t* out_capacity); +extern int32_t axrb_audio_start(int64_t handle); +extern int32_t axrb_audio_stop(int64_t handle); +extern int32_t axrb_audio_flush(int64_t handle); +extern int32_t axrb_audio_wait(int64_t handle, int64_t timeout_ns); +extern int32_t axrb_audio_write(int64_t handle, const void* data, int32_t frames); +extern int32_t axrb_audio_position(int64_t handle, int64_t* frames, int64_t* time_ns); +extern void axrb_audio_close(int64_t handle); +AXRB_HOST_FUNCTION(axrb_audio_open, "audio.open"); +AXRB_HOST_FUNCTION(axrb_audio_start, "audio.start"); +AXRB_HOST_FUNCTION(axrb_audio_stop, "audio.stop"); +AXRB_HOST_FUNCTION(axrb_audio_flush, "audio.flush"); +AXRB_HOST_FUNCTION(axrb_audio_wait, "audio.wait"); +AXRB_HOST_FUNCTION(axrb_audio_write, "audio.write"); +AXRB_HOST_FUNCTION(axrb_audio_position, "audio.position"); +AXRB_HOST_FUNCTION(axrb_audio_close, "audio.close"); + +static int32_t sample_bytes(aaudio_format_t f) { + switch (f) { + case AAUDIO_FORMAT_PCM_I16: return 2; + case AAUDIO_FORMAT_PCM_I24_PACKED: return 3; + default: return 4; + } +} + +const char* AAudio_convertResultToText(aaudio_result_t r) { + switch (r) { + case AAUDIO_OK: return "AAUDIO_OK"; + case AAUDIO_ERROR_DISCONNECTED: return "AAUDIO_ERROR_DISCONNECTED"; + case AAUDIO_ERROR_ILLEGAL_ARGUMENT: return "AAUDIO_ERROR_ILLEGAL_ARGUMENT"; + case AAUDIO_ERROR_INTERNAL: return "AAUDIO_ERROR_INTERNAL"; + case AAUDIO_ERROR_INVALID_STATE: return "AAUDIO_ERROR_INVALID_STATE"; + case AAUDIO_ERROR_INVALID_HANDLE: return "AAUDIO_ERROR_INVALID_HANDLE"; + case AAUDIO_ERROR_UNIMPLEMENTED: return "AAUDIO_ERROR_UNIMPLEMENTED"; + case AAUDIO_ERROR_UNAVAILABLE: return "AAUDIO_ERROR_UNAVAILABLE"; + case AAUDIO_ERROR_NO_MEMORY: return "AAUDIO_ERROR_NO_MEMORY"; + case AAUDIO_ERROR_NULL: return "AAUDIO_ERROR_NULL"; + case AAUDIO_ERROR_TIMEOUT: return "AAUDIO_ERROR_TIMEOUT"; + case AAUDIO_ERROR_WOULD_BLOCK: return "AAUDIO_ERROR_WOULD_BLOCK"; + case AAUDIO_ERROR_INVALID_FORMAT: return "AAUDIO_ERROR_INVALID_FORMAT"; + case AAUDIO_ERROR_OUT_OF_RANGE: return "AAUDIO_ERROR_OUT_OF_RANGE"; + case AAUDIO_ERROR_NO_SERVICE: return "AAUDIO_ERROR_NO_SERVICE"; + case AAUDIO_ERROR_INVALID_RATE: return "AAUDIO_ERROR_INVALID_RATE"; + default: return "Unrecognized AAudio error."; + } +} + +const char* AAudio_convertStreamStateToText(aaudio_stream_state_t s) { + static const char* names[] = {"UNINITIALIZED", "UNKNOWN", "OPEN", "STARTING", "STARTED", "PAUSING", "PAUSED", + "FLUSHING", "FLUSHED", "STOPPING", "STOPPED", "CLOSING", "CLOSED", "DISCONNECTED"}; + return s >= 0 && s < (int)(sizeof names / sizeof names[0]) ? names[s] : "UNKNOWN"; +} + +// --- builder ---------------------------------------------------------------------------------- + +aaudio_result_t AAudio_createStreamBuilder(AAudioStreamBuilder** builder) { + AAudioStreamBuilder* b = calloc(1, sizeof *b); + if (!b) return AAUDIO_ERROR_NO_MEMORY; + b->sharing_mode = AAUDIO_SHARING_MODE_SHARED; + b->performance_mode = AAUDIO_PERFORMANCE_MODE_NONE; + b->direction = AAUDIO_DIRECTION_OUTPUT; + b->usage = 1; + b->content_type = 2; + b->input_preset = 6; + b->capture_policy = 1; + *builder = b; + return AAUDIO_OK; +} + +aaudio_result_t AAudioStreamBuilder_delete(AAudioStreamBuilder* builder) { + free(builder); + return AAUDIO_OK; +} + +#define SETTER(name, field, type) \ + void AAudioStreamBuilder_##name(AAudioStreamBuilder* b, type v) { b->field = v; } +SETTER(setDeviceId, device_id, int32_t) +SETTER(setSampleRate, sample_rate, int32_t) +SETTER(setChannelCount, channel_count, int32_t) +SETTER(setSamplesPerFrame, channel_count, int32_t) +SETTER(setFormat, format, aaudio_format_t) +SETTER(setSharingMode, sharing_mode, aaudio_sharing_mode_t) +SETTER(setDirection, direction, aaudio_direction_t) +SETTER(setBufferCapacityInFrames, buffer_capacity, int32_t) +SETTER(setPerformanceMode, performance_mode, aaudio_performance_mode_t) +SETTER(setUsage, usage, int32_t) +SETTER(setContentType, content_type, int32_t) +SETTER(setInputPreset, input_preset, int32_t) +SETTER(setAllowedCapturePolicy, capture_policy, int32_t) +SETTER(setSessionId, session_id, int32_t) +SETTER(setPrivacySensitive, privacy_sensitive, int32_t) +SETTER(setFramesPerDataCallback, frames_per_callback, int32_t) +SETTER(setChannelMask, channel_mask, uint32_t) +void AAudioStreamBuilder_setPackageName(AAudioStreamBuilder* b, const char* name) {} +void AAudioStreamBuilder_setAttributionTag(AAudioStreamBuilder* b, const char* tag) {} +void AAudioStreamBuilder_setIsContentSpatialized(AAudioStreamBuilder* b, int v) {} +void AAudioStreamBuilder_setSpatializationBehavior(AAudioStreamBuilder* b, int32_t v) {} + +void AAudioStreamBuilder_setDataCallback(AAudioStreamBuilder* b, AAudioStream_dataCallback callback, void* user) { + b->data_callback = callback; + b->data_user = user; +} + +void AAudioStreamBuilder_setErrorCallback(AAudioStreamBuilder* b, AAudioStream_errorCallback callback, void* user) { + b->error_callback = callback; + b->error_user = user; +} + +static int32_t channels_of(const AAudioStreamBuilder* b) { + if (b->channel_count > 0) return b->channel_count; + if (b->channel_mask) return __builtin_popcount(b->channel_mask); + return 2; +} + +aaudio_result_t AAudioStreamBuilder_openStream(AAudioStreamBuilder* b, AAudioStream** out) { + if (!out) return AAUDIO_ERROR_NULL; + *out = NULL; + if (b->direction == AAUDIO_DIRECTION_INPUT) return AAUDIO_ERROR_UNAVAILABLE; + AAudioStream* s = calloc(1, sizeof *s); + if (!s) return AAUDIO_ERROR_NO_MEMORY; + s->params = *b; + s->params.channel_count = channels_of(b); + if (s->params.format == AAUDIO_FORMAT_UNSPECIFIED) s->params.format = AAUDIO_FORMAT_PCM_FLOAT; + int32_t rate = 0, burst = 0, capacity = 0; + int64_t h = axrb_audio_open(s->params.sample_rate, s->params.channel_count, s->params.format, &rate, &burst, + &capacity); + if (h <= 0) { + free(s); + return h < 0 ? (aaudio_result_t)h : AAUDIO_ERROR_UNAVAILABLE; + } + s->host = h; + s->params.sample_rate = rate; + s->params.sharing_mode = AAUDIO_SHARING_MODE_SHARED; + if (s->params.session_id > 0) s->params.session_id = 1; + s->burst = burst; + s->params.buffer_capacity = capacity; + s->buffer_size = capacity; + s->frame_bytes = sample_bytes(s->params.format) * s->params.channel_count; + s->state = AAUDIO_STREAM_STATE_OPEN; + pthread_mutex_init(&s->lock, NULL); + pthread_cond_init(&s->changed, NULL); + *out = s; + return AAUDIO_OK; +} + +// --- stream ----------------------------------------------------------------------------------- + +static void set_state(AAudioStream* s, aaudio_stream_state_t state) { + pthread_mutex_lock(&s->lock); + s->state = state; + pthread_cond_broadcast(&s->changed); + pthread_mutex_unlock(&s->lock); +} + +static void* callback_thread(void* arg) { + AAudioStream* s = arg; + int32_t frames = s->params.frames_per_callback > 0 ? s->params.frames_per_callback : s->burst; + if (s->callback_capacity < frames) { + free(s->callback_buffer); + s->callback_buffer = malloc((size_t)frames * s->frame_bytes); + s->callback_capacity = frames; + } + while (!s->stop_thread) { + int32_t room = axrb_audio_wait(s->host, 50 * 1000000LL); + if (room < 0) { + if (s->params.error_callback) s->params.error_callback(s, s->params.error_user, AAUDIO_ERROR_DISCONNECTED); + set_state(s, AAUDIO_STREAM_STATE_DISCONNECTED); + break; + } + while (!s->stop_thread && room >= frames) { + aaudio_data_callback_result_t r = + s->params.data_callback(s, s->params.data_user, s->callback_buffer, frames); + axrb_audio_write(s->host, s->callback_buffer, frames); + s->frames_written += frames; + room -= frames; + if (r == AAUDIO_CALLBACK_RESULT_STOP) { + s->stop_thread = 1; + axrb_audio_stop(s->host); + set_state(s, AAUDIO_STREAM_STATE_STOPPED); + } + } + } + return NULL; +} + +static void join_callback_thread(AAudioStream* s) { + if (!s->thread_running) return; + s->stop_thread = 1; + if (!pthread_equal(pthread_self(), s->thread)) pthread_join(s->thread, NULL); + else pthread_detach(s->thread); + s->thread_running = 0; +} + +aaudio_result_t AAudioStream_requestStart(AAudioStream* s) { + if (s->state == AAUDIO_STREAM_STATE_CLOSED) return AAUDIO_ERROR_INVALID_STATE; + if (s->state == AAUDIO_STREAM_STATE_STARTED) return AAUDIO_OK; + int32_t r = axrb_audio_start(s->host); + if (r < 0) return r; + set_state(s, AAUDIO_STREAM_STATE_STARTED); + if (s->params.data_callback && !s->thread_running) { + s->stop_thread = 0; + if (pthread_create(&s->thread, NULL, callback_thread, s) == 0) s->thread_running = 1; + } + return AAUDIO_OK; +} + +static aaudio_result_t halt(AAudioStream* s, aaudio_stream_state_t final_state) { + if (s->state == AAUDIO_STREAM_STATE_CLOSED) return AAUDIO_ERROR_INVALID_STATE; + join_callback_thread(s); + axrb_audio_stop(s->host); + set_state(s, final_state); + return AAUDIO_OK; +} + +aaudio_result_t AAudioStream_requestStop(AAudioStream* s) { return halt(s, AAUDIO_STREAM_STATE_STOPPED); } +aaudio_result_t AAudioStream_requestPause(AAudioStream* s) { return halt(s, AAUDIO_STREAM_STATE_PAUSED); } + +aaudio_result_t AAudioStream_requestFlush(AAudioStream* s) { + if (s->state != AAUDIO_STREAM_STATE_PAUSED && s->state != AAUDIO_STREAM_STATE_STOPPED) + return AAUDIO_ERROR_INVALID_STATE; + axrb_audio_flush(s->host); + set_state(s, AAUDIO_STREAM_STATE_FLUSHED); + return AAUDIO_OK; +} + +aaudio_result_t AAudioStream_release(AAudioStream* s) { + if (s->state != AAUDIO_STREAM_STATE_CLOSED) { + join_callback_thread(s); + axrb_audio_close(s->host); + s->host = 0; + set_state(s, AAUDIO_STREAM_STATE_CLOSING); + } + return AAUDIO_OK; +} + +aaudio_result_t AAudioStream_close(AAudioStream* s) { + AAudioStream_release(s); + free(s->callback_buffer); + pthread_mutex_destroy(&s->lock); + pthread_cond_destroy(&s->changed); + free(s); + return AAUDIO_OK; +} + +aaudio_stream_state_t AAudioStream_getState(AAudioStream* s) { return s->state; } + +aaudio_result_t AAudioStream_waitForStateChange(AAudioStream* s, aaudio_stream_state_t input, + aaudio_stream_state_t* next, int64_t timeout_ns) { + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += timeout_ns / 1000000000LL; + deadline.tv_nsec += timeout_ns % 1000000000LL; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000L; + } + pthread_mutex_lock(&s->lock); + int r = 0; + while (s->state == input && r != ETIMEDOUT) r = pthread_cond_timedwait(&s->changed, &s->lock, &deadline); + aaudio_stream_state_t state = s->state; + pthread_mutex_unlock(&s->lock); + if (next) *next = state; + return state == input ? AAUDIO_ERROR_TIMEOUT : AAUDIO_OK; +} + +aaudio_result_t AAudioStream_read(AAudioStream* s, void* buffer, int32_t frames, int64_t timeout_ns) { + return AAUDIO_ERROR_UNIMPLEMENTED; +} + +aaudio_result_t AAudioStream_write(AAudioStream* s, const void* buffer, int32_t frames, int64_t timeout_ns) { + if (s->params.data_callback) return AAUDIO_ERROR_INVALID_STATE; + if (s->state != AAUDIO_STREAM_STATE_STARTED && s->state != AAUDIO_STREAM_STATE_OPEN && + s->state != AAUDIO_STREAM_STATE_PAUSED && s->state != AAUDIO_STREAM_STATE_FLUSHED && + s->state != AAUDIO_STREAM_STATE_STOPPED) + return AAUDIO_ERROR_INVALID_STATE; + const char* p = buffer; + int32_t done = 0; + struct timespec start, now; + clock_gettime(CLOCK_MONOTONIC, &start); + while (done < frames) { + int64_t waited = 0; + clock_gettime(CLOCK_MONOTONIC, &now); + waited = (now.tv_sec - start.tv_sec) * 1000000000LL + (now.tv_nsec - start.tv_nsec); + int64_t left = timeout_ns - waited; + int32_t room = axrb_audio_wait(s->host, left > 0 ? left : 0); + if (room < 0) return done ? done : AAUDIO_ERROR_DISCONNECTED; + if (room == 0) { + if (left <= 0) break; + continue; + } + int32_t n = frames - done < room ? frames - done : room; + n = axrb_audio_write(s->host, p + (size_t)done * s->frame_bytes, n); + if (n < 0) return done ? done : n; + done += n; + s->frames_written += n; + } + return done; +} + +aaudio_result_t AAudioStream_setBufferSizeInFrames(AAudioStream* s, int32_t frames) { + if (frames <= 0 || frames > s->params.buffer_capacity) frames = s->params.buffer_capacity; + s->buffer_size = frames; + return frames; +} + +int32_t AAudioStream_getBufferSizeInFrames(AAudioStream* s) { return s->buffer_size; } +int32_t AAudioStream_getFramesPerBurst(AAudioStream* s) { return s->burst; } +int32_t AAudioStream_getBufferCapacityInFrames(AAudioStream* s) { return s->params.buffer_capacity; } +int32_t AAudioStream_getFramesPerDataCallback(AAudioStream* s) { + return s->params.frames_per_callback > 0 ? s->params.frames_per_callback : AAUDIO_UNSPECIFIED; +} +int32_t AAudioStream_getXRunCount(AAudioStream* s) { return 0; } +int32_t AAudioStream_getSampleRate(AAudioStream* s) { return s->params.sample_rate; } +int32_t AAudioStream_getHardwareSampleRate(AAudioStream* s) { return s->params.sample_rate; } +int32_t AAudioStream_getChannelCount(AAudioStream* s) { return s->params.channel_count; } +int32_t AAudioStream_getHardwareChannelCount(AAudioStream* s) { return s->params.channel_count; } +int32_t AAudioStream_getSamplesPerFrame(AAudioStream* s) { return s->params.channel_count; } +int32_t AAudioStream_getDeviceId(AAudioStream* s) { return 2; } +aaudio_format_t AAudioStream_getFormat(AAudioStream* s) { return s->params.format; } +aaudio_format_t AAudioStream_getHardwareFormat(AAudioStream* s) { return AAUDIO_FORMAT_PCM_FLOAT; } +aaudio_sharing_mode_t AAudioStream_getSharingMode(AAudioStream* s) { return s->params.sharing_mode; } +aaudio_performance_mode_t AAudioStream_getPerformanceMode(AAudioStream* s) { return s->params.performance_mode; } +aaudio_direction_t AAudioStream_getDirection(AAudioStream* s) { return s->params.direction; } +int64_t AAudioStream_getFramesWritten(AAudioStream* s) { return s->frames_written; } +int64_t AAudioStream_getFramesRead(AAudioStream* s) { + int64_t frames = 0, t = 0; + axrb_audio_position(s->host, &frames, &t); + return frames; +} +int32_t AAudioStream_getSessionId(AAudioStream* s) { return s->params.session_id > 0 ? s->params.session_id : -1; } +int32_t AAudioStream_getUsage(AAudioStream* s) { return s->params.usage; } +int32_t AAudioStream_getContentType(AAudioStream* s) { return s->params.content_type; } +int32_t AAudioStream_getInputPreset(AAudioStream* s) { return s->params.input_preset; } +int32_t AAudioStream_getAllowedCapturePolicy(AAudioStream* s) { return s->params.capture_policy; } +int AAudioStream_isPrivacySensitive(AAudioStream* s) { return s->params.privacy_sensitive; } +uint32_t AAudioStream_getChannelMask(AAudioStream* s) { return s->params.channel_mask; } +int AAudioStream_isContentSpatialized(AAudioStream* s) { return 0; } +int32_t AAudioStream_getSpatializationBehavior(AAudioStream* s) { return 1; } + +aaudio_result_t AAudioStream_getTimestamp(AAudioStream* s, clockid_t clockid, int64_t* frame_position, + int64_t* time_ns) { + if (s->state != AAUDIO_STREAM_STATE_STARTED) return AAUDIO_ERROR_INVALID_STATE; + int64_t frames = 0, t = 0; + if (axrb_audio_position(s->host, &frames, &t) < 0 || frames <= 0) return AAUDIO_ERROR_INVALID_STATE; + if (frame_position) *frame_position = frames; + if (time_ns) *time_ns = t; + return AAUDIO_OK; +} diff --git a/direct/guest/proxy/libandroid.c b/direct/guest/proxy/libandroid.c new file mode 100644 index 0000000..82a56b4 --- /dev/null +++ b/direct/guest/proxy/libandroid.c @@ -0,0 +1,365 @@ +// libandroid.so for AXRB Direct. +// +// Windows and assets live on the host (direct/src/ndk_android.cpp): their +// functions trap there. Loopers, sensors and tracing are plain guest code -- +// a looper is epoll plus an eventfd, as in libutils, and the device has no +// motion sensors (the headset pose comes from OpenXR). +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../axrb_guest.h" + +#define HOST(name) AXRB_HOST_FUNCTION(name, "android." #name) + +// --- native windows ------------------------------------------------------------------ +#if defined(AXRB_NATIVE_X64_BRIDGE) +__asm__(".text\n.global ANativeWindow_fromSurface\n.type ANativeWindow_fromSurface,%function\nANativeWindow_fromSurface:\nsvc #0x380\nret\n.size ANativeWindow_fromSurface,.-ANativeWindow_fromSurface\n"); +__asm__(".text\n.global ANativeWindow_acquire\n.type ANativeWindow_acquire,%function\nANativeWindow_acquire:\nsvc #0x381\nret\n.size ANativeWindow_acquire,.-ANativeWindow_acquire\n"); +__asm__(".text\n.global ANativeWindow_release\n.type ANativeWindow_release,%function\nANativeWindow_release:\nsvc #0x382\nret\n.size ANativeWindow_release,.-ANativeWindow_release\n"); +__asm__(".text\n.global ANativeWindow_getWidth\n.type ANativeWindow_getWidth,%function\nANativeWindow_getWidth:\nsvc #0x383\nret\n.size ANativeWindow_getWidth,.-ANativeWindow_getWidth\n"); +__asm__(".text\n.global ANativeWindow_getHeight\n.type ANativeWindow_getHeight,%function\nANativeWindow_getHeight:\nsvc #0x384\nret\n.size ANativeWindow_getHeight,.-ANativeWindow_getHeight\n"); +__asm__(".text\n.global ANativeWindow_setBuffersGeometry\n.type ANativeWindow_setBuffersGeometry,%function\nANativeWindow_setBuffersGeometry:\nsvc #0x385\nret\n.size ANativeWindow_setBuffersGeometry,.-ANativeWindow_setBuffersGeometry\n"); +#else +HOST(ANativeWindow_fromSurface); +HOST(ANativeWindow_acquire); +HOST(ANativeWindow_release); +HOST(ANativeWindow_getWidth); +HOST(ANativeWindow_getHeight); +HOST(ANativeWindow_setBuffersGeometry); +#endif +HOST(ANativeWindow_toSurface); +HOST(ANativeWindow_getFormat); +HOST(ANativeWindow_setBuffersTransform); +HOST(ANativeWindow_setBuffersDataSpace); +HOST(ANativeWindow_getBuffersDataSpace); +HOST(ANativeWindow_setFrameRate); +HOST(ANativeWindow_setFrameRateWithChangeStrategy); +HOST(ANativeWindow_tryAllocateBuffers); +HOST(ANativeWindow_lock); +HOST(ANativeWindow_unlockAndPost); + +// --- assets ------------------------------------------------------------------------------ +HOST(AAssetManager_fromJava); +HOST(AAssetManager_open); +HOST(AAssetManager_openDir); +HOST(AAssetDir_getNextFileName); +HOST(AAssetDir_rewind); +HOST(AAssetDir_close); +HOST(AAsset_read); +HOST(AAsset_seek); +HOST(AAsset_seek64); +HOST(AAsset_close); +HOST(AAsset_getBuffer); +HOST(AAsset_getLength); +HOST(AAsset_getLength64); +HOST(AAsset_getRemainingLength); +HOST(AAsset_getRemainingLength64); +HOST(AAsset_openFileDescriptor); +HOST(AAsset_openFileDescriptor64); +HOST(AAsset_isAllocated); + +// --- configuration (host: the session's display and locale) ----------------------------- +HOST(AConfiguration_new); +HOST(AConfiguration_delete); +HOST(AConfiguration_fromAssetManager); +HOST(AConfiguration_getDensity); +HOST(AConfiguration_getOrientation); +HOST(AConfiguration_getScreenWidthDp); +HOST(AConfiguration_getScreenHeightDp); +HOST(AConfiguration_getSmallestScreenWidthDp); +HOST(AConfiguration_getScreenSize); +HOST(AConfiguration_getSdkVersion); +HOST(AConfiguration_getLanguage); +HOST(AConfiguration_getCountry); +HOST(AConfiguration_getTouchscreen); +HOST(AConfiguration_getKeyboard); +HOST(AConfiguration_getNavigation); +HOST(AConfiguration_getUiModeType); +HOST(AConfiguration_getUiModeNight); +HOST(AConfiguration_getLayoutDirection); + +// --- loopers ----------------------------------------------------------------------------- + +typedef int (*ALooper_callbackFunc)(int fd, int events, void* data); + +enum { + ALOOPER_PREPARE_ALLOW_NON_CALLBACKS = 1, + ALOOPER_POLL_WAKE = -1, + ALOOPER_POLL_CALLBACK = -2, + ALOOPER_POLL_TIMEOUT = -3, + ALOOPER_POLL_ERROR = -4, + ALOOPER_EVENT_INPUT = 1, + ALOOPER_EVENT_OUTPUT = 2, + ALOOPER_EVENT_ERROR = 4, + ALOOPER_EVENT_HANGUP = 8, + ALOOPER_EVENT_INVALID = 16, +}; + +struct looper_entry { + int fd; + int ident; + int events; + ALooper_callbackFunc callback; + void* data; +}; + +typedef struct ALooper { + int epoll_fd; + int wake_fd; + int refs; + int allow_non_callbacks; + pthread_mutex_t lock; + struct looper_entry* entries; + int count, capacity; +} ALooper; + +static pthread_key_t g_looper_key; +static pthread_once_t g_looper_once = PTHREAD_ONCE_INIT; + +static void looper_key_init(void) { pthread_key_create(&g_looper_key, NULL); } + +static ALooper* looper_new(int allow_non_callbacks) { + ALooper* l = calloc(1, sizeof *l); + l->epoll_fd = epoll_create1(EPOLL_CLOEXEC); + l->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + l->refs = 1; + l->allow_non_callbacks = allow_non_callbacks; + pthread_mutex_init(&l->lock, NULL); + struct epoll_event ev = {.events = EPOLLIN, .data.fd = l->wake_fd}; + epoll_ctl(l->epoll_fd, EPOLL_CTL_ADD, l->wake_fd, &ev); + return l; +} + +ALooper* ALooper_forThread(void) { + pthread_once(&g_looper_once, looper_key_init); + return pthread_getspecific(g_looper_key); +} + +ALooper* ALooper_prepare(int opts) { + ALooper* l = ALooper_forThread(); + if (!l) { + l = looper_new(opts & ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + pthread_setspecific(g_looper_key, l); + } + return l; +} + +void ALooper_acquire(ALooper* looper) { __atomic_add_fetch(&looper->refs, 1, __ATOMIC_RELAXED); } + +void ALooper_release(ALooper* looper) { + if (__atomic_sub_fetch(&looper->refs, 1, __ATOMIC_ACQ_REL) == 0) { + close(looper->epoll_fd); + close(looper->wake_fd); + free(looper->entries); + free(looper); + } +} + +void ALooper_wake(ALooper* looper) { + uint64_t one = 1; + while (write(looper->wake_fd, &one, sizeof one) < 0 && errno == EINTR) {} +} + +static uint32_t to_epoll(int events) { + uint32_t e = 0; + if (events & ALOOPER_EVENT_INPUT) e |= EPOLLIN; + if (events & ALOOPER_EVENT_OUTPUT) e |= EPOLLOUT; + return e; +} + +static int from_epoll(uint32_t e) { + int events = 0; + if (e & EPOLLIN) events |= ALOOPER_EVENT_INPUT; + if (e & EPOLLOUT) events |= ALOOPER_EVENT_OUTPUT; + if (e & EPOLLERR) events |= ALOOPER_EVENT_ERROR; + if (e & EPOLLHUP) events |= ALOOPER_EVENT_HANGUP; + return events; +} + +int ALooper_addFd(ALooper* looper, int fd, int ident, int events, ALooper_callbackFunc callback, void* data) { + if (!callback && ident < 0) return -1; + if (callback) ident = ALOOPER_POLL_CALLBACK; + pthread_mutex_lock(&looper->lock); + struct epoll_event ev = {.events = to_epoll(events), .data.fd = fd}; + int i; + for (i = 0; i < looper->count; ++i) + if (looper->entries[i].fd == fd) break; + if (i == looper->count) { + if (looper->count == looper->capacity) { + looper->capacity = looper->capacity ? looper->capacity * 2 : 8; + looper->entries = realloc(looper->entries, sizeof(struct looper_entry) * looper->capacity); + } + looper->count++; + epoll_ctl(looper->epoll_fd, EPOLL_CTL_ADD, fd, &ev); + } else { + epoll_ctl(looper->epoll_fd, EPOLL_CTL_MOD, fd, &ev); + } + looper->entries[i] = (struct looper_entry){fd, ident, events, callback, data}; + pthread_mutex_unlock(&looper->lock); + return 1; +} + +int ALooper_removeFd(ALooper* looper, int fd) { + pthread_mutex_lock(&looper->lock); + int removed = 0; + for (int i = 0; i < looper->count; ++i) { + if (looper->entries[i].fd == fd) { + looper->entries[i] = looper->entries[--looper->count]; + epoll_ctl(looper->epoll_fd, EPOLL_CTL_DEL, fd, NULL); + removed = 1; + break; + } + } + pthread_mutex_unlock(&looper->lock); + return removed; +} + +static int find_entry(ALooper* looper, int fd, struct looper_entry* out) { + pthread_mutex_lock(&looper->lock); + int found = 0; + for (int i = 0; i < looper->count; ++i) { + if (looper->entries[i].fd == fd) { + *out = looper->entries[i]; + found = 1; + break; + } + } + pthread_mutex_unlock(&looper->lock); + return found; +} + +static int64_t now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +// One wait: returns an ident, WAKE, TIMEOUT, ERROR, or CALLBACK after running callbacks. +static int poll_once(ALooper* looper, int timeout_ms, int* out_fd, int* out_events, void** out_data) { + struct epoll_event events[16]; + int n = epoll_wait(looper->epoll_fd, events, 16, timeout_ms); + if (n < 0) return errno == EINTR ? ALOOPER_POLL_WAKE : ALOOPER_POLL_ERROR; + if (n == 0) return ALOOPER_POLL_TIMEOUT; + int result = ALOOPER_POLL_WAKE; + int ran_callback = 0; + for (int i = 0; i < n; ++i) { + int fd = events[i].data.fd; + if (fd == looper->wake_fd) { + uint64_t value; + while (read(looper->wake_fd, &value, sizeof value) > 0) {} + continue; + } + struct looper_entry e; + if (!find_entry(looper, fd, &e)) continue; + int ev = from_epoll(events[i].events); + if (e.callback) { + if (e.callback(fd, ev, e.data) == 0) ALooper_removeFd(looper, fd); + ran_callback = 1; + } else if (result == ALOOPER_POLL_WAKE) { + if (out_fd) *out_fd = fd; + if (out_events) *out_events = ev; + if (out_data) *out_data = e.data; + result = e.ident; + } + } + if (result == ALOOPER_POLL_WAKE && ran_callback) return ALOOPER_POLL_CALLBACK; + return result; +} + +int ALooper_pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { + ALooper* looper = ALooper_forThread(); + if (!looper) return ALOOPER_POLL_ERROR; + return poll_once(looper, timeoutMillis, outFd, outEvents, outData); +} + +int ALooper_pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outData) { + ALooper* looper = ALooper_forThread(); + if (!looper) return ALOOPER_POLL_ERROR; + int64_t deadline = timeoutMillis > 0 ? now_ms() + timeoutMillis : 0; + for (;;) { + int left = timeoutMillis; + if (timeoutMillis > 0) { + int64_t l = deadline - now_ms(); + left = l > 0 ? (int)l : 0; + } + int r = poll_once(looper, left, outFd, outEvents, outData); + if (r != ALOOPER_POLL_CALLBACK) return r; + if (timeoutMillis == 0) return ALOOPER_POLL_CALLBACK; + if (timeoutMillis > 0 && now_ms() >= deadline) return ALOOPER_POLL_TIMEOUT; + } +} + +// --- sensors: none ------------------------------------------------------------------------- + +typedef struct ASensorManager { int unused; } ASensorManager; +typedef struct ASensorEventQueue { int unused; } ASensorEventQueue; +typedef struct ASensor ASensor; +typedef ASensor const* ASensorRef; +typedef ASensorRef const* ASensorList; + +static ASensorManager g_sensor_manager; +static ASensorRef g_no_sensors[1] = {NULL}; + +ASensorManager* ASensorManager_getInstance(void) { return &g_sensor_manager; } +ASensorManager* ASensorManager_getInstanceForPackage(const char* packageName) { return &g_sensor_manager; } + +int ASensorManager_getSensorList(ASensorManager* manager, ASensorList* list) { + if (list) *list = g_no_sensors; + return 0; +} + +ASensorRef ASensorManager_getDefaultSensor(ASensorManager* manager, int type) { return NULL; } +ASensorRef ASensorManager_getDefaultSensorEx(ASensorManager* manager, int type, int wakeUp) { return NULL; } + +ASensorEventQueue* ASensorManager_createEventQueue(ASensorManager* manager, ALooper* looper, int ident, + ALooper_callbackFunc callback, void* data) { + return calloc(1, sizeof(ASensorEventQueue)); +} + +int ASensorManager_destroyEventQueue(ASensorManager* manager, ASensorEventQueue* queue) { + free(queue); + return 0; +} + +int ASensorEventQueue_registerSensor(ASensorEventQueue* queue, ASensor const* sensor, int32_t samplingPeriodUs, + int64_t maxBatchReportLatencyUs) { + return -EINVAL; +} +int ASensorEventQueue_enableSensor(ASensorEventQueue* queue, ASensor const* sensor) { return -EINVAL; } +int ASensorEventQueue_disableSensor(ASensorEventQueue* queue, ASensor const* sensor) { return 0; } +int ASensorEventQueue_setEventRate(ASensorEventQueue* queue, ASensor const* sensor, int32_t usec) { return -EINVAL; } +int ASensorEventQueue_hasEvents(ASensorEventQueue* queue) { return 0; } +ssize_t ASensorEventQueue_getEvents(ASensorEventQueue* queue, void* events, size_t count) { return 0; } +const char* ASensor_getName(ASensor const* sensor) { return ""; } +const char* ASensor_getVendor(ASensor const* sensor) { return ""; } +int ASensor_getType(ASensor const* sensor) { return -1; } +float ASensor_getResolution(ASensor const* sensor) { return 0; } +int ASensor_getMinDelay(ASensor const* sensor) { return 0; } +int ASensor_getFifoMaxEventCount(ASensor const* sensor) { return 0; } +int ASensor_getReportingMode(ASensor const* sensor) { return 0; } +int ASensor_isWakeUpSensor(ASensor const* sensor) { return 0; } + +// --- tracing: disabled -------------------------------------------------------------------- + +int ATrace_isEnabled(void) { return 0; } +void ATrace_beginSection(const char* sectionName) {} +void ATrace_endSection(void) {} +void ATrace_beginAsyncSection(const char* sectionName, int32_t cookie) {} +void ATrace_endAsyncSection(const char* sectionName, int32_t cookie) {} +void ATrace_setCounter(const char* counterName, int64_t counterValue) {} + +// --- hardware buffers ---------------------------------------------------------------------- +// No gralloc: allocation fails as on a device out of buffer memory. +int AHardwareBuffer_allocate(const void* desc, void** out) { + if (out) *out = NULL; + return -ENOMEM; +} +void AHardwareBuffer_acquire(void* buffer) {} +void AHardwareBuffer_release(void* buffer) {} +void AHardwareBuffer_describe(const void* buffer, void* desc) {} diff --git a/direct/guest/proxy/libvulkan.cpp b/direct/guest/proxy/libvulkan.cpp new file mode 100644 index 0000000..9af56b9 --- /dev/null +++ b/direct/guest/proxy/libvulkan.cpp @@ -0,0 +1,99 @@ +// libvulkan.so for AXRB Direct: the Vulkan loader, forwarding to the host's GPU. +// +// Every command is a thunk that packs its arguments into 64-bit slots and traps +// to the host ("vk.call" with the command's index), which unpacks them into the +// host driver's function (direct/src/vk_host.cpp). Structures are not copied: +// guest memory is the host's, and guest and host Vulkan structures have the same +// layout. The thunks are generated (direct/tools/gen_vulkan.py). +#define VK_USE_PLATFORM_ANDROID_KHR 1 +#include + +#include +#include + +#include + +#include "../axrb_guest.h" + +extern "C" uint64_t axrb_vk_call(uint32_t id, const uint64_t* args); +// Whether the host has `id` for the given instance (kInstance) or device +// (kDevice) handle; global commands are always present. +extern "C" uint64_t axrb_vk_query(uint32_t id, uint64_t handle, uint32_t level); +AXRB_HOST_FUNCTION(axrb_vk_call, "vk.call"); +AXRB_HOST_FUNCTION(axrb_vk_query, "vk.query"); + +namespace { + +enum Level : uint32_t { kGlobal, kInstance, kDevice }; + +struct VkCommand { + const char* name; + PFN_vkVoidFunction fn; + Level level; +}; + +template +inline uint64_t u64(T v) { + if constexpr (std::is_same_v) { + uint32_t bits; + memcpy(&bits, &v, sizeof bits); + return bits; + } else if constexpr (std::is_pointer_v) { + return reinterpret_cast(v); + } else { + return static_cast(v); + } +} + +template +inline R from_u64(uint64_t v) { + if constexpr (std::is_pointer_v) { + return reinterpret_cast(static_cast(v)); + } else if constexpr (std::is_enum_v) { + return static_cast(static_cast>(v)); + } else { + return static_cast(v); + } +} + +} // namespace + +#include "vk_thunks.inc" + +namespace { + +const VkCommand* find_command(const char* name, uint32_t* id) { + if (name == nullptr) return nullptr; + for (uint32_t i = 0; i < sizeof kCommands / sizeof kCommands[0]; ++i) { + if (strcmp(kCommands[i].name, name) == 0) { + *id = i; + return &kCommands[i]; + } + } + return nullptr; +} + +} // namespace + +extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device, const char* pName); + +extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance, const char* pName) { + if (pName == nullptr) return nullptr; + if (strcmp(pName, "vkGetInstanceProcAddr") == 0) return reinterpret_cast(vkGetInstanceProcAddr); + if (strcmp(pName, "vkGetDeviceProcAddr") == 0) return reinterpret_cast(vkGetDeviceProcAddr); + uint32_t id; + const VkCommand* c = find_command(pName, &id); + if (c == nullptr) return nullptr; + if (c->level == kGlobal) return c->fn; + if (instance == VK_NULL_HANDLE) return nullptr; + return axrb_vk_query(id, u64(instance), kInstance) ? c->fn : nullptr; +} + +extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device, const char* pName) { + if (pName == nullptr || device == VK_NULL_HANDLE) return nullptr; + if (strcmp(pName, "vkGetDeviceProcAddr") == 0) return reinterpret_cast(vkGetDeviceProcAddr); + uint32_t id; + const VkCommand* c = find_command(pName, &id); + if (c == nullptr || c->level != kDevice) return nullptr; + return axrb_vk_query(id, u64(device), kDevice) ? c->fn : nullptr; +} diff --git a/direct/guest/proxy/native_activity.c b/direct/guest/proxy/native_activity.c new file mode 100644 index 0000000..3b4824d --- /dev/null +++ b/direct/guest/proxy/native_activity.c @@ -0,0 +1,354 @@ +// The native half of android.app.NativeActivity (frameworks/base +// core/jni/android_app_NativeActivity.cpp), as plain guest code in +// libandroid.so: the framework's NativeActivity loads this library and its +// natives resolve here. Callbacks run on the activity's main thread, as on +// Android. There is no input queue: VR input comes from OpenXR. +#include +#include +#include +#include +#include + +typedef struct ANativeWindow ANativeWindow; +typedef struct AAssetManager AAssetManager; +typedef struct AInputQueue AInputQueue; +typedef struct ARect { + int32_t left, top, right, bottom; +} ARect; + +struct ANativeActivity; +typedef struct ANativeActivityCallbacks { + void (*onStart)(struct ANativeActivity*); + void (*onResume)(struct ANativeActivity*); + void* (*onSaveInstanceState)(struct ANativeActivity*, size_t* outSize); + void (*onPause)(struct ANativeActivity*); + void (*onStop)(struct ANativeActivity*); + void (*onDestroy)(struct ANativeActivity*); + void (*onWindowFocusChanged)(struct ANativeActivity*, int hasFocus); + void (*onNativeWindowCreated)(struct ANativeActivity*, ANativeWindow*); + void (*onNativeWindowResized)(struct ANativeActivity*, ANativeWindow*); + void (*onNativeWindowRedrawNeeded)(struct ANativeActivity*, ANativeWindow*); + void (*onNativeWindowDestroyed)(struct ANativeActivity*, ANativeWindow*); + void (*onInputQueueCreated)(struct ANativeActivity*, AInputQueue*); + void (*onInputQueueDestroyed)(struct ANativeActivity*, AInputQueue*); + void (*onContentRectChanged)(struct ANativeActivity*, const ARect*); + void (*onConfigurationChanged)(struct ANativeActivity*); + void (*onLowMemory)(struct ANativeActivity*); +} ANativeActivityCallbacks; + +typedef struct ANativeActivity { + ANativeActivityCallbacks* callbacks; + JavaVM* vm; + JNIEnv* env; + jobject clazz; + const char* internalDataPath; + const char* externalDataPath; + int32_t sdkVersion; + void* instance; + AAssetManager* assetManager; + const char* obbPath; +} ANativeActivity; + +typedef void ANativeActivity_createFunc(ANativeActivity*, void* savedState, size_t savedStateSize); + +ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface); +void ANativeWindow_release(ANativeWindow* window); +AAssetManager* AAssetManager_fromJava(JNIEnv* env, jobject assetManager); + +// NativeCode in the AOSP implementation. +typedef struct NativeCode { + ANativeActivity activity; // first: ANativeActivity* and NativeCode* are interchangeable + ANativeActivityCallbacks callbacks; + void* dlhandle; + ANativeWindow* window; + jobject assets; // global reference keeping the AssetManager alive + char* internal_path; + char* external_path; + char* obb_path; + int32_t last_window_width, last_window_height; +} NativeCode; + +static char* utf(JNIEnv* env, jstring s) { + if (!s) return NULL; + const char* chars = (*env)->GetStringUTFChars(env, s, NULL); + char* copy = strdup(chars ? chars : ""); + if (chars) (*env)->ReleaseStringUTFChars(env, s, chars); + return copy; +} + +static char g_dlerror[512]; + +static NativeCode* code_of(jlong handle) { return (NativeCode*)(intptr_t)handle; } + +JNIEXPORT jlong JNICALL Java_android_app_NativeActivity_loadNativeCode( + JNIEnv* env, jobject clazz, jstring path, jstring funcName, jobject messageQueue, jstring internalDataDir, + jstring obbDir, jstring externalDataDir, jint sdkVersion, jobject jAssetMgr, jbyteArray savedState, + jobject classLoader, jstring libraryPath) { + (void)messageQueue; + (void)classLoader; + (void)libraryPath; + char* lib = utf(env, path); + char* func = utf(env, funcName); + void* handle = dlopen(lib, RTLD_NOW); + if (!handle) { + const char* e = dlerror(); + strncpy(g_dlerror, e ? e : "dlopen failed", sizeof g_dlerror - 1); + free(lib); + free(func); + return 0; + } + ANativeActivity_createFunc* create = (ANativeActivity_createFunc*)dlsym(handle, func); + if (!create) { + strncpy(g_dlerror, "native activity entry point not found", sizeof g_dlerror - 1); + free(lib); + free(func); + return 0; + } + free(lib); + free(func); + + NativeCode* code = calloc(1, sizeof *code); + code->dlhandle = handle; + code->activity.callbacks = &code->callbacks; + (*env)->GetJavaVM(env, &code->activity.vm); + code->activity.env = env; + code->activity.clazz = (*env)->NewGlobalRef(env, clazz); + code->internal_path = utf(env, internalDataDir); + code->external_path = utf(env, externalDataDir); + code->obb_path = utf(env, obbDir); + code->activity.internalDataPath = code->internal_path; + code->activity.externalDataPath = code->external_path; + code->activity.obbPath = code->obb_path; + code->activity.sdkVersion = sdkVersion; + code->assets = jAssetMgr ? (*env)->NewGlobalRef(env, jAssetMgr) : NULL; + code->activity.assetManager = jAssetMgr ? AAssetManager_fromJava(env, jAssetMgr) : NULL; + + jbyte* state = NULL; + jsize state_size = 0; + if (savedState) { + state_size = (*env)->GetArrayLength(env, savedState); + if (state_size > 0) { + state = malloc(state_size); + (*env)->GetByteArrayRegion(env, savedState, 0, state_size, state); + } + } + create(&code->activity, state, (size_t)state_size); + free(state); + return (jlong)(intptr_t)code; +} + +JNIEXPORT jstring JNICALL Java_android_app_NativeActivity_getDlError(JNIEnv* env, jobject clazz) { + (void)clazz; + return (*env)->NewStringUTF(env, g_dlerror); +} + +JNIEXPORT void JNICALL Java_android_app_NativeActivity_unloadNativeCode(JNIEnv* env, jobject clazz, jlong handle) { + (void)clazz; + NativeCode* code = code_of(handle); + if (!code) return; + if (code->callbacks.onDestroy) code->callbacks.onDestroy(&code->activity); + if (code->window) ANativeWindow_release(code->window); + if (code->activity.clazz) (*env)->DeleteGlobalRef(env, code->activity.clazz); + if (code->assets) (*env)->DeleteGlobalRef(env, code->assets); + // Like Android, the library stays loaded: its threads may still be running. + free(code->internal_path); + free(code->external_path); + free(code->obb_path); + free(code); +} + +#define LIFECYCLE(java_name, callback) \ + JNIEXPORT void JNICALL Java_android_app_NativeActivity_##java_name(JNIEnv* env, jobject clazz, \ + jlong handle) { \ + (void)env; \ + (void)clazz; \ + NativeCode* code = code_of(handle); \ + if (code && code->callbacks.callback) code->callbacks.callback(&code->activity); \ + } + +LIFECYCLE(onStartNative, onStart) +LIFECYCLE(onResumeNative, onResume) +LIFECYCLE(onPauseNative, onPause) +LIFECYCLE(onStopNative, onStop) +LIFECYCLE(onConfigurationChangedNative, onConfigurationChanged) +LIFECYCLE(onLowMemoryNative, onLowMemory) + +JNIEXPORT jbyteArray JNICALL Java_android_app_NativeActivity_onSaveInstanceStateNative(JNIEnv* env, jobject clazz, + jlong handle) { + (void)clazz; + NativeCode* code = code_of(handle); + if (!code || !code->callbacks.onSaveInstanceState) return NULL; + size_t size = 0; + jbyte* state = (jbyte*)code->callbacks.onSaveInstanceState(&code->activity, &size); + jbyteArray array = NULL; + if (state) { + if (size > 0) { + array = (*env)->NewByteArray(env, (jsize)size); + if (array) (*env)->SetByteArrayRegion(env, array, 0, (jsize)size, state); + } + free(state); + } + return array; +} + +JNIEXPORT void JNICALL Java_android_app_NativeActivity_onWindowFocusChangedNative(JNIEnv* env, jobject clazz, + jlong handle, jboolean focused) { + (void)env; + (void)clazz; + NativeCode* code = code_of(handle); + if (code && code->callbacks.onWindowFocusChanged) code->callbacks.onWindowFocusChanged(&code->activity, focused ? 1 : 0); +} + +static void destroy_window(NativeCode* code) { + if (!code->window) return; + if (code->callbacks.onNativeWindowDestroyed) code->callbacks.onNativeWindowDestroyed(&code->activity, code->window); + ANativeWindow_release(code->window); + code->window = NULL; +} + +JNIEXPORT void JNICALL Java_android_app_NativeActivity_onSurfaceCreatedNative(JNIEnv* env, jobject clazz, jlong handle, + jobject surface) { + (void)clazz; + NativeCode* code = code_of(handle); + if (!code) return; + destroy_window(code); + code->window = surface ? ANativeWindow_fromSurface(env, surface) : NULL; + if (code->window && code->callbacks.onNativeWindowCreated) + code->callbacks.onNativeWindowCreated(&code->activity, code->window); +} + +JNIEXPORT void JNICALL Java_android_app_NativeActivity_onSurfaceChangedNative(JNIEnv* env, jobject clazz, jlong handle, + jobject surface, jint format, jint width, + jint height) { + (void)clazz; + (void)format; + NativeCode* code = code_of(handle); + if (!code) return; + ANativeWindow* old = code->window; + ANativeWindow* fresh = surface ? ANativeWindow_fromSurface(env, surface) : NULL; + if (fresh && old && fresh != old) { + // The host hands out one ANativeWindow per surface: a different one is a new surface. + destroy_window(code); + code->window = fresh; + if (code->callbacks.onNativeWindowCreated) code->callbacks.onNativeWindowCreated(&code->activity, fresh); + } else if (!old && fresh) { + code->window = fresh; + if (code->callbacks.onNativeWindowCreated) code->callbacks.onNativeWindowCreated(&code->activity, fresh); + } else { + if (fresh) ANativeWindow_release(fresh); + if (code->window && (width != code->last_window_width || height != code->last_window_height) && + code->callbacks.onNativeWindowResized) + code->callbacks.onNativeWindowResized(&code->activity, code->window); + } + code->last_window_width = width; + code->last_window_height = height; +} + +JNIEXPORT void JNICALL Java_android_app_NativeActivity_onSurfaceRedrawNeededNative(JNIEnv* env, jobject clazz, + jlong handle, jobject surface) { + (void)env; + (void)clazz; + (void)surface; + NativeCode* code = code_of(handle); + if (code && code->window && code->callbacks.onNativeWindowRedrawNeeded) + code->callbacks.onNativeWindowRedrawNeeded(&code->activity, code->window); +} + +JNIEXPORT void JNICALL Java_android_app_NativeActivity_onSurfaceDestroyedNative(JNIEnv* env, jobject clazz, + jlong handle) { + (void)env; + (void)clazz; + NativeCode* code = code_of(handle); + if (code) destroy_window(code); +} + +JNIEXPORT void JNICALL Java_android_app_NativeActivity_onContentRectChangedNative(JNIEnv* env, jobject clazz, + jlong handle, jint x, jint y, + jint w, jint h) { + (void)env; + (void)clazz; + NativeCode* code = code_of(handle); + if (!code || !code->callbacks.onContentRectChanged) return; + ARect rect = {x, y, x + w, y + h}; + code->callbacks.onContentRectChanged(&code->activity, &rect); +} + +// --- ANativeActivity_* : calls back into the Java activity --------------------------------- + +static void call_activity(ANativeActivity* activity, const char* name, const char* sig, int a, int b, int argc) { + JNIEnv* env = NULL; + if ((*activity->vm)->GetEnv(activity->vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*activity->vm)->AttachCurrentThread(activity->vm, &env, NULL) != JNI_OK) return; + } + jclass cls = (*env)->GetObjectClass(env, activity->clazz); + jmethodID m = (*env)->GetMethodID(env, cls, name, sig); + if (m) { + if (argc == 0) (*env)->CallVoidMethod(env, activity->clazz, m); + else if (argc == 1) (*env)->CallVoidMethod(env, activity->clazz, m, a); + else (*env)->CallVoidMethod(env, activity->clazz, m, a, b); + } + if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); + (*env)->DeleteLocalRef(env, cls); +} + +void ANativeActivity_finish(ANativeActivity* activity) { call_activity(activity, "finish", "()V", 0, 0, 0); } + +void ANativeActivity_setWindowFormat(ANativeActivity* activity, int32_t format) { + call_activity(activity, "setWindowFormat", "(I)V", format, 0, 1); +} + +void ANativeActivity_setWindowFlags(ANativeActivity* activity, uint32_t addFlags, uint32_t removeFlags) { + call_activity(activity, "setWindowFlags", "(II)V", (int)addFlags, (int)(addFlags | removeFlags), 2); +} + +void ANativeActivity_showSoftInput(ANativeActivity* activity, uint32_t flags) { (void)activity; (void)flags; } + +void ANativeActivity_hideSoftInput(ANativeActivity* activity, uint32_t flags) { (void)activity; (void)flags; } + +// --- input events: no queue ever exists (NativeActivity never creates one) ---------------- + +typedef struct AInputEvent AInputEvent; +typedef struct ALooper ALooper; + +void AInputQueue_attachLooper(AInputQueue* queue, ALooper* looper, int ident, void* callback, void* data) {} +void AInputQueue_detachLooper(AInputQueue* queue) {} +int32_t AInputQueue_hasEvents(AInputQueue* queue) { return 0; } +int32_t AInputQueue_getEvent(AInputQueue* queue, AInputEvent** outEvent) { + if (outEvent) *outEvent = NULL; + return -1; +} +int32_t AInputQueue_preDispatchEvent(AInputQueue* queue, AInputEvent* event) { return 0; } +void AInputQueue_finishEvent(AInputQueue* queue, AInputEvent* event, int handled) {} + +#define EVENT_GETTER(type, name) \ + type name(const AInputEvent* event) { return 0; } +EVENT_GETTER(int32_t, AInputEvent_getType) +EVENT_GETTER(int32_t, AInputEvent_getDeviceId) +EVENT_GETTER(int32_t, AInputEvent_getSource) +EVENT_GETTER(int32_t, AKeyEvent_getAction) +EVENT_GETTER(int32_t, AKeyEvent_getFlags) +EVENT_GETTER(int32_t, AKeyEvent_getKeyCode) +EVENT_GETTER(int32_t, AKeyEvent_getScanCode) +EVENT_GETTER(int32_t, AKeyEvent_getMetaState) +EVENT_GETTER(int32_t, AKeyEvent_getRepeatCount) +EVENT_GETTER(int64_t, AKeyEvent_getDownTime) +EVENT_GETTER(int64_t, AKeyEvent_getEventTime) +EVENT_GETTER(int32_t, AMotionEvent_getAction) +EVENT_GETTER(int32_t, AMotionEvent_getFlags) +EVENT_GETTER(int32_t, AMotionEvent_getMetaState) +EVENT_GETTER(int32_t, AMotionEvent_getButtonState) +EVENT_GETTER(int32_t, AMotionEvent_getEdgeFlags) +EVENT_GETTER(int64_t, AMotionEvent_getDownTime) +EVENT_GETTER(int64_t, AMotionEvent_getEventTime) +EVENT_GETTER(size_t, AMotionEvent_getPointerCount) +EVENT_GETTER(size_t, AMotionEvent_getHistorySize) + +int32_t AMotionEvent_getPointerId(const AInputEvent* event, size_t pointerIndex) { return 0; } +int32_t AMotionEvent_getToolType(const AInputEvent* event, size_t pointerIndex) { return 0; } +float AMotionEvent_getX(const AInputEvent* event, size_t pointerIndex) { return 0; } +float AMotionEvent_getY(const AInputEvent* event, size_t pointerIndex) { return 0; } +float AMotionEvent_getRawX(const AInputEvent* event, size_t pointerIndex) { return 0; } +float AMotionEvent_getRawY(const AInputEvent* event, size_t pointerIndex) { return 0; } +float AMotionEvent_getPressure(const AInputEvent* event, size_t pointerIndex) { return 0; } +float AMotionEvent_getSize(const AInputEvent* event, size_t pointerIndex) { return 0; } +float AMotionEvent_getAxisValue(const AInputEvent* event, int32_t axis, size_t pointerIndex) { return 0; } diff --git a/direct/guest/proxy/opensles_output.inc b/direct/guest/proxy/opensles_output.inc new file mode 100644 index 0000000..d7caff2 --- /dev/null +++ b/direct/guest/proxy/opensles_output.inc @@ -0,0 +1,495 @@ +#include +#include +#include +#include +#include + +typedef struct OutputObject OutputObject; +typedef struct { + const void *table; + OutputObject *owner; +} OutputInterface; +typedef struct { + const uint8_t *data; + uint32_t bytes, used; +} OutputBuffer; +struct OutputObject { + OutputInterface object, engine, play, queue, volume, config; + SLuint32 kind, state, playState, queueCapacity, head, count, completed; + SLDataFormat_PCM pcm; + SLuint32 representation, streamType, performance; + SLmillibel level; + SLboolean mute; + slAndroidSimpleBufferQueueCallback callback; + void *callbackContext; + uint64_t frames; + pthread_mutex_t lock; + AAudioStream *stream; + OutputBuffer *buffers; +}; +static OutputObject *owner(const void *self) { + return ((const OutputInterface *)self)->owner; +} +static int iid_equal(SLInterfaceID a, SLInterfaceID b) { + return a && b && !memcmp(a, b, sizeof(*a)); +} +static SLresult unsupported(void) { return SL_RESULT_FEATURE_UNSUPPORTED; } +static SLresult object_interface(SLObjectItf self, const SLInterfaceID iid, + void *result) { + if (!result) + return SL_RESULT_PARAMETER_INVALID; + OutputObject *o = owner(self); + void *itf = 0; + if (iid_equal(iid, SL_IID_OBJECT)) + itf = &o->object; + if (o->kind == SL_OBJECTID_ENGINE && iid_equal(iid, SL_IID_ENGINE)) + itf = &o->engine; + if (o->kind == SL_OBJECTID_AUDIOPLAYER) { + if (iid_equal(iid, SL_IID_PLAY)) + itf = &o->play; + if (iid_equal(iid, SL_IID_BUFFERQUEUE) || + iid_equal(iid, SL_IID_ANDROIDSIMPLEBUFFERQUEUE)) + itf = &o->queue; + if (iid_equal(iid, SL_IID_VOLUME)) + itf = &o->volume; + if (iid_equal(iid, SL_IID_ANDROIDCONFIGURATION)) + itf = &o->config; + } + *(void **)result = itf; + return itf ? SL_RESULT_SUCCESS : SL_RESULT_FEATURE_UNSUPPORTED; +} +static aaudio_data_callback_result_t output_callback(AAudioStream *stream, + void *context, + void *output, + int32_t frames) { + (void)stream; + OutputObject *o = context; + float *dst = output; + int32_t done = 0; + while (done < frames) { + pthread_mutex_lock(&o->lock); + if (!o->count || o->playState != SL_PLAYSTATE_PLAYING) { + pthread_mutex_unlock(&o->lock); + break; + } + OutputBuffer *b = &o->buffers[o->head]; + uint32_t sampleBytes = o->pcm.containerSize / 8, + frameBytes = sampleBytes * o->pcm.numChannels; + uint32_t count = (b->bytes - b->used) / frameBytes; + if (count > (uint32_t)(frames - done)) + count = frames - done; + float gain = o->mute ? 0.f : powf(10.f, o->level / 2000.f); + const uint8_t *src = b->data + b->used; + for (uint32_t i = 0; i < count * o->pcm.numChannels; ++i) { + float value = 0; + if (o->representation == SL_ANDROID_PCM_REPRESENTATION_FLOAT && + sampleBytes == 4) + memcpy(&value, src + i * 4, 4); + else if (sampleBytes == 2) { + int16_t n; + memcpy(&n, src + i * 2, 2); + value = n / 32768.f; + } else if (sampleBytes == 1) + value = (src[i] - 128) / 128.f; + else if (sampleBytes == 4) { + int32_t n; + memcpy(&n, src + i * 4, 4); + value = n / 2147483648.f; + } else if (sampleBytes == 3) { + const uint8_t *s = src + i * 3; + int32_t n = (int32_t)((uint32_t)s[0] << 8 | (uint32_t)s[1] << 16 | + (uint32_t)s[2] << 24); + value = n / 2147483648.f; + } + dst[(size_t)done * o->pcm.numChannels + i] = value * gain; + } + b->used += count * frameBytes; + done += count; + o->frames += count; + int completed = b->used == b->bytes; + slAndroidSimpleBufferQueueCallback callback = o->callback; + void *user = o->callbackContext; + if (completed) { + o->head = (o->head + 1) % o->queueCapacity; + --o->count; + ++o->completed; + } + pthread_mutex_unlock(&o->lock); + if (completed && callback) + callback((SLAndroidSimpleBufferQueueItf)&o->queue, user); + } + memset(dst + (size_t)done * o->pcm.numChannels, 0, + (size_t)(frames - done) * o->pcm.numChannels * sizeof(float)); + return AAUDIO_CALLBACK_RESULT_CONTINUE; +} +static SLresult object_realize(SLObjectItf self, SLboolean async) { + OutputObject *o = owner(self); + if (async) + return SL_RESULT_FEATURE_UNSUPPORTED; + if (o->state == SL_OBJECT_STATE_REALIZED) + return SL_RESULT_PRECONDITIONS_VIOLATED; + if (o->kind == SL_OBJECTID_AUDIOPLAYER) { + AAudioStreamBuilder *builder = 0; + if (AAudio_createStreamBuilder(&builder) != AAUDIO_OK) + return SL_RESULT_RESOURCE_ERROR; + AAudioStreamBuilder_setSampleRate(builder, o->pcm.samplesPerSec / 1000); + AAudioStreamBuilder_setChannelCount(builder, o->pcm.numChannels); + AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT); + AAudioStreamBuilder_setDataCallback(builder, output_callback, o); + AAudioStreamBuilder_setPerformanceMode(builder, + AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); + aaudio_result_t result = + AAudioStreamBuilder_openStream(builder, &o->stream); + AAudioStreamBuilder_delete(builder); + if (result != AAUDIO_OK) + return SL_RESULT_RESOURCE_ERROR; + } + o->state = SL_OBJECT_STATE_REALIZED; + return SL_RESULT_SUCCESS; +} +static SLresult object_state(SLObjectItf self, SLuint32 *state) { + if (!state) + return SL_RESULT_PARAMETER_INVALID; + *state = owner(self)->state; + return 0; +} +static void object_destroy(SLObjectItf self) { + OutputObject *o = owner(self); + if (o->stream) { + AAudioStream_requestStop(o->stream); + AAudioStream_close(o->stream); + } + pthread_mutex_destroy(&o->lock); + free(o->buffers); + free(o); +} +static SLresult play_set(SLPlayItf self, SLuint32 state) { + OutputObject *o = owner(self); + if (state < SL_PLAYSTATE_STOPPED || state > SL_PLAYSTATE_PLAYING || + !o->stream) + return SL_RESULT_PRECONDITIONS_VIOLATED; + pthread_mutex_lock(&o->lock); + o->playState = state; + pthread_mutex_unlock(&o->lock); + aaudio_result_t result = + state == SL_PLAYSTATE_PLAYING ? AAudioStream_requestStart(o->stream) + : state == SL_PLAYSTATE_PAUSED ? AAudioStream_requestPause(o->stream) + : AAudioStream_requestStop(o->stream); + return result == AAUDIO_OK ? 0 : SL_RESULT_RESOURCE_ERROR; +} +static SLresult play_state(SLPlayItf self, SLuint32 *value) { + if (!value) + return 2; + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + *value = o->playState; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult play_position(SLPlayItf self, SLmillisecond *value) { + if (!value) + return 2; + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + *value = (SLmillisecond)(o->frames * 1000000 / o->pcm.samplesPerSec); + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult play_duration(SLPlayItf self, SLmillisecond *value) { + (void)self; + if (!value) + return 2; + *value = SL_TIME_UNKNOWN; + return 0; +} +static SLresult queue_enqueue(SLAndroidSimpleBufferQueueItf self, + const void *data, SLuint32 bytes) { + OutputObject *o = owner(self); + if (!data || !bytes || + bytes % (o->pcm.numChannels * (o->pcm.containerSize / 8))) + return SL_RESULT_PARAMETER_INVALID; + pthread_mutex_lock(&o->lock); + if (o->count == o->queueCapacity) { + pthread_mutex_unlock(&o->lock); + return SL_RESULT_BUFFER_INSUFFICIENT; + } + o->buffers[(o->head + o->count) % o->queueCapacity] = + (OutputBuffer){data, bytes, 0}; + ++o->count; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult queue_clear(SLAndroidSimpleBufferQueueItf self) { + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + o->head = o->count = o->completed = 0; + pthread_mutex_unlock(&o->lock); + if (o->stream) + AAudioStream_requestFlush(o->stream); + return 0; +} +static SLresult queue_state(SLAndroidSimpleBufferQueueItf self, + SLAndroidSimpleBufferQueueState *state) { + if (!state) + return 2; + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + state->count = o->count; + state->index = o->completed; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult queue_callback(SLAndroidSimpleBufferQueueItf self, + slAndroidSimpleBufferQueueCallback cb, + void *user) { + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + o->callback = cb; + o->callbackContext = user; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult volume_set(SLVolumeItf self, SLmillibel level) { + if (level > 0) + return 2; + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + o->level = level; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult volume_get(SLVolumeItf self, SLmillibel *value) { + if (!value) + return 2; + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + *value = o->level; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult volume_max(SLVolumeItf self, SLmillibel *value) { + (void)self; + if (!value) + return 2; + *value = 0; + return 0; +} +static SLresult mute_set(SLVolumeItf self, SLboolean value) { + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + o->mute = value; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLresult mute_get(SLVolumeItf self, SLboolean *value) { + if (!value) + return 2; + OutputObject *o = owner(self); + pthread_mutex_lock(&o->lock); + *value = o->mute; + pthread_mutex_unlock(&o->lock); + return 0; +} +static SLuint32 *configuration(OutputObject *o, const SLchar *key) { + if (!key) + return 0; + if (!strcmp((const char *)key, (const char *)SL_ANDROID_KEY_STREAM_TYPE)) + return &o->streamType; + if (!strcmp((const char *)key, (const char *)SL_ANDROID_KEY_PERFORMANCE_MODE)) + return &o->performance; + return 0; +} +static SLresult config_set(SLAndroidConfigurationItf self, const SLchar *key, + const void *value, SLuint32 size) { + OutputObject *o = owner(self); + SLuint32 *slot = configuration(o, key); + if (!slot) + return SL_RESULT_FEATURE_UNSUPPORTED; + if (!value || size != 4) + return 2; + if (o->state != SL_OBJECT_STATE_UNREALIZED) + return 1; + memcpy(slot, value, 4); + return 0; +} +static SLresult config_get(SLAndroidConfigurationItf self, const SLchar *key, + SLuint32 *size, void *value) { + SLuint32 *slot = configuration(owner(self), key); + if (!slot) + return SL_RESULT_FEATURE_UNSUPPORTED; + if (!size) + return 2; + SLuint32 capacity = *size; + *size = 4; + if (!value) + return 0; + if (capacity < 4) + return SL_RESULT_BUFFER_INSUFFICIENT; + memcpy(value, slot, 4); + return 0; +} +static struct SLObjectItf_ objectTable; +static struct SLEngineItf_ engineTable; +static struct SLPlayItf_ playTable; +static struct SLAndroidSimpleBufferQueueItf_ queueTable; +static struct SLVolumeItf_ volumeTable; +static struct SLAndroidConfigurationItf_ configTable; +static pthread_once_t tablesOnce = PTHREAD_ONCE_INIT; +static void initialize_tables(void); +static OutputObject *create_object(SLuint32 kind) { + pthread_once(&tablesOnce, initialize_tables); + OutputObject *o = calloc(1, sizeof(*o)); + if (!o) + return 0; + pthread_mutex_init(&o->lock, 0); + o->kind = kind; + o->state = SL_OBJECT_STATE_UNREALIZED; + o->playState = SL_PLAYSTATE_STOPPED; + o->streamType = SL_ANDROID_STREAM_MEDIA; + o->object = (OutputInterface){&objectTable, o}; + o->engine = (OutputInterface){&engineTable, o}; + o->play = (OutputInterface){&playTable, o}; + o->queue = (OutputInterface){&queueTable, o}; + o->volume = (OutputInterface){&volumeTable, o}; + o->config = (OutputInterface){&configTable, o}; + return o; +} +static SLresult check_interfaces(OutputObject *o, SLuint32 count, + const SLInterfaceID *ids, + const SLboolean *required) { + if (count && (!ids || !required)) + return 2; + for (SLuint32 i = 0; i < count; ++i) { + void *p; + if (required[i] && object_interface((SLObjectItf)&o->object, ids[i], &p)) + return SL_RESULT_FEATURE_UNSUPPORTED; + } + return 0; +} +static SLresult engine_mix(SLEngineItf self, SLObjectItf *out, SLuint32 count, + const SLInterfaceID *ids, + const SLboolean *required) { + (void)self; + if (!out) + return 2; + *out = 0; + OutputObject *o = create_object(SL_OBJECTID_OUTPUTMIX); + if (!o) + return 3; + SLresult result = check_interfaces(o, count, ids, required); + if (result) { + object_destroy((SLObjectItf)&o->object); + return result; + } + *out = (SLObjectItf)&o->object; + return 0; +} +static SLresult engine_player(SLEngineItf self, SLObjectItf *out, + SLDataSource *source, SLDataSink *sink, + SLuint32 count, const SLInterfaceID *ids, + const SLboolean *required) { + (void)self; + if (!out || !source || !source->pLocator || !source->pFormat || !sink || + !sink->pLocator) + return 2; + *out = 0; + SLDataLocator_AndroidSimpleBufferQueue *locator = source->pLocator; + SLDataFormat_PCM *pcm = source->pFormat; + if ((locator->locatorType != SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE && + locator->locatorType != SL_DATALOCATOR_BUFFERQUEUE) || + (pcm->formatType != SL_DATAFORMAT_PCM && + pcm->formatType != SL_ANDROID_DATAFORMAT_PCM_EX) || + *(SLuint32 *)sink->pLocator != SL_DATALOCATOR_OUTPUTMIX) + return SL_RESULT_CONTENT_UNSUPPORTED; + if (!locator->numBuffers || locator->numBuffers > 1024 || !pcm->numChannels || + pcm->numChannels > 8 || !pcm->samplesPerSec || + pcm->endianness != SL_BYTEORDER_LITTLEENDIAN || + (pcm->containerSize != 8 && pcm->containerSize != 16 && + pcm->containerSize != 24 && pcm->containerSize != 32)) + return SL_RESULT_PARAMETER_INVALID; + OutputObject *o = create_object(SL_OBJECTID_AUDIOPLAYER); + if (!o) + return 3; + o->pcm = *pcm; + o->queueCapacity = locator->numBuffers; + o->representation = pcm->formatType == SL_ANDROID_DATAFORMAT_PCM_EX + ? ((SLAndroidDataFormat_PCM_EX *)pcm)->representation + : SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT; + o->buffers = calloc(o->queueCapacity, sizeof(OutputBuffer)); + if (!o->buffers) { + object_destroy((SLObjectItf)&o->object); + return 3; + } + SLresult result = check_interfaces(o, count, ids, required); + if (result) { + object_destroy((SLObjectItf)&o->object); + return result; + } + *out = (SLObjectItf)&o->object; + return 0; +} +static void fill_table(void *table, size_t bytes) { + void **slots = table; + for (size_t i = 0; i < bytes / sizeof(void *); ++i) + slots[i] = (void *)unsupported; +} +static void initialize_tables(void) { + fill_table(&objectTable, sizeof(objectTable)); + fill_table(&engineTable, sizeof(engineTable)); + fill_table(&playTable, sizeof(playTable)); + fill_table(&queueTable, sizeof(queueTable)); + fill_table(&volumeTable, sizeof(volumeTable)); + fill_table(&configTable, sizeof(configTable)); + objectTable.Realize = object_realize; + objectTable.GetState = object_state; + objectTable.GetInterface = object_interface; + objectTable.Destroy = object_destroy; + engineTable.CreateAudioPlayer = engine_player; + engineTable.CreateOutputMix = engine_mix; + playTable.SetPlayState = play_set; + playTable.GetPlayState = play_state; + playTable.GetDuration = play_duration; + playTable.GetPosition = play_position; + queueTable.Enqueue = queue_enqueue; + queueTable.Clear = queue_clear; + queueTable.GetState = queue_state; + queueTable.RegisterCallback = queue_callback; + volumeTable.SetVolumeLevel = volume_set; + volumeTable.GetVolumeLevel = volume_get; + volumeTable.GetMaxVolumeLevel = volume_max; + volumeTable.SetMute = mute_set; + volumeTable.GetMute = mute_get; + configTable.SetConfiguration = config_set; + configTable.GetConfiguration = config_get; +} +SLresult slCreateEngine(SLObjectItf *out, SLuint32 optionCount, + const SLEngineOption *options, SLuint32 count, + const SLInterfaceID *ids, const SLboolean *required) { + (void)optionCount; + (void)options; + if (!out) + return 2; + *out = 0; + OutputObject *o = create_object(SL_OBJECTID_ENGINE); + if (!o) + return 3; + SLresult result = check_interfaces(o, count, ids, required); + if (result) { + object_destroy((SLObjectItf)&o->object); + return result; + } + *out = (SLObjectItf)&o->object; + return 0; +} +SLresult slQueryNumSupportedEngineInterfaces(SLuint32 *count) { + if (!count) + return 2; + *count = 1; + return 0; +} +SLresult slQuerySupportedEngineInterfaces(SLuint32 index, SLInterfaceID *iid) { + if (!iid || index) + return 2; + *iid = SL_IID_ENGINE; + return 0; +} diff --git a/direct/guest/proxy/vk_thunks.inc b/direct/guest/proxy/vk_thunks.inc new file mode 100644 index 0000000..f6459e5 --- /dev/null +++ b/direct/guest/proxy/vk_thunks.inc @@ -0,0 +1,3125 @@ +// Generated by direct/tools/gen_vulkan.py from the NDK's Vulkan headers. Do not edit. +// 624 commands +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance) { + const uint64_t a[] = {u64(pCreateInfo), u64(pAllocator), u64(pInstance)}; + return from_u64(axrb_vk_call(0, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(instance), u64(pAllocator)}; + axrb_vk_call(1, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumeratePhysicalDevices(VkInstance instance, uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices) { + const uint64_t a[] = {u64(instance), u64(pPhysicalDeviceCount), u64(pPhysicalDevices)}; + return from_u64(axrb_vk_call(2, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures(VkPhysicalDevice physicalDevice, VkPhysicalDeviceFeatures* pFeatures) { + const uint64_t a[] = {u64(physicalDevice), u64(pFeatures)}; + axrb_vk_call(3, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties(VkPhysicalDevice physicalDevice, VkFormat format, VkFormatProperties* pFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(format), u64(pFormatProperties)}; + axrb_vk_call(4, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties(VkPhysicalDevice physicalDevice, VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties* pImageFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(format), u64(type), u64(tiling), u64(usage), u64(flags), u64(pImageFormatProperties)}; + return from_u64(axrb_vk_call(5, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceProperties(VkPhysicalDevice physicalDevice, VkPhysicalDeviceProperties* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pProperties)}; + axrb_vk_call(6, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyProperties(VkPhysicalDevice physicalDevice, uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties* pQueueFamilyProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pQueueFamilyPropertyCount), u64(pQueueFamilyProperties)}; + axrb_vk_call(7, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties(VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties* pMemoryProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pMemoryProperties)}; + axrb_vk_call(8, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice) { + const uint64_t a[] = {u64(physicalDevice), u64(pCreateInfo), u64(pAllocator), u64(pDevice)}; + return from_u64(axrb_vk_call(9, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(pAllocator)}; + axrb_vk_call(10, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceExtensionProperties(const char* pLayerName, uint32_t* pPropertyCount, VkExtensionProperties* pProperties) { + const uint64_t a[] = {u64(pLayerName), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(11, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateDeviceExtensionProperties(VkPhysicalDevice physicalDevice, const char* pLayerName, uint32_t* pPropertyCount, VkExtensionProperties* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pLayerName), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(12, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceLayerProperties(uint32_t* pPropertyCount, VkLayerProperties* pProperties) { + const uint64_t a[] = {u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(13, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateDeviceLayerProperties(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkLayerProperties* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(14, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceQueue(VkDevice device, uint32_t queueFamilyIndex, uint32_t queueIndex, VkQueue* pQueue) { + const uint64_t a[] = {u64(device), u64(queueFamilyIndex), u64(queueIndex), u64(pQueue)}; + axrb_vk_call(15, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(VkQueue queue, uint32_t submitCount, const VkSubmitInfo* pSubmits, VkFence fence) { + const uint64_t a[] = {u64(queue), u64(submitCount), u64(pSubmits), u64(fence)}; + return from_u64(axrb_vk_call(16, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkQueueWaitIdle(VkQueue queue) { + const uint64_t a[] = {u64(queue)}; + return from_u64(axrb_vk_call(17, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkDeviceWaitIdle(VkDevice device) { + const uint64_t a[] = {u64(device)}; + return from_u64(axrb_vk_call(18, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory(VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator, VkDeviceMemory* pMemory) { + const uint64_t a[] = {u64(device), u64(pAllocateInfo), u64(pAllocator), u64(pMemory)}; + return from_u64(axrb_vk_call(19, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(memory), u64(pAllocator)}; + axrb_vk_call(20, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize size, VkMemoryMapFlags flags, void** ppData) { + const uint64_t a[] = {u64(device), u64(memory), u64(offset), u64(size), u64(flags), u64(ppData)}; + return from_u64(axrb_vk_call(21, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice device, VkDeviceMemory memory) { + const uint64_t a[] = {u64(device), u64(memory)}; + axrb_vk_call(22, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkFlushMappedMemoryRanges(VkDevice device, uint32_t memoryRangeCount, const VkMappedMemoryRange* pMemoryRanges) { + const uint64_t a[] = {u64(device), u64(memoryRangeCount), u64(pMemoryRanges)}; + return from_u64(axrb_vk_call(23, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkInvalidateMappedMemoryRanges(VkDevice device, uint32_t memoryRangeCount, const VkMappedMemoryRange* pMemoryRanges) { + const uint64_t a[] = {u64(device), u64(memoryRangeCount), u64(pMemoryRanges)}; + return from_u64(axrb_vk_call(24, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceMemoryCommitment(VkDevice device, VkDeviceMemory memory, VkDeviceSize* pCommittedMemoryInBytes) { + const uint64_t a[] = {u64(device), u64(memory), u64(pCommittedMemoryInBytes)}; + axrb_vk_call(25, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize memoryOffset) { + const uint64_t a[] = {u64(device), u64(buffer), u64(memory), u64(memoryOffset)}; + return from_u64(axrb_vk_call(26, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize memoryOffset) { + const uint64_t a[] = {u64(device), u64(image), u64(memory), u64(memoryOffset)}; + return from_u64(axrb_vk_call(27, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements(VkDevice device, VkBuffer buffer, VkMemoryRequirements* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(buffer), u64(pMemoryRequirements)}; + axrb_vk_call(28, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements(VkDevice device, VkImage image, VkMemoryRequirements* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(image), u64(pMemoryRequirements)}; + axrb_vk_call(29, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageSparseMemoryRequirements(VkDevice device, VkImage image, uint32_t* pSparseMemoryRequirementCount, VkSparseImageMemoryRequirements* pSparseMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(image), u64(pSparseMemoryRequirementCount), u64(pSparseMemoryRequirements)}; + axrb_vk_call(30, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceSparseImageFormatProperties(VkPhysicalDevice physicalDevice, VkFormat format, VkImageType type, VkSampleCountFlagBits samples, VkImageUsageFlags usage, VkImageTiling tiling, uint32_t* pPropertyCount, VkSparseImageFormatProperties* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(format), u64(type), u64(samples), u64(usage), u64(tiling), u64(pPropertyCount), u64(pProperties)}; + axrb_vk_call(31, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkQueueBindSparse(VkQueue queue, uint32_t bindInfoCount, const VkBindSparseInfo* pBindInfo, VkFence fence) { + const uint64_t a[] = {u64(queue), u64(bindInfoCount), u64(pBindInfo), u64(fence)}; + return from_u64(axrb_vk_call(32, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateFence(VkDevice device, const VkFenceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkFence* pFence) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pFence)}; + return from_u64(axrb_vk_call(33, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(fence), u64(pAllocator)}; + axrb_vk_call(34, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkResetFences(VkDevice device, uint32_t fenceCount, const VkFence* pFences) { + const uint64_t a[] = {u64(device), u64(fenceCount), u64(pFences)}; + return from_u64(axrb_vk_call(35, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetFenceStatus(VkDevice device, VkFence fence) { + const uint64_t a[] = {u64(device), u64(fence)}; + return from_u64(axrb_vk_call(36, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkWaitForFences(VkDevice device, uint32_t fenceCount, const VkFence* pFences, VkBool32 waitAll, uint64_t timeout) { + const uint64_t a[] = {u64(device), u64(fenceCount), u64(pFences), u64(waitAll), u64(timeout)}; + return from_u64(axrb_vk_call(37, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateSemaphore(VkDevice device, const VkSemaphoreCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSemaphore* pSemaphore) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pSemaphore)}; + return from_u64(axrb_vk_call(38, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroySemaphore(VkDevice device, VkSemaphore semaphore, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(semaphore), u64(pAllocator)}; + axrb_vk_call(39, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateEvent(VkDevice device, const VkEventCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkEvent* pEvent) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pEvent)}; + return from_u64(axrb_vk_call(40, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyEvent(VkDevice device, VkEvent event, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(event), u64(pAllocator)}; + axrb_vk_call(41, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetEventStatus(VkDevice device, VkEvent event) { + const uint64_t a[] = {u64(device), u64(event)}; + return from_u64(axrb_vk_call(42, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSetEvent(VkDevice device, VkEvent event) { + const uint64_t a[] = {u64(device), u64(event)}; + return from_u64(axrb_vk_call(43, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkResetEvent(VkDevice device, VkEvent event) { + const uint64_t a[] = {u64(device), u64(event)}; + return from_u64(axrb_vk_call(44, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateQueryPool(VkDevice device, const VkQueryPoolCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkQueryPool* pQueryPool) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pQueryPool)}; + return from_u64(axrb_vk_call(45, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyQueryPool(VkDevice device, VkQueryPool queryPool, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(queryPool), u64(pAllocator)}; + axrb_vk_call(46, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetQueryPoolResults(VkDevice device, VkQueryPool queryPool, uint32_t firstQuery, uint32_t queryCount, size_t dataSize, void* pData, VkDeviceSize stride, VkQueryResultFlags flags) { + const uint64_t a[] = {u64(device), u64(queryPool), u64(firstQuery), u64(queryCount), u64(dataSize), u64(pData), u64(stride), u64(flags)}; + return from_u64(axrb_vk_call(47, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateBuffer(VkDevice device, const VkBufferCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkBuffer* pBuffer) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pBuffer)}; + return from_u64(axrb_vk_call(48, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(buffer), u64(pAllocator)}; + axrb_vk_call(49, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateBufferView(VkDevice device, const VkBufferViewCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkBufferView* pView) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pView)}; + return from_u64(axrb_vk_call(50, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyBufferView(VkDevice device, VkBufferView bufferView, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(bufferView), u64(pAllocator)}; + axrb_vk_call(51, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(VkDevice device, const VkImageCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImage* pImage) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pImage)}; + return from_u64(axrb_vk_call(52, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(image), u64(pAllocator)}; + axrb_vk_call(53, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageSubresourceLayout(VkDevice device, VkImage image, const VkImageSubresource* pSubresource, VkSubresourceLayout* pLayout) { + const uint64_t a[] = {u64(device), u64(image), u64(pSubresource), u64(pLayout)}; + axrb_vk_call(54, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateImageView(VkDevice device, const VkImageViewCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImageView* pView) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pView)}; + return from_u64(axrb_vk_call(55, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyImageView(VkDevice device, VkImageView imageView, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(imageView), u64(pAllocator)}; + axrb_vk_call(56, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateShaderModule(VkDevice device, const VkShaderModuleCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkShaderModule* pShaderModule) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pShaderModule)}; + return from_u64(axrb_vk_call(57, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyShaderModule(VkDevice device, VkShaderModule shaderModule, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(shaderModule), u64(pAllocator)}; + axrb_vk_call(58, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreatePipelineCache(VkDevice device, const VkPipelineCacheCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkPipelineCache* pPipelineCache) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pPipelineCache)}; + return from_u64(axrb_vk_call(59, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyPipelineCache(VkDevice device, VkPipelineCache pipelineCache, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(pipelineCache), u64(pAllocator)}; + axrb_vk_call(60, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPipelineCacheData(VkDevice device, VkPipelineCache pipelineCache, size_t* pDataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(pipelineCache), u64(pDataSize), u64(pData)}; + return from_u64(axrb_vk_call(61, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkMergePipelineCaches(VkDevice device, VkPipelineCache dstCache, uint32_t srcCacheCount, const VkPipelineCache* pSrcCaches) { + const uint64_t a[] = {u64(device), u64(dstCache), u64(srcCacheCount), u64(pSrcCaches)}; + return from_u64(axrb_vk_call(62, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateGraphicsPipelines(VkDevice device, VkPipelineCache pipelineCache, uint32_t createInfoCount, const VkGraphicsPipelineCreateInfo* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkPipeline* pPipelines) { + const uint64_t a[] = {u64(device), u64(pipelineCache), u64(createInfoCount), u64(pCreateInfos), u64(pAllocator), u64(pPipelines)}; + return from_u64(axrb_vk_call(63, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateComputePipelines(VkDevice device, VkPipelineCache pipelineCache, uint32_t createInfoCount, const VkComputePipelineCreateInfo* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkPipeline* pPipelines) { + const uint64_t a[] = {u64(device), u64(pipelineCache), u64(createInfoCount), u64(pCreateInfos), u64(pAllocator), u64(pPipelines)}; + return from_u64(axrb_vk_call(64, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyPipeline(VkDevice device, VkPipeline pipeline, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(pipeline), u64(pAllocator)}; + axrb_vk_call(65, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreatePipelineLayout(VkDevice device, const VkPipelineLayoutCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkPipelineLayout* pPipelineLayout) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pPipelineLayout)}; + return from_u64(axrb_vk_call(66, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyPipelineLayout(VkDevice device, VkPipelineLayout pipelineLayout, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(pipelineLayout), u64(pAllocator)}; + axrb_vk_call(67, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateSampler(VkDevice device, const VkSamplerCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSampler* pSampler) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pSampler)}; + return from_u64(axrb_vk_call(68, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroySampler(VkDevice device, VkSampler sampler, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(sampler), u64(pAllocator)}; + axrb_vk_call(69, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDescriptorSetLayout(VkDevice device, const VkDescriptorSetLayoutCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDescriptorSetLayout* pSetLayout) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pSetLayout)}; + return from_u64(axrb_vk_call(70, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDescriptorSetLayout(VkDevice device, VkDescriptorSetLayout descriptorSetLayout, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(descriptorSetLayout), u64(pAllocator)}; + axrb_vk_call(71, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDescriptorPool(VkDevice device, const VkDescriptorPoolCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDescriptorPool* pDescriptorPool) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pDescriptorPool)}; + return from_u64(axrb_vk_call(72, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDescriptorPool(VkDevice device, VkDescriptorPool descriptorPool, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(descriptorPool), u64(pAllocator)}; + axrb_vk_call(73, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkResetDescriptorPool(VkDevice device, VkDescriptorPool descriptorPool, VkDescriptorPoolResetFlags flags) { + const uint64_t a[] = {u64(device), u64(descriptorPool), u64(flags)}; + return from_u64(axrb_vk_call(74, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAllocateDescriptorSets(VkDevice device, const VkDescriptorSetAllocateInfo* pAllocateInfo, VkDescriptorSet* pDescriptorSets) { + const uint64_t a[] = {u64(device), u64(pAllocateInfo), u64(pDescriptorSets)}; + return from_u64(axrb_vk_call(75, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkFreeDescriptorSets(VkDevice device, VkDescriptorPool descriptorPool, uint32_t descriptorSetCount, const VkDescriptorSet* pDescriptorSets) { + const uint64_t a[] = {u64(device), u64(descriptorPool), u64(descriptorSetCount), u64(pDescriptorSets)}; + return from_u64(axrb_vk_call(76, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkUpdateDescriptorSets(VkDevice device, uint32_t descriptorWriteCount, const VkWriteDescriptorSet* pDescriptorWrites, uint32_t descriptorCopyCount, const VkCopyDescriptorSet* pDescriptorCopies) { + const uint64_t a[] = {u64(device), u64(descriptorWriteCount), u64(pDescriptorWrites), u64(descriptorCopyCount), u64(pDescriptorCopies)}; + axrb_vk_call(77, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateFramebuffer(VkDevice device, const VkFramebufferCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkFramebuffer* pFramebuffer) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pFramebuffer)}; + return from_u64(axrb_vk_call(78, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyFramebuffer(VkDevice device, VkFramebuffer framebuffer, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(framebuffer), u64(pAllocator)}; + axrb_vk_call(79, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateRenderPass(VkDevice device, const VkRenderPassCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkRenderPass* pRenderPass) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pRenderPass)}; + return from_u64(axrb_vk_call(80, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyRenderPass(VkDevice device, VkRenderPass renderPass, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(renderPass), u64(pAllocator)}; + axrb_vk_call(81, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetRenderAreaGranularity(VkDevice device, VkRenderPass renderPass, VkExtent2D* pGranularity) { + const uint64_t a[] = {u64(device), u64(renderPass), u64(pGranularity)}; + axrb_vk_call(82, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(VkDevice device, const VkCommandPoolCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkCommandPool* pCommandPool) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pCommandPool)}; + return from_u64(axrb_vk_call(83, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool(VkDevice device, VkCommandPool commandPool, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(commandPool), u64(pAllocator)}; + axrb_vk_call(84, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(VkDevice device, VkCommandPool commandPool, VkCommandPoolResetFlags flags) { + const uint64_t a[] = {u64(device), u64(commandPool), u64(flags)}; + return from_u64(axrb_vk_call(85, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAllocateCommandBuffers(VkDevice device, const VkCommandBufferAllocateInfo* pAllocateInfo, VkCommandBuffer* pCommandBuffers) { + const uint64_t a[] = {u64(device), u64(pAllocateInfo), u64(pCommandBuffers)}; + return from_u64(axrb_vk_call(86, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers(VkDevice device, VkCommandPool commandPool, uint32_t commandBufferCount, const VkCommandBuffer* pCommandBuffers) { + const uint64_t a[] = {u64(device), u64(commandPool), u64(commandBufferCount), u64(pCommandBuffers)}; + axrb_vk_call(87, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(VkCommandBuffer commandBuffer, const VkCommandBufferBeginInfo* pBeginInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pBeginInfo)}; + return from_u64(axrb_vk_call(88, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEndCommandBuffer(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + return from_u64(axrb_vk_call(89, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(VkCommandBuffer commandBuffer, VkCommandBufferResetFlags flags) { + const uint64_t a[] = {u64(commandBuffer), u64(flags)}; + return from_u64(axrb_vk_call(90, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindPipeline(VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineBindPoint), u64(pipeline)}; + axrb_vk_call(91, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetViewport(VkCommandBuffer commandBuffer, uint32_t firstViewport, uint32_t viewportCount, const VkViewport* pViewports) { + const uint64_t a[] = {u64(commandBuffer), u64(firstViewport), u64(viewportCount), u64(pViewports)}; + axrb_vk_call(92, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetScissor(VkCommandBuffer commandBuffer, uint32_t firstScissor, uint32_t scissorCount, const VkRect2D* pScissors) { + const uint64_t a[] = {u64(commandBuffer), u64(firstScissor), u64(scissorCount), u64(pScissors)}; + axrb_vk_call(93, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetLineWidth(VkCommandBuffer commandBuffer, float lineWidth) { + const uint64_t a[] = {u64(commandBuffer), u64(lineWidth)}; + axrb_vk_call(94, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthBias(VkCommandBuffer commandBuffer, float depthBiasConstantFactor, float depthBiasClamp, float depthBiasSlopeFactor) { + const uint64_t a[] = {u64(commandBuffer), u64(depthBiasConstantFactor), u64(depthBiasClamp), u64(depthBiasSlopeFactor)}; + axrb_vk_call(95, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetBlendConstants(VkCommandBuffer commandBuffer, const float* blendConstants) { + const uint64_t a[] = {u64(commandBuffer), u64(blendConstants)}; + axrb_vk_call(96, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthBounds(VkCommandBuffer commandBuffer, float minDepthBounds, float maxDepthBounds) { + const uint64_t a[] = {u64(commandBuffer), u64(minDepthBounds), u64(maxDepthBounds)}; + axrb_vk_call(97, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetStencilCompareMask(VkCommandBuffer commandBuffer, VkStencilFaceFlags faceMask, uint32_t compareMask) { + const uint64_t a[] = {u64(commandBuffer), u64(faceMask), u64(compareMask)}; + axrb_vk_call(98, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetStencilWriteMask(VkCommandBuffer commandBuffer, VkStencilFaceFlags faceMask, uint32_t writeMask) { + const uint64_t a[] = {u64(commandBuffer), u64(faceMask), u64(writeMask)}; + axrb_vk_call(99, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetStencilReference(VkCommandBuffer commandBuffer, VkStencilFaceFlags faceMask, uint32_t reference) { + const uint64_t a[] = {u64(commandBuffer), u64(faceMask), u64(reference)}; + axrb_vk_call(100, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorSets(VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipelineLayout layout, uint32_t firstSet, uint32_t descriptorSetCount, const VkDescriptorSet* pDescriptorSets, uint32_t dynamicOffsetCount, const uint32_t* pDynamicOffsets) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineBindPoint), u64(layout), u64(firstSet), u64(descriptorSetCount), u64(pDescriptorSets), u64(dynamicOffsetCount), u64(pDynamicOffsets)}; + axrb_vk_call(101, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindIndexBuffer(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkIndexType indexType) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(indexType)}; + axrb_vk_call(102, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindVertexBuffers(VkCommandBuffer commandBuffer, uint32_t firstBinding, uint32_t bindingCount, const VkBuffer* pBuffers, const VkDeviceSize* pOffsets) { + const uint64_t a[] = {u64(commandBuffer), u64(firstBinding), u64(bindingCount), u64(pBuffers), u64(pOffsets)}; + axrb_vk_call(103, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDraw(VkCommandBuffer commandBuffer, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) { + const uint64_t a[] = {u64(commandBuffer), u64(vertexCount), u64(instanceCount), u64(firstVertex), u64(firstInstance)}; + axrb_vk_call(104, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexed(VkCommandBuffer commandBuffer, uint32_t indexCount, uint32_t instanceCount, uint32_t firstIndex, int32_t vertexOffset, uint32_t firstInstance) { + const uint64_t a[] = {u64(commandBuffer), u64(indexCount), u64(instanceCount), u64(firstIndex), u64(vertexOffset), u64(firstInstance)}; + axrb_vk_call(105, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirect(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, uint32_t drawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(drawCount), u64(stride)}; + axrb_vk_call(106, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirect(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, uint32_t drawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(drawCount), u64(stride)}; + axrb_vk_call(107, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDispatch(VkCommandBuffer commandBuffer, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { + const uint64_t a[] = {u64(commandBuffer), u64(groupCountX), u64(groupCountY), u64(groupCountZ)}; + axrb_vk_call(108, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDispatchIndirect(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset)}; + axrb_vk_call(109, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyBuffer(VkCommandBuffer commandBuffer, VkBuffer srcBuffer, VkBuffer dstBuffer, uint32_t regionCount, const VkBufferCopy* pRegions) { + const uint64_t a[] = {u64(commandBuffer), u64(srcBuffer), u64(dstBuffer), u64(regionCount), u64(pRegions)}; + axrb_vk_call(110, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage(VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout, VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, const VkImageCopy* pRegions) { + const uint64_t a[] = {u64(commandBuffer), u64(srcImage), u64(srcImageLayout), u64(dstImage), u64(dstImageLayout), u64(regionCount), u64(pRegions)}; + axrb_vk_call(111, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBlitImage(VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout, VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, const VkImageBlit* pRegions, VkFilter filter) { + const uint64_t a[] = {u64(commandBuffer), u64(srcImage), u64(srcImageLayout), u64(dstImage), u64(dstImageLayout), u64(regionCount), u64(pRegions), u64(filter)}; + axrb_vk_call(112, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage(VkCommandBuffer commandBuffer, VkBuffer srcBuffer, VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, const VkBufferImageCopy* pRegions) { + const uint64_t a[] = {u64(commandBuffer), u64(srcBuffer), u64(dstImage), u64(dstImageLayout), u64(regionCount), u64(pRegions)}; + axrb_vk_call(113, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyImageToBuffer(VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout, VkBuffer dstBuffer, uint32_t regionCount, const VkBufferImageCopy* pRegions) { + const uint64_t a[] = {u64(commandBuffer), u64(srcImage), u64(srcImageLayout), u64(dstBuffer), u64(regionCount), u64(pRegions)}; + axrb_vk_call(114, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdUpdateBuffer(VkCommandBuffer commandBuffer, VkBuffer dstBuffer, VkDeviceSize dstOffset, VkDeviceSize dataSize, const void* pData) { + const uint64_t a[] = {u64(commandBuffer), u64(dstBuffer), u64(dstOffset), u64(dataSize), u64(pData)}; + axrb_vk_call(115, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdFillBuffer(VkCommandBuffer commandBuffer, VkBuffer dstBuffer, VkDeviceSize dstOffset, VkDeviceSize size, uint32_t data) { + const uint64_t a[] = {u64(commandBuffer), u64(dstBuffer), u64(dstOffset), u64(size), u64(data)}; + axrb_vk_call(116, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdClearColorImage(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout imageLayout, const VkClearColorValue* pColor, uint32_t rangeCount, const VkImageSubresourceRange* pRanges) { + const uint64_t a[] = {u64(commandBuffer), u64(image), u64(imageLayout), u64(pColor), u64(rangeCount), u64(pRanges)}; + axrb_vk_call(117, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdClearDepthStencilImage(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout imageLayout, const VkClearDepthStencilValue* pDepthStencil, uint32_t rangeCount, const VkImageSubresourceRange* pRanges) { + const uint64_t a[] = {u64(commandBuffer), u64(image), u64(imageLayout), u64(pDepthStencil), u64(rangeCount), u64(pRanges)}; + axrb_vk_call(118, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdClearAttachments(VkCommandBuffer commandBuffer, uint32_t attachmentCount, const VkClearAttachment* pAttachments, uint32_t rectCount, const VkClearRect* pRects) { + const uint64_t a[] = {u64(commandBuffer), u64(attachmentCount), u64(pAttachments), u64(rectCount), u64(pRects)}; + axrb_vk_call(119, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdResolveImage(VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout, VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, const VkImageResolve* pRegions) { + const uint64_t a[] = {u64(commandBuffer), u64(srcImage), u64(srcImageLayout), u64(dstImage), u64(dstImageLayout), u64(regionCount), u64(pRegions)}; + axrb_vk_call(120, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetEvent(VkCommandBuffer commandBuffer, VkEvent event, VkPipelineStageFlags stageMask) { + const uint64_t a[] = {u64(commandBuffer), u64(event), u64(stageMask)}; + axrb_vk_call(121, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdResetEvent(VkCommandBuffer commandBuffer, VkEvent event, VkPipelineStageFlags stageMask) { + const uint64_t a[] = {u64(commandBuffer), u64(event), u64(stageMask)}; + axrb_vk_call(122, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWaitEvents(VkCommandBuffer commandBuffer, uint32_t eventCount, const VkEvent* pEvents, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, uint32_t memoryBarrierCount, const VkMemoryBarrier* pMemoryBarriers, uint32_t bufferMemoryBarrierCount, const VkBufferMemoryBarrier* pBufferMemoryBarriers, uint32_t imageMemoryBarrierCount, const VkImageMemoryBarrier* pImageMemoryBarriers) { + const uint64_t a[] = {u64(commandBuffer), u64(eventCount), u64(pEvents), u64(srcStageMask), u64(dstStageMask), u64(memoryBarrierCount), u64(pMemoryBarriers), u64(bufferMemoryBarrierCount), u64(pBufferMemoryBarriers), u64(imageMemoryBarrierCount), u64(pImageMemoryBarriers)}; + axrb_vk_call(123, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier(VkCommandBuffer commandBuffer, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, VkDependencyFlags dependencyFlags, uint32_t memoryBarrierCount, const VkMemoryBarrier* pMemoryBarriers, uint32_t bufferMemoryBarrierCount, const VkBufferMemoryBarrier* pBufferMemoryBarriers, uint32_t imageMemoryBarrierCount, const VkImageMemoryBarrier* pImageMemoryBarriers) { + const uint64_t a[] = {u64(commandBuffer), u64(srcStageMask), u64(dstStageMask), u64(dependencyFlags), u64(memoryBarrierCount), u64(pMemoryBarriers), u64(bufferMemoryBarrierCount), u64(pBufferMemoryBarriers), u64(imageMemoryBarrierCount), u64(pImageMemoryBarriers)}; + axrb_vk_call(124, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginQuery(VkCommandBuffer commandBuffer, VkQueryPool queryPool, uint32_t query, VkQueryControlFlags flags) { + const uint64_t a[] = {u64(commandBuffer), u64(queryPool), u64(query), u64(flags)}; + axrb_vk_call(125, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndQuery(VkCommandBuffer commandBuffer, VkQueryPool queryPool, uint32_t query) { + const uint64_t a[] = {u64(commandBuffer), u64(queryPool), u64(query)}; + axrb_vk_call(126, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdResetQueryPool(VkCommandBuffer commandBuffer, VkQueryPool queryPool, uint32_t firstQuery, uint32_t queryCount) { + const uint64_t a[] = {u64(commandBuffer), u64(queryPool), u64(firstQuery), u64(queryCount)}; + axrb_vk_call(127, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteTimestamp(VkCommandBuffer commandBuffer, VkPipelineStageFlagBits pipelineStage, VkQueryPool queryPool, uint32_t query) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineStage), u64(queryPool), u64(query)}; + axrb_vk_call(128, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyQueryPoolResults(VkCommandBuffer commandBuffer, VkQueryPool queryPool, uint32_t firstQuery, uint32_t queryCount, VkBuffer dstBuffer, VkDeviceSize dstOffset, VkDeviceSize stride, VkQueryResultFlags flags) { + const uint64_t a[] = {u64(commandBuffer), u64(queryPool), u64(firstQuery), u64(queryCount), u64(dstBuffer), u64(dstOffset), u64(stride), u64(flags)}; + axrb_vk_call(129, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPushConstants(VkCommandBuffer commandBuffer, VkPipelineLayout layout, VkShaderStageFlags stageFlags, uint32_t offset, uint32_t size, const void* pValues) { + const uint64_t a[] = {u64(commandBuffer), u64(layout), u64(stageFlags), u64(offset), u64(size), u64(pValues)}; + axrb_vk_call(130, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginRenderPass(VkCommandBuffer commandBuffer, const VkRenderPassBeginInfo* pRenderPassBegin, VkSubpassContents contents) { + const uint64_t a[] = {u64(commandBuffer), u64(pRenderPassBegin), u64(contents)}; + axrb_vk_call(131, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdNextSubpass(VkCommandBuffer commandBuffer, VkSubpassContents contents) { + const uint64_t a[] = {u64(commandBuffer), u64(contents)}; + axrb_vk_call(132, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + axrb_vk_call(133, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdExecuteCommands(VkCommandBuffer commandBuffer, uint32_t commandBufferCount, const VkCommandBuffer* pCommandBuffers) { + const uint64_t a[] = {u64(commandBuffer), u64(commandBufferCount), u64(pCommandBuffers)}; + axrb_vk_call(134, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceVersion(uint32_t* pApiVersion) { + const uint64_t a[] = {u64(pApiVersion)}; + return from_u64(axrb_vk_call(135, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2(VkDevice device, uint32_t bindInfoCount, const VkBindBufferMemoryInfo* pBindInfos) { + const uint64_t a[] = {u64(device), u64(bindInfoCount), u64(pBindInfos)}; + return from_u64(axrb_vk_call(136, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory2(VkDevice device, uint32_t bindInfoCount, const VkBindImageMemoryInfo* pBindInfos) { + const uint64_t a[] = {u64(device), u64(bindInfoCount), u64(pBindInfos)}; + return from_u64(axrb_vk_call(137, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceGroupPeerMemoryFeatures(VkDevice device, uint32_t heapIndex, uint32_t localDeviceIndex, uint32_t remoteDeviceIndex, VkPeerMemoryFeatureFlags* pPeerMemoryFeatures) { + const uint64_t a[] = {u64(device), u64(heapIndex), u64(localDeviceIndex), u64(remoteDeviceIndex), u64(pPeerMemoryFeatures)}; + axrb_vk_call(138, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDeviceMask(VkCommandBuffer commandBuffer, uint32_t deviceMask) { + const uint64_t a[] = {u64(commandBuffer), u64(deviceMask)}; + axrb_vk_call(139, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDispatchBase(VkCommandBuffer commandBuffer, uint32_t baseGroupX, uint32_t baseGroupY, uint32_t baseGroupZ, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { + const uint64_t a[] = {u64(commandBuffer), u64(baseGroupX), u64(baseGroupY), u64(baseGroupZ), u64(groupCountX), u64(groupCountY), u64(groupCountZ)}; + axrb_vk_call(140, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumeratePhysicalDeviceGroups(VkInstance instance, uint32_t* pPhysicalDeviceGroupCount, VkPhysicalDeviceGroupProperties* pPhysicalDeviceGroupProperties) { + const uint64_t a[] = {u64(instance), u64(pPhysicalDeviceGroupCount), u64(pPhysicalDeviceGroupProperties)}; + return from_u64(axrb_vk_call(141, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements2(VkDevice device, const VkImageMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(142, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements2(VkDevice device, const VkBufferMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(143, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageSparseMemoryRequirements2(VkDevice device, const VkImageSparseMemoryRequirementsInfo2* pInfo, uint32_t* pSparseMemoryRequirementCount, VkSparseImageMemoryRequirements2* pSparseMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pSparseMemoryRequirementCount), u64(pSparseMemoryRequirements)}; + axrb_vk_call(144, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures2(VkPhysicalDevice physicalDevice, VkPhysicalDeviceFeatures2* pFeatures) { + const uint64_t a[] = {u64(physicalDevice), u64(pFeatures)}; + axrb_vk_call(145, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceProperties2(VkPhysicalDevice physicalDevice, VkPhysicalDeviceProperties2* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pProperties)}; + axrb_vk_call(146, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties2(VkPhysicalDevice physicalDevice, VkFormat format, VkFormatProperties2* pFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(format), u64(pFormatProperties)}; + axrb_vk_call(147, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties2(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceImageFormatInfo2* pImageFormatInfo, VkImageFormatProperties2* pImageFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pImageFormatInfo), u64(pImageFormatProperties)}; + return from_u64(axrb_vk_call(148, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyProperties2(VkPhysicalDevice physicalDevice, uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties2* pQueueFamilyProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pQueueFamilyPropertyCount), u64(pQueueFamilyProperties)}; + axrb_vk_call(149, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties2(VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties2* pMemoryProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pMemoryProperties)}; + axrb_vk_call(150, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceSparseImageFormatProperties2(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSparseImageFormatInfo2* pFormatInfo, uint32_t* pPropertyCount, VkSparseImageFormatProperties2* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pFormatInfo), u64(pPropertyCount), u64(pProperties)}; + axrb_vk_call(151, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkTrimCommandPool(VkDevice device, VkCommandPool commandPool, VkCommandPoolTrimFlags flags) { + const uint64_t a[] = {u64(device), u64(commandPool), u64(flags)}; + axrb_vk_call(152, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceQueue2(VkDevice device, const VkDeviceQueueInfo2* pQueueInfo, VkQueue* pQueue) { + const uint64_t a[] = {u64(device), u64(pQueueInfo), u64(pQueue)}; + axrb_vk_call(153, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateSamplerYcbcrConversion(VkDevice device, const VkSamplerYcbcrConversionCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSamplerYcbcrConversion* pYcbcrConversion) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pYcbcrConversion)}; + return from_u64(axrb_vk_call(154, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroySamplerYcbcrConversion(VkDevice device, VkSamplerYcbcrConversion ycbcrConversion, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(ycbcrConversion), u64(pAllocator)}; + axrb_vk_call(155, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDescriptorUpdateTemplate(VkDevice device, const VkDescriptorUpdateTemplateCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDescriptorUpdateTemplate* pDescriptorUpdateTemplate) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pDescriptorUpdateTemplate)}; + return from_u64(axrb_vk_call(156, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDescriptorUpdateTemplate(VkDevice device, VkDescriptorUpdateTemplate descriptorUpdateTemplate, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(descriptorUpdateTemplate), u64(pAllocator)}; + axrb_vk_call(157, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkUpdateDescriptorSetWithTemplate(VkDevice device, VkDescriptorSet descriptorSet, VkDescriptorUpdateTemplate descriptorUpdateTemplate, const void* pData) { + const uint64_t a[] = {u64(device), u64(descriptorSet), u64(descriptorUpdateTemplate), u64(pData)}; + axrb_vk_call(158, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceExternalBufferProperties(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalBufferInfo* pExternalBufferInfo, VkExternalBufferProperties* pExternalBufferProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pExternalBufferInfo), u64(pExternalBufferProperties)}; + axrb_vk_call(159, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceExternalFenceProperties(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalFenceInfo* pExternalFenceInfo, VkExternalFenceProperties* pExternalFenceProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pExternalFenceInfo), u64(pExternalFenceProperties)}; + axrb_vk_call(160, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceExternalSemaphoreProperties(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalSemaphoreInfo* pExternalSemaphoreInfo, VkExternalSemaphoreProperties* pExternalSemaphoreProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pExternalSemaphoreInfo), u64(pExternalSemaphoreProperties)}; + axrb_vk_call(161, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDescriptorSetLayoutSupport(VkDevice device, const VkDescriptorSetLayoutCreateInfo* pCreateInfo, VkDescriptorSetLayoutSupport* pSupport) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pSupport)}; + axrb_vk_call(162, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirectCount(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(163, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirectCount(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(164, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateRenderPass2(VkDevice device, const VkRenderPassCreateInfo2* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkRenderPass* pRenderPass) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pRenderPass)}; + return from_u64(axrb_vk_call(165, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginRenderPass2(VkCommandBuffer commandBuffer, const VkRenderPassBeginInfo* pRenderPassBegin, const VkSubpassBeginInfo* pSubpassBeginInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pRenderPassBegin), u64(pSubpassBeginInfo)}; + axrb_vk_call(166, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdNextSubpass2(VkCommandBuffer commandBuffer, const VkSubpassBeginInfo* pSubpassBeginInfo, const VkSubpassEndInfo* pSubpassEndInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pSubpassBeginInfo), u64(pSubpassEndInfo)}; + axrb_vk_call(167, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass2(VkCommandBuffer commandBuffer, const VkSubpassEndInfo* pSubpassEndInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pSubpassEndInfo)}; + axrb_vk_call(168, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkResetQueryPool(VkDevice device, VkQueryPool queryPool, uint32_t firstQuery, uint32_t queryCount) { + const uint64_t a[] = {u64(device), u64(queryPool), u64(firstQuery), u64(queryCount)}; + axrb_vk_call(169, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetSemaphoreCounterValue(VkDevice device, VkSemaphore semaphore, uint64_t* pValue) { + const uint64_t a[] = {u64(device), u64(semaphore), u64(pValue)}; + return from_u64(axrb_vk_call(170, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkWaitSemaphores(VkDevice device, const VkSemaphoreWaitInfo* pWaitInfo, uint64_t timeout) { + const uint64_t a[] = {u64(device), u64(pWaitInfo), u64(timeout)}; + return from_u64(axrb_vk_call(171, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSignalSemaphore(VkDevice device, const VkSemaphoreSignalInfo* pSignalInfo) { + const uint64_t a[] = {u64(device), u64(pSignalInfo)}; + return from_u64(axrb_vk_call(172, a)); +} +extern "C" VKAPI_ATTR VkDeviceAddress VKAPI_CALL vkGetBufferDeviceAddress(VkDevice device, const VkBufferDeviceAddressInfo* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(173, a)); +} +extern "C" VKAPI_ATTR uint64_t VKAPI_CALL vkGetBufferOpaqueCaptureAddress(VkDevice device, const VkBufferDeviceAddressInfo* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(174, a)); +} +extern "C" VKAPI_ATTR uint64_t VKAPI_CALL vkGetDeviceMemoryOpaqueCaptureAddress(VkDevice device, const VkDeviceMemoryOpaqueCaptureAddressInfo* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(175, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceToolProperties(VkPhysicalDevice physicalDevice, uint32_t* pToolCount, VkPhysicalDeviceToolProperties* pToolProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pToolCount), u64(pToolProperties)}; + return from_u64(axrb_vk_call(176, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreatePrivateDataSlot(VkDevice device, const VkPrivateDataSlotCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkPrivateDataSlot* pPrivateDataSlot) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pPrivateDataSlot)}; + return from_u64(axrb_vk_call(177, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyPrivateDataSlot(VkDevice device, VkPrivateDataSlot privateDataSlot, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(privateDataSlot), u64(pAllocator)}; + axrb_vk_call(178, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSetPrivateData(VkDevice device, VkObjectType objectType, uint64_t objectHandle, VkPrivateDataSlot privateDataSlot, uint64_t data) { + const uint64_t a[] = {u64(device), u64(objectType), u64(objectHandle), u64(privateDataSlot), u64(data)}; + return from_u64(axrb_vk_call(179, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPrivateData(VkDevice device, VkObjectType objectType, uint64_t objectHandle, VkPrivateDataSlot privateDataSlot, uint64_t* pData) { + const uint64_t a[] = {u64(device), u64(objectType), u64(objectHandle), u64(privateDataSlot), u64(pData)}; + axrb_vk_call(180, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetEvent2(VkCommandBuffer commandBuffer, VkEvent event, const VkDependencyInfo* pDependencyInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(event), u64(pDependencyInfo)}; + axrb_vk_call(181, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdResetEvent2(VkCommandBuffer commandBuffer, VkEvent event, VkPipelineStageFlags2 stageMask) { + const uint64_t a[] = {u64(commandBuffer), u64(event), u64(stageMask)}; + axrb_vk_call(182, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWaitEvents2(VkCommandBuffer commandBuffer, uint32_t eventCount, const VkEvent* pEvents, const VkDependencyInfo* pDependencyInfos) { + const uint64_t a[] = {u64(commandBuffer), u64(eventCount), u64(pEvents), u64(pDependencyInfos)}; + axrb_vk_call(183, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier2(VkCommandBuffer commandBuffer, const VkDependencyInfo* pDependencyInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pDependencyInfo)}; + axrb_vk_call(184, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteTimestamp2(VkCommandBuffer commandBuffer, VkPipelineStageFlags2 stage, VkQueryPool queryPool, uint32_t query) { + const uint64_t a[] = {u64(commandBuffer), u64(stage), u64(queryPool), u64(query)}; + axrb_vk_call(185, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2(VkQueue queue, uint32_t submitCount, const VkSubmitInfo2* pSubmits, VkFence fence) { + const uint64_t a[] = {u64(queue), u64(submitCount), u64(pSubmits), u64(fence)}; + return from_u64(axrb_vk_call(186, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyBuffer2(VkCommandBuffer commandBuffer, const VkCopyBufferInfo2* pCopyBufferInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyBufferInfo)}; + axrb_vk_call(187, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage2(VkCommandBuffer commandBuffer, const VkCopyImageInfo2* pCopyImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyImageInfo)}; + axrb_vk_call(188, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage2(VkCommandBuffer commandBuffer, const VkCopyBufferToImageInfo2* pCopyBufferToImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyBufferToImageInfo)}; + axrb_vk_call(189, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyImageToBuffer2(VkCommandBuffer commandBuffer, const VkCopyImageToBufferInfo2* pCopyImageToBufferInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyImageToBufferInfo)}; + axrb_vk_call(190, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBlitImage2(VkCommandBuffer commandBuffer, const VkBlitImageInfo2* pBlitImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pBlitImageInfo)}; + axrb_vk_call(191, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdResolveImage2(VkCommandBuffer commandBuffer, const VkResolveImageInfo2* pResolveImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pResolveImageInfo)}; + axrb_vk_call(192, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginRendering(VkCommandBuffer commandBuffer, const VkRenderingInfo* pRenderingInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pRenderingInfo)}; + axrb_vk_call(193, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndRendering(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + axrb_vk_call(194, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCullMode(VkCommandBuffer commandBuffer, VkCullModeFlags cullMode) { + const uint64_t a[] = {u64(commandBuffer), u64(cullMode)}; + axrb_vk_call(195, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetFrontFace(VkCommandBuffer commandBuffer, VkFrontFace frontFace) { + const uint64_t a[] = {u64(commandBuffer), u64(frontFace)}; + axrb_vk_call(196, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetPrimitiveTopology(VkCommandBuffer commandBuffer, VkPrimitiveTopology primitiveTopology) { + const uint64_t a[] = {u64(commandBuffer), u64(primitiveTopology)}; + axrb_vk_call(197, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetViewportWithCount(VkCommandBuffer commandBuffer, uint32_t viewportCount, const VkViewport* pViewports) { + const uint64_t a[] = {u64(commandBuffer), u64(viewportCount), u64(pViewports)}; + axrb_vk_call(198, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetScissorWithCount(VkCommandBuffer commandBuffer, uint32_t scissorCount, const VkRect2D* pScissors) { + const uint64_t a[] = {u64(commandBuffer), u64(scissorCount), u64(pScissors)}; + axrb_vk_call(199, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindVertexBuffers2(VkCommandBuffer commandBuffer, uint32_t firstBinding, uint32_t bindingCount, const VkBuffer* pBuffers, const VkDeviceSize* pOffsets, const VkDeviceSize* pSizes, const VkDeviceSize* pStrides) { + const uint64_t a[] = {u64(commandBuffer), u64(firstBinding), u64(bindingCount), u64(pBuffers), u64(pOffsets), u64(pSizes), u64(pStrides)}; + axrb_vk_call(200, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthTestEnable(VkCommandBuffer commandBuffer, VkBool32 depthTestEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthTestEnable)}; + axrb_vk_call(201, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthWriteEnable(VkCommandBuffer commandBuffer, VkBool32 depthWriteEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthWriteEnable)}; + axrb_vk_call(202, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthCompareOp(VkCommandBuffer commandBuffer, VkCompareOp depthCompareOp) { + const uint64_t a[] = {u64(commandBuffer), u64(depthCompareOp)}; + axrb_vk_call(203, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthBoundsTestEnable(VkCommandBuffer commandBuffer, VkBool32 depthBoundsTestEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthBoundsTestEnable)}; + axrb_vk_call(204, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetStencilTestEnable(VkCommandBuffer commandBuffer, VkBool32 stencilTestEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(stencilTestEnable)}; + axrb_vk_call(205, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetStencilOp(VkCommandBuffer commandBuffer, VkStencilFaceFlags faceMask, VkStencilOp failOp, VkStencilOp passOp, VkStencilOp depthFailOp, VkCompareOp compareOp) { + const uint64_t a[] = {u64(commandBuffer), u64(faceMask), u64(failOp), u64(passOp), u64(depthFailOp), u64(compareOp)}; + axrb_vk_call(206, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetRasterizerDiscardEnable(VkCommandBuffer commandBuffer, VkBool32 rasterizerDiscardEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(rasterizerDiscardEnable)}; + axrb_vk_call(207, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthBiasEnable(VkCommandBuffer commandBuffer, VkBool32 depthBiasEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthBiasEnable)}; + axrb_vk_call(208, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetPrimitiveRestartEnable(VkCommandBuffer commandBuffer, VkBool32 primitiveRestartEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(primitiveRestartEnable)}; + axrb_vk_call(209, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceBufferMemoryRequirements(VkDevice device, const VkDeviceBufferMemoryRequirements* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(210, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceImageMemoryRequirements(VkDevice device, const VkDeviceImageMemoryRequirements* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(211, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceImageSparseMemoryRequirements(VkDevice device, const VkDeviceImageMemoryRequirements* pInfo, uint32_t* pSparseMemoryRequirementCount, VkSparseImageMemoryRequirements2* pSparseMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pSparseMemoryRequirementCount), u64(pSparseMemoryRequirements)}; + axrb_vk_call(212, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroySurfaceKHR(VkInstance instance, VkSurfaceKHR surface, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(instance), u64(surface), u64(pAllocator)}; + axrb_vk_call(213, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSurfaceSupportKHR(VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, VkSurfaceKHR surface, VkBool32* pSupported) { + const uint64_t a[] = {u64(physicalDevice), u64(queueFamilyIndex), u64(surface), u64(pSupported)}; + return from_u64(axrb_vk_call(214, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSurfaceCapabilitiesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR* pSurfaceCapabilities) { + const uint64_t a[] = {u64(physicalDevice), u64(surface), u64(pSurfaceCapabilities)}; + return from_u64(axrb_vk_call(215, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSurfaceFormatsKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t* pSurfaceFormatCount, VkSurfaceFormatKHR* pSurfaceFormats) { + const uint64_t a[] = {u64(physicalDevice), u64(surface), u64(pSurfaceFormatCount), u64(pSurfaceFormats)}; + return from_u64(axrb_vk_call(216, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSurfacePresentModesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t* pPresentModeCount, VkPresentModeKHR* pPresentModes) { + const uint64_t a[] = {u64(physicalDevice), u64(surface), u64(pPresentModeCount), u64(pPresentModes)}; + return from_u64(axrb_vk_call(217, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSwapchainKHR* pSwapchain) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pSwapchain)}; + return from_u64(axrb_vk_call(218, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroySwapchainKHR(VkDevice device, VkSwapchainKHR swapchain, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pAllocator)}; + axrb_vk_call(219, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetSwapchainImagesKHR(VkDevice device, VkSwapchainKHR swapchain, uint32_t* pSwapchainImageCount, VkImage* pSwapchainImages) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pSwapchainImageCount), u64(pSwapchainImages)}; + return from_u64(axrb_vk_call(220, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAcquireNextImageKHR(VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout, VkSemaphore semaphore, VkFence fence, uint32_t* pImageIndex) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(timeout), u64(semaphore), u64(fence), u64(pImageIndex)}; + return from_u64(axrb_vk_call(221, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* pPresentInfo) { + const uint64_t a[] = {u64(queue), u64(pPresentInfo)}; + return from_u64(axrb_vk_call(222, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDeviceGroupPresentCapabilitiesKHR(VkDevice device, VkDeviceGroupPresentCapabilitiesKHR* pDeviceGroupPresentCapabilities) { + const uint64_t a[] = {u64(device), u64(pDeviceGroupPresentCapabilities)}; + return from_u64(axrb_vk_call(223, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDeviceGroupSurfacePresentModesKHR(VkDevice device, VkSurfaceKHR surface, VkDeviceGroupPresentModeFlagsKHR* pModes) { + const uint64_t a[] = {u64(device), u64(surface), u64(pModes)}; + return from_u64(axrb_vk_call(224, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDevicePresentRectanglesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t* pRectCount, VkRect2D* pRects) { + const uint64_t a[] = {u64(physicalDevice), u64(surface), u64(pRectCount), u64(pRects)}; + return from_u64(axrb_vk_call(225, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAcquireNextImage2KHR(VkDevice device, const VkAcquireNextImageInfoKHR* pAcquireInfo, uint32_t* pImageIndex) { + const uint64_t a[] = {u64(device), u64(pAcquireInfo), u64(pImageIndex)}; + return from_u64(axrb_vk_call(226, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceDisplayPropertiesKHR(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkDisplayPropertiesKHR* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(227, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceDisplayPlanePropertiesKHR(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkDisplayPlanePropertiesKHR* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(228, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDisplayPlaneSupportedDisplaysKHR(VkPhysicalDevice physicalDevice, uint32_t planeIndex, uint32_t* pDisplayCount, VkDisplayKHR* pDisplays) { + const uint64_t a[] = {u64(physicalDevice), u64(planeIndex), u64(pDisplayCount), u64(pDisplays)}; + return from_u64(axrb_vk_call(229, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDisplayModePropertiesKHR(VkPhysicalDevice physicalDevice, VkDisplayKHR display, uint32_t* pPropertyCount, VkDisplayModePropertiesKHR* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(display), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(230, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDisplayModeKHR(VkPhysicalDevice physicalDevice, VkDisplayKHR display, const VkDisplayModeCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDisplayModeKHR* pMode) { + const uint64_t a[] = {u64(physicalDevice), u64(display), u64(pCreateInfo), u64(pAllocator), u64(pMode)}; + return from_u64(axrb_vk_call(231, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDisplayPlaneCapabilitiesKHR(VkPhysicalDevice physicalDevice, VkDisplayModeKHR mode, uint32_t planeIndex, VkDisplayPlaneCapabilitiesKHR* pCapabilities) { + const uint64_t a[] = {u64(physicalDevice), u64(mode), u64(planeIndex), u64(pCapabilities)}; + return from_u64(axrb_vk_call(232, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDisplayPlaneSurfaceKHR(VkInstance instance, const VkDisplaySurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) { + const uint64_t a[] = {u64(instance), u64(pCreateInfo), u64(pAllocator), u64(pSurface)}; + return from_u64(axrb_vk_call(233, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateSharedSwapchainsKHR(VkDevice device, uint32_t swapchainCount, const VkSwapchainCreateInfoKHR* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkSwapchainKHR* pSwapchains) { + const uint64_t a[] = {u64(device), u64(swapchainCount), u64(pCreateInfos), u64(pAllocator), u64(pSwapchains)}; + return from_u64(axrb_vk_call(234, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceVideoCapabilitiesKHR(VkPhysicalDevice physicalDevice, const VkVideoProfileInfoKHR* pVideoProfile, VkVideoCapabilitiesKHR* pCapabilities) { + const uint64_t a[] = {u64(physicalDevice), u64(pVideoProfile), u64(pCapabilities)}; + return from_u64(axrb_vk_call(235, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceVideoFormatPropertiesKHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceVideoFormatInfoKHR* pVideoFormatInfo, uint32_t* pVideoFormatPropertyCount, VkVideoFormatPropertiesKHR* pVideoFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pVideoFormatInfo), u64(pVideoFormatPropertyCount), u64(pVideoFormatProperties)}; + return from_u64(axrb_vk_call(236, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateVideoSessionKHR(VkDevice device, const VkVideoSessionCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkVideoSessionKHR* pVideoSession) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pVideoSession)}; + return from_u64(axrb_vk_call(237, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyVideoSessionKHR(VkDevice device, VkVideoSessionKHR videoSession, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(videoSession), u64(pAllocator)}; + axrb_vk_call(238, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetVideoSessionMemoryRequirementsKHR(VkDevice device, VkVideoSessionKHR videoSession, uint32_t* pMemoryRequirementsCount, VkVideoSessionMemoryRequirementsKHR* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(videoSession), u64(pMemoryRequirementsCount), u64(pMemoryRequirements)}; + return from_u64(axrb_vk_call(239, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindVideoSessionMemoryKHR(VkDevice device, VkVideoSessionKHR videoSession, uint32_t bindSessionMemoryInfoCount, const VkBindVideoSessionMemoryInfoKHR* pBindSessionMemoryInfos) { + const uint64_t a[] = {u64(device), u64(videoSession), u64(bindSessionMemoryInfoCount), u64(pBindSessionMemoryInfos)}; + return from_u64(axrb_vk_call(240, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateVideoSessionParametersKHR(VkDevice device, const VkVideoSessionParametersCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkVideoSessionParametersKHR* pVideoSessionParameters) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pVideoSessionParameters)}; + return from_u64(axrb_vk_call(241, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkUpdateVideoSessionParametersKHR(VkDevice device, VkVideoSessionParametersKHR videoSessionParameters, const VkVideoSessionParametersUpdateInfoKHR* pUpdateInfo) { + const uint64_t a[] = {u64(device), u64(videoSessionParameters), u64(pUpdateInfo)}; + return from_u64(axrb_vk_call(242, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyVideoSessionParametersKHR(VkDevice device, VkVideoSessionParametersKHR videoSessionParameters, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(videoSessionParameters), u64(pAllocator)}; + axrb_vk_call(243, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginVideoCodingKHR(VkCommandBuffer commandBuffer, const VkVideoBeginCodingInfoKHR* pBeginInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pBeginInfo)}; + axrb_vk_call(244, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndVideoCodingKHR(VkCommandBuffer commandBuffer, const VkVideoEndCodingInfoKHR* pEndCodingInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pEndCodingInfo)}; + axrb_vk_call(245, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdControlVideoCodingKHR(VkCommandBuffer commandBuffer, const VkVideoCodingControlInfoKHR* pCodingControlInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCodingControlInfo)}; + axrb_vk_call(246, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDecodeVideoKHR(VkCommandBuffer commandBuffer, const VkVideoDecodeInfoKHR* pDecodeInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pDecodeInfo)}; + axrb_vk_call(247, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginRenderingKHR(VkCommandBuffer commandBuffer, const VkRenderingInfo* pRenderingInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pRenderingInfo)}; + axrb_vk_call(248, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderingKHR(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + axrb_vk_call(249, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFeatures2KHR(VkPhysicalDevice physicalDevice, VkPhysicalDeviceFeatures2* pFeatures) { + const uint64_t a[] = {u64(physicalDevice), u64(pFeatures)}; + axrb_vk_call(250, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceProperties2KHR(VkPhysicalDevice physicalDevice, VkPhysicalDeviceProperties2* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pProperties)}; + axrb_vk_call(251, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceFormatProperties2KHR(VkPhysicalDevice physicalDevice, VkFormat format, VkFormatProperties2* pFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(format), u64(pFormatProperties)}; + axrb_vk_call(252, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceImageFormatProperties2KHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceImageFormatInfo2* pImageFormatInfo, VkImageFormatProperties2* pImageFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pImageFormatInfo), u64(pImageFormatProperties)}; + return from_u64(axrb_vk_call(253, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyProperties2KHR(VkPhysicalDevice physicalDevice, uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties2* pQueueFamilyProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pQueueFamilyPropertyCount), u64(pQueueFamilyProperties)}; + axrb_vk_call(254, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties2KHR(VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties2* pMemoryProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pMemoryProperties)}; + axrb_vk_call(255, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceSparseImageFormatProperties2KHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSparseImageFormatInfo2* pFormatInfo, uint32_t* pPropertyCount, VkSparseImageFormatProperties2* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pFormatInfo), u64(pPropertyCount), u64(pProperties)}; + axrb_vk_call(256, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceGroupPeerMemoryFeaturesKHR(VkDevice device, uint32_t heapIndex, uint32_t localDeviceIndex, uint32_t remoteDeviceIndex, VkPeerMemoryFeatureFlags* pPeerMemoryFeatures) { + const uint64_t a[] = {u64(device), u64(heapIndex), u64(localDeviceIndex), u64(remoteDeviceIndex), u64(pPeerMemoryFeatures)}; + axrb_vk_call(257, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDeviceMaskKHR(VkCommandBuffer commandBuffer, uint32_t deviceMask) { + const uint64_t a[] = {u64(commandBuffer), u64(deviceMask)}; + axrb_vk_call(258, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDispatchBaseKHR(VkCommandBuffer commandBuffer, uint32_t baseGroupX, uint32_t baseGroupY, uint32_t baseGroupZ, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { + const uint64_t a[] = {u64(commandBuffer), u64(baseGroupX), u64(baseGroupY), u64(baseGroupZ), u64(groupCountX), u64(groupCountY), u64(groupCountZ)}; + axrb_vk_call(259, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkTrimCommandPoolKHR(VkDevice device, VkCommandPool commandPool, VkCommandPoolTrimFlags flags) { + const uint64_t a[] = {u64(device), u64(commandPool), u64(flags)}; + axrb_vk_call(260, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumeratePhysicalDeviceGroupsKHR(VkInstance instance, uint32_t* pPhysicalDeviceGroupCount, VkPhysicalDeviceGroupProperties* pPhysicalDeviceGroupProperties) { + const uint64_t a[] = {u64(instance), u64(pPhysicalDeviceGroupCount), u64(pPhysicalDeviceGroupProperties)}; + return from_u64(axrb_vk_call(261, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceExternalBufferPropertiesKHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalBufferInfo* pExternalBufferInfo, VkExternalBufferProperties* pExternalBufferProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pExternalBufferInfo), u64(pExternalBufferProperties)}; + axrb_vk_call(262, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetMemoryFdKHR(VkDevice device, const VkMemoryGetFdInfoKHR* pGetFdInfo, int* pFd) { + const uint64_t a[] = {u64(device), u64(pGetFdInfo), u64(pFd)}; + return from_u64(axrb_vk_call(263, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetMemoryFdPropertiesKHR(VkDevice device, VkExternalMemoryHandleTypeFlagBits handleType, int fd, VkMemoryFdPropertiesKHR* pMemoryFdProperties) { + const uint64_t a[] = {u64(device), u64(handleType), u64(fd), u64(pMemoryFdProperties)}; + return from_u64(axrb_vk_call(264, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceExternalSemaphorePropertiesKHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalSemaphoreInfo* pExternalSemaphoreInfo, VkExternalSemaphoreProperties* pExternalSemaphoreProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pExternalSemaphoreInfo), u64(pExternalSemaphoreProperties)}; + axrb_vk_call(265, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkImportSemaphoreFdKHR(VkDevice device, const VkImportSemaphoreFdInfoKHR* pImportSemaphoreFdInfo) { + const uint64_t a[] = {u64(device), u64(pImportSemaphoreFdInfo)}; + return from_u64(axrb_vk_call(266, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetSemaphoreFdKHR(VkDevice device, const VkSemaphoreGetFdInfoKHR* pGetFdInfo, int* pFd) { + const uint64_t a[] = {u64(device), u64(pGetFdInfo), u64(pFd)}; + return from_u64(axrb_vk_call(267, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPushDescriptorSetKHR(VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipelineLayout layout, uint32_t set, uint32_t descriptorWriteCount, const VkWriteDescriptorSet* pDescriptorWrites) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineBindPoint), u64(layout), u64(set), u64(descriptorWriteCount), u64(pDescriptorWrites)}; + axrb_vk_call(268, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPushDescriptorSetWithTemplateKHR(VkCommandBuffer commandBuffer, VkDescriptorUpdateTemplate descriptorUpdateTemplate, VkPipelineLayout layout, uint32_t set, const void* pData) { + const uint64_t a[] = {u64(commandBuffer), u64(descriptorUpdateTemplate), u64(layout), u64(set), u64(pData)}; + axrb_vk_call(269, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDescriptorUpdateTemplateKHR(VkDevice device, const VkDescriptorUpdateTemplateCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDescriptorUpdateTemplate* pDescriptorUpdateTemplate) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pDescriptorUpdateTemplate)}; + return from_u64(axrb_vk_call(270, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDescriptorUpdateTemplateKHR(VkDevice device, VkDescriptorUpdateTemplate descriptorUpdateTemplate, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(descriptorUpdateTemplate), u64(pAllocator)}; + axrb_vk_call(271, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkUpdateDescriptorSetWithTemplateKHR(VkDevice device, VkDescriptorSet descriptorSet, VkDescriptorUpdateTemplate descriptorUpdateTemplate, const void* pData) { + const uint64_t a[] = {u64(device), u64(descriptorSet), u64(descriptorUpdateTemplate), u64(pData)}; + axrb_vk_call(272, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateRenderPass2KHR(VkDevice device, const VkRenderPassCreateInfo2* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkRenderPass* pRenderPass) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pRenderPass)}; + return from_u64(axrb_vk_call(273, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginRenderPass2KHR(VkCommandBuffer commandBuffer, const VkRenderPassBeginInfo* pRenderPassBegin, const VkSubpassBeginInfo* pSubpassBeginInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pRenderPassBegin), u64(pSubpassBeginInfo)}; + axrb_vk_call(274, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdNextSubpass2KHR(VkCommandBuffer commandBuffer, const VkSubpassBeginInfo* pSubpassBeginInfo, const VkSubpassEndInfo* pSubpassEndInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pSubpassBeginInfo), u64(pSubpassEndInfo)}; + axrb_vk_call(275, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass2KHR(VkCommandBuffer commandBuffer, const VkSubpassEndInfo* pSubpassEndInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pSubpassEndInfo)}; + axrb_vk_call(276, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetSwapchainStatusKHR(VkDevice device, VkSwapchainKHR swapchain) { + const uint64_t a[] = {u64(device), u64(swapchain)}; + return from_u64(axrb_vk_call(277, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceExternalFencePropertiesKHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceExternalFenceInfo* pExternalFenceInfo, VkExternalFenceProperties* pExternalFenceProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pExternalFenceInfo), u64(pExternalFenceProperties)}; + axrb_vk_call(278, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkImportFenceFdKHR(VkDevice device, const VkImportFenceFdInfoKHR* pImportFenceFdInfo) { + const uint64_t a[] = {u64(device), u64(pImportFenceFdInfo)}; + return from_u64(axrb_vk_call(279, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetFenceFdKHR(VkDevice device, const VkFenceGetFdInfoKHR* pGetFdInfo, int* pFd) { + const uint64_t a[] = {u64(device), u64(pGetFdInfo), u64(pFd)}; + return from_u64(axrb_vk_call(280, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR(VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, uint32_t* pCounterCount, VkPerformanceCounterKHR* pCounters, VkPerformanceCounterDescriptionKHR* pCounterDescriptions) { + const uint64_t a[] = {u64(physicalDevice), u64(queueFamilyIndex), u64(pCounterCount), u64(pCounters), u64(pCounterDescriptions)}; + return from_u64(axrb_vk_call(281, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyPerformanceQueryPassesKHR(VkPhysicalDevice physicalDevice, const VkQueryPoolPerformanceCreateInfoKHR* pPerformanceQueryCreateInfo, uint32_t* pNumPasses) { + const uint64_t a[] = {u64(physicalDevice), u64(pPerformanceQueryCreateInfo), u64(pNumPasses)}; + axrb_vk_call(282, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAcquireProfilingLockKHR(VkDevice device, const VkAcquireProfilingLockInfoKHR* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(283, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkReleaseProfilingLockKHR(VkDevice device) { + const uint64_t a[] = {u64(device)}; + axrb_vk_call(284, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSurfaceCapabilities2KHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSurfaceInfo2KHR* pSurfaceInfo, VkSurfaceCapabilities2KHR* pSurfaceCapabilities) { + const uint64_t a[] = {u64(physicalDevice), u64(pSurfaceInfo), u64(pSurfaceCapabilities)}; + return from_u64(axrb_vk_call(285, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSurfaceFormats2KHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSurfaceInfo2KHR* pSurfaceInfo, uint32_t* pSurfaceFormatCount, VkSurfaceFormat2KHR* pSurfaceFormats) { + const uint64_t a[] = {u64(physicalDevice), u64(pSurfaceInfo), u64(pSurfaceFormatCount), u64(pSurfaceFormats)}; + return from_u64(axrb_vk_call(286, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceDisplayProperties2KHR(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkDisplayProperties2KHR* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(287, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceDisplayPlaneProperties2KHR(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkDisplayPlaneProperties2KHR* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(288, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDisplayModeProperties2KHR(VkPhysicalDevice physicalDevice, VkDisplayKHR display, uint32_t* pPropertyCount, VkDisplayModeProperties2KHR* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(display), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(289, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDisplayPlaneCapabilities2KHR(VkPhysicalDevice physicalDevice, const VkDisplayPlaneInfo2KHR* pDisplayPlaneInfo, VkDisplayPlaneCapabilities2KHR* pCapabilities) { + const uint64_t a[] = {u64(physicalDevice), u64(pDisplayPlaneInfo), u64(pCapabilities)}; + return from_u64(axrb_vk_call(290, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements2KHR(VkDevice device, const VkImageMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(291, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements2KHR(VkDevice device, const VkBufferMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(292, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageSparseMemoryRequirements2KHR(VkDevice device, const VkImageSparseMemoryRequirementsInfo2* pInfo, uint32_t* pSparseMemoryRequirementCount, VkSparseImageMemoryRequirements2* pSparseMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pSparseMemoryRequirementCount), u64(pSparseMemoryRequirements)}; + axrb_vk_call(293, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateSamplerYcbcrConversionKHR(VkDevice device, const VkSamplerYcbcrConversionCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSamplerYcbcrConversion* pYcbcrConversion) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pYcbcrConversion)}; + return from_u64(axrb_vk_call(294, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroySamplerYcbcrConversionKHR(VkDevice device, VkSamplerYcbcrConversion ycbcrConversion, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(ycbcrConversion), u64(pAllocator)}; + axrb_vk_call(295, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2KHR(VkDevice device, uint32_t bindInfoCount, const VkBindBufferMemoryInfo* pBindInfos) { + const uint64_t a[] = {u64(device), u64(bindInfoCount), u64(pBindInfos)}; + return from_u64(axrb_vk_call(296, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory2KHR(VkDevice device, uint32_t bindInfoCount, const VkBindImageMemoryInfo* pBindInfos) { + const uint64_t a[] = {u64(device), u64(bindInfoCount), u64(pBindInfos)}; + return from_u64(axrb_vk_call(297, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDescriptorSetLayoutSupportKHR(VkDevice device, const VkDescriptorSetLayoutCreateInfo* pCreateInfo, VkDescriptorSetLayoutSupport* pSupport) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pSupport)}; + axrb_vk_call(298, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirectCountKHR(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(299, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirectCountKHR(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(300, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetSemaphoreCounterValueKHR(VkDevice device, VkSemaphore semaphore, uint64_t* pValue) { + const uint64_t a[] = {u64(device), u64(semaphore), u64(pValue)}; + return from_u64(axrb_vk_call(301, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkWaitSemaphoresKHR(VkDevice device, const VkSemaphoreWaitInfo* pWaitInfo, uint64_t timeout) { + const uint64_t a[] = {u64(device), u64(pWaitInfo), u64(timeout)}; + return from_u64(axrb_vk_call(302, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSignalSemaphoreKHR(VkDevice device, const VkSemaphoreSignalInfo* pSignalInfo) { + const uint64_t a[] = {u64(device), u64(pSignalInfo)}; + return from_u64(axrb_vk_call(303, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceFragmentShadingRatesKHR(VkPhysicalDevice physicalDevice, uint32_t* pFragmentShadingRateCount, VkPhysicalDeviceFragmentShadingRateKHR* pFragmentShadingRates) { + const uint64_t a[] = {u64(physicalDevice), u64(pFragmentShadingRateCount), u64(pFragmentShadingRates)}; + return from_u64(axrb_vk_call(304, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetFragmentShadingRateKHR(VkCommandBuffer commandBuffer, const VkExtent2D* pFragmentSize, const VkFragmentShadingRateCombinerOpKHR* combinerOps) { + const uint64_t a[] = {u64(commandBuffer), u64(pFragmentSize), u64(combinerOps)}; + axrb_vk_call(305, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkWaitForPresentKHR(VkDevice device, VkSwapchainKHR swapchain, uint64_t presentId, uint64_t timeout) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(presentId), u64(timeout)}; + return from_u64(axrb_vk_call(306, a)); +} +extern "C" VKAPI_ATTR VkDeviceAddress VKAPI_CALL vkGetBufferDeviceAddressKHR(VkDevice device, const VkBufferDeviceAddressInfo* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(307, a)); +} +extern "C" VKAPI_ATTR uint64_t VKAPI_CALL vkGetBufferOpaqueCaptureAddressKHR(VkDevice device, const VkBufferDeviceAddressInfo* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(308, a)); +} +extern "C" VKAPI_ATTR uint64_t VKAPI_CALL vkGetDeviceMemoryOpaqueCaptureAddressKHR(VkDevice device, const VkDeviceMemoryOpaqueCaptureAddressInfo* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(309, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDeferredOperationKHR(VkDevice device, const VkAllocationCallbacks* pAllocator, VkDeferredOperationKHR* pDeferredOperation) { + const uint64_t a[] = {u64(device), u64(pAllocator), u64(pDeferredOperation)}; + return from_u64(axrb_vk_call(310, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDeferredOperationKHR(VkDevice device, VkDeferredOperationKHR operation, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(operation), u64(pAllocator)}; + axrb_vk_call(311, a); +} +extern "C" VKAPI_ATTR uint32_t VKAPI_CALL vkGetDeferredOperationMaxConcurrencyKHR(VkDevice device, VkDeferredOperationKHR operation) { + const uint64_t a[] = {u64(device), u64(operation)}; + return from_u64(axrb_vk_call(312, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDeferredOperationResultKHR(VkDevice device, VkDeferredOperationKHR operation) { + const uint64_t a[] = {u64(device), u64(operation)}; + return from_u64(axrb_vk_call(313, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkDeferredOperationJoinKHR(VkDevice device, VkDeferredOperationKHR operation) { + const uint64_t a[] = {u64(device), u64(operation)}; + return from_u64(axrb_vk_call(314, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPipelineExecutablePropertiesKHR(VkDevice device, const VkPipelineInfoKHR* pPipelineInfo, uint32_t* pExecutableCount, VkPipelineExecutablePropertiesKHR* pProperties) { + const uint64_t a[] = {u64(device), u64(pPipelineInfo), u64(pExecutableCount), u64(pProperties)}; + return from_u64(axrb_vk_call(315, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPipelineExecutableStatisticsKHR(VkDevice device, const VkPipelineExecutableInfoKHR* pExecutableInfo, uint32_t* pStatisticCount, VkPipelineExecutableStatisticKHR* pStatistics) { + const uint64_t a[] = {u64(device), u64(pExecutableInfo), u64(pStatisticCount), u64(pStatistics)}; + return from_u64(axrb_vk_call(316, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPipelineExecutableInternalRepresentationsKHR(VkDevice device, const VkPipelineExecutableInfoKHR* pExecutableInfo, uint32_t* pInternalRepresentationCount, VkPipelineExecutableInternalRepresentationKHR* pInternalRepresentations) { + const uint64_t a[] = {u64(device), u64(pExecutableInfo), u64(pInternalRepresentationCount), u64(pInternalRepresentations)}; + return from_u64(axrb_vk_call(317, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory2KHR(VkDevice device, const VkMemoryMapInfoKHR* pMemoryMapInfo, void** ppData) { + const uint64_t a[] = {u64(device), u64(pMemoryMapInfo), u64(ppData)}; + return from_u64(axrb_vk_call(318, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkUnmapMemory2KHR(VkDevice device, const VkMemoryUnmapInfoKHR* pMemoryUnmapInfo) { + const uint64_t a[] = {u64(device), u64(pMemoryUnmapInfo)}; + return from_u64(axrb_vk_call(319, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR(VkPhysicalDevice physicalDevice, const VkPhysicalDeviceVideoEncodeQualityLevelInfoKHR* pQualityLevelInfo, VkVideoEncodeQualityLevelPropertiesKHR* pQualityLevelProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pQualityLevelInfo), u64(pQualityLevelProperties)}; + return from_u64(axrb_vk_call(320, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetEncodedVideoSessionParametersKHR(VkDevice device, const VkVideoEncodeSessionParametersGetInfoKHR* pVideoSessionParametersInfo, VkVideoEncodeSessionParametersFeedbackInfoKHR* pFeedbackInfo, size_t* pDataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(pVideoSessionParametersInfo), u64(pFeedbackInfo), u64(pDataSize), u64(pData)}; + return from_u64(axrb_vk_call(321, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEncodeVideoKHR(VkCommandBuffer commandBuffer, const VkVideoEncodeInfoKHR* pEncodeInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pEncodeInfo)}; + axrb_vk_call(322, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetEvent2KHR(VkCommandBuffer commandBuffer, VkEvent event, const VkDependencyInfo* pDependencyInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(event), u64(pDependencyInfo)}; + axrb_vk_call(323, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdResetEvent2KHR(VkCommandBuffer commandBuffer, VkEvent event, VkPipelineStageFlags2 stageMask) { + const uint64_t a[] = {u64(commandBuffer), u64(event), u64(stageMask)}; + axrb_vk_call(324, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWaitEvents2KHR(VkCommandBuffer commandBuffer, uint32_t eventCount, const VkEvent* pEvents, const VkDependencyInfo* pDependencyInfos) { + const uint64_t a[] = {u64(commandBuffer), u64(eventCount), u64(pEvents), u64(pDependencyInfos)}; + axrb_vk_call(325, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier2KHR(VkCommandBuffer commandBuffer, const VkDependencyInfo* pDependencyInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pDependencyInfo)}; + axrb_vk_call(326, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteTimestamp2KHR(VkCommandBuffer commandBuffer, VkPipelineStageFlags2 stage, VkQueryPool queryPool, uint32_t query) { + const uint64_t a[] = {u64(commandBuffer), u64(stage), u64(queryPool), u64(query)}; + axrb_vk_call(327, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit2KHR(VkQueue queue, uint32_t submitCount, const VkSubmitInfo2* pSubmits, VkFence fence) { + const uint64_t a[] = {u64(queue), u64(submitCount), u64(pSubmits), u64(fence)}; + return from_u64(axrb_vk_call(328, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteBufferMarker2AMD(VkCommandBuffer commandBuffer, VkPipelineStageFlags2 stage, VkBuffer dstBuffer, VkDeviceSize dstOffset, uint32_t marker) { + const uint64_t a[] = {u64(commandBuffer), u64(stage), u64(dstBuffer), u64(dstOffset), u64(marker)}; + axrb_vk_call(329, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetQueueCheckpointData2NV(VkQueue queue, uint32_t* pCheckpointDataCount, VkCheckpointData2NV* pCheckpointData) { + const uint64_t a[] = {u64(queue), u64(pCheckpointDataCount), u64(pCheckpointData)}; + axrb_vk_call(330, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyBuffer2KHR(VkCommandBuffer commandBuffer, const VkCopyBufferInfo2* pCopyBufferInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyBufferInfo)}; + axrb_vk_call(331, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyImage2KHR(VkCommandBuffer commandBuffer, const VkCopyImageInfo2* pCopyImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyImageInfo)}; + axrb_vk_call(332, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage2KHR(VkCommandBuffer commandBuffer, const VkCopyBufferToImageInfo2* pCopyBufferToImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyBufferToImageInfo)}; + axrb_vk_call(333, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyImageToBuffer2KHR(VkCommandBuffer commandBuffer, const VkCopyImageToBufferInfo2* pCopyImageToBufferInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pCopyImageToBufferInfo)}; + axrb_vk_call(334, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBlitImage2KHR(VkCommandBuffer commandBuffer, const VkBlitImageInfo2* pBlitImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pBlitImageInfo)}; + axrb_vk_call(335, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdResolveImage2KHR(VkCommandBuffer commandBuffer, const VkResolveImageInfo2* pResolveImageInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pResolveImageInfo)}; + axrb_vk_call(336, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdTraceRaysIndirect2KHR(VkCommandBuffer commandBuffer, VkDeviceAddress indirectDeviceAddress) { + const uint64_t a[] = {u64(commandBuffer), u64(indirectDeviceAddress)}; + axrb_vk_call(337, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceBufferMemoryRequirementsKHR(VkDevice device, const VkDeviceBufferMemoryRequirements* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(338, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceImageMemoryRequirementsKHR(VkDevice device, const VkDeviceImageMemoryRequirements* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(339, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceImageSparseMemoryRequirementsKHR(VkDevice device, const VkDeviceImageMemoryRequirements* pInfo, uint32_t* pSparseMemoryRequirementCount, VkSparseImageMemoryRequirements2* pSparseMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pSparseMemoryRequirementCount), u64(pSparseMemoryRequirements)}; + axrb_vk_call(340, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindIndexBuffer2KHR(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkDeviceSize size, VkIndexType indexType) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(size), u64(indexType)}; + axrb_vk_call(341, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetRenderingAreaGranularityKHR(VkDevice device, const VkRenderingAreaInfoKHR* pRenderingAreaInfo, VkExtent2D* pGranularity) { + const uint64_t a[] = {u64(device), u64(pRenderingAreaInfo), u64(pGranularity)}; + axrb_vk_call(342, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceImageSubresourceLayoutKHR(VkDevice device, const VkDeviceImageSubresourceInfoKHR* pInfo, VkSubresourceLayout2KHR* pLayout) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pLayout)}; + axrb_vk_call(343, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageSubresourceLayout2KHR(VkDevice device, VkImage image, const VkImageSubresource2KHR* pSubresource, VkSubresourceLayout2KHR* pLayout) { + const uint64_t a[] = {u64(device), u64(image), u64(pSubresource), u64(pLayout)}; + axrb_vk_call(344, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkCooperativeMatrixPropertiesKHR* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(345, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceCalibrateableTimeDomainsKHR(VkPhysicalDevice physicalDevice, uint32_t* pTimeDomainCount, VkTimeDomainKHR* pTimeDomains) { + const uint64_t a[] = {u64(physicalDevice), u64(pTimeDomainCount), u64(pTimeDomains)}; + return from_u64(axrb_vk_call(346, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetCalibratedTimestampsKHR(VkDevice device, uint32_t timestampCount, const VkCalibratedTimestampInfoKHR* pTimestampInfos, uint64_t* pTimestamps, uint64_t* pMaxDeviation) { + const uint64_t a[] = {u64(device), u64(timestampCount), u64(pTimestampInfos), u64(pTimestamps), u64(pMaxDeviation)}; + return from_u64(axrb_vk_call(347, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorSets2KHR(VkCommandBuffer commandBuffer, const VkBindDescriptorSetsInfoKHR* pBindDescriptorSetsInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pBindDescriptorSetsInfo)}; + axrb_vk_call(348, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPushConstants2KHR(VkCommandBuffer commandBuffer, const VkPushConstantsInfoKHR* pPushConstantsInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pPushConstantsInfo)}; + axrb_vk_call(349, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPushDescriptorSet2KHR(VkCommandBuffer commandBuffer, const VkPushDescriptorSetInfoKHR* pPushDescriptorSetInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pPushDescriptorSetInfo)}; + axrb_vk_call(350, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPushDescriptorSetWithTemplate2KHR(VkCommandBuffer commandBuffer, const VkPushDescriptorSetWithTemplateInfoKHR* pPushDescriptorSetWithTemplateInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pPushDescriptorSetWithTemplateInfo)}; + axrb_vk_call(351, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDescriptorBufferOffsets2EXT(VkCommandBuffer commandBuffer, const VkSetDescriptorBufferOffsetsInfoEXT* pSetDescriptorBufferOffsetsInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pSetDescriptorBufferOffsetsInfo)}; + axrb_vk_call(352, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorBufferEmbeddedSamplers2EXT(VkCommandBuffer commandBuffer, const VkBindDescriptorBufferEmbeddedSamplersInfoEXT* pBindDescriptorBufferEmbeddedSamplersInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pBindDescriptorBufferEmbeddedSamplersInfo)}; + axrb_vk_call(353, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDebugReportCallbackEXT(VkInstance instance, const VkDebugReportCallbackCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugReportCallbackEXT* pCallback) { + const uint64_t a[] = {u64(instance), u64(pCreateInfo), u64(pAllocator), u64(pCallback)}; + return from_u64(axrb_vk_call(354, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDebugReportCallbackEXT(VkInstance instance, VkDebugReportCallbackEXT callback, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(instance), u64(callback), u64(pAllocator)}; + axrb_vk_call(355, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDebugReportMessageEXT(VkInstance instance, VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objectType, uint64_t object, size_t location, int32_t messageCode, const char* pLayerPrefix, const char* pMessage) { + const uint64_t a[] = {u64(instance), u64(flags), u64(objectType), u64(object), u64(location), u64(messageCode), u64(pLayerPrefix), u64(pMessage)}; + axrb_vk_call(356, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkDebugMarkerSetObjectTagEXT(VkDevice device, const VkDebugMarkerObjectTagInfoEXT* pTagInfo) { + const uint64_t a[] = {u64(device), u64(pTagInfo)}; + return from_u64(axrb_vk_call(357, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkDebugMarkerSetObjectNameEXT(VkDevice device, const VkDebugMarkerObjectNameInfoEXT* pNameInfo) { + const uint64_t a[] = {u64(device), u64(pNameInfo)}; + return from_u64(axrb_vk_call(358, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDebugMarkerBeginEXT(VkCommandBuffer commandBuffer, const VkDebugMarkerMarkerInfoEXT* pMarkerInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pMarkerInfo)}; + axrb_vk_call(359, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDebugMarkerEndEXT(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + axrb_vk_call(360, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDebugMarkerInsertEXT(VkCommandBuffer commandBuffer, const VkDebugMarkerMarkerInfoEXT* pMarkerInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pMarkerInfo)}; + axrb_vk_call(361, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindTransformFeedbackBuffersEXT(VkCommandBuffer commandBuffer, uint32_t firstBinding, uint32_t bindingCount, const VkBuffer* pBuffers, const VkDeviceSize* pOffsets, const VkDeviceSize* pSizes) { + const uint64_t a[] = {u64(commandBuffer), u64(firstBinding), u64(bindingCount), u64(pBuffers), u64(pOffsets), u64(pSizes)}; + axrb_vk_call(362, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginTransformFeedbackEXT(VkCommandBuffer commandBuffer, uint32_t firstCounterBuffer, uint32_t counterBufferCount, const VkBuffer* pCounterBuffers, const VkDeviceSize* pCounterBufferOffsets) { + const uint64_t a[] = {u64(commandBuffer), u64(firstCounterBuffer), u64(counterBufferCount), u64(pCounterBuffers), u64(pCounterBufferOffsets)}; + axrb_vk_call(363, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndTransformFeedbackEXT(VkCommandBuffer commandBuffer, uint32_t firstCounterBuffer, uint32_t counterBufferCount, const VkBuffer* pCounterBuffers, const VkDeviceSize* pCounterBufferOffsets) { + const uint64_t a[] = {u64(commandBuffer), u64(firstCounterBuffer), u64(counterBufferCount), u64(pCounterBuffers), u64(pCounterBufferOffsets)}; + axrb_vk_call(364, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginQueryIndexedEXT(VkCommandBuffer commandBuffer, VkQueryPool queryPool, uint32_t query, VkQueryControlFlags flags, uint32_t index) { + const uint64_t a[] = {u64(commandBuffer), u64(queryPool), u64(query), u64(flags), u64(index)}; + axrb_vk_call(365, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndQueryIndexedEXT(VkCommandBuffer commandBuffer, VkQueryPool queryPool, uint32_t query, uint32_t index) { + const uint64_t a[] = {u64(commandBuffer), u64(queryPool), u64(query), u64(index)}; + axrb_vk_call(366, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirectByteCountEXT(VkCommandBuffer commandBuffer, uint32_t instanceCount, uint32_t firstInstance, VkBuffer counterBuffer, VkDeviceSize counterBufferOffset, uint32_t counterOffset, uint32_t vertexStride) { + const uint64_t a[] = {u64(commandBuffer), u64(instanceCount), u64(firstInstance), u64(counterBuffer), u64(counterBufferOffset), u64(counterOffset), u64(vertexStride)}; + axrb_vk_call(367, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateCuModuleNVX(VkDevice device, const VkCuModuleCreateInfoNVX* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkCuModuleNVX* pModule) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pModule)}; + return from_u64(axrb_vk_call(368, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateCuFunctionNVX(VkDevice device, const VkCuFunctionCreateInfoNVX* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkCuFunctionNVX* pFunction) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pFunction)}; + return from_u64(axrb_vk_call(369, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyCuModuleNVX(VkDevice device, VkCuModuleNVX module, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(module), u64(pAllocator)}; + axrb_vk_call(370, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyCuFunctionNVX(VkDevice device, VkCuFunctionNVX function, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(function), u64(pAllocator)}; + axrb_vk_call(371, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCuLaunchKernelNVX(VkCommandBuffer commandBuffer, const VkCuLaunchInfoNVX* pLaunchInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pLaunchInfo)}; + axrb_vk_call(372, a); +} +extern "C" VKAPI_ATTR uint32_t VKAPI_CALL vkGetImageViewHandleNVX(VkDevice device, const VkImageViewHandleInfoNVX* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(373, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetImageViewAddressNVX(VkDevice device, VkImageView imageView, VkImageViewAddressPropertiesNVX* pProperties) { + const uint64_t a[] = {u64(device), u64(imageView), u64(pProperties)}; + return from_u64(axrb_vk_call(374, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndirectCountAMD(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(375, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexedIndirectCountAMD(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(376, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetShaderInfoAMD(VkDevice device, VkPipeline pipeline, VkShaderStageFlagBits shaderStage, VkShaderInfoTypeAMD infoType, size_t* pInfoSize, void* pInfo) { + const uint64_t a[] = {u64(device), u64(pipeline), u64(shaderStage), u64(infoType), u64(pInfoSize), u64(pInfo)}; + return from_u64(axrb_vk_call(377, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceExternalImageFormatPropertiesNV(VkPhysicalDevice physicalDevice, VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkExternalMemoryHandleTypeFlagsNV externalHandleType, VkExternalImageFormatPropertiesNV* pExternalImageFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(format), u64(type), u64(tiling), u64(usage), u64(flags), u64(externalHandleType), u64(pExternalImageFormatProperties)}; + return from_u64(axrb_vk_call(378, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginConditionalRenderingEXT(VkCommandBuffer commandBuffer, const VkConditionalRenderingBeginInfoEXT* pConditionalRenderingBegin) { + const uint64_t a[] = {u64(commandBuffer), u64(pConditionalRenderingBegin)}; + axrb_vk_call(379, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndConditionalRenderingEXT(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + axrb_vk_call(380, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetViewportWScalingNV(VkCommandBuffer commandBuffer, uint32_t firstViewport, uint32_t viewportCount, const VkViewportWScalingNV* pViewportWScalings) { + const uint64_t a[] = {u64(commandBuffer), u64(firstViewport), u64(viewportCount), u64(pViewportWScalings)}; + axrb_vk_call(381, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkReleaseDisplayEXT(VkPhysicalDevice physicalDevice, VkDisplayKHR display) { + const uint64_t a[] = {u64(physicalDevice), u64(display)}; + return from_u64(axrb_vk_call(382, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSurfaceCapabilities2EXT(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilities2EXT* pSurfaceCapabilities) { + const uint64_t a[] = {u64(physicalDevice), u64(surface), u64(pSurfaceCapabilities)}; + return from_u64(axrb_vk_call(383, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkDisplayPowerControlEXT(VkDevice device, VkDisplayKHR display, const VkDisplayPowerInfoEXT* pDisplayPowerInfo) { + const uint64_t a[] = {u64(device), u64(display), u64(pDisplayPowerInfo)}; + return from_u64(axrb_vk_call(384, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkRegisterDeviceEventEXT(VkDevice device, const VkDeviceEventInfoEXT* pDeviceEventInfo, const VkAllocationCallbacks* pAllocator, VkFence* pFence) { + const uint64_t a[] = {u64(device), u64(pDeviceEventInfo), u64(pAllocator), u64(pFence)}; + return from_u64(axrb_vk_call(385, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkRegisterDisplayEventEXT(VkDevice device, VkDisplayKHR display, const VkDisplayEventInfoEXT* pDisplayEventInfo, const VkAllocationCallbacks* pAllocator, VkFence* pFence) { + const uint64_t a[] = {u64(device), u64(display), u64(pDisplayEventInfo), u64(pAllocator), u64(pFence)}; + return from_u64(axrb_vk_call(386, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetSwapchainCounterEXT(VkDevice device, VkSwapchainKHR swapchain, VkSurfaceCounterFlagBitsEXT counter, uint64_t* pCounterValue) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(counter), u64(pCounterValue)}; + return from_u64(axrb_vk_call(387, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetRefreshCycleDurationGOOGLE(VkDevice device, VkSwapchainKHR swapchain, VkRefreshCycleDurationGOOGLE* pDisplayTimingProperties) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pDisplayTimingProperties)}; + return from_u64(axrb_vk_call(388, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPastPresentationTimingGOOGLE(VkDevice device, VkSwapchainKHR swapchain, uint32_t* pPresentationTimingCount, VkPastPresentationTimingGOOGLE* pPresentationTimings) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pPresentationTimingCount), u64(pPresentationTimings)}; + return from_u64(axrb_vk_call(389, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDiscardRectangleEXT(VkCommandBuffer commandBuffer, uint32_t firstDiscardRectangle, uint32_t discardRectangleCount, const VkRect2D* pDiscardRectangles) { + const uint64_t a[] = {u64(commandBuffer), u64(firstDiscardRectangle), u64(discardRectangleCount), u64(pDiscardRectangles)}; + axrb_vk_call(390, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDiscardRectangleEnableEXT(VkCommandBuffer commandBuffer, VkBool32 discardRectangleEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(discardRectangleEnable)}; + axrb_vk_call(391, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDiscardRectangleModeEXT(VkCommandBuffer commandBuffer, VkDiscardRectangleModeEXT discardRectangleMode) { + const uint64_t a[] = {u64(commandBuffer), u64(discardRectangleMode)}; + axrb_vk_call(392, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkSetHdrMetadataEXT(VkDevice device, uint32_t swapchainCount, const VkSwapchainKHR* pSwapchains, const VkHdrMetadataEXT* pMetadata) { + const uint64_t a[] = {u64(device), u64(swapchainCount), u64(pSwapchains), u64(pMetadata)}; + axrb_vk_call(393, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSetDebugUtilsObjectNameEXT(VkDevice device, const VkDebugUtilsObjectNameInfoEXT* pNameInfo) { + const uint64_t a[] = {u64(device), u64(pNameInfo)}; + return from_u64(axrb_vk_call(394, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSetDebugUtilsObjectTagEXT(VkDevice device, const VkDebugUtilsObjectTagInfoEXT* pTagInfo) { + const uint64_t a[] = {u64(device), u64(pTagInfo)}; + return from_u64(axrb_vk_call(395, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkQueueBeginDebugUtilsLabelEXT(VkQueue queue, const VkDebugUtilsLabelEXT* pLabelInfo) { + const uint64_t a[] = {u64(queue), u64(pLabelInfo)}; + axrb_vk_call(396, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkQueueEndDebugUtilsLabelEXT(VkQueue queue) { + const uint64_t a[] = {u64(queue)}; + axrb_vk_call(397, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkQueueInsertDebugUtilsLabelEXT(VkQueue queue, const VkDebugUtilsLabelEXT* pLabelInfo) { + const uint64_t a[] = {u64(queue), u64(pLabelInfo)}; + axrb_vk_call(398, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBeginDebugUtilsLabelEXT(VkCommandBuffer commandBuffer, const VkDebugUtilsLabelEXT* pLabelInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pLabelInfo)}; + axrb_vk_call(399, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdEndDebugUtilsLabelEXT(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + axrb_vk_call(400, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdInsertDebugUtilsLabelEXT(VkCommandBuffer commandBuffer, const VkDebugUtilsLabelEXT* pLabelInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pLabelInfo)}; + axrb_vk_call(401, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pMessenger) { + const uint64_t a[] = {u64(instance), u64(pCreateInfo), u64(pAllocator), u64(pMessenger)}; + return from_u64(axrb_vk_call(402, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT messenger, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(instance), u64(messenger), u64(pAllocator)}; + axrb_vk_call(403, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkSubmitDebugUtilsMessageEXT(VkInstance instance, VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageTypes, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData) { + const uint64_t a[] = {u64(instance), u64(messageSeverity), u64(messageTypes), u64(pCallbackData)}; + axrb_vk_call(404, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetSampleLocationsEXT(VkCommandBuffer commandBuffer, const VkSampleLocationsInfoEXT* pSampleLocationsInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pSampleLocationsInfo)}; + axrb_vk_call(405, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMultisamplePropertiesEXT(VkPhysicalDevice physicalDevice, VkSampleCountFlagBits samples, VkMultisamplePropertiesEXT* pMultisampleProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(samples), u64(pMultisampleProperties)}; + axrb_vk_call(406, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetImageDrmFormatModifierPropertiesEXT(VkDevice device, VkImage image, VkImageDrmFormatModifierPropertiesEXT* pProperties) { + const uint64_t a[] = {u64(device), u64(image), u64(pProperties)}; + return from_u64(axrb_vk_call(407, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateValidationCacheEXT(VkDevice device, const VkValidationCacheCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkValidationCacheEXT* pValidationCache) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pValidationCache)}; + return from_u64(axrb_vk_call(408, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyValidationCacheEXT(VkDevice device, VkValidationCacheEXT validationCache, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(validationCache), u64(pAllocator)}; + axrb_vk_call(409, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkMergeValidationCachesEXT(VkDevice device, VkValidationCacheEXT dstCache, uint32_t srcCacheCount, const VkValidationCacheEXT* pSrcCaches) { + const uint64_t a[] = {u64(device), u64(dstCache), u64(srcCacheCount), u64(pSrcCaches)}; + return from_u64(axrb_vk_call(410, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetValidationCacheDataEXT(VkDevice device, VkValidationCacheEXT validationCache, size_t* pDataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(validationCache), u64(pDataSize), u64(pData)}; + return from_u64(axrb_vk_call(411, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindShadingRateImageNV(VkCommandBuffer commandBuffer, VkImageView imageView, VkImageLayout imageLayout) { + const uint64_t a[] = {u64(commandBuffer), u64(imageView), u64(imageLayout)}; + axrb_vk_call(412, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetViewportShadingRatePaletteNV(VkCommandBuffer commandBuffer, uint32_t firstViewport, uint32_t viewportCount, const VkShadingRatePaletteNV* pShadingRatePalettes) { + const uint64_t a[] = {u64(commandBuffer), u64(firstViewport), u64(viewportCount), u64(pShadingRatePalettes)}; + axrb_vk_call(413, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCoarseSampleOrderNV(VkCommandBuffer commandBuffer, VkCoarseSampleOrderTypeNV sampleOrderType, uint32_t customSampleOrderCount, const VkCoarseSampleOrderCustomNV* pCustomSampleOrders) { + const uint64_t a[] = {u64(commandBuffer), u64(sampleOrderType), u64(customSampleOrderCount), u64(pCustomSampleOrders)}; + axrb_vk_call(414, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateAccelerationStructureNV(VkDevice device, const VkAccelerationStructureCreateInfoNV* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkAccelerationStructureNV* pAccelerationStructure) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pAccelerationStructure)}; + return from_u64(axrb_vk_call(415, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyAccelerationStructureNV(VkDevice device, VkAccelerationStructureNV accelerationStructure, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(accelerationStructure), u64(pAllocator)}; + axrb_vk_call(416, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetAccelerationStructureMemoryRequirementsNV(VkDevice device, const VkAccelerationStructureMemoryRequirementsInfoNV* pInfo, VkMemoryRequirements2KHR* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(417, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindAccelerationStructureMemoryNV(VkDevice device, uint32_t bindInfoCount, const VkBindAccelerationStructureMemoryInfoNV* pBindInfos) { + const uint64_t a[] = {u64(device), u64(bindInfoCount), u64(pBindInfos)}; + return from_u64(axrb_vk_call(418, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBuildAccelerationStructureNV(VkCommandBuffer commandBuffer, const VkAccelerationStructureInfoNV* pInfo, VkBuffer instanceData, VkDeviceSize instanceOffset, VkBool32 update, VkAccelerationStructureNV dst, VkAccelerationStructureNV src, VkBuffer scratch, VkDeviceSize scratchOffset) { + const uint64_t a[] = {u64(commandBuffer), u64(pInfo), u64(instanceData), u64(instanceOffset), u64(update), u64(dst), u64(src), u64(scratch), u64(scratchOffset)}; + axrb_vk_call(419, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyAccelerationStructureNV(VkCommandBuffer commandBuffer, VkAccelerationStructureNV dst, VkAccelerationStructureNV src, VkCopyAccelerationStructureModeKHR mode) { + const uint64_t a[] = {u64(commandBuffer), u64(dst), u64(src), u64(mode)}; + axrb_vk_call(420, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdTraceRaysNV(VkCommandBuffer commandBuffer, VkBuffer raygenShaderBindingTableBuffer, VkDeviceSize raygenShaderBindingOffset, VkBuffer missShaderBindingTableBuffer, VkDeviceSize missShaderBindingOffset, VkDeviceSize missShaderBindingStride, VkBuffer hitShaderBindingTableBuffer, VkDeviceSize hitShaderBindingOffset, VkDeviceSize hitShaderBindingStride, VkBuffer callableShaderBindingTableBuffer, VkDeviceSize callableShaderBindingOffset, VkDeviceSize callableShaderBindingStride, uint32_t width, uint32_t height, uint32_t depth) { + const uint64_t a[] = {u64(commandBuffer), u64(raygenShaderBindingTableBuffer), u64(raygenShaderBindingOffset), u64(missShaderBindingTableBuffer), u64(missShaderBindingOffset), u64(missShaderBindingStride), u64(hitShaderBindingTableBuffer), u64(hitShaderBindingOffset), u64(hitShaderBindingStride), u64(callableShaderBindingTableBuffer), u64(callableShaderBindingOffset), u64(callableShaderBindingStride), u64(width), u64(height), u64(depth)}; + axrb_vk_call(421, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateRayTracingPipelinesNV(VkDevice device, VkPipelineCache pipelineCache, uint32_t createInfoCount, const VkRayTracingPipelineCreateInfoNV* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkPipeline* pPipelines) { + const uint64_t a[] = {u64(device), u64(pipelineCache), u64(createInfoCount), u64(pCreateInfos), u64(pAllocator), u64(pPipelines)}; + return from_u64(axrb_vk_call(422, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetRayTracingShaderGroupHandlesKHR(VkDevice device, VkPipeline pipeline, uint32_t firstGroup, uint32_t groupCount, size_t dataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(pipeline), u64(firstGroup), u64(groupCount), u64(dataSize), u64(pData)}; + return from_u64(axrb_vk_call(423, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetRayTracingShaderGroupHandlesNV(VkDevice device, VkPipeline pipeline, uint32_t firstGroup, uint32_t groupCount, size_t dataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(pipeline), u64(firstGroup), u64(groupCount), u64(dataSize), u64(pData)}; + return from_u64(axrb_vk_call(424, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetAccelerationStructureHandleNV(VkDevice device, VkAccelerationStructureNV accelerationStructure, size_t dataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(accelerationStructure), u64(dataSize), u64(pData)}; + return from_u64(axrb_vk_call(425, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteAccelerationStructuresPropertiesNV(VkCommandBuffer commandBuffer, uint32_t accelerationStructureCount, const VkAccelerationStructureNV* pAccelerationStructures, VkQueryType queryType, VkQueryPool queryPool, uint32_t firstQuery) { + const uint64_t a[] = {u64(commandBuffer), u64(accelerationStructureCount), u64(pAccelerationStructures), u64(queryType), u64(queryPool), u64(firstQuery)}; + axrb_vk_call(426, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCompileDeferredNV(VkDevice device, VkPipeline pipeline, uint32_t shader) { + const uint64_t a[] = {u64(device), u64(pipeline), u64(shader)}; + return from_u64(axrb_vk_call(427, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetMemoryHostPointerPropertiesEXT(VkDevice device, VkExternalMemoryHandleTypeFlagBits handleType, const void* pHostPointer, VkMemoryHostPointerPropertiesEXT* pMemoryHostPointerProperties) { + const uint64_t a[] = {u64(device), u64(handleType), u64(pHostPointer), u64(pMemoryHostPointerProperties)}; + return from_u64(axrb_vk_call(428, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteBufferMarkerAMD(VkCommandBuffer commandBuffer, VkPipelineStageFlagBits pipelineStage, VkBuffer dstBuffer, VkDeviceSize dstOffset, uint32_t marker) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineStage), u64(dstBuffer), u64(dstOffset), u64(marker)}; + axrb_vk_call(429, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceCalibrateableTimeDomainsEXT(VkPhysicalDevice physicalDevice, uint32_t* pTimeDomainCount, VkTimeDomainKHR* pTimeDomains) { + const uint64_t a[] = {u64(physicalDevice), u64(pTimeDomainCount), u64(pTimeDomains)}; + return from_u64(axrb_vk_call(430, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetCalibratedTimestampsEXT(VkDevice device, uint32_t timestampCount, const VkCalibratedTimestampInfoKHR* pTimestampInfos, uint64_t* pTimestamps, uint64_t* pMaxDeviation) { + const uint64_t a[] = {u64(device), u64(timestampCount), u64(pTimestampInfos), u64(pTimestamps), u64(pMaxDeviation)}; + return from_u64(axrb_vk_call(431, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMeshTasksNV(VkCommandBuffer commandBuffer, uint32_t taskCount, uint32_t firstTask) { + const uint64_t a[] = {u64(commandBuffer), u64(taskCount), u64(firstTask)}; + axrb_vk_call(432, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMeshTasksIndirectNV(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, uint32_t drawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(drawCount), u64(stride)}; + axrb_vk_call(433, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMeshTasksIndirectCountNV(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(434, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetExclusiveScissorEnableNV(VkCommandBuffer commandBuffer, uint32_t firstExclusiveScissor, uint32_t exclusiveScissorCount, const VkBool32* pExclusiveScissorEnables) { + const uint64_t a[] = {u64(commandBuffer), u64(firstExclusiveScissor), u64(exclusiveScissorCount), u64(pExclusiveScissorEnables)}; + axrb_vk_call(435, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetExclusiveScissorNV(VkCommandBuffer commandBuffer, uint32_t firstExclusiveScissor, uint32_t exclusiveScissorCount, const VkRect2D* pExclusiveScissors) { + const uint64_t a[] = {u64(commandBuffer), u64(firstExclusiveScissor), u64(exclusiveScissorCount), u64(pExclusiveScissors)}; + axrb_vk_call(436, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCheckpointNV(VkCommandBuffer commandBuffer, const void* pCheckpointMarker) { + const uint64_t a[] = {u64(commandBuffer), u64(pCheckpointMarker)}; + axrb_vk_call(437, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetQueueCheckpointDataNV(VkQueue queue, uint32_t* pCheckpointDataCount, VkCheckpointDataNV* pCheckpointData) { + const uint64_t a[] = {u64(queue), u64(pCheckpointDataCount), u64(pCheckpointData)}; + axrb_vk_call(438, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkInitializePerformanceApiINTEL(VkDevice device, const VkInitializePerformanceApiInfoINTEL* pInitializeInfo) { + const uint64_t a[] = {u64(device), u64(pInitializeInfo)}; + return from_u64(axrb_vk_call(439, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkUninitializePerformanceApiINTEL(VkDevice device) { + const uint64_t a[] = {u64(device)}; + axrb_vk_call(440, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCmdSetPerformanceMarkerINTEL(VkCommandBuffer commandBuffer, const VkPerformanceMarkerInfoINTEL* pMarkerInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pMarkerInfo)}; + return from_u64(axrb_vk_call(441, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCmdSetPerformanceStreamMarkerINTEL(VkCommandBuffer commandBuffer, const VkPerformanceStreamMarkerInfoINTEL* pMarkerInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pMarkerInfo)}; + return from_u64(axrb_vk_call(442, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCmdSetPerformanceOverrideINTEL(VkCommandBuffer commandBuffer, const VkPerformanceOverrideInfoINTEL* pOverrideInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pOverrideInfo)}; + return from_u64(axrb_vk_call(443, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAcquirePerformanceConfigurationINTEL(VkDevice device, const VkPerformanceConfigurationAcquireInfoINTEL* pAcquireInfo, VkPerformanceConfigurationINTEL* pConfiguration) { + const uint64_t a[] = {u64(device), u64(pAcquireInfo), u64(pConfiguration)}; + return from_u64(axrb_vk_call(444, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkReleasePerformanceConfigurationINTEL(VkDevice device, VkPerformanceConfigurationINTEL configuration) { + const uint64_t a[] = {u64(device), u64(configuration)}; + return from_u64(axrb_vk_call(445, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkQueueSetPerformanceConfigurationINTEL(VkQueue queue, VkPerformanceConfigurationINTEL configuration) { + const uint64_t a[] = {u64(queue), u64(configuration)}; + return from_u64(axrb_vk_call(446, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPerformanceParameterINTEL(VkDevice device, VkPerformanceParameterTypeINTEL parameter, VkPerformanceValueINTEL* pValue) { + const uint64_t a[] = {u64(device), u64(parameter), u64(pValue)}; + return from_u64(axrb_vk_call(447, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkSetLocalDimmingAMD(VkDevice device, VkSwapchainKHR swapChain, VkBool32 localDimmingEnable) { + const uint64_t a[] = {u64(device), u64(swapChain), u64(localDimmingEnable)}; + axrb_vk_call(448, a); +} +extern "C" VKAPI_ATTR VkDeviceAddress VKAPI_CALL vkGetBufferDeviceAddressEXT(VkDevice device, const VkBufferDeviceAddressInfo* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(449, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceToolPropertiesEXT(VkPhysicalDevice physicalDevice, uint32_t* pToolCount, VkPhysicalDeviceToolProperties* pToolProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pToolCount), u64(pToolProperties)}; + return from_u64(axrb_vk_call(450, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceCooperativeMatrixPropertiesNV(VkPhysicalDevice physicalDevice, uint32_t* pPropertyCount, VkCooperativeMatrixPropertiesNV* pProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pPropertyCount), u64(pProperties)}; + return from_u64(axrb_vk_call(451, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV(VkPhysicalDevice physicalDevice, uint32_t* pCombinationCount, VkFramebufferMixedSamplesCombinationNV* pCombinations) { + const uint64_t a[] = {u64(physicalDevice), u64(pCombinationCount), u64(pCombinations)}; + return from_u64(axrb_vk_call(452, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateHeadlessSurfaceEXT(VkInstance instance, const VkHeadlessSurfaceCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) { + const uint64_t a[] = {u64(instance), u64(pCreateInfo), u64(pAllocator), u64(pSurface)}; + return from_u64(axrb_vk_call(453, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetLineStippleEXT(VkCommandBuffer commandBuffer, uint32_t lineStippleFactor, uint16_t lineStipplePattern) { + const uint64_t a[] = {u64(commandBuffer), u64(lineStippleFactor), u64(lineStipplePattern)}; + axrb_vk_call(454, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkResetQueryPoolEXT(VkDevice device, VkQueryPool queryPool, uint32_t firstQuery, uint32_t queryCount) { + const uint64_t a[] = {u64(device), u64(queryPool), u64(firstQuery), u64(queryCount)}; + axrb_vk_call(455, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCullModeEXT(VkCommandBuffer commandBuffer, VkCullModeFlags cullMode) { + const uint64_t a[] = {u64(commandBuffer), u64(cullMode)}; + axrb_vk_call(456, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetFrontFaceEXT(VkCommandBuffer commandBuffer, VkFrontFace frontFace) { + const uint64_t a[] = {u64(commandBuffer), u64(frontFace)}; + axrb_vk_call(457, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetPrimitiveTopologyEXT(VkCommandBuffer commandBuffer, VkPrimitiveTopology primitiveTopology) { + const uint64_t a[] = {u64(commandBuffer), u64(primitiveTopology)}; + axrb_vk_call(458, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetViewportWithCountEXT(VkCommandBuffer commandBuffer, uint32_t viewportCount, const VkViewport* pViewports) { + const uint64_t a[] = {u64(commandBuffer), u64(viewportCount), u64(pViewports)}; + axrb_vk_call(459, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetScissorWithCountEXT(VkCommandBuffer commandBuffer, uint32_t scissorCount, const VkRect2D* pScissors) { + const uint64_t a[] = {u64(commandBuffer), u64(scissorCount), u64(pScissors)}; + axrb_vk_call(460, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindVertexBuffers2EXT(VkCommandBuffer commandBuffer, uint32_t firstBinding, uint32_t bindingCount, const VkBuffer* pBuffers, const VkDeviceSize* pOffsets, const VkDeviceSize* pSizes, const VkDeviceSize* pStrides) { + const uint64_t a[] = {u64(commandBuffer), u64(firstBinding), u64(bindingCount), u64(pBuffers), u64(pOffsets), u64(pSizes), u64(pStrides)}; + axrb_vk_call(461, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthTestEnableEXT(VkCommandBuffer commandBuffer, VkBool32 depthTestEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthTestEnable)}; + axrb_vk_call(462, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthWriteEnableEXT(VkCommandBuffer commandBuffer, VkBool32 depthWriteEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthWriteEnable)}; + axrb_vk_call(463, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthCompareOpEXT(VkCommandBuffer commandBuffer, VkCompareOp depthCompareOp) { + const uint64_t a[] = {u64(commandBuffer), u64(depthCompareOp)}; + axrb_vk_call(464, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthBoundsTestEnableEXT(VkCommandBuffer commandBuffer, VkBool32 depthBoundsTestEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthBoundsTestEnable)}; + axrb_vk_call(465, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetStencilTestEnableEXT(VkCommandBuffer commandBuffer, VkBool32 stencilTestEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(stencilTestEnable)}; + axrb_vk_call(466, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetStencilOpEXT(VkCommandBuffer commandBuffer, VkStencilFaceFlags faceMask, VkStencilOp failOp, VkStencilOp passOp, VkStencilOp depthFailOp, VkCompareOp compareOp) { + const uint64_t a[] = {u64(commandBuffer), u64(faceMask), u64(failOp), u64(passOp), u64(depthFailOp), u64(compareOp)}; + axrb_vk_call(467, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyMemoryToImageEXT(VkDevice device, const VkCopyMemoryToImageInfoEXT* pCopyMemoryToImageInfo) { + const uint64_t a[] = {u64(device), u64(pCopyMemoryToImageInfo)}; + return from_u64(axrb_vk_call(468, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyImageToMemoryEXT(VkDevice device, const VkCopyImageToMemoryInfoEXT* pCopyImageToMemoryInfo) { + const uint64_t a[] = {u64(device), u64(pCopyImageToMemoryInfo)}; + return from_u64(axrb_vk_call(469, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyImageToImageEXT(VkDevice device, const VkCopyImageToImageInfoEXT* pCopyImageToImageInfo) { + const uint64_t a[] = {u64(device), u64(pCopyImageToImageInfo)}; + return from_u64(axrb_vk_call(470, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkTransitionImageLayoutEXT(VkDevice device, uint32_t transitionCount, const VkHostImageLayoutTransitionInfoEXT* pTransitions) { + const uint64_t a[] = {u64(device), u64(transitionCount), u64(pTransitions)}; + return from_u64(axrb_vk_call(471, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetImageSubresourceLayout2EXT(VkDevice device, VkImage image, const VkImageSubresource2KHR* pSubresource, VkSubresourceLayout2KHR* pLayout) { + const uint64_t a[] = {u64(device), u64(image), u64(pSubresource), u64(pLayout)}; + axrb_vk_call(472, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkReleaseSwapchainImagesEXT(VkDevice device, const VkReleaseSwapchainImagesInfoEXT* pReleaseInfo) { + const uint64_t a[] = {u64(device), u64(pReleaseInfo)}; + return from_u64(axrb_vk_call(473, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetGeneratedCommandsMemoryRequirementsNV(VkDevice device, const VkGeneratedCommandsMemoryRequirementsInfoNV* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pMemoryRequirements)}; + axrb_vk_call(474, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdPreprocessGeneratedCommandsNV(VkCommandBuffer commandBuffer, const VkGeneratedCommandsInfoNV* pGeneratedCommandsInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pGeneratedCommandsInfo)}; + axrb_vk_call(475, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdExecuteGeneratedCommandsNV(VkCommandBuffer commandBuffer, VkBool32 isPreprocessed, const VkGeneratedCommandsInfoNV* pGeneratedCommandsInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(isPreprocessed), u64(pGeneratedCommandsInfo)}; + axrb_vk_call(476, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindPipelineShaderGroupNV(VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline, uint32_t groupIndex) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineBindPoint), u64(pipeline), u64(groupIndex)}; + axrb_vk_call(477, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateIndirectCommandsLayoutNV(VkDevice device, const VkIndirectCommandsLayoutCreateInfoNV* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkIndirectCommandsLayoutNV* pIndirectCommandsLayout) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pIndirectCommandsLayout)}; + return from_u64(axrb_vk_call(478, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyIndirectCommandsLayoutNV(VkDevice device, VkIndirectCommandsLayoutNV indirectCommandsLayout, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(indirectCommandsLayout), u64(pAllocator)}; + axrb_vk_call(479, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthBias2EXT(VkCommandBuffer commandBuffer, const VkDepthBiasInfoEXT* pDepthBiasInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pDepthBiasInfo)}; + axrb_vk_call(480, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkAcquireDrmDisplayEXT(VkPhysicalDevice physicalDevice, int32_t drmFd, VkDisplayKHR display) { + const uint64_t a[] = {u64(physicalDevice), u64(drmFd), u64(display)}; + return from_u64(axrb_vk_call(481, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDrmDisplayEXT(VkPhysicalDevice physicalDevice, int32_t drmFd, uint32_t connectorId, VkDisplayKHR* display) { + const uint64_t a[] = {u64(physicalDevice), u64(drmFd), u64(connectorId), u64(display)}; + return from_u64(axrb_vk_call(482, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreatePrivateDataSlotEXT(VkDevice device, const VkPrivateDataSlotCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkPrivateDataSlot* pPrivateDataSlot) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pPrivateDataSlot)}; + return from_u64(axrb_vk_call(483, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyPrivateDataSlotEXT(VkDevice device, VkPrivateDataSlot privateDataSlot, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(privateDataSlot), u64(pAllocator)}; + axrb_vk_call(484, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSetPrivateDataEXT(VkDevice device, VkObjectType objectType, uint64_t objectHandle, VkPrivateDataSlot privateDataSlot, uint64_t data) { + const uint64_t a[] = {u64(device), u64(objectType), u64(objectHandle), u64(privateDataSlot), u64(data)}; + return from_u64(axrb_vk_call(485, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPrivateDataEXT(VkDevice device, VkObjectType objectType, uint64_t objectHandle, VkPrivateDataSlot privateDataSlot, uint64_t* pData) { + const uint64_t a[] = {u64(device), u64(objectType), u64(objectHandle), u64(privateDataSlot), u64(pData)}; + axrb_vk_call(486, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateCudaModuleNV(VkDevice device, const VkCudaModuleCreateInfoNV* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkCudaModuleNV* pModule) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pModule)}; + return from_u64(axrb_vk_call(487, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetCudaModuleCacheNV(VkDevice device, VkCudaModuleNV module, size_t* pCacheSize, void* pCacheData) { + const uint64_t a[] = {u64(device), u64(module), u64(pCacheSize), u64(pCacheData)}; + return from_u64(axrb_vk_call(488, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateCudaFunctionNV(VkDevice device, const VkCudaFunctionCreateInfoNV* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkCudaFunctionNV* pFunction) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pFunction)}; + return from_u64(axrb_vk_call(489, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyCudaModuleNV(VkDevice device, VkCudaModuleNV module, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(module), u64(pAllocator)}; + axrb_vk_call(490, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyCudaFunctionNV(VkDevice device, VkCudaFunctionNV function, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(function), u64(pAllocator)}; + axrb_vk_call(491, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCudaLaunchKernelNV(VkCommandBuffer commandBuffer, const VkCudaLaunchInfoNV* pLaunchInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pLaunchInfo)}; + axrb_vk_call(492, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDescriptorSetLayoutSizeEXT(VkDevice device, VkDescriptorSetLayout layout, VkDeviceSize* pLayoutSizeInBytes) { + const uint64_t a[] = {u64(device), u64(layout), u64(pLayoutSizeInBytes)}; + axrb_vk_call(493, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDescriptorSetLayoutBindingOffsetEXT(VkDevice device, VkDescriptorSetLayout layout, uint32_t binding, VkDeviceSize* pOffset) { + const uint64_t a[] = {u64(device), u64(layout), u64(binding), u64(pOffset)}; + axrb_vk_call(494, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDescriptorEXT(VkDevice device, const VkDescriptorGetInfoEXT* pDescriptorInfo, size_t dataSize, void* pDescriptor) { + const uint64_t a[] = {u64(device), u64(pDescriptorInfo), u64(dataSize), u64(pDescriptor)}; + axrb_vk_call(495, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorBuffersEXT(VkCommandBuffer commandBuffer, uint32_t bufferCount, const VkDescriptorBufferBindingInfoEXT* pBindingInfos) { + const uint64_t a[] = {u64(commandBuffer), u64(bufferCount), u64(pBindingInfos)}; + axrb_vk_call(496, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDescriptorBufferOffsetsEXT(VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipelineLayout layout, uint32_t firstSet, uint32_t setCount, const uint32_t* pBufferIndices, const VkDeviceSize* pOffsets) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineBindPoint), u64(layout), u64(firstSet), u64(setCount), u64(pBufferIndices), u64(pOffsets)}; + axrb_vk_call(497, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorBufferEmbeddedSamplersEXT(VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipelineLayout layout, uint32_t set) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineBindPoint), u64(layout), u64(set)}; + axrb_vk_call(498, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetBufferOpaqueCaptureDescriptorDataEXT(VkDevice device, const VkBufferCaptureDescriptorDataInfoEXT* pInfo, void* pData) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pData)}; + return from_u64(axrb_vk_call(499, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetImageOpaqueCaptureDescriptorDataEXT(VkDevice device, const VkImageCaptureDescriptorDataInfoEXT* pInfo, void* pData) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pData)}; + return from_u64(axrb_vk_call(500, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetImageViewOpaqueCaptureDescriptorDataEXT(VkDevice device, const VkImageViewCaptureDescriptorDataInfoEXT* pInfo, void* pData) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pData)}; + return from_u64(axrb_vk_call(501, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetSamplerOpaqueCaptureDescriptorDataEXT(VkDevice device, const VkSamplerCaptureDescriptorDataInfoEXT* pInfo, void* pData) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pData)}; + return from_u64(axrb_vk_call(502, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetAccelerationStructureOpaqueCaptureDescriptorDataEXT(VkDevice device, const VkAccelerationStructureCaptureDescriptorDataInfoEXT* pInfo, void* pData) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pData)}; + return from_u64(axrb_vk_call(503, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetFragmentShadingRateEnumNV(VkCommandBuffer commandBuffer, VkFragmentShadingRateNV shadingRate, const VkFragmentShadingRateCombinerOpKHR* combinerOps) { + const uint64_t a[] = {u64(commandBuffer), u64(shadingRate), u64(combinerOps)}; + axrb_vk_call(504, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDeviceFaultInfoEXT(VkDevice device, VkDeviceFaultCountsEXT* pFaultCounts, VkDeviceFaultInfoEXT* pFaultInfo) { + const uint64_t a[] = {u64(device), u64(pFaultCounts), u64(pFaultInfo)}; + return from_u64(axrb_vk_call(505, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetVertexInputEXT(VkCommandBuffer commandBuffer, uint32_t vertexBindingDescriptionCount, const VkVertexInputBindingDescription2EXT* pVertexBindingDescriptions, uint32_t vertexAttributeDescriptionCount, const VkVertexInputAttributeDescription2EXT* pVertexAttributeDescriptions) { + const uint64_t a[] = {u64(commandBuffer), u64(vertexBindingDescriptionCount), u64(pVertexBindingDescriptions), u64(vertexAttributeDescriptionCount), u64(pVertexAttributeDescriptions)}; + axrb_vk_call(506, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDeviceSubpassShadingMaxWorkgroupSizeHUAWEI(VkDevice device, VkRenderPass renderpass, VkExtent2D* pMaxWorkgroupSize) { + const uint64_t a[] = {u64(device), u64(renderpass), u64(pMaxWorkgroupSize)}; + return from_u64(axrb_vk_call(507, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSubpassShadingHUAWEI(VkCommandBuffer commandBuffer) { + const uint64_t a[] = {u64(commandBuffer)}; + axrb_vk_call(508, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindInvocationMaskHUAWEI(VkCommandBuffer commandBuffer, VkImageView imageView, VkImageLayout imageLayout) { + const uint64_t a[] = {u64(commandBuffer), u64(imageView), u64(imageLayout)}; + axrb_vk_call(509, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetMemoryRemoteAddressNV(VkDevice device, const VkMemoryGetRemoteAddressInfoNV* pMemoryGetRemoteAddressInfo, VkRemoteAddressNV* pAddress) { + const uint64_t a[] = {u64(device), u64(pMemoryGetRemoteAddressInfo), u64(pAddress)}; + return from_u64(axrb_vk_call(510, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPipelinePropertiesEXT(VkDevice device, const VkPipelineInfoEXT* pPipelineInfo, VkBaseOutStructure* pPipelineProperties) { + const uint64_t a[] = {u64(device), u64(pPipelineInfo), u64(pPipelineProperties)}; + return from_u64(axrb_vk_call(511, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetPatchControlPointsEXT(VkCommandBuffer commandBuffer, uint32_t patchControlPoints) { + const uint64_t a[] = {u64(commandBuffer), u64(patchControlPoints)}; + axrb_vk_call(512, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetRasterizerDiscardEnableEXT(VkCommandBuffer commandBuffer, VkBool32 rasterizerDiscardEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(rasterizerDiscardEnable)}; + axrb_vk_call(513, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthBiasEnableEXT(VkCommandBuffer commandBuffer, VkBool32 depthBiasEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthBiasEnable)}; + axrb_vk_call(514, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetLogicOpEXT(VkCommandBuffer commandBuffer, VkLogicOp logicOp) { + const uint64_t a[] = {u64(commandBuffer), u64(logicOp)}; + axrb_vk_call(515, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetPrimitiveRestartEnableEXT(VkCommandBuffer commandBuffer, VkBool32 primitiveRestartEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(primitiveRestartEnable)}; + axrb_vk_call(516, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetColorWriteEnableEXT(VkCommandBuffer commandBuffer, uint32_t attachmentCount, const VkBool32* pColorWriteEnables) { + const uint64_t a[] = {u64(commandBuffer), u64(attachmentCount), u64(pColorWriteEnables)}; + axrb_vk_call(517, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMultiEXT(VkCommandBuffer commandBuffer, uint32_t drawCount, const VkMultiDrawInfoEXT* pVertexInfo, uint32_t instanceCount, uint32_t firstInstance, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(drawCount), u64(pVertexInfo), u64(instanceCount), u64(firstInstance), u64(stride)}; + axrb_vk_call(518, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMultiIndexedEXT(VkCommandBuffer commandBuffer, uint32_t drawCount, const VkMultiDrawIndexedInfoEXT* pIndexInfo, uint32_t instanceCount, uint32_t firstInstance, uint32_t stride, const int32_t* pVertexOffset) { + const uint64_t a[] = {u64(commandBuffer), u64(drawCount), u64(pIndexInfo), u64(instanceCount), u64(firstInstance), u64(stride), u64(pVertexOffset)}; + axrb_vk_call(519, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateMicromapEXT(VkDevice device, const VkMicromapCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkMicromapEXT* pMicromap) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pMicromap)}; + return from_u64(axrb_vk_call(520, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyMicromapEXT(VkDevice device, VkMicromapEXT micromap, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(micromap), u64(pAllocator)}; + axrb_vk_call(521, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBuildMicromapsEXT(VkCommandBuffer commandBuffer, uint32_t infoCount, const VkMicromapBuildInfoEXT* pInfos) { + const uint64_t a[] = {u64(commandBuffer), u64(infoCount), u64(pInfos)}; + axrb_vk_call(522, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBuildMicromapsEXT(VkDevice device, VkDeferredOperationKHR deferredOperation, uint32_t infoCount, const VkMicromapBuildInfoEXT* pInfos) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(infoCount), u64(pInfos)}; + return from_u64(axrb_vk_call(523, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyMicromapEXT(VkDevice device, VkDeferredOperationKHR deferredOperation, const VkCopyMicromapInfoEXT* pInfo) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(pInfo)}; + return from_u64(axrb_vk_call(524, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyMicromapToMemoryEXT(VkDevice device, VkDeferredOperationKHR deferredOperation, const VkCopyMicromapToMemoryInfoEXT* pInfo) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(pInfo)}; + return from_u64(axrb_vk_call(525, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyMemoryToMicromapEXT(VkDevice device, VkDeferredOperationKHR deferredOperation, const VkCopyMemoryToMicromapInfoEXT* pInfo) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(pInfo)}; + return from_u64(axrb_vk_call(526, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkWriteMicromapsPropertiesEXT(VkDevice device, uint32_t micromapCount, const VkMicromapEXT* pMicromaps, VkQueryType queryType, size_t dataSize, void* pData, size_t stride) { + const uint64_t a[] = {u64(device), u64(micromapCount), u64(pMicromaps), u64(queryType), u64(dataSize), u64(pData), u64(stride)}; + return from_u64(axrb_vk_call(527, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyMicromapEXT(VkCommandBuffer commandBuffer, const VkCopyMicromapInfoEXT* pInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pInfo)}; + axrb_vk_call(528, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyMicromapToMemoryEXT(VkCommandBuffer commandBuffer, const VkCopyMicromapToMemoryInfoEXT* pInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pInfo)}; + axrb_vk_call(529, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyMemoryToMicromapEXT(VkCommandBuffer commandBuffer, const VkCopyMemoryToMicromapInfoEXT* pInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pInfo)}; + axrb_vk_call(530, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteMicromapsPropertiesEXT(VkCommandBuffer commandBuffer, uint32_t micromapCount, const VkMicromapEXT* pMicromaps, VkQueryType queryType, VkQueryPool queryPool, uint32_t firstQuery) { + const uint64_t a[] = {u64(commandBuffer), u64(micromapCount), u64(pMicromaps), u64(queryType), u64(queryPool), u64(firstQuery)}; + axrb_vk_call(531, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceMicromapCompatibilityEXT(VkDevice device, const VkMicromapVersionInfoEXT* pVersionInfo, VkAccelerationStructureCompatibilityKHR* pCompatibility) { + const uint64_t a[] = {u64(device), u64(pVersionInfo), u64(pCompatibility)}; + axrb_vk_call(532, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetMicromapBuildSizesEXT(VkDevice device, VkAccelerationStructureBuildTypeKHR buildType, const VkMicromapBuildInfoEXT* pBuildInfo, VkMicromapBuildSizesInfoEXT* pSizeInfo) { + const uint64_t a[] = {u64(device), u64(buildType), u64(pBuildInfo), u64(pSizeInfo)}; + axrb_vk_call(533, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawClusterHUAWEI(VkCommandBuffer commandBuffer, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { + const uint64_t a[] = {u64(commandBuffer), u64(groupCountX), u64(groupCountY), u64(groupCountZ)}; + axrb_vk_call(534, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawClusterIndirectHUAWEI(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset)}; + axrb_vk_call(535, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkSetDeviceMemoryPriorityEXT(VkDevice device, VkDeviceMemory memory, float priority) { + const uint64_t a[] = {u64(device), u64(memory), u64(priority)}; + axrb_vk_call(536, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDescriptorSetLayoutHostMappingInfoVALVE(VkDevice device, const VkDescriptorSetBindingReferenceVALVE* pBindingReference, VkDescriptorSetLayoutHostMappingInfoVALVE* pHostMapping) { + const uint64_t a[] = {u64(device), u64(pBindingReference), u64(pHostMapping)}; + axrb_vk_call(537, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDescriptorSetHostMappingVALVE(VkDevice device, VkDescriptorSet descriptorSet, void** ppData) { + const uint64_t a[] = {u64(device), u64(descriptorSet), u64(ppData)}; + axrb_vk_call(538, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyMemoryIndirectNV(VkCommandBuffer commandBuffer, VkDeviceAddress copyBufferAddress, uint32_t copyCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(copyBufferAddress), u64(copyCount), u64(stride)}; + axrb_vk_call(539, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyMemoryToImageIndirectNV(VkCommandBuffer commandBuffer, VkDeviceAddress copyBufferAddress, uint32_t copyCount, uint32_t stride, VkImage dstImage, VkImageLayout dstImageLayout, const VkImageSubresourceLayers* pImageSubresources) { + const uint64_t a[] = {u64(commandBuffer), u64(copyBufferAddress), u64(copyCount), u64(stride), u64(dstImage), u64(dstImageLayout), u64(pImageSubresources)}; + axrb_vk_call(540, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDecompressMemoryNV(VkCommandBuffer commandBuffer, uint32_t decompressRegionCount, const VkDecompressMemoryRegionNV* pDecompressMemoryRegions) { + const uint64_t a[] = {u64(commandBuffer), u64(decompressRegionCount), u64(pDecompressMemoryRegions)}; + axrb_vk_call(541, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDecompressMemoryIndirectCountNV(VkCommandBuffer commandBuffer, VkDeviceAddress indirectCommandsAddress, VkDeviceAddress indirectCommandsCountAddress, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(indirectCommandsAddress), u64(indirectCommandsCountAddress), u64(stride)}; + axrb_vk_call(542, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetPipelineIndirectMemoryRequirementsNV(VkDevice device, const VkComputePipelineCreateInfo* pCreateInfo, VkMemoryRequirements2* pMemoryRequirements) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pMemoryRequirements)}; + axrb_vk_call(543, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdUpdatePipelineIndirectBufferNV(VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineBindPoint), u64(pipeline)}; + axrb_vk_call(544, a); +} +extern "C" VKAPI_ATTR VkDeviceAddress VKAPI_CALL vkGetPipelineIndirectDeviceAddressNV(VkDevice device, const VkPipelineIndirectDeviceAddressInfoNV* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(545, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetTessellationDomainOriginEXT(VkCommandBuffer commandBuffer, VkTessellationDomainOrigin domainOrigin) { + const uint64_t a[] = {u64(commandBuffer), u64(domainOrigin)}; + axrb_vk_call(546, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthClampEnableEXT(VkCommandBuffer commandBuffer, VkBool32 depthClampEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthClampEnable)}; + axrb_vk_call(547, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetPolygonModeEXT(VkCommandBuffer commandBuffer, VkPolygonMode polygonMode) { + const uint64_t a[] = {u64(commandBuffer), u64(polygonMode)}; + axrb_vk_call(548, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetRasterizationSamplesEXT(VkCommandBuffer commandBuffer, VkSampleCountFlagBits rasterizationSamples) { + const uint64_t a[] = {u64(commandBuffer), u64(rasterizationSamples)}; + axrb_vk_call(549, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetSampleMaskEXT(VkCommandBuffer commandBuffer, VkSampleCountFlagBits samples, const VkSampleMask* pSampleMask) { + const uint64_t a[] = {u64(commandBuffer), u64(samples), u64(pSampleMask)}; + axrb_vk_call(550, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetAlphaToCoverageEnableEXT(VkCommandBuffer commandBuffer, VkBool32 alphaToCoverageEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(alphaToCoverageEnable)}; + axrb_vk_call(551, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetAlphaToOneEnableEXT(VkCommandBuffer commandBuffer, VkBool32 alphaToOneEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(alphaToOneEnable)}; + axrb_vk_call(552, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetLogicOpEnableEXT(VkCommandBuffer commandBuffer, VkBool32 logicOpEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(logicOpEnable)}; + axrb_vk_call(553, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetColorBlendEnableEXT(VkCommandBuffer commandBuffer, uint32_t firstAttachment, uint32_t attachmentCount, const VkBool32* pColorBlendEnables) { + const uint64_t a[] = {u64(commandBuffer), u64(firstAttachment), u64(attachmentCount), u64(pColorBlendEnables)}; + axrb_vk_call(554, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetColorBlendEquationEXT(VkCommandBuffer commandBuffer, uint32_t firstAttachment, uint32_t attachmentCount, const VkColorBlendEquationEXT* pColorBlendEquations) { + const uint64_t a[] = {u64(commandBuffer), u64(firstAttachment), u64(attachmentCount), u64(pColorBlendEquations)}; + axrb_vk_call(555, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetColorWriteMaskEXT(VkCommandBuffer commandBuffer, uint32_t firstAttachment, uint32_t attachmentCount, const VkColorComponentFlags* pColorWriteMasks) { + const uint64_t a[] = {u64(commandBuffer), u64(firstAttachment), u64(attachmentCount), u64(pColorWriteMasks)}; + axrb_vk_call(556, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetRasterizationStreamEXT(VkCommandBuffer commandBuffer, uint32_t rasterizationStream) { + const uint64_t a[] = {u64(commandBuffer), u64(rasterizationStream)}; + axrb_vk_call(557, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetConservativeRasterizationModeEXT(VkCommandBuffer commandBuffer, VkConservativeRasterizationModeEXT conservativeRasterizationMode) { + const uint64_t a[] = {u64(commandBuffer), u64(conservativeRasterizationMode)}; + axrb_vk_call(558, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetExtraPrimitiveOverestimationSizeEXT(VkCommandBuffer commandBuffer, float extraPrimitiveOverestimationSize) { + const uint64_t a[] = {u64(commandBuffer), u64(extraPrimitiveOverestimationSize)}; + axrb_vk_call(559, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthClipEnableEXT(VkCommandBuffer commandBuffer, VkBool32 depthClipEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(depthClipEnable)}; + axrb_vk_call(560, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetSampleLocationsEnableEXT(VkCommandBuffer commandBuffer, VkBool32 sampleLocationsEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(sampleLocationsEnable)}; + axrb_vk_call(561, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetColorBlendAdvancedEXT(VkCommandBuffer commandBuffer, uint32_t firstAttachment, uint32_t attachmentCount, const VkColorBlendAdvancedEXT* pColorBlendAdvanced) { + const uint64_t a[] = {u64(commandBuffer), u64(firstAttachment), u64(attachmentCount), u64(pColorBlendAdvanced)}; + axrb_vk_call(562, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetProvokingVertexModeEXT(VkCommandBuffer commandBuffer, VkProvokingVertexModeEXT provokingVertexMode) { + const uint64_t a[] = {u64(commandBuffer), u64(provokingVertexMode)}; + axrb_vk_call(563, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetLineRasterizationModeEXT(VkCommandBuffer commandBuffer, VkLineRasterizationModeEXT lineRasterizationMode) { + const uint64_t a[] = {u64(commandBuffer), u64(lineRasterizationMode)}; + axrb_vk_call(564, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetLineStippleEnableEXT(VkCommandBuffer commandBuffer, VkBool32 stippledLineEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(stippledLineEnable)}; + axrb_vk_call(565, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetDepthClipNegativeOneToOneEXT(VkCommandBuffer commandBuffer, VkBool32 negativeOneToOne) { + const uint64_t a[] = {u64(commandBuffer), u64(negativeOneToOne)}; + axrb_vk_call(566, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetViewportWScalingEnableNV(VkCommandBuffer commandBuffer, VkBool32 viewportWScalingEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(viewportWScalingEnable)}; + axrb_vk_call(567, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetViewportSwizzleNV(VkCommandBuffer commandBuffer, uint32_t firstViewport, uint32_t viewportCount, const VkViewportSwizzleNV* pViewportSwizzles) { + const uint64_t a[] = {u64(commandBuffer), u64(firstViewport), u64(viewportCount), u64(pViewportSwizzles)}; + axrb_vk_call(568, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCoverageToColorEnableNV(VkCommandBuffer commandBuffer, VkBool32 coverageToColorEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(coverageToColorEnable)}; + axrb_vk_call(569, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCoverageToColorLocationNV(VkCommandBuffer commandBuffer, uint32_t coverageToColorLocation) { + const uint64_t a[] = {u64(commandBuffer), u64(coverageToColorLocation)}; + axrb_vk_call(570, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCoverageModulationModeNV(VkCommandBuffer commandBuffer, VkCoverageModulationModeNV coverageModulationMode) { + const uint64_t a[] = {u64(commandBuffer), u64(coverageModulationMode)}; + axrb_vk_call(571, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCoverageModulationTableEnableNV(VkCommandBuffer commandBuffer, VkBool32 coverageModulationTableEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(coverageModulationTableEnable)}; + axrb_vk_call(572, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCoverageModulationTableNV(VkCommandBuffer commandBuffer, uint32_t coverageModulationTableCount, const float* pCoverageModulationTable) { + const uint64_t a[] = {u64(commandBuffer), u64(coverageModulationTableCount), u64(pCoverageModulationTable)}; + axrb_vk_call(573, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetShadingRateImageEnableNV(VkCommandBuffer commandBuffer, VkBool32 shadingRateImageEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(shadingRateImageEnable)}; + axrb_vk_call(574, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetRepresentativeFragmentTestEnableNV(VkCommandBuffer commandBuffer, VkBool32 representativeFragmentTestEnable) { + const uint64_t a[] = {u64(commandBuffer), u64(representativeFragmentTestEnable)}; + axrb_vk_call(575, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetCoverageReductionModeNV(VkCommandBuffer commandBuffer, VkCoverageReductionModeNV coverageReductionMode) { + const uint64_t a[] = {u64(commandBuffer), u64(coverageReductionMode)}; + axrb_vk_call(576, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetShaderModuleIdentifierEXT(VkDevice device, VkShaderModule shaderModule, VkShaderModuleIdentifierEXT* pIdentifier) { + const uint64_t a[] = {u64(device), u64(shaderModule), u64(pIdentifier)}; + axrb_vk_call(577, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetShaderModuleCreateInfoIdentifierEXT(VkDevice device, const VkShaderModuleCreateInfo* pCreateInfo, VkShaderModuleIdentifierEXT* pIdentifier) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pIdentifier)}; + axrb_vk_call(578, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetPhysicalDeviceOpticalFlowImageFormatsNV(VkPhysicalDevice physicalDevice, const VkOpticalFlowImageFormatInfoNV* pOpticalFlowImageFormatInfo, uint32_t* pFormatCount, VkOpticalFlowImageFormatPropertiesNV* pImageFormatProperties) { + const uint64_t a[] = {u64(physicalDevice), u64(pOpticalFlowImageFormatInfo), u64(pFormatCount), u64(pImageFormatProperties)}; + return from_u64(axrb_vk_call(579, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateOpticalFlowSessionNV(VkDevice device, const VkOpticalFlowSessionCreateInfoNV* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkOpticalFlowSessionNV* pSession) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pSession)}; + return from_u64(axrb_vk_call(580, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyOpticalFlowSessionNV(VkDevice device, VkOpticalFlowSessionNV session, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(session), u64(pAllocator)}; + axrb_vk_call(581, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBindOpticalFlowSessionImageNV(VkDevice device, VkOpticalFlowSessionNV session, VkOpticalFlowSessionBindingPointNV bindingPoint, VkImageView view, VkImageLayout layout) { + const uint64_t a[] = {u64(device), u64(session), u64(bindingPoint), u64(view), u64(layout)}; + return from_u64(axrb_vk_call(582, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdOpticalFlowExecuteNV(VkCommandBuffer commandBuffer, VkOpticalFlowSessionNV session, const VkOpticalFlowExecuteInfoNV* pExecuteInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(session), u64(pExecuteInfo)}; + axrb_vk_call(583, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateShadersEXT(VkDevice device, uint32_t createInfoCount, const VkShaderCreateInfoEXT* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkShaderEXT* pShaders) { + const uint64_t a[] = {u64(device), u64(createInfoCount), u64(pCreateInfos), u64(pAllocator), u64(pShaders)}; + return from_u64(axrb_vk_call(584, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyShaderEXT(VkDevice device, VkShaderEXT shader, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(shader), u64(pAllocator)}; + axrb_vk_call(585, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetShaderBinaryDataEXT(VkDevice device, VkShaderEXT shader, size_t* pDataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(shader), u64(pDataSize), u64(pData)}; + return from_u64(axrb_vk_call(586, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBindShadersEXT(VkCommandBuffer commandBuffer, uint32_t stageCount, const VkShaderStageFlagBits* pStages, const VkShaderEXT* pShaders) { + const uint64_t a[] = {u64(commandBuffer), u64(stageCount), u64(pStages), u64(pShaders)}; + axrb_vk_call(587, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetFramebufferTilePropertiesQCOM(VkDevice device, VkFramebuffer framebuffer, uint32_t* pPropertiesCount, VkTilePropertiesQCOM* pProperties) { + const uint64_t a[] = {u64(device), u64(framebuffer), u64(pPropertiesCount), u64(pProperties)}; + return from_u64(axrb_vk_call(588, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetDynamicRenderingTilePropertiesQCOM(VkDevice device, const VkRenderingInfo* pRenderingInfo, VkTilePropertiesQCOM* pProperties) { + const uint64_t a[] = {u64(device), u64(pRenderingInfo), u64(pProperties)}; + return from_u64(axrb_vk_call(589, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkSetLatencySleepModeNV(VkDevice device, VkSwapchainKHR swapchain, const VkLatencySleepModeInfoNV* pSleepModeInfo) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pSleepModeInfo)}; + return from_u64(axrb_vk_call(590, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkLatencySleepNV(VkDevice device, VkSwapchainKHR swapchain, const VkLatencySleepInfoNV* pSleepInfo) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pSleepInfo)}; + return from_u64(axrb_vk_call(591, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkSetLatencyMarkerNV(VkDevice device, VkSwapchainKHR swapchain, const VkSetLatencyMarkerInfoNV* pLatencyMarkerInfo) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pLatencyMarkerInfo)}; + axrb_vk_call(592, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetLatencyTimingsNV(VkDevice device, VkSwapchainKHR swapchain, VkGetLatencyMarkerInfoNV* pLatencyMarkerInfo) { + const uint64_t a[] = {u64(device), u64(swapchain), u64(pLatencyMarkerInfo)}; + axrb_vk_call(593, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkQueueNotifyOutOfBandNV(VkQueue queue, const VkOutOfBandQueueTypeInfoNV* pQueueTypeInfo) { + const uint64_t a[] = {u64(queue), u64(pQueueTypeInfo)}; + axrb_vk_call(594, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetAttachmentFeedbackLoopEnableEXT(VkCommandBuffer commandBuffer, VkImageAspectFlags aspectMask) { + const uint64_t a[] = {u64(commandBuffer), u64(aspectMask)}; + axrb_vk_call(595, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateAccelerationStructureKHR(VkDevice device, const VkAccelerationStructureCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkAccelerationStructureKHR* pAccelerationStructure) { + const uint64_t a[] = {u64(device), u64(pCreateInfo), u64(pAllocator), u64(pAccelerationStructure)}; + return from_u64(axrb_vk_call(596, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkDestroyAccelerationStructureKHR(VkDevice device, VkAccelerationStructureKHR accelerationStructure, const VkAllocationCallbacks* pAllocator) { + const uint64_t a[] = {u64(device), u64(accelerationStructure), u64(pAllocator)}; + axrb_vk_call(597, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBuildAccelerationStructuresKHR(VkCommandBuffer commandBuffer, uint32_t infoCount, const VkAccelerationStructureBuildGeometryInfoKHR* pInfos, const VkAccelerationStructureBuildRangeInfoKHR* const* ppBuildRangeInfos) { + const uint64_t a[] = {u64(commandBuffer), u64(infoCount), u64(pInfos), u64(ppBuildRangeInfos)}; + axrb_vk_call(598, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdBuildAccelerationStructuresIndirectKHR(VkCommandBuffer commandBuffer, uint32_t infoCount, const VkAccelerationStructureBuildGeometryInfoKHR* pInfos, const VkDeviceAddress* pIndirectDeviceAddresses, const uint32_t* pIndirectStrides, const uint32_t* const* ppMaxPrimitiveCounts) { + const uint64_t a[] = {u64(commandBuffer), u64(infoCount), u64(pInfos), u64(pIndirectDeviceAddresses), u64(pIndirectStrides), u64(ppMaxPrimitiveCounts)}; + axrb_vk_call(599, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkBuildAccelerationStructuresKHR(VkDevice device, VkDeferredOperationKHR deferredOperation, uint32_t infoCount, const VkAccelerationStructureBuildGeometryInfoKHR* pInfos, const VkAccelerationStructureBuildRangeInfoKHR* const* ppBuildRangeInfos) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(infoCount), u64(pInfos), u64(ppBuildRangeInfos)}; + return from_u64(axrb_vk_call(600, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyAccelerationStructureKHR(VkDevice device, VkDeferredOperationKHR deferredOperation, const VkCopyAccelerationStructureInfoKHR* pInfo) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(pInfo)}; + return from_u64(axrb_vk_call(601, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyAccelerationStructureToMemoryKHR(VkDevice device, VkDeferredOperationKHR deferredOperation, const VkCopyAccelerationStructureToMemoryInfoKHR* pInfo) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(pInfo)}; + return from_u64(axrb_vk_call(602, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCopyMemoryToAccelerationStructureKHR(VkDevice device, VkDeferredOperationKHR deferredOperation, const VkCopyMemoryToAccelerationStructureInfoKHR* pInfo) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(pInfo)}; + return from_u64(axrb_vk_call(603, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkWriteAccelerationStructuresPropertiesKHR(VkDevice device, uint32_t accelerationStructureCount, const VkAccelerationStructureKHR* pAccelerationStructures, VkQueryType queryType, size_t dataSize, void* pData, size_t stride) { + const uint64_t a[] = {u64(device), u64(accelerationStructureCount), u64(pAccelerationStructures), u64(queryType), u64(dataSize), u64(pData), u64(stride)}; + return from_u64(axrb_vk_call(604, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyAccelerationStructureKHR(VkCommandBuffer commandBuffer, const VkCopyAccelerationStructureInfoKHR* pInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pInfo)}; + axrb_vk_call(605, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyAccelerationStructureToMemoryKHR(VkCommandBuffer commandBuffer, const VkCopyAccelerationStructureToMemoryInfoKHR* pInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pInfo)}; + axrb_vk_call(606, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdCopyMemoryToAccelerationStructureKHR(VkCommandBuffer commandBuffer, const VkCopyMemoryToAccelerationStructureInfoKHR* pInfo) { + const uint64_t a[] = {u64(commandBuffer), u64(pInfo)}; + axrb_vk_call(607, a); +} +extern "C" VKAPI_ATTR VkDeviceAddress VKAPI_CALL vkGetAccelerationStructureDeviceAddressKHR(VkDevice device, const VkAccelerationStructureDeviceAddressInfoKHR* pInfo) { + const uint64_t a[] = {u64(device), u64(pInfo)}; + return from_u64(axrb_vk_call(608, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdWriteAccelerationStructuresPropertiesKHR(VkCommandBuffer commandBuffer, uint32_t accelerationStructureCount, const VkAccelerationStructureKHR* pAccelerationStructures, VkQueryType queryType, VkQueryPool queryPool, uint32_t firstQuery) { + const uint64_t a[] = {u64(commandBuffer), u64(accelerationStructureCount), u64(pAccelerationStructures), u64(queryType), u64(queryPool), u64(firstQuery)}; + axrb_vk_call(609, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetDeviceAccelerationStructureCompatibilityKHR(VkDevice device, const VkAccelerationStructureVersionInfoKHR* pVersionInfo, VkAccelerationStructureCompatibilityKHR* pCompatibility) { + const uint64_t a[] = {u64(device), u64(pVersionInfo), u64(pCompatibility)}; + axrb_vk_call(610, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkGetAccelerationStructureBuildSizesKHR(VkDevice device, VkAccelerationStructureBuildTypeKHR buildType, const VkAccelerationStructureBuildGeometryInfoKHR* pBuildInfo, const uint32_t* pMaxPrimitiveCounts, VkAccelerationStructureBuildSizesInfoKHR* pSizeInfo) { + const uint64_t a[] = {u64(device), u64(buildType), u64(pBuildInfo), u64(pMaxPrimitiveCounts), u64(pSizeInfo)}; + axrb_vk_call(611, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdTraceRaysKHR(VkCommandBuffer commandBuffer, const VkStridedDeviceAddressRegionKHR* pRaygenShaderBindingTable, const VkStridedDeviceAddressRegionKHR* pMissShaderBindingTable, const VkStridedDeviceAddressRegionKHR* pHitShaderBindingTable, const VkStridedDeviceAddressRegionKHR* pCallableShaderBindingTable, uint32_t width, uint32_t height, uint32_t depth) { + const uint64_t a[] = {u64(commandBuffer), u64(pRaygenShaderBindingTable), u64(pMissShaderBindingTable), u64(pHitShaderBindingTable), u64(pCallableShaderBindingTable), u64(width), u64(height), u64(depth)}; + axrb_vk_call(612, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateRayTracingPipelinesKHR(VkDevice device, VkDeferredOperationKHR deferredOperation, VkPipelineCache pipelineCache, uint32_t createInfoCount, const VkRayTracingPipelineCreateInfoKHR* pCreateInfos, const VkAllocationCallbacks* pAllocator, VkPipeline* pPipelines) { + const uint64_t a[] = {u64(device), u64(deferredOperation), u64(pipelineCache), u64(createInfoCount), u64(pCreateInfos), u64(pAllocator), u64(pPipelines)}; + return from_u64(axrb_vk_call(613, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetRayTracingCaptureReplayShaderGroupHandlesKHR(VkDevice device, VkPipeline pipeline, uint32_t firstGroup, uint32_t groupCount, size_t dataSize, void* pData) { + const uint64_t a[] = {u64(device), u64(pipeline), u64(firstGroup), u64(groupCount), u64(dataSize), u64(pData)}; + return from_u64(axrb_vk_call(614, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdTraceRaysIndirectKHR(VkCommandBuffer commandBuffer, const VkStridedDeviceAddressRegionKHR* pRaygenShaderBindingTable, const VkStridedDeviceAddressRegionKHR* pMissShaderBindingTable, const VkStridedDeviceAddressRegionKHR* pHitShaderBindingTable, const VkStridedDeviceAddressRegionKHR* pCallableShaderBindingTable, VkDeviceAddress indirectDeviceAddress) { + const uint64_t a[] = {u64(commandBuffer), u64(pRaygenShaderBindingTable), u64(pMissShaderBindingTable), u64(pHitShaderBindingTable), u64(pCallableShaderBindingTable), u64(indirectDeviceAddress)}; + axrb_vk_call(615, a); +} +extern "C" VKAPI_ATTR VkDeviceSize VKAPI_CALL vkGetRayTracingShaderGroupStackSizeKHR(VkDevice device, VkPipeline pipeline, uint32_t group, VkShaderGroupShaderKHR groupShader) { + const uint64_t a[] = {u64(device), u64(pipeline), u64(group), u64(groupShader)}; + return from_u64(axrb_vk_call(616, a)); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdSetRayTracingPipelineStackSizeKHR(VkCommandBuffer commandBuffer, uint32_t pipelineStackSize) { + const uint64_t a[] = {u64(commandBuffer), u64(pipelineStackSize)}; + axrb_vk_call(617, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMeshTasksEXT(VkCommandBuffer commandBuffer, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { + const uint64_t a[] = {u64(commandBuffer), u64(groupCountX), u64(groupCountY), u64(groupCountZ)}; + axrb_vk_call(618, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMeshTasksIndirectEXT(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, uint32_t drawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(drawCount), u64(stride)}; + axrb_vk_call(619, a); +} +extern "C" VKAPI_ATTR void VKAPI_CALL vkCmdDrawMeshTasksIndirectCountEXT(VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkBuffer countBuffer, VkDeviceSize countBufferOffset, uint32_t maxDrawCount, uint32_t stride) { + const uint64_t a[] = {u64(commandBuffer), u64(buffer), u64(offset), u64(countBuffer), u64(countBufferOffset), u64(maxDrawCount), u64(stride)}; + axrb_vk_call(620, a); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkCreateAndroidSurfaceKHR(VkInstance instance, const VkAndroidSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) { + const uint64_t a[] = {u64(instance), u64(pCreateInfo), u64(pAllocator), u64(pSurface)}; + return from_u64(axrb_vk_call(621, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetAndroidHardwareBufferPropertiesANDROID(VkDevice device, const struct AHardwareBuffer* buffer, VkAndroidHardwareBufferPropertiesANDROID* pProperties) { + const uint64_t a[] = {u64(device), u64(buffer), u64(pProperties)}; + return from_u64(axrb_vk_call(622, a)); +} +extern "C" VKAPI_ATTR VkResult VKAPI_CALL vkGetMemoryAndroidHardwareBufferANDROID(VkDevice device, const VkMemoryGetAndroidHardwareBufferInfoANDROID* pInfo, struct AHardwareBuffer** pBuffer) { + const uint64_t a[] = {u64(device), u64(pInfo), u64(pBuffer)}; + return from_u64(axrb_vk_call(623, a)); +} + +static const VkCommand kCommands[] = { + {"vkCreateInstance", reinterpret_cast(vkCreateInstance), kGlobal}, + {"vkDestroyInstance", reinterpret_cast(vkDestroyInstance), kInstance}, + {"vkEnumeratePhysicalDevices", reinterpret_cast(vkEnumeratePhysicalDevices), kInstance}, + {"vkGetPhysicalDeviceFeatures", reinterpret_cast(vkGetPhysicalDeviceFeatures), kInstance}, + {"vkGetPhysicalDeviceFormatProperties", reinterpret_cast(vkGetPhysicalDeviceFormatProperties), kInstance}, + {"vkGetPhysicalDeviceImageFormatProperties", reinterpret_cast(vkGetPhysicalDeviceImageFormatProperties), kInstance}, + {"vkGetPhysicalDeviceProperties", reinterpret_cast(vkGetPhysicalDeviceProperties), kInstance}, + {"vkGetPhysicalDeviceQueueFamilyProperties", reinterpret_cast(vkGetPhysicalDeviceQueueFamilyProperties), kInstance}, + {"vkGetPhysicalDeviceMemoryProperties", reinterpret_cast(vkGetPhysicalDeviceMemoryProperties), kInstance}, + {"vkCreateDevice", reinterpret_cast(vkCreateDevice), kInstance}, + {"vkDestroyDevice", reinterpret_cast(vkDestroyDevice), kDevice}, + {"vkEnumerateInstanceExtensionProperties", reinterpret_cast(vkEnumerateInstanceExtensionProperties), kGlobal}, + {"vkEnumerateDeviceExtensionProperties", reinterpret_cast(vkEnumerateDeviceExtensionProperties), kInstance}, + {"vkEnumerateInstanceLayerProperties", reinterpret_cast(vkEnumerateInstanceLayerProperties), kGlobal}, + {"vkEnumerateDeviceLayerProperties", reinterpret_cast(vkEnumerateDeviceLayerProperties), kInstance}, + {"vkGetDeviceQueue", reinterpret_cast(vkGetDeviceQueue), kDevice}, + {"vkQueueSubmit", reinterpret_cast(vkQueueSubmit), kDevice}, + {"vkQueueWaitIdle", reinterpret_cast(vkQueueWaitIdle), kDevice}, + {"vkDeviceWaitIdle", reinterpret_cast(vkDeviceWaitIdle), kDevice}, + {"vkAllocateMemory", reinterpret_cast(vkAllocateMemory), kDevice}, + {"vkFreeMemory", reinterpret_cast(vkFreeMemory), kDevice}, + {"vkMapMemory", reinterpret_cast(vkMapMemory), kDevice}, + {"vkUnmapMemory", reinterpret_cast(vkUnmapMemory), kDevice}, + {"vkFlushMappedMemoryRanges", reinterpret_cast(vkFlushMappedMemoryRanges), kDevice}, + {"vkInvalidateMappedMemoryRanges", reinterpret_cast(vkInvalidateMappedMemoryRanges), kDevice}, + {"vkGetDeviceMemoryCommitment", reinterpret_cast(vkGetDeviceMemoryCommitment), kDevice}, + {"vkBindBufferMemory", reinterpret_cast(vkBindBufferMemory), kDevice}, + {"vkBindImageMemory", reinterpret_cast(vkBindImageMemory), kDevice}, + {"vkGetBufferMemoryRequirements", reinterpret_cast(vkGetBufferMemoryRequirements), kDevice}, + {"vkGetImageMemoryRequirements", reinterpret_cast(vkGetImageMemoryRequirements), kDevice}, + {"vkGetImageSparseMemoryRequirements", reinterpret_cast(vkGetImageSparseMemoryRequirements), kDevice}, + {"vkGetPhysicalDeviceSparseImageFormatProperties", reinterpret_cast(vkGetPhysicalDeviceSparseImageFormatProperties), kInstance}, + {"vkQueueBindSparse", reinterpret_cast(vkQueueBindSparse), kDevice}, + {"vkCreateFence", reinterpret_cast(vkCreateFence), kDevice}, + {"vkDestroyFence", reinterpret_cast(vkDestroyFence), kDevice}, + {"vkResetFences", reinterpret_cast(vkResetFences), kDevice}, + {"vkGetFenceStatus", reinterpret_cast(vkGetFenceStatus), kDevice}, + {"vkWaitForFences", reinterpret_cast(vkWaitForFences), kDevice}, + {"vkCreateSemaphore", reinterpret_cast(vkCreateSemaphore), kDevice}, + {"vkDestroySemaphore", reinterpret_cast(vkDestroySemaphore), kDevice}, + {"vkCreateEvent", reinterpret_cast(vkCreateEvent), kDevice}, + {"vkDestroyEvent", reinterpret_cast(vkDestroyEvent), kDevice}, + {"vkGetEventStatus", reinterpret_cast(vkGetEventStatus), kDevice}, + {"vkSetEvent", reinterpret_cast(vkSetEvent), kDevice}, + {"vkResetEvent", reinterpret_cast(vkResetEvent), kDevice}, + {"vkCreateQueryPool", reinterpret_cast(vkCreateQueryPool), kDevice}, + {"vkDestroyQueryPool", reinterpret_cast(vkDestroyQueryPool), kDevice}, + {"vkGetQueryPoolResults", reinterpret_cast(vkGetQueryPoolResults), kDevice}, + {"vkCreateBuffer", reinterpret_cast(vkCreateBuffer), kDevice}, + {"vkDestroyBuffer", reinterpret_cast(vkDestroyBuffer), kDevice}, + {"vkCreateBufferView", reinterpret_cast(vkCreateBufferView), kDevice}, + {"vkDestroyBufferView", reinterpret_cast(vkDestroyBufferView), kDevice}, + {"vkCreateImage", reinterpret_cast(vkCreateImage), kDevice}, + {"vkDestroyImage", reinterpret_cast(vkDestroyImage), kDevice}, + {"vkGetImageSubresourceLayout", reinterpret_cast(vkGetImageSubresourceLayout), kDevice}, + {"vkCreateImageView", reinterpret_cast(vkCreateImageView), kDevice}, + {"vkDestroyImageView", reinterpret_cast(vkDestroyImageView), kDevice}, + {"vkCreateShaderModule", reinterpret_cast(vkCreateShaderModule), kDevice}, + {"vkDestroyShaderModule", reinterpret_cast(vkDestroyShaderModule), kDevice}, + {"vkCreatePipelineCache", reinterpret_cast(vkCreatePipelineCache), kDevice}, + {"vkDestroyPipelineCache", reinterpret_cast(vkDestroyPipelineCache), kDevice}, + {"vkGetPipelineCacheData", reinterpret_cast(vkGetPipelineCacheData), kDevice}, + {"vkMergePipelineCaches", reinterpret_cast(vkMergePipelineCaches), kDevice}, + {"vkCreateGraphicsPipelines", reinterpret_cast(vkCreateGraphicsPipelines), kDevice}, + {"vkCreateComputePipelines", reinterpret_cast(vkCreateComputePipelines), kDevice}, + {"vkDestroyPipeline", reinterpret_cast(vkDestroyPipeline), kDevice}, + {"vkCreatePipelineLayout", reinterpret_cast(vkCreatePipelineLayout), kDevice}, + {"vkDestroyPipelineLayout", reinterpret_cast(vkDestroyPipelineLayout), kDevice}, + {"vkCreateSampler", reinterpret_cast(vkCreateSampler), kDevice}, + {"vkDestroySampler", reinterpret_cast(vkDestroySampler), kDevice}, + {"vkCreateDescriptorSetLayout", reinterpret_cast(vkCreateDescriptorSetLayout), kDevice}, + {"vkDestroyDescriptorSetLayout", reinterpret_cast(vkDestroyDescriptorSetLayout), kDevice}, + {"vkCreateDescriptorPool", reinterpret_cast(vkCreateDescriptorPool), kDevice}, + {"vkDestroyDescriptorPool", reinterpret_cast(vkDestroyDescriptorPool), kDevice}, + {"vkResetDescriptorPool", reinterpret_cast(vkResetDescriptorPool), kDevice}, + {"vkAllocateDescriptorSets", reinterpret_cast(vkAllocateDescriptorSets), kDevice}, + {"vkFreeDescriptorSets", reinterpret_cast(vkFreeDescriptorSets), kDevice}, + {"vkUpdateDescriptorSets", reinterpret_cast(vkUpdateDescriptorSets), kDevice}, + {"vkCreateFramebuffer", reinterpret_cast(vkCreateFramebuffer), kDevice}, + {"vkDestroyFramebuffer", reinterpret_cast(vkDestroyFramebuffer), kDevice}, + {"vkCreateRenderPass", reinterpret_cast(vkCreateRenderPass), kDevice}, + {"vkDestroyRenderPass", reinterpret_cast(vkDestroyRenderPass), kDevice}, + {"vkGetRenderAreaGranularity", reinterpret_cast(vkGetRenderAreaGranularity), kDevice}, + {"vkCreateCommandPool", reinterpret_cast(vkCreateCommandPool), kDevice}, + {"vkDestroyCommandPool", reinterpret_cast(vkDestroyCommandPool), kDevice}, + {"vkResetCommandPool", reinterpret_cast(vkResetCommandPool), kDevice}, + {"vkAllocateCommandBuffers", reinterpret_cast(vkAllocateCommandBuffers), kDevice}, + {"vkFreeCommandBuffers", reinterpret_cast(vkFreeCommandBuffers), kDevice}, + {"vkBeginCommandBuffer", reinterpret_cast(vkBeginCommandBuffer), kDevice}, + {"vkEndCommandBuffer", reinterpret_cast(vkEndCommandBuffer), kDevice}, + {"vkResetCommandBuffer", reinterpret_cast(vkResetCommandBuffer), kDevice}, + {"vkCmdBindPipeline", reinterpret_cast(vkCmdBindPipeline), kDevice}, + {"vkCmdSetViewport", reinterpret_cast(vkCmdSetViewport), kDevice}, + {"vkCmdSetScissor", reinterpret_cast(vkCmdSetScissor), kDevice}, + {"vkCmdSetLineWidth", reinterpret_cast(vkCmdSetLineWidth), kDevice}, + {"vkCmdSetDepthBias", reinterpret_cast(vkCmdSetDepthBias), kDevice}, + {"vkCmdSetBlendConstants", reinterpret_cast(vkCmdSetBlendConstants), kDevice}, + {"vkCmdSetDepthBounds", reinterpret_cast(vkCmdSetDepthBounds), kDevice}, + {"vkCmdSetStencilCompareMask", reinterpret_cast(vkCmdSetStencilCompareMask), kDevice}, + {"vkCmdSetStencilWriteMask", reinterpret_cast(vkCmdSetStencilWriteMask), kDevice}, + {"vkCmdSetStencilReference", reinterpret_cast(vkCmdSetStencilReference), kDevice}, + {"vkCmdBindDescriptorSets", reinterpret_cast(vkCmdBindDescriptorSets), kDevice}, + {"vkCmdBindIndexBuffer", reinterpret_cast(vkCmdBindIndexBuffer), kDevice}, + {"vkCmdBindVertexBuffers", reinterpret_cast(vkCmdBindVertexBuffers), kDevice}, + {"vkCmdDraw", reinterpret_cast(vkCmdDraw), kDevice}, + {"vkCmdDrawIndexed", reinterpret_cast(vkCmdDrawIndexed), kDevice}, + {"vkCmdDrawIndirect", reinterpret_cast(vkCmdDrawIndirect), kDevice}, + {"vkCmdDrawIndexedIndirect", reinterpret_cast(vkCmdDrawIndexedIndirect), kDevice}, + {"vkCmdDispatch", reinterpret_cast(vkCmdDispatch), kDevice}, + {"vkCmdDispatchIndirect", reinterpret_cast(vkCmdDispatchIndirect), kDevice}, + {"vkCmdCopyBuffer", reinterpret_cast(vkCmdCopyBuffer), kDevice}, + {"vkCmdCopyImage", reinterpret_cast(vkCmdCopyImage), kDevice}, + {"vkCmdBlitImage", reinterpret_cast(vkCmdBlitImage), kDevice}, + {"vkCmdCopyBufferToImage", reinterpret_cast(vkCmdCopyBufferToImage), kDevice}, + {"vkCmdCopyImageToBuffer", reinterpret_cast(vkCmdCopyImageToBuffer), kDevice}, + {"vkCmdUpdateBuffer", reinterpret_cast(vkCmdUpdateBuffer), kDevice}, + {"vkCmdFillBuffer", reinterpret_cast(vkCmdFillBuffer), kDevice}, + {"vkCmdClearColorImage", reinterpret_cast(vkCmdClearColorImage), kDevice}, + {"vkCmdClearDepthStencilImage", reinterpret_cast(vkCmdClearDepthStencilImage), kDevice}, + {"vkCmdClearAttachments", reinterpret_cast(vkCmdClearAttachments), kDevice}, + {"vkCmdResolveImage", reinterpret_cast(vkCmdResolveImage), kDevice}, + {"vkCmdSetEvent", reinterpret_cast(vkCmdSetEvent), kDevice}, + {"vkCmdResetEvent", reinterpret_cast(vkCmdResetEvent), kDevice}, + {"vkCmdWaitEvents", reinterpret_cast(vkCmdWaitEvents), kDevice}, + {"vkCmdPipelineBarrier", reinterpret_cast(vkCmdPipelineBarrier), kDevice}, + {"vkCmdBeginQuery", reinterpret_cast(vkCmdBeginQuery), kDevice}, + {"vkCmdEndQuery", reinterpret_cast(vkCmdEndQuery), kDevice}, + {"vkCmdResetQueryPool", reinterpret_cast(vkCmdResetQueryPool), kDevice}, + {"vkCmdWriteTimestamp", reinterpret_cast(vkCmdWriteTimestamp), kDevice}, + {"vkCmdCopyQueryPoolResults", reinterpret_cast(vkCmdCopyQueryPoolResults), kDevice}, + {"vkCmdPushConstants", reinterpret_cast(vkCmdPushConstants), kDevice}, + {"vkCmdBeginRenderPass", reinterpret_cast(vkCmdBeginRenderPass), kDevice}, + {"vkCmdNextSubpass", reinterpret_cast(vkCmdNextSubpass), kDevice}, + {"vkCmdEndRenderPass", reinterpret_cast(vkCmdEndRenderPass), kDevice}, + {"vkCmdExecuteCommands", reinterpret_cast(vkCmdExecuteCommands), kDevice}, + {"vkEnumerateInstanceVersion", reinterpret_cast(vkEnumerateInstanceVersion), kGlobal}, + {"vkBindBufferMemory2", reinterpret_cast(vkBindBufferMemory2), kDevice}, + {"vkBindImageMemory2", reinterpret_cast(vkBindImageMemory2), kDevice}, + {"vkGetDeviceGroupPeerMemoryFeatures", reinterpret_cast(vkGetDeviceGroupPeerMemoryFeatures), kDevice}, + {"vkCmdSetDeviceMask", reinterpret_cast(vkCmdSetDeviceMask), kDevice}, + {"vkCmdDispatchBase", reinterpret_cast(vkCmdDispatchBase), kDevice}, + {"vkEnumeratePhysicalDeviceGroups", reinterpret_cast(vkEnumeratePhysicalDeviceGroups), kInstance}, + {"vkGetImageMemoryRequirements2", reinterpret_cast(vkGetImageMemoryRequirements2), kDevice}, + {"vkGetBufferMemoryRequirements2", reinterpret_cast(vkGetBufferMemoryRequirements2), kDevice}, + {"vkGetImageSparseMemoryRequirements2", reinterpret_cast(vkGetImageSparseMemoryRequirements2), kDevice}, + {"vkGetPhysicalDeviceFeatures2", reinterpret_cast(vkGetPhysicalDeviceFeatures2), kInstance}, + {"vkGetPhysicalDeviceProperties2", reinterpret_cast(vkGetPhysicalDeviceProperties2), kInstance}, + {"vkGetPhysicalDeviceFormatProperties2", reinterpret_cast(vkGetPhysicalDeviceFormatProperties2), kInstance}, + {"vkGetPhysicalDeviceImageFormatProperties2", reinterpret_cast(vkGetPhysicalDeviceImageFormatProperties2), kInstance}, + {"vkGetPhysicalDeviceQueueFamilyProperties2", reinterpret_cast(vkGetPhysicalDeviceQueueFamilyProperties2), kInstance}, + {"vkGetPhysicalDeviceMemoryProperties2", reinterpret_cast(vkGetPhysicalDeviceMemoryProperties2), kInstance}, + {"vkGetPhysicalDeviceSparseImageFormatProperties2", reinterpret_cast(vkGetPhysicalDeviceSparseImageFormatProperties2), kInstance}, + {"vkTrimCommandPool", reinterpret_cast(vkTrimCommandPool), kDevice}, + {"vkGetDeviceQueue2", reinterpret_cast(vkGetDeviceQueue2), kDevice}, + {"vkCreateSamplerYcbcrConversion", reinterpret_cast(vkCreateSamplerYcbcrConversion), kDevice}, + {"vkDestroySamplerYcbcrConversion", reinterpret_cast(vkDestroySamplerYcbcrConversion), kDevice}, + {"vkCreateDescriptorUpdateTemplate", reinterpret_cast(vkCreateDescriptorUpdateTemplate), kDevice}, + {"vkDestroyDescriptorUpdateTemplate", reinterpret_cast(vkDestroyDescriptorUpdateTemplate), kDevice}, + {"vkUpdateDescriptorSetWithTemplate", reinterpret_cast(vkUpdateDescriptorSetWithTemplate), kDevice}, + {"vkGetPhysicalDeviceExternalBufferProperties", reinterpret_cast(vkGetPhysicalDeviceExternalBufferProperties), kInstance}, + {"vkGetPhysicalDeviceExternalFenceProperties", reinterpret_cast(vkGetPhysicalDeviceExternalFenceProperties), kInstance}, + {"vkGetPhysicalDeviceExternalSemaphoreProperties", reinterpret_cast(vkGetPhysicalDeviceExternalSemaphoreProperties), kInstance}, + {"vkGetDescriptorSetLayoutSupport", reinterpret_cast(vkGetDescriptorSetLayoutSupport), kDevice}, + {"vkCmdDrawIndirectCount", reinterpret_cast(vkCmdDrawIndirectCount), kDevice}, + {"vkCmdDrawIndexedIndirectCount", reinterpret_cast(vkCmdDrawIndexedIndirectCount), kDevice}, + {"vkCreateRenderPass2", reinterpret_cast(vkCreateRenderPass2), kDevice}, + {"vkCmdBeginRenderPass2", reinterpret_cast(vkCmdBeginRenderPass2), kDevice}, + {"vkCmdNextSubpass2", reinterpret_cast(vkCmdNextSubpass2), kDevice}, + {"vkCmdEndRenderPass2", reinterpret_cast(vkCmdEndRenderPass2), kDevice}, + {"vkResetQueryPool", reinterpret_cast(vkResetQueryPool), kDevice}, + {"vkGetSemaphoreCounterValue", reinterpret_cast(vkGetSemaphoreCounterValue), kDevice}, + {"vkWaitSemaphores", reinterpret_cast(vkWaitSemaphores), kDevice}, + {"vkSignalSemaphore", reinterpret_cast(vkSignalSemaphore), kDevice}, + {"vkGetBufferDeviceAddress", reinterpret_cast(vkGetBufferDeviceAddress), kDevice}, + {"vkGetBufferOpaqueCaptureAddress", reinterpret_cast(vkGetBufferOpaqueCaptureAddress), kDevice}, + {"vkGetDeviceMemoryOpaqueCaptureAddress", reinterpret_cast(vkGetDeviceMemoryOpaqueCaptureAddress), kDevice}, + {"vkGetPhysicalDeviceToolProperties", reinterpret_cast(vkGetPhysicalDeviceToolProperties), kInstance}, + {"vkCreatePrivateDataSlot", reinterpret_cast(vkCreatePrivateDataSlot), kDevice}, + {"vkDestroyPrivateDataSlot", reinterpret_cast(vkDestroyPrivateDataSlot), kDevice}, + {"vkSetPrivateData", reinterpret_cast(vkSetPrivateData), kDevice}, + {"vkGetPrivateData", reinterpret_cast(vkGetPrivateData), kDevice}, + {"vkCmdSetEvent2", reinterpret_cast(vkCmdSetEvent2), kDevice}, + {"vkCmdResetEvent2", reinterpret_cast(vkCmdResetEvent2), kDevice}, + {"vkCmdWaitEvents2", reinterpret_cast(vkCmdWaitEvents2), kDevice}, + {"vkCmdPipelineBarrier2", reinterpret_cast(vkCmdPipelineBarrier2), kDevice}, + {"vkCmdWriteTimestamp2", reinterpret_cast(vkCmdWriteTimestamp2), kDevice}, + {"vkQueueSubmit2", reinterpret_cast(vkQueueSubmit2), kDevice}, + {"vkCmdCopyBuffer2", reinterpret_cast(vkCmdCopyBuffer2), kDevice}, + {"vkCmdCopyImage2", reinterpret_cast(vkCmdCopyImage2), kDevice}, + {"vkCmdCopyBufferToImage2", reinterpret_cast(vkCmdCopyBufferToImage2), kDevice}, + {"vkCmdCopyImageToBuffer2", reinterpret_cast(vkCmdCopyImageToBuffer2), kDevice}, + {"vkCmdBlitImage2", reinterpret_cast(vkCmdBlitImage2), kDevice}, + {"vkCmdResolveImage2", reinterpret_cast(vkCmdResolveImage2), kDevice}, + {"vkCmdBeginRendering", reinterpret_cast(vkCmdBeginRendering), kDevice}, + {"vkCmdEndRendering", reinterpret_cast(vkCmdEndRendering), kDevice}, + {"vkCmdSetCullMode", reinterpret_cast(vkCmdSetCullMode), kDevice}, + {"vkCmdSetFrontFace", reinterpret_cast(vkCmdSetFrontFace), kDevice}, + {"vkCmdSetPrimitiveTopology", reinterpret_cast(vkCmdSetPrimitiveTopology), kDevice}, + {"vkCmdSetViewportWithCount", reinterpret_cast(vkCmdSetViewportWithCount), kDevice}, + {"vkCmdSetScissorWithCount", reinterpret_cast(vkCmdSetScissorWithCount), kDevice}, + {"vkCmdBindVertexBuffers2", reinterpret_cast(vkCmdBindVertexBuffers2), kDevice}, + {"vkCmdSetDepthTestEnable", reinterpret_cast(vkCmdSetDepthTestEnable), kDevice}, + {"vkCmdSetDepthWriteEnable", reinterpret_cast(vkCmdSetDepthWriteEnable), kDevice}, + {"vkCmdSetDepthCompareOp", reinterpret_cast(vkCmdSetDepthCompareOp), kDevice}, + {"vkCmdSetDepthBoundsTestEnable", reinterpret_cast(vkCmdSetDepthBoundsTestEnable), kDevice}, + {"vkCmdSetStencilTestEnable", reinterpret_cast(vkCmdSetStencilTestEnable), kDevice}, + {"vkCmdSetStencilOp", reinterpret_cast(vkCmdSetStencilOp), kDevice}, + {"vkCmdSetRasterizerDiscardEnable", reinterpret_cast(vkCmdSetRasterizerDiscardEnable), kDevice}, + {"vkCmdSetDepthBiasEnable", reinterpret_cast(vkCmdSetDepthBiasEnable), kDevice}, + {"vkCmdSetPrimitiveRestartEnable", reinterpret_cast(vkCmdSetPrimitiveRestartEnable), kDevice}, + {"vkGetDeviceBufferMemoryRequirements", reinterpret_cast(vkGetDeviceBufferMemoryRequirements), kDevice}, + {"vkGetDeviceImageMemoryRequirements", reinterpret_cast(vkGetDeviceImageMemoryRequirements), kDevice}, + {"vkGetDeviceImageSparseMemoryRequirements", reinterpret_cast(vkGetDeviceImageSparseMemoryRequirements), kDevice}, + {"vkDestroySurfaceKHR", reinterpret_cast(vkDestroySurfaceKHR), kInstance}, + {"vkGetPhysicalDeviceSurfaceSupportKHR", reinterpret_cast(vkGetPhysicalDeviceSurfaceSupportKHR), kInstance}, + {"vkGetPhysicalDeviceSurfaceCapabilitiesKHR", reinterpret_cast(vkGetPhysicalDeviceSurfaceCapabilitiesKHR), kInstance}, + {"vkGetPhysicalDeviceSurfaceFormatsKHR", reinterpret_cast(vkGetPhysicalDeviceSurfaceFormatsKHR), kInstance}, + {"vkGetPhysicalDeviceSurfacePresentModesKHR", reinterpret_cast(vkGetPhysicalDeviceSurfacePresentModesKHR), kInstance}, + {"vkCreateSwapchainKHR", reinterpret_cast(vkCreateSwapchainKHR), kDevice}, + {"vkDestroySwapchainKHR", reinterpret_cast(vkDestroySwapchainKHR), kDevice}, + {"vkGetSwapchainImagesKHR", reinterpret_cast(vkGetSwapchainImagesKHR), kDevice}, + {"vkAcquireNextImageKHR", reinterpret_cast(vkAcquireNextImageKHR), kDevice}, + {"vkQueuePresentKHR", reinterpret_cast(vkQueuePresentKHR), kDevice}, + {"vkGetDeviceGroupPresentCapabilitiesKHR", reinterpret_cast(vkGetDeviceGroupPresentCapabilitiesKHR), kDevice}, + {"vkGetDeviceGroupSurfacePresentModesKHR", reinterpret_cast(vkGetDeviceGroupSurfacePresentModesKHR), kDevice}, + {"vkGetPhysicalDevicePresentRectanglesKHR", reinterpret_cast(vkGetPhysicalDevicePresentRectanglesKHR), kInstance}, + {"vkAcquireNextImage2KHR", reinterpret_cast(vkAcquireNextImage2KHR), kDevice}, + {"vkGetPhysicalDeviceDisplayPropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceDisplayPropertiesKHR), kInstance}, + {"vkGetPhysicalDeviceDisplayPlanePropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceDisplayPlanePropertiesKHR), kInstance}, + {"vkGetDisplayPlaneSupportedDisplaysKHR", reinterpret_cast(vkGetDisplayPlaneSupportedDisplaysKHR), kInstance}, + {"vkGetDisplayModePropertiesKHR", reinterpret_cast(vkGetDisplayModePropertiesKHR), kInstance}, + {"vkCreateDisplayModeKHR", reinterpret_cast(vkCreateDisplayModeKHR), kInstance}, + {"vkGetDisplayPlaneCapabilitiesKHR", reinterpret_cast(vkGetDisplayPlaneCapabilitiesKHR), kInstance}, + {"vkCreateDisplayPlaneSurfaceKHR", reinterpret_cast(vkCreateDisplayPlaneSurfaceKHR), kInstance}, + {"vkCreateSharedSwapchainsKHR", reinterpret_cast(vkCreateSharedSwapchainsKHR), kDevice}, + {"vkGetPhysicalDeviceVideoCapabilitiesKHR", reinterpret_cast(vkGetPhysicalDeviceVideoCapabilitiesKHR), kInstance}, + {"vkGetPhysicalDeviceVideoFormatPropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceVideoFormatPropertiesKHR), kInstance}, + {"vkCreateVideoSessionKHR", reinterpret_cast(vkCreateVideoSessionKHR), kDevice}, + {"vkDestroyVideoSessionKHR", reinterpret_cast(vkDestroyVideoSessionKHR), kDevice}, + {"vkGetVideoSessionMemoryRequirementsKHR", reinterpret_cast(vkGetVideoSessionMemoryRequirementsKHR), kDevice}, + {"vkBindVideoSessionMemoryKHR", reinterpret_cast(vkBindVideoSessionMemoryKHR), kDevice}, + {"vkCreateVideoSessionParametersKHR", reinterpret_cast(vkCreateVideoSessionParametersKHR), kDevice}, + {"vkUpdateVideoSessionParametersKHR", reinterpret_cast(vkUpdateVideoSessionParametersKHR), kDevice}, + {"vkDestroyVideoSessionParametersKHR", reinterpret_cast(vkDestroyVideoSessionParametersKHR), kDevice}, + {"vkCmdBeginVideoCodingKHR", reinterpret_cast(vkCmdBeginVideoCodingKHR), kDevice}, + {"vkCmdEndVideoCodingKHR", reinterpret_cast(vkCmdEndVideoCodingKHR), kDevice}, + {"vkCmdControlVideoCodingKHR", reinterpret_cast(vkCmdControlVideoCodingKHR), kDevice}, + {"vkCmdDecodeVideoKHR", reinterpret_cast(vkCmdDecodeVideoKHR), kDevice}, + {"vkCmdBeginRenderingKHR", reinterpret_cast(vkCmdBeginRenderingKHR), kDevice}, + {"vkCmdEndRenderingKHR", reinterpret_cast(vkCmdEndRenderingKHR), kDevice}, + {"vkGetPhysicalDeviceFeatures2KHR", reinterpret_cast(vkGetPhysicalDeviceFeatures2KHR), kInstance}, + {"vkGetPhysicalDeviceProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceProperties2KHR), kInstance}, + {"vkGetPhysicalDeviceFormatProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceFormatProperties2KHR), kInstance}, + {"vkGetPhysicalDeviceImageFormatProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceImageFormatProperties2KHR), kInstance}, + {"vkGetPhysicalDeviceQueueFamilyProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceQueueFamilyProperties2KHR), kInstance}, + {"vkGetPhysicalDeviceMemoryProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceMemoryProperties2KHR), kInstance}, + {"vkGetPhysicalDeviceSparseImageFormatProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceSparseImageFormatProperties2KHR), kInstance}, + {"vkGetDeviceGroupPeerMemoryFeaturesKHR", reinterpret_cast(vkGetDeviceGroupPeerMemoryFeaturesKHR), kDevice}, + {"vkCmdSetDeviceMaskKHR", reinterpret_cast(vkCmdSetDeviceMaskKHR), kDevice}, + {"vkCmdDispatchBaseKHR", reinterpret_cast(vkCmdDispatchBaseKHR), kDevice}, + {"vkTrimCommandPoolKHR", reinterpret_cast(vkTrimCommandPoolKHR), kDevice}, + {"vkEnumeratePhysicalDeviceGroupsKHR", reinterpret_cast(vkEnumeratePhysicalDeviceGroupsKHR), kInstance}, + {"vkGetPhysicalDeviceExternalBufferPropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceExternalBufferPropertiesKHR), kInstance}, + {"vkGetMemoryFdKHR", reinterpret_cast(vkGetMemoryFdKHR), kDevice}, + {"vkGetMemoryFdPropertiesKHR", reinterpret_cast(vkGetMemoryFdPropertiesKHR), kDevice}, + {"vkGetPhysicalDeviceExternalSemaphorePropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceExternalSemaphorePropertiesKHR), kInstance}, + {"vkImportSemaphoreFdKHR", reinterpret_cast(vkImportSemaphoreFdKHR), kDevice}, + {"vkGetSemaphoreFdKHR", reinterpret_cast(vkGetSemaphoreFdKHR), kDevice}, + {"vkCmdPushDescriptorSetKHR", reinterpret_cast(vkCmdPushDescriptorSetKHR), kDevice}, + {"vkCmdPushDescriptorSetWithTemplateKHR", reinterpret_cast(vkCmdPushDescriptorSetWithTemplateKHR), kDevice}, + {"vkCreateDescriptorUpdateTemplateKHR", reinterpret_cast(vkCreateDescriptorUpdateTemplateKHR), kDevice}, + {"vkDestroyDescriptorUpdateTemplateKHR", reinterpret_cast(vkDestroyDescriptorUpdateTemplateKHR), kDevice}, + {"vkUpdateDescriptorSetWithTemplateKHR", reinterpret_cast(vkUpdateDescriptorSetWithTemplateKHR), kDevice}, + {"vkCreateRenderPass2KHR", reinterpret_cast(vkCreateRenderPass2KHR), kDevice}, + {"vkCmdBeginRenderPass2KHR", reinterpret_cast(vkCmdBeginRenderPass2KHR), kDevice}, + {"vkCmdNextSubpass2KHR", reinterpret_cast(vkCmdNextSubpass2KHR), kDevice}, + {"vkCmdEndRenderPass2KHR", reinterpret_cast(vkCmdEndRenderPass2KHR), kDevice}, + {"vkGetSwapchainStatusKHR", reinterpret_cast(vkGetSwapchainStatusKHR), kDevice}, + {"vkGetPhysicalDeviceExternalFencePropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceExternalFencePropertiesKHR), kInstance}, + {"vkImportFenceFdKHR", reinterpret_cast(vkImportFenceFdKHR), kDevice}, + {"vkGetFenceFdKHR", reinterpret_cast(vkGetFenceFdKHR), kDevice}, + {"vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR", reinterpret_cast(vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR), kInstance}, + {"vkGetPhysicalDeviceQueueFamilyPerformanceQueryPassesKHR", reinterpret_cast(vkGetPhysicalDeviceQueueFamilyPerformanceQueryPassesKHR), kInstance}, + {"vkAcquireProfilingLockKHR", reinterpret_cast(vkAcquireProfilingLockKHR), kDevice}, + {"vkReleaseProfilingLockKHR", reinterpret_cast(vkReleaseProfilingLockKHR), kDevice}, + {"vkGetPhysicalDeviceSurfaceCapabilities2KHR", reinterpret_cast(vkGetPhysicalDeviceSurfaceCapabilities2KHR), kInstance}, + {"vkGetPhysicalDeviceSurfaceFormats2KHR", reinterpret_cast(vkGetPhysicalDeviceSurfaceFormats2KHR), kInstance}, + {"vkGetPhysicalDeviceDisplayProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceDisplayProperties2KHR), kInstance}, + {"vkGetPhysicalDeviceDisplayPlaneProperties2KHR", reinterpret_cast(vkGetPhysicalDeviceDisplayPlaneProperties2KHR), kInstance}, + {"vkGetDisplayModeProperties2KHR", reinterpret_cast(vkGetDisplayModeProperties2KHR), kInstance}, + {"vkGetDisplayPlaneCapabilities2KHR", reinterpret_cast(vkGetDisplayPlaneCapabilities2KHR), kInstance}, + {"vkGetImageMemoryRequirements2KHR", reinterpret_cast(vkGetImageMemoryRequirements2KHR), kDevice}, + {"vkGetBufferMemoryRequirements2KHR", reinterpret_cast(vkGetBufferMemoryRequirements2KHR), kDevice}, + {"vkGetImageSparseMemoryRequirements2KHR", reinterpret_cast(vkGetImageSparseMemoryRequirements2KHR), kDevice}, + {"vkCreateSamplerYcbcrConversionKHR", reinterpret_cast(vkCreateSamplerYcbcrConversionKHR), kDevice}, + {"vkDestroySamplerYcbcrConversionKHR", reinterpret_cast(vkDestroySamplerYcbcrConversionKHR), kDevice}, + {"vkBindBufferMemory2KHR", reinterpret_cast(vkBindBufferMemory2KHR), kDevice}, + {"vkBindImageMemory2KHR", reinterpret_cast(vkBindImageMemory2KHR), kDevice}, + {"vkGetDescriptorSetLayoutSupportKHR", reinterpret_cast(vkGetDescriptorSetLayoutSupportKHR), kDevice}, + {"vkCmdDrawIndirectCountKHR", reinterpret_cast(vkCmdDrawIndirectCountKHR), kDevice}, + {"vkCmdDrawIndexedIndirectCountKHR", reinterpret_cast(vkCmdDrawIndexedIndirectCountKHR), kDevice}, + {"vkGetSemaphoreCounterValueKHR", reinterpret_cast(vkGetSemaphoreCounterValueKHR), kDevice}, + {"vkWaitSemaphoresKHR", reinterpret_cast(vkWaitSemaphoresKHR), kDevice}, + {"vkSignalSemaphoreKHR", reinterpret_cast(vkSignalSemaphoreKHR), kDevice}, + {"vkGetPhysicalDeviceFragmentShadingRatesKHR", reinterpret_cast(vkGetPhysicalDeviceFragmentShadingRatesKHR), kInstance}, + {"vkCmdSetFragmentShadingRateKHR", reinterpret_cast(vkCmdSetFragmentShadingRateKHR), kDevice}, + {"vkWaitForPresentKHR", reinterpret_cast(vkWaitForPresentKHR), kDevice}, + {"vkGetBufferDeviceAddressKHR", reinterpret_cast(vkGetBufferDeviceAddressKHR), kDevice}, + {"vkGetBufferOpaqueCaptureAddressKHR", reinterpret_cast(vkGetBufferOpaqueCaptureAddressKHR), kDevice}, + {"vkGetDeviceMemoryOpaqueCaptureAddressKHR", reinterpret_cast(vkGetDeviceMemoryOpaqueCaptureAddressKHR), kDevice}, + {"vkCreateDeferredOperationKHR", reinterpret_cast(vkCreateDeferredOperationKHR), kDevice}, + {"vkDestroyDeferredOperationKHR", reinterpret_cast(vkDestroyDeferredOperationKHR), kDevice}, + {"vkGetDeferredOperationMaxConcurrencyKHR", reinterpret_cast(vkGetDeferredOperationMaxConcurrencyKHR), kDevice}, + {"vkGetDeferredOperationResultKHR", reinterpret_cast(vkGetDeferredOperationResultKHR), kDevice}, + {"vkDeferredOperationJoinKHR", reinterpret_cast(vkDeferredOperationJoinKHR), kDevice}, + {"vkGetPipelineExecutablePropertiesKHR", reinterpret_cast(vkGetPipelineExecutablePropertiesKHR), kDevice}, + {"vkGetPipelineExecutableStatisticsKHR", reinterpret_cast(vkGetPipelineExecutableStatisticsKHR), kDevice}, + {"vkGetPipelineExecutableInternalRepresentationsKHR", reinterpret_cast(vkGetPipelineExecutableInternalRepresentationsKHR), kDevice}, + {"vkMapMemory2KHR", reinterpret_cast(vkMapMemory2KHR), kDevice}, + {"vkUnmapMemory2KHR", reinterpret_cast(vkUnmapMemory2KHR), kDevice}, + {"vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR), kInstance}, + {"vkGetEncodedVideoSessionParametersKHR", reinterpret_cast(vkGetEncodedVideoSessionParametersKHR), kDevice}, + {"vkCmdEncodeVideoKHR", reinterpret_cast(vkCmdEncodeVideoKHR), kDevice}, + {"vkCmdSetEvent2KHR", reinterpret_cast(vkCmdSetEvent2KHR), kDevice}, + {"vkCmdResetEvent2KHR", reinterpret_cast(vkCmdResetEvent2KHR), kDevice}, + {"vkCmdWaitEvents2KHR", reinterpret_cast(vkCmdWaitEvents2KHR), kDevice}, + {"vkCmdPipelineBarrier2KHR", reinterpret_cast(vkCmdPipelineBarrier2KHR), kDevice}, + {"vkCmdWriteTimestamp2KHR", reinterpret_cast(vkCmdWriteTimestamp2KHR), kDevice}, + {"vkQueueSubmit2KHR", reinterpret_cast(vkQueueSubmit2KHR), kDevice}, + {"vkCmdWriteBufferMarker2AMD", reinterpret_cast(vkCmdWriteBufferMarker2AMD), kDevice}, + {"vkGetQueueCheckpointData2NV", reinterpret_cast(vkGetQueueCheckpointData2NV), kDevice}, + {"vkCmdCopyBuffer2KHR", reinterpret_cast(vkCmdCopyBuffer2KHR), kDevice}, + {"vkCmdCopyImage2KHR", reinterpret_cast(vkCmdCopyImage2KHR), kDevice}, + {"vkCmdCopyBufferToImage2KHR", reinterpret_cast(vkCmdCopyBufferToImage2KHR), kDevice}, + {"vkCmdCopyImageToBuffer2KHR", reinterpret_cast(vkCmdCopyImageToBuffer2KHR), kDevice}, + {"vkCmdBlitImage2KHR", reinterpret_cast(vkCmdBlitImage2KHR), kDevice}, + {"vkCmdResolveImage2KHR", reinterpret_cast(vkCmdResolveImage2KHR), kDevice}, + {"vkCmdTraceRaysIndirect2KHR", reinterpret_cast(vkCmdTraceRaysIndirect2KHR), kDevice}, + {"vkGetDeviceBufferMemoryRequirementsKHR", reinterpret_cast(vkGetDeviceBufferMemoryRequirementsKHR), kDevice}, + {"vkGetDeviceImageMemoryRequirementsKHR", reinterpret_cast(vkGetDeviceImageMemoryRequirementsKHR), kDevice}, + {"vkGetDeviceImageSparseMemoryRequirementsKHR", reinterpret_cast(vkGetDeviceImageSparseMemoryRequirementsKHR), kDevice}, + {"vkCmdBindIndexBuffer2KHR", reinterpret_cast(vkCmdBindIndexBuffer2KHR), kDevice}, + {"vkGetRenderingAreaGranularityKHR", reinterpret_cast(vkGetRenderingAreaGranularityKHR), kDevice}, + {"vkGetDeviceImageSubresourceLayoutKHR", reinterpret_cast(vkGetDeviceImageSubresourceLayoutKHR), kDevice}, + {"vkGetImageSubresourceLayout2KHR", reinterpret_cast(vkGetImageSubresourceLayout2KHR), kDevice}, + {"vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR", reinterpret_cast(vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR), kInstance}, + {"vkGetPhysicalDeviceCalibrateableTimeDomainsKHR", reinterpret_cast(vkGetPhysicalDeviceCalibrateableTimeDomainsKHR), kInstance}, + {"vkGetCalibratedTimestampsKHR", reinterpret_cast(vkGetCalibratedTimestampsKHR), kDevice}, + {"vkCmdBindDescriptorSets2KHR", reinterpret_cast(vkCmdBindDescriptorSets2KHR), kDevice}, + {"vkCmdPushConstants2KHR", reinterpret_cast(vkCmdPushConstants2KHR), kDevice}, + {"vkCmdPushDescriptorSet2KHR", reinterpret_cast(vkCmdPushDescriptorSet2KHR), kDevice}, + {"vkCmdPushDescriptorSetWithTemplate2KHR", reinterpret_cast(vkCmdPushDescriptorSetWithTemplate2KHR), kDevice}, + {"vkCmdSetDescriptorBufferOffsets2EXT", reinterpret_cast(vkCmdSetDescriptorBufferOffsets2EXT), kDevice}, + {"vkCmdBindDescriptorBufferEmbeddedSamplers2EXT", reinterpret_cast(vkCmdBindDescriptorBufferEmbeddedSamplers2EXT), kDevice}, + {"vkCreateDebugReportCallbackEXT", reinterpret_cast(vkCreateDebugReportCallbackEXT), kInstance}, + {"vkDestroyDebugReportCallbackEXT", reinterpret_cast(vkDestroyDebugReportCallbackEXT), kInstance}, + {"vkDebugReportMessageEXT", reinterpret_cast(vkDebugReportMessageEXT), kInstance}, + {"vkDebugMarkerSetObjectTagEXT", reinterpret_cast(vkDebugMarkerSetObjectTagEXT), kDevice}, + {"vkDebugMarkerSetObjectNameEXT", reinterpret_cast(vkDebugMarkerSetObjectNameEXT), kDevice}, + {"vkCmdDebugMarkerBeginEXT", reinterpret_cast(vkCmdDebugMarkerBeginEXT), kDevice}, + {"vkCmdDebugMarkerEndEXT", reinterpret_cast(vkCmdDebugMarkerEndEXT), kDevice}, + {"vkCmdDebugMarkerInsertEXT", reinterpret_cast(vkCmdDebugMarkerInsertEXT), kDevice}, + {"vkCmdBindTransformFeedbackBuffersEXT", reinterpret_cast(vkCmdBindTransformFeedbackBuffersEXT), kDevice}, + {"vkCmdBeginTransformFeedbackEXT", reinterpret_cast(vkCmdBeginTransformFeedbackEXT), kDevice}, + {"vkCmdEndTransformFeedbackEXT", reinterpret_cast(vkCmdEndTransformFeedbackEXT), kDevice}, + {"vkCmdBeginQueryIndexedEXT", reinterpret_cast(vkCmdBeginQueryIndexedEXT), kDevice}, + {"vkCmdEndQueryIndexedEXT", reinterpret_cast(vkCmdEndQueryIndexedEXT), kDevice}, + {"vkCmdDrawIndirectByteCountEXT", reinterpret_cast(vkCmdDrawIndirectByteCountEXT), kDevice}, + {"vkCreateCuModuleNVX", reinterpret_cast(vkCreateCuModuleNVX), kDevice}, + {"vkCreateCuFunctionNVX", reinterpret_cast(vkCreateCuFunctionNVX), kDevice}, + {"vkDestroyCuModuleNVX", reinterpret_cast(vkDestroyCuModuleNVX), kDevice}, + {"vkDestroyCuFunctionNVX", reinterpret_cast(vkDestroyCuFunctionNVX), kDevice}, + {"vkCmdCuLaunchKernelNVX", reinterpret_cast(vkCmdCuLaunchKernelNVX), kDevice}, + {"vkGetImageViewHandleNVX", reinterpret_cast(vkGetImageViewHandleNVX), kDevice}, + {"vkGetImageViewAddressNVX", reinterpret_cast(vkGetImageViewAddressNVX), kDevice}, + {"vkCmdDrawIndirectCountAMD", reinterpret_cast(vkCmdDrawIndirectCountAMD), kDevice}, + {"vkCmdDrawIndexedIndirectCountAMD", reinterpret_cast(vkCmdDrawIndexedIndirectCountAMD), kDevice}, + {"vkGetShaderInfoAMD", reinterpret_cast(vkGetShaderInfoAMD), kDevice}, + {"vkGetPhysicalDeviceExternalImageFormatPropertiesNV", reinterpret_cast(vkGetPhysicalDeviceExternalImageFormatPropertiesNV), kInstance}, + {"vkCmdBeginConditionalRenderingEXT", reinterpret_cast(vkCmdBeginConditionalRenderingEXT), kDevice}, + {"vkCmdEndConditionalRenderingEXT", reinterpret_cast(vkCmdEndConditionalRenderingEXT), kDevice}, + {"vkCmdSetViewportWScalingNV", reinterpret_cast(vkCmdSetViewportWScalingNV), kDevice}, + {"vkReleaseDisplayEXT", reinterpret_cast(vkReleaseDisplayEXT), kInstance}, + {"vkGetPhysicalDeviceSurfaceCapabilities2EXT", reinterpret_cast(vkGetPhysicalDeviceSurfaceCapabilities2EXT), kInstance}, + {"vkDisplayPowerControlEXT", reinterpret_cast(vkDisplayPowerControlEXT), kDevice}, + {"vkRegisterDeviceEventEXT", reinterpret_cast(vkRegisterDeviceEventEXT), kDevice}, + {"vkRegisterDisplayEventEXT", reinterpret_cast(vkRegisterDisplayEventEXT), kDevice}, + {"vkGetSwapchainCounterEXT", reinterpret_cast(vkGetSwapchainCounterEXT), kDevice}, + {"vkGetRefreshCycleDurationGOOGLE", reinterpret_cast(vkGetRefreshCycleDurationGOOGLE), kDevice}, + {"vkGetPastPresentationTimingGOOGLE", reinterpret_cast(vkGetPastPresentationTimingGOOGLE), kDevice}, + {"vkCmdSetDiscardRectangleEXT", reinterpret_cast(vkCmdSetDiscardRectangleEXT), kDevice}, + {"vkCmdSetDiscardRectangleEnableEXT", reinterpret_cast(vkCmdSetDiscardRectangleEnableEXT), kDevice}, + {"vkCmdSetDiscardRectangleModeEXT", reinterpret_cast(vkCmdSetDiscardRectangleModeEXT), kDevice}, + {"vkSetHdrMetadataEXT", reinterpret_cast(vkSetHdrMetadataEXT), kDevice}, + {"vkSetDebugUtilsObjectNameEXT", reinterpret_cast(vkSetDebugUtilsObjectNameEXT), kDevice}, + {"vkSetDebugUtilsObjectTagEXT", reinterpret_cast(vkSetDebugUtilsObjectTagEXT), kDevice}, + {"vkQueueBeginDebugUtilsLabelEXT", reinterpret_cast(vkQueueBeginDebugUtilsLabelEXT), kDevice}, + {"vkQueueEndDebugUtilsLabelEXT", reinterpret_cast(vkQueueEndDebugUtilsLabelEXT), kDevice}, + {"vkQueueInsertDebugUtilsLabelEXT", reinterpret_cast(vkQueueInsertDebugUtilsLabelEXT), kDevice}, + {"vkCmdBeginDebugUtilsLabelEXT", reinterpret_cast(vkCmdBeginDebugUtilsLabelEXT), kDevice}, + {"vkCmdEndDebugUtilsLabelEXT", reinterpret_cast(vkCmdEndDebugUtilsLabelEXT), kDevice}, + {"vkCmdInsertDebugUtilsLabelEXT", reinterpret_cast(vkCmdInsertDebugUtilsLabelEXT), kDevice}, + {"vkCreateDebugUtilsMessengerEXT", reinterpret_cast(vkCreateDebugUtilsMessengerEXT), kInstance}, + {"vkDestroyDebugUtilsMessengerEXT", reinterpret_cast(vkDestroyDebugUtilsMessengerEXT), kInstance}, + {"vkSubmitDebugUtilsMessageEXT", reinterpret_cast(vkSubmitDebugUtilsMessageEXT), kInstance}, + {"vkCmdSetSampleLocationsEXT", reinterpret_cast(vkCmdSetSampleLocationsEXT), kDevice}, + {"vkGetPhysicalDeviceMultisamplePropertiesEXT", reinterpret_cast(vkGetPhysicalDeviceMultisamplePropertiesEXT), kInstance}, + {"vkGetImageDrmFormatModifierPropertiesEXT", reinterpret_cast(vkGetImageDrmFormatModifierPropertiesEXT), kDevice}, + {"vkCreateValidationCacheEXT", reinterpret_cast(vkCreateValidationCacheEXT), kDevice}, + {"vkDestroyValidationCacheEXT", reinterpret_cast(vkDestroyValidationCacheEXT), kDevice}, + {"vkMergeValidationCachesEXT", reinterpret_cast(vkMergeValidationCachesEXT), kDevice}, + {"vkGetValidationCacheDataEXT", reinterpret_cast(vkGetValidationCacheDataEXT), kDevice}, + {"vkCmdBindShadingRateImageNV", reinterpret_cast(vkCmdBindShadingRateImageNV), kDevice}, + {"vkCmdSetViewportShadingRatePaletteNV", reinterpret_cast(vkCmdSetViewportShadingRatePaletteNV), kDevice}, + {"vkCmdSetCoarseSampleOrderNV", reinterpret_cast(vkCmdSetCoarseSampleOrderNV), kDevice}, + {"vkCreateAccelerationStructureNV", reinterpret_cast(vkCreateAccelerationStructureNV), kDevice}, + {"vkDestroyAccelerationStructureNV", reinterpret_cast(vkDestroyAccelerationStructureNV), kDevice}, + {"vkGetAccelerationStructureMemoryRequirementsNV", reinterpret_cast(vkGetAccelerationStructureMemoryRequirementsNV), kDevice}, + {"vkBindAccelerationStructureMemoryNV", reinterpret_cast(vkBindAccelerationStructureMemoryNV), kDevice}, + {"vkCmdBuildAccelerationStructureNV", reinterpret_cast(vkCmdBuildAccelerationStructureNV), kDevice}, + {"vkCmdCopyAccelerationStructureNV", reinterpret_cast(vkCmdCopyAccelerationStructureNV), kDevice}, + {"vkCmdTraceRaysNV", reinterpret_cast(vkCmdTraceRaysNV), kDevice}, + {"vkCreateRayTracingPipelinesNV", reinterpret_cast(vkCreateRayTracingPipelinesNV), kDevice}, + {"vkGetRayTracingShaderGroupHandlesKHR", reinterpret_cast(vkGetRayTracingShaderGroupHandlesKHR), kDevice}, + {"vkGetRayTracingShaderGroupHandlesNV", reinterpret_cast(vkGetRayTracingShaderGroupHandlesNV), kDevice}, + {"vkGetAccelerationStructureHandleNV", reinterpret_cast(vkGetAccelerationStructureHandleNV), kDevice}, + {"vkCmdWriteAccelerationStructuresPropertiesNV", reinterpret_cast(vkCmdWriteAccelerationStructuresPropertiesNV), kDevice}, + {"vkCompileDeferredNV", reinterpret_cast(vkCompileDeferredNV), kDevice}, + {"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast(vkGetMemoryHostPointerPropertiesEXT), kDevice}, + {"vkCmdWriteBufferMarkerAMD", reinterpret_cast(vkCmdWriteBufferMarkerAMD), kDevice}, + {"vkGetPhysicalDeviceCalibrateableTimeDomainsEXT", reinterpret_cast(vkGetPhysicalDeviceCalibrateableTimeDomainsEXT), kInstance}, + {"vkGetCalibratedTimestampsEXT", reinterpret_cast(vkGetCalibratedTimestampsEXT), kDevice}, + {"vkCmdDrawMeshTasksNV", reinterpret_cast(vkCmdDrawMeshTasksNV), kDevice}, + {"vkCmdDrawMeshTasksIndirectNV", reinterpret_cast(vkCmdDrawMeshTasksIndirectNV), kDevice}, + {"vkCmdDrawMeshTasksIndirectCountNV", reinterpret_cast(vkCmdDrawMeshTasksIndirectCountNV), kDevice}, + {"vkCmdSetExclusiveScissorEnableNV", reinterpret_cast(vkCmdSetExclusiveScissorEnableNV), kDevice}, + {"vkCmdSetExclusiveScissorNV", reinterpret_cast(vkCmdSetExclusiveScissorNV), kDevice}, + {"vkCmdSetCheckpointNV", reinterpret_cast(vkCmdSetCheckpointNV), kDevice}, + {"vkGetQueueCheckpointDataNV", reinterpret_cast(vkGetQueueCheckpointDataNV), kDevice}, + {"vkInitializePerformanceApiINTEL", reinterpret_cast(vkInitializePerformanceApiINTEL), kDevice}, + {"vkUninitializePerformanceApiINTEL", reinterpret_cast(vkUninitializePerformanceApiINTEL), kDevice}, + {"vkCmdSetPerformanceMarkerINTEL", reinterpret_cast(vkCmdSetPerformanceMarkerINTEL), kDevice}, + {"vkCmdSetPerformanceStreamMarkerINTEL", reinterpret_cast(vkCmdSetPerformanceStreamMarkerINTEL), kDevice}, + {"vkCmdSetPerformanceOverrideINTEL", reinterpret_cast(vkCmdSetPerformanceOverrideINTEL), kDevice}, + {"vkAcquirePerformanceConfigurationINTEL", reinterpret_cast(vkAcquirePerformanceConfigurationINTEL), kDevice}, + {"vkReleasePerformanceConfigurationINTEL", reinterpret_cast(vkReleasePerformanceConfigurationINTEL), kDevice}, + {"vkQueueSetPerformanceConfigurationINTEL", reinterpret_cast(vkQueueSetPerformanceConfigurationINTEL), kDevice}, + {"vkGetPerformanceParameterINTEL", reinterpret_cast(vkGetPerformanceParameterINTEL), kDevice}, + {"vkSetLocalDimmingAMD", reinterpret_cast(vkSetLocalDimmingAMD), kDevice}, + {"vkGetBufferDeviceAddressEXT", reinterpret_cast(vkGetBufferDeviceAddressEXT), kDevice}, + {"vkGetPhysicalDeviceToolPropertiesEXT", reinterpret_cast(vkGetPhysicalDeviceToolPropertiesEXT), kInstance}, + {"vkGetPhysicalDeviceCooperativeMatrixPropertiesNV", reinterpret_cast(vkGetPhysicalDeviceCooperativeMatrixPropertiesNV), kInstance}, + {"vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV", reinterpret_cast(vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV), kInstance}, + {"vkCreateHeadlessSurfaceEXT", reinterpret_cast(vkCreateHeadlessSurfaceEXT), kInstance}, + {"vkCmdSetLineStippleEXT", reinterpret_cast(vkCmdSetLineStippleEXT), kDevice}, + {"vkResetQueryPoolEXT", reinterpret_cast(vkResetQueryPoolEXT), kDevice}, + {"vkCmdSetCullModeEXT", reinterpret_cast(vkCmdSetCullModeEXT), kDevice}, + {"vkCmdSetFrontFaceEXT", reinterpret_cast(vkCmdSetFrontFaceEXT), kDevice}, + {"vkCmdSetPrimitiveTopologyEXT", reinterpret_cast(vkCmdSetPrimitiveTopologyEXT), kDevice}, + {"vkCmdSetViewportWithCountEXT", reinterpret_cast(vkCmdSetViewportWithCountEXT), kDevice}, + {"vkCmdSetScissorWithCountEXT", reinterpret_cast(vkCmdSetScissorWithCountEXT), kDevice}, + {"vkCmdBindVertexBuffers2EXT", reinterpret_cast(vkCmdBindVertexBuffers2EXT), kDevice}, + {"vkCmdSetDepthTestEnableEXT", reinterpret_cast(vkCmdSetDepthTestEnableEXT), kDevice}, + {"vkCmdSetDepthWriteEnableEXT", reinterpret_cast(vkCmdSetDepthWriteEnableEXT), kDevice}, + {"vkCmdSetDepthCompareOpEXT", reinterpret_cast(vkCmdSetDepthCompareOpEXT), kDevice}, + {"vkCmdSetDepthBoundsTestEnableEXT", reinterpret_cast(vkCmdSetDepthBoundsTestEnableEXT), kDevice}, + {"vkCmdSetStencilTestEnableEXT", reinterpret_cast(vkCmdSetStencilTestEnableEXT), kDevice}, + {"vkCmdSetStencilOpEXT", reinterpret_cast(vkCmdSetStencilOpEXT), kDevice}, + {"vkCopyMemoryToImageEXT", reinterpret_cast(vkCopyMemoryToImageEXT), kDevice}, + {"vkCopyImageToMemoryEXT", reinterpret_cast(vkCopyImageToMemoryEXT), kDevice}, + {"vkCopyImageToImageEXT", reinterpret_cast(vkCopyImageToImageEXT), kDevice}, + {"vkTransitionImageLayoutEXT", reinterpret_cast(vkTransitionImageLayoutEXT), kDevice}, + {"vkGetImageSubresourceLayout2EXT", reinterpret_cast(vkGetImageSubresourceLayout2EXT), kDevice}, + {"vkReleaseSwapchainImagesEXT", reinterpret_cast(vkReleaseSwapchainImagesEXT), kDevice}, + {"vkGetGeneratedCommandsMemoryRequirementsNV", reinterpret_cast(vkGetGeneratedCommandsMemoryRequirementsNV), kDevice}, + {"vkCmdPreprocessGeneratedCommandsNV", reinterpret_cast(vkCmdPreprocessGeneratedCommandsNV), kDevice}, + {"vkCmdExecuteGeneratedCommandsNV", reinterpret_cast(vkCmdExecuteGeneratedCommandsNV), kDevice}, + {"vkCmdBindPipelineShaderGroupNV", reinterpret_cast(vkCmdBindPipelineShaderGroupNV), kDevice}, + {"vkCreateIndirectCommandsLayoutNV", reinterpret_cast(vkCreateIndirectCommandsLayoutNV), kDevice}, + {"vkDestroyIndirectCommandsLayoutNV", reinterpret_cast(vkDestroyIndirectCommandsLayoutNV), kDevice}, + {"vkCmdSetDepthBias2EXT", reinterpret_cast(vkCmdSetDepthBias2EXT), kDevice}, + {"vkAcquireDrmDisplayEXT", reinterpret_cast(vkAcquireDrmDisplayEXT), kInstance}, + {"vkGetDrmDisplayEXT", reinterpret_cast(vkGetDrmDisplayEXT), kInstance}, + {"vkCreatePrivateDataSlotEXT", reinterpret_cast(vkCreatePrivateDataSlotEXT), kDevice}, + {"vkDestroyPrivateDataSlotEXT", reinterpret_cast(vkDestroyPrivateDataSlotEXT), kDevice}, + {"vkSetPrivateDataEXT", reinterpret_cast(vkSetPrivateDataEXT), kDevice}, + {"vkGetPrivateDataEXT", reinterpret_cast(vkGetPrivateDataEXT), kDevice}, + {"vkCreateCudaModuleNV", reinterpret_cast(vkCreateCudaModuleNV), kDevice}, + {"vkGetCudaModuleCacheNV", reinterpret_cast(vkGetCudaModuleCacheNV), kDevice}, + {"vkCreateCudaFunctionNV", reinterpret_cast(vkCreateCudaFunctionNV), kDevice}, + {"vkDestroyCudaModuleNV", reinterpret_cast(vkDestroyCudaModuleNV), kDevice}, + {"vkDestroyCudaFunctionNV", reinterpret_cast(vkDestroyCudaFunctionNV), kDevice}, + {"vkCmdCudaLaunchKernelNV", reinterpret_cast(vkCmdCudaLaunchKernelNV), kDevice}, + {"vkGetDescriptorSetLayoutSizeEXT", reinterpret_cast(vkGetDescriptorSetLayoutSizeEXT), kDevice}, + {"vkGetDescriptorSetLayoutBindingOffsetEXT", reinterpret_cast(vkGetDescriptorSetLayoutBindingOffsetEXT), kDevice}, + {"vkGetDescriptorEXT", reinterpret_cast(vkGetDescriptorEXT), kDevice}, + {"vkCmdBindDescriptorBuffersEXT", reinterpret_cast(vkCmdBindDescriptorBuffersEXT), kDevice}, + {"vkCmdSetDescriptorBufferOffsetsEXT", reinterpret_cast(vkCmdSetDescriptorBufferOffsetsEXT), kDevice}, + {"vkCmdBindDescriptorBufferEmbeddedSamplersEXT", reinterpret_cast(vkCmdBindDescriptorBufferEmbeddedSamplersEXT), kDevice}, + {"vkGetBufferOpaqueCaptureDescriptorDataEXT", reinterpret_cast(vkGetBufferOpaqueCaptureDescriptorDataEXT), kDevice}, + {"vkGetImageOpaqueCaptureDescriptorDataEXT", reinterpret_cast(vkGetImageOpaqueCaptureDescriptorDataEXT), kDevice}, + {"vkGetImageViewOpaqueCaptureDescriptorDataEXT", reinterpret_cast(vkGetImageViewOpaqueCaptureDescriptorDataEXT), kDevice}, + {"vkGetSamplerOpaqueCaptureDescriptorDataEXT", reinterpret_cast(vkGetSamplerOpaqueCaptureDescriptorDataEXT), kDevice}, + {"vkGetAccelerationStructureOpaqueCaptureDescriptorDataEXT", reinterpret_cast(vkGetAccelerationStructureOpaqueCaptureDescriptorDataEXT), kDevice}, + {"vkCmdSetFragmentShadingRateEnumNV", reinterpret_cast(vkCmdSetFragmentShadingRateEnumNV), kDevice}, + {"vkGetDeviceFaultInfoEXT", reinterpret_cast(vkGetDeviceFaultInfoEXT), kDevice}, + {"vkCmdSetVertexInputEXT", reinterpret_cast(vkCmdSetVertexInputEXT), kDevice}, + {"vkGetDeviceSubpassShadingMaxWorkgroupSizeHUAWEI", reinterpret_cast(vkGetDeviceSubpassShadingMaxWorkgroupSizeHUAWEI), kDevice}, + {"vkCmdSubpassShadingHUAWEI", reinterpret_cast(vkCmdSubpassShadingHUAWEI), kDevice}, + {"vkCmdBindInvocationMaskHUAWEI", reinterpret_cast(vkCmdBindInvocationMaskHUAWEI), kDevice}, + {"vkGetMemoryRemoteAddressNV", reinterpret_cast(vkGetMemoryRemoteAddressNV), kDevice}, + {"vkGetPipelinePropertiesEXT", reinterpret_cast(vkGetPipelinePropertiesEXT), kDevice}, + {"vkCmdSetPatchControlPointsEXT", reinterpret_cast(vkCmdSetPatchControlPointsEXT), kDevice}, + {"vkCmdSetRasterizerDiscardEnableEXT", reinterpret_cast(vkCmdSetRasterizerDiscardEnableEXT), kDevice}, + {"vkCmdSetDepthBiasEnableEXT", reinterpret_cast(vkCmdSetDepthBiasEnableEXT), kDevice}, + {"vkCmdSetLogicOpEXT", reinterpret_cast(vkCmdSetLogicOpEXT), kDevice}, + {"vkCmdSetPrimitiveRestartEnableEXT", reinterpret_cast(vkCmdSetPrimitiveRestartEnableEXT), kDevice}, + {"vkCmdSetColorWriteEnableEXT", reinterpret_cast(vkCmdSetColorWriteEnableEXT), kDevice}, + {"vkCmdDrawMultiEXT", reinterpret_cast(vkCmdDrawMultiEXT), kDevice}, + {"vkCmdDrawMultiIndexedEXT", reinterpret_cast(vkCmdDrawMultiIndexedEXT), kDevice}, + {"vkCreateMicromapEXT", reinterpret_cast(vkCreateMicromapEXT), kDevice}, + {"vkDestroyMicromapEXT", reinterpret_cast(vkDestroyMicromapEXT), kDevice}, + {"vkCmdBuildMicromapsEXT", reinterpret_cast(vkCmdBuildMicromapsEXT), kDevice}, + {"vkBuildMicromapsEXT", reinterpret_cast(vkBuildMicromapsEXT), kDevice}, + {"vkCopyMicromapEXT", reinterpret_cast(vkCopyMicromapEXT), kDevice}, + {"vkCopyMicromapToMemoryEXT", reinterpret_cast(vkCopyMicromapToMemoryEXT), kDevice}, + {"vkCopyMemoryToMicromapEXT", reinterpret_cast(vkCopyMemoryToMicromapEXT), kDevice}, + {"vkWriteMicromapsPropertiesEXT", reinterpret_cast(vkWriteMicromapsPropertiesEXT), kDevice}, + {"vkCmdCopyMicromapEXT", reinterpret_cast(vkCmdCopyMicromapEXT), kDevice}, + {"vkCmdCopyMicromapToMemoryEXT", reinterpret_cast(vkCmdCopyMicromapToMemoryEXT), kDevice}, + {"vkCmdCopyMemoryToMicromapEXT", reinterpret_cast(vkCmdCopyMemoryToMicromapEXT), kDevice}, + {"vkCmdWriteMicromapsPropertiesEXT", reinterpret_cast(vkCmdWriteMicromapsPropertiesEXT), kDevice}, + {"vkGetDeviceMicromapCompatibilityEXT", reinterpret_cast(vkGetDeviceMicromapCompatibilityEXT), kDevice}, + {"vkGetMicromapBuildSizesEXT", reinterpret_cast(vkGetMicromapBuildSizesEXT), kDevice}, + {"vkCmdDrawClusterHUAWEI", reinterpret_cast(vkCmdDrawClusterHUAWEI), kDevice}, + {"vkCmdDrawClusterIndirectHUAWEI", reinterpret_cast(vkCmdDrawClusterIndirectHUAWEI), kDevice}, + {"vkSetDeviceMemoryPriorityEXT", reinterpret_cast(vkSetDeviceMemoryPriorityEXT), kDevice}, + {"vkGetDescriptorSetLayoutHostMappingInfoVALVE", reinterpret_cast(vkGetDescriptorSetLayoutHostMappingInfoVALVE), kDevice}, + {"vkGetDescriptorSetHostMappingVALVE", reinterpret_cast(vkGetDescriptorSetHostMappingVALVE), kDevice}, + {"vkCmdCopyMemoryIndirectNV", reinterpret_cast(vkCmdCopyMemoryIndirectNV), kDevice}, + {"vkCmdCopyMemoryToImageIndirectNV", reinterpret_cast(vkCmdCopyMemoryToImageIndirectNV), kDevice}, + {"vkCmdDecompressMemoryNV", reinterpret_cast(vkCmdDecompressMemoryNV), kDevice}, + {"vkCmdDecompressMemoryIndirectCountNV", reinterpret_cast(vkCmdDecompressMemoryIndirectCountNV), kDevice}, + {"vkGetPipelineIndirectMemoryRequirementsNV", reinterpret_cast(vkGetPipelineIndirectMemoryRequirementsNV), kDevice}, + {"vkCmdUpdatePipelineIndirectBufferNV", reinterpret_cast(vkCmdUpdatePipelineIndirectBufferNV), kDevice}, + {"vkGetPipelineIndirectDeviceAddressNV", reinterpret_cast(vkGetPipelineIndirectDeviceAddressNV), kDevice}, + {"vkCmdSetTessellationDomainOriginEXT", reinterpret_cast(vkCmdSetTessellationDomainOriginEXT), kDevice}, + {"vkCmdSetDepthClampEnableEXT", reinterpret_cast(vkCmdSetDepthClampEnableEXT), kDevice}, + {"vkCmdSetPolygonModeEXT", reinterpret_cast(vkCmdSetPolygonModeEXT), kDevice}, + {"vkCmdSetRasterizationSamplesEXT", reinterpret_cast(vkCmdSetRasterizationSamplesEXT), kDevice}, + {"vkCmdSetSampleMaskEXT", reinterpret_cast(vkCmdSetSampleMaskEXT), kDevice}, + {"vkCmdSetAlphaToCoverageEnableEXT", reinterpret_cast(vkCmdSetAlphaToCoverageEnableEXT), kDevice}, + {"vkCmdSetAlphaToOneEnableEXT", reinterpret_cast(vkCmdSetAlphaToOneEnableEXT), kDevice}, + {"vkCmdSetLogicOpEnableEXT", reinterpret_cast(vkCmdSetLogicOpEnableEXT), kDevice}, + {"vkCmdSetColorBlendEnableEXT", reinterpret_cast(vkCmdSetColorBlendEnableEXT), kDevice}, + {"vkCmdSetColorBlendEquationEXT", reinterpret_cast(vkCmdSetColorBlendEquationEXT), kDevice}, + {"vkCmdSetColorWriteMaskEXT", reinterpret_cast(vkCmdSetColorWriteMaskEXT), kDevice}, + {"vkCmdSetRasterizationStreamEXT", reinterpret_cast(vkCmdSetRasterizationStreamEXT), kDevice}, + {"vkCmdSetConservativeRasterizationModeEXT", reinterpret_cast(vkCmdSetConservativeRasterizationModeEXT), kDevice}, + {"vkCmdSetExtraPrimitiveOverestimationSizeEXT", reinterpret_cast(vkCmdSetExtraPrimitiveOverestimationSizeEXT), kDevice}, + {"vkCmdSetDepthClipEnableEXT", reinterpret_cast(vkCmdSetDepthClipEnableEXT), kDevice}, + {"vkCmdSetSampleLocationsEnableEXT", reinterpret_cast(vkCmdSetSampleLocationsEnableEXT), kDevice}, + {"vkCmdSetColorBlendAdvancedEXT", reinterpret_cast(vkCmdSetColorBlendAdvancedEXT), kDevice}, + {"vkCmdSetProvokingVertexModeEXT", reinterpret_cast(vkCmdSetProvokingVertexModeEXT), kDevice}, + {"vkCmdSetLineRasterizationModeEXT", reinterpret_cast(vkCmdSetLineRasterizationModeEXT), kDevice}, + {"vkCmdSetLineStippleEnableEXT", reinterpret_cast(vkCmdSetLineStippleEnableEXT), kDevice}, + {"vkCmdSetDepthClipNegativeOneToOneEXT", reinterpret_cast(vkCmdSetDepthClipNegativeOneToOneEXT), kDevice}, + {"vkCmdSetViewportWScalingEnableNV", reinterpret_cast(vkCmdSetViewportWScalingEnableNV), kDevice}, + {"vkCmdSetViewportSwizzleNV", reinterpret_cast(vkCmdSetViewportSwizzleNV), kDevice}, + {"vkCmdSetCoverageToColorEnableNV", reinterpret_cast(vkCmdSetCoverageToColorEnableNV), kDevice}, + {"vkCmdSetCoverageToColorLocationNV", reinterpret_cast(vkCmdSetCoverageToColorLocationNV), kDevice}, + {"vkCmdSetCoverageModulationModeNV", reinterpret_cast(vkCmdSetCoverageModulationModeNV), kDevice}, + {"vkCmdSetCoverageModulationTableEnableNV", reinterpret_cast(vkCmdSetCoverageModulationTableEnableNV), kDevice}, + {"vkCmdSetCoverageModulationTableNV", reinterpret_cast(vkCmdSetCoverageModulationTableNV), kDevice}, + {"vkCmdSetShadingRateImageEnableNV", reinterpret_cast(vkCmdSetShadingRateImageEnableNV), kDevice}, + {"vkCmdSetRepresentativeFragmentTestEnableNV", reinterpret_cast(vkCmdSetRepresentativeFragmentTestEnableNV), kDevice}, + {"vkCmdSetCoverageReductionModeNV", reinterpret_cast(vkCmdSetCoverageReductionModeNV), kDevice}, + {"vkGetShaderModuleIdentifierEXT", reinterpret_cast(vkGetShaderModuleIdentifierEXT), kDevice}, + {"vkGetShaderModuleCreateInfoIdentifierEXT", reinterpret_cast(vkGetShaderModuleCreateInfoIdentifierEXT), kDevice}, + {"vkGetPhysicalDeviceOpticalFlowImageFormatsNV", reinterpret_cast(vkGetPhysicalDeviceOpticalFlowImageFormatsNV), kInstance}, + {"vkCreateOpticalFlowSessionNV", reinterpret_cast(vkCreateOpticalFlowSessionNV), kDevice}, + {"vkDestroyOpticalFlowSessionNV", reinterpret_cast(vkDestroyOpticalFlowSessionNV), kDevice}, + {"vkBindOpticalFlowSessionImageNV", reinterpret_cast(vkBindOpticalFlowSessionImageNV), kDevice}, + {"vkCmdOpticalFlowExecuteNV", reinterpret_cast(vkCmdOpticalFlowExecuteNV), kDevice}, + {"vkCreateShadersEXT", reinterpret_cast(vkCreateShadersEXT), kDevice}, + {"vkDestroyShaderEXT", reinterpret_cast(vkDestroyShaderEXT), kDevice}, + {"vkGetShaderBinaryDataEXT", reinterpret_cast(vkGetShaderBinaryDataEXT), kDevice}, + {"vkCmdBindShadersEXT", reinterpret_cast(vkCmdBindShadersEXT), kDevice}, + {"vkGetFramebufferTilePropertiesQCOM", reinterpret_cast(vkGetFramebufferTilePropertiesQCOM), kDevice}, + {"vkGetDynamicRenderingTilePropertiesQCOM", reinterpret_cast(vkGetDynamicRenderingTilePropertiesQCOM), kDevice}, + {"vkSetLatencySleepModeNV", reinterpret_cast(vkSetLatencySleepModeNV), kDevice}, + {"vkLatencySleepNV", reinterpret_cast(vkLatencySleepNV), kDevice}, + {"vkSetLatencyMarkerNV", reinterpret_cast(vkSetLatencyMarkerNV), kDevice}, + {"vkGetLatencyTimingsNV", reinterpret_cast(vkGetLatencyTimingsNV), kDevice}, + {"vkQueueNotifyOutOfBandNV", reinterpret_cast(vkQueueNotifyOutOfBandNV), kDevice}, + {"vkCmdSetAttachmentFeedbackLoopEnableEXT", reinterpret_cast(vkCmdSetAttachmentFeedbackLoopEnableEXT), kDevice}, + {"vkCreateAccelerationStructureKHR", reinterpret_cast(vkCreateAccelerationStructureKHR), kDevice}, + {"vkDestroyAccelerationStructureKHR", reinterpret_cast(vkDestroyAccelerationStructureKHR), kDevice}, + {"vkCmdBuildAccelerationStructuresKHR", reinterpret_cast(vkCmdBuildAccelerationStructuresKHR), kDevice}, + {"vkCmdBuildAccelerationStructuresIndirectKHR", reinterpret_cast(vkCmdBuildAccelerationStructuresIndirectKHR), kDevice}, + {"vkBuildAccelerationStructuresKHR", reinterpret_cast(vkBuildAccelerationStructuresKHR), kDevice}, + {"vkCopyAccelerationStructureKHR", reinterpret_cast(vkCopyAccelerationStructureKHR), kDevice}, + {"vkCopyAccelerationStructureToMemoryKHR", reinterpret_cast(vkCopyAccelerationStructureToMemoryKHR), kDevice}, + {"vkCopyMemoryToAccelerationStructureKHR", reinterpret_cast(vkCopyMemoryToAccelerationStructureKHR), kDevice}, + {"vkWriteAccelerationStructuresPropertiesKHR", reinterpret_cast(vkWriteAccelerationStructuresPropertiesKHR), kDevice}, + {"vkCmdCopyAccelerationStructureKHR", reinterpret_cast(vkCmdCopyAccelerationStructureKHR), kDevice}, + {"vkCmdCopyAccelerationStructureToMemoryKHR", reinterpret_cast(vkCmdCopyAccelerationStructureToMemoryKHR), kDevice}, + {"vkCmdCopyMemoryToAccelerationStructureKHR", reinterpret_cast(vkCmdCopyMemoryToAccelerationStructureKHR), kDevice}, + {"vkGetAccelerationStructureDeviceAddressKHR", reinterpret_cast(vkGetAccelerationStructureDeviceAddressKHR), kDevice}, + {"vkCmdWriteAccelerationStructuresPropertiesKHR", reinterpret_cast(vkCmdWriteAccelerationStructuresPropertiesKHR), kDevice}, + {"vkGetDeviceAccelerationStructureCompatibilityKHR", reinterpret_cast(vkGetDeviceAccelerationStructureCompatibilityKHR), kDevice}, + {"vkGetAccelerationStructureBuildSizesKHR", reinterpret_cast(vkGetAccelerationStructureBuildSizesKHR), kDevice}, + {"vkCmdTraceRaysKHR", reinterpret_cast(vkCmdTraceRaysKHR), kDevice}, + {"vkCreateRayTracingPipelinesKHR", reinterpret_cast(vkCreateRayTracingPipelinesKHR), kDevice}, + {"vkGetRayTracingCaptureReplayShaderGroupHandlesKHR", reinterpret_cast(vkGetRayTracingCaptureReplayShaderGroupHandlesKHR), kDevice}, + {"vkCmdTraceRaysIndirectKHR", reinterpret_cast(vkCmdTraceRaysIndirectKHR), kDevice}, + {"vkGetRayTracingShaderGroupStackSizeKHR", reinterpret_cast(vkGetRayTracingShaderGroupStackSizeKHR), kDevice}, + {"vkCmdSetRayTracingPipelineStackSizeKHR", reinterpret_cast(vkCmdSetRayTracingPipelineStackSizeKHR), kDevice}, + {"vkCmdDrawMeshTasksEXT", reinterpret_cast(vkCmdDrawMeshTasksEXT), kDevice}, + {"vkCmdDrawMeshTasksIndirectEXT", reinterpret_cast(vkCmdDrawMeshTasksIndirectEXT), kDevice}, + {"vkCmdDrawMeshTasksIndirectCountEXT", reinterpret_cast(vkCmdDrawMeshTasksIndirectCountEXT), kDevice}, + {"vkCreateAndroidSurfaceKHR", reinterpret_cast(vkCreateAndroidSurfaceKHR), kInstance}, + {"vkGetAndroidHardwareBufferPropertiesANDROID", reinterpret_cast(vkGetAndroidHardwareBufferPropertiesANDROID), kDevice}, + {"vkGetMemoryAndroidHardwareBufferANDROID", reinterpret_cast(vkGetMemoryAndroidHardwareBufferANDROID), kDevice}, +}; diff --git a/direct/guest/selftest.cpp b/direct/guest/selftest.cpp new file mode 100644 index 0000000..1e1d1e8 --- /dev/null +++ b/direct/guest/selftest.cpp @@ -0,0 +1,202 @@ +// Exercises the parts of the Linux/bionic contract that games depend on: +// threads, futex-based locks, TLS, C++ exceptions and unwinding, atomics +// (both LL/SC and LSE encodings), files, directories, dlopen, signals, time. +// Every check compares a value; a crash or a wrong value fails the run. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int failures = 0; +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + ++failures; \ + } \ + } while (0) + +static thread_local int tls_value = 7; +static volatile sig_atomic_t got_signal = 0; +static void on_usr1(int sig, siginfo_t* info, void*) { got_signal = sig + (info->si_code == SI_TKILL ? 1000 : 0); } + +static int thrower(int depth) { + if (depth == 0) throw std::runtime_error("deep"); + return thrower(depth - 1) + 1; +} + +static uint32_t lse_add(uint32_t* p, uint32_t v) { + uint32_t old; + asm volatile(".arch_extension lse\n ldaddal %w[v], %w[old], [%[p]]" : [old] "=r"(old) : [v] "r"(v), [p] "r"(p) : "memory"); + return old; +} +static uint64_t lse_cas(uint64_t* p, uint64_t expected, uint64_t desired) { + asm volatile(".arch_extension lse\n casal %[e], %[d], [%[p]]" : [e] "+r"(expected) : [d] "r"(desired), [p] "r"(p) : "memory"); + return expected; +} + +int main() { + std::printf("selftest: start\n"); + // TLS and threads. + { + std::atomic sum{0}; + std::vector threads; + for (int i = 0; i < 8; ++i) + threads.emplace_back([&sum, i] { + tls_value = i * 10; + for (int k = 0; k < 10000; ++k) sum.fetch_add(1, std::memory_order_relaxed); + CHECK(tls_value == i * 10); + }); + for (auto& t : threads) t.join(); + CHECK(sum.load() == 80000); + CHECK(tls_value == 7); + } + // Mutex + condition variable handoff (futex wait/wake). + { + std::mutex m; + std::condition_variable cv; + int stage = 0; + std::thread other([&] { + for (int i = 0; i < 1000; ++i) { + std::unique_lock lock(m); + cv.wait(lock, [&] { return stage % 2 == 1; }); + ++stage; + cv.notify_all(); + } + }); + for (int i = 0; i < 1000; ++i) { + std::unique_lock lock(m); + ++stage; + cv.notify_all(); + cv.wait(lock, [&] { return stage % 2 == 0; }); + } + other.join(); + CHECK(stage == 2000); + } + // Timed waits. + { + std::mutex m; + std::condition_variable cv; + std::unique_lock lock(m); + auto start = std::chrono::steady_clock::now(); + cv.wait_for(lock, std::chrono::milliseconds(30)); + auto ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + CHECK(ms >= 25 && ms < 1000); + } + // C++ exceptions through frames (unwinder, eh_frame, dl_iterate_phdr). + { + std::string what; + try { + thrower(20); + } catch (const std::exception& e) { + what = e.what(); + } + CHECK(what == "deep"); + } + // LSE atomics (not in Dynarmic: served by the fallback interpreter). + { + uint32_t counter = 5; + CHECK(lse_add(&counter, 3) == 5); + CHECK(counter == 8); + uint64_t word = 11; + CHECK(lse_cas(&word, 11, 99) == 11); + CHECK(word == 99); + CHECK(lse_cas(&word, 11, 5) == 99); + CHECK(word == 99); + } + // Files and directories. + { + mkdir("/data/local/tmp/selftest", 0755); + FILE* f = std::fopen("/data/local/tmp/selftest/a.txt", "w"); + CHECK(f != nullptr); + if (f) { + std::fprintf(f, "line %d\n", 42); + std::fclose(f); + } + f = std::fopen("/data/local/tmp/selftest/a.txt", "r"); + char buffer[64] = {}; + if (f) { + std::fgets(buffer, sizeof buffer, f); + std::fclose(f); + } + CHECK(std::strcmp(buffer, "line 42\n") == 0); + struct stat st {}; + CHECK(stat("/data/local/tmp/selftest/a.txt", &st) == 0 && st.st_size == 8 && S_ISREG(st.st_mode)); + int seen = 0; + if (DIR* d = opendir("/data/local/tmp/selftest")) { + while (dirent* e = readdir(d)) seen += std::strcmp(e->d_name, "a.txt") == 0; + closedir(d); + } + CHECK(seen == 1); + CHECK(unlink("/data/local/tmp/selftest/a.txt") == 0); + CHECK(access("/data/local/tmp/selftest/a.txt", F_OK) != 0); + } + // mmap / mprotect. + { + void* p = mmap(nullptr, 1 << 20, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + CHECK(p != MAP_FAILED); + CHECK(mprotect(p, 8192, PROT_READ | PROT_WRITE) == 0); + static_cast(p)[8191] = 3; + CHECK(static_cast(p)[0] == 0); + CHECK(munmap(p, 1 << 20) == 0); + } + // Transitive system dependencies must also resolve libraries in APEXes. + // Unity loads harfbuzz, which needs libicu from com.android.i18n. + for (const char* name : {"libicu.so", "libharfbuzz_ng.so"}) { + void* library = dlopen(name, RTLD_NOW); + if (!library) std::printf("dlopen %s: %s\n", name, dlerror()); + CHECK(library != nullptr); + if (library) dlclose(library); + } + // dlopen of a system library. + { + void* m = dlopen("libm.so", RTLD_NOW); + CHECK(m != nullptr); + auto cosf_ptr = m ? reinterpret_cast(dlsym(m, "cosf")) : nullptr; + CHECK(cosf_ptr && std::fabs(cosf_ptr(0.0f) - 1.0f) < 1e-6f); + } + // Signals delivered to a thread. + { + struct sigaction sa {}; + sa.sa_sigaction = on_usr1; + sa.sa_flags = SA_SIGINFO; + CHECK(sigaction(SIGUSR1, &sa, nullptr) == 0); + pthread_kill(pthread_self(), SIGUSR1); + CHECK(got_signal == SIGUSR1 + 1000); + } + // Clocks. + { + timespec a{}, b{}; + clock_gettime(CLOCK_MONOTONIC, &a); + usleep(20000); + clock_gettime(CLOCK_MONOTONIC, &b); + long long ns = (b.tv_sec - a.tv_sec) * 1000000000LL + (b.tv_nsec - a.tv_nsec); + CHECK(ns >= 15000000 && ns < 2000000000LL); + CHECK(time(nullptr) > 1700000000); + } + // System properties come from the generated property area. + { + char value[PROP_VALUE_MAX] = {}; + __system_property_get("ro.build.version.sdk", value); + CHECK(std::atoi(value) >= 29); + } + std::printf("selftest: %s (%d failures)\n", failures ? "FAILED" : "passed", failures); + return failures ? 1 : 0; +} diff --git a/direct/patches/digitalis-address-tags.patch b/direct/patches/digitalis-address-tags.patch new file mode 100644 index 0000000..be51b60 --- /dev/null +++ b/direct/patches/digitalis-address-tags.patch @@ -0,0 +1,145 @@ +diff --git a/code_gen_lib/include/berberis/code_gen_lib/code_gen_lib.h b/code_gen_lib/include/berberis/code_gen_lib/code_gen_lib.h +index 6b1d4b6..14a8f7f 100644 +--- a/code_gen_lib/include/berberis/code_gen_lib/code_gen_lib.h ++++ b/code_gen_lib/include/berberis/code_gen_lib/code_gen_lib.h +@@ -45,6 +45,11 @@ void EmitIndirectJump(Assembler* as, Assembler::Register target); + void EmitSyscall(x86_64::Assembler* as, GuestAddr pc); + void EmitDirectDispatch(x86_64::Assembler* as, GuestAddr pc, bool check_pending_signals); + void EmitIndirectDispatch(x86_64::Assembler* as, x86_64::Assembler::Register target); ++// Whether the guest instruction at this address clears the top byte of the ++// addresses it accesses (ARM64 top-byte-ignore). Where it does not, a tagged ++// pointer faults in the host, and the embedder switches it on for that ++// instruction: only for accesses that have fault recovery. ++bool MaskAddressTopByte(GuestAddr insn_addr); + void EmitExitGeneratedCode(x86_64::Assembler* as, x86_64::Assembler::Register target); + void EmitAllocStackFrame(x86_64::Assembler* as, uint32_t frame_size); + void EmitFreeStackFrame(x86_64::Assembler* as, uint32_t frame_size); +diff --git a/heavy_optimizer/arm64/frontend.h b/heavy_optimizer/arm64/frontend.h +index f613d1c..00c422d 100644 +--- a/heavy_optimizer/arm64/frontend.h ++++ b/heavy_optimizer/arm64/frontend.h +@@ -24,6 +24,7 @@ + #include "berberis/backend/x86_64/machine_ir_builder.h" + #include "berberis/base/arena_map.h" + #include "berberis/base/checks.h" ++#include "berberis/code_gen_lib/code_gen_lib.h" + #include "berberis/decoder/arm64/decoder.h" + #include "berberis/decoder/arm64/semantics_player.h" + #include "berberis/guest_state/guest_addr.h" +@@ -710,7 +711,14 @@ class HeavyOptimizerFrontend { + // pointers in load/store; x86 does not, so we mask it ourselves. Returns a + // fresh register holding (base & 0x00FF'FFFF'FFFF'FFFF). Mirrors + // lite_translator.h::ApplyTbi (movq; shlq 8; shrq 8). +- [[nodiscard]] Register ApplyTbi(Register base) { ++ // recoverable: every access through the result has a recovery block, so it may go ++ // without the mask until a tagged pointer faults there (see MaskAddressTopByte). ++ [[nodiscard]] Register ApplyTbi(Register base, bool recoverable = false) { ++ if (recoverable && !MaskAddressTopByte(GetInsnAddr())) { ++ // A move the backend keeps, not Copy(): copy renaming would use `base` itself as the ++ // address, and its other uses may want a register class that a memory base's only overlaps. ++ return std::get<0>(Gen(base)); ++ } + Register tbi = Copy(base); + tbi = std::get<0>(Gen(tbi, int8_t{8})); + tbi = std::get<0>(Gen(tbi, int8_t{8})); +diff --git a/lite_translator/arm64_to_x86_64/lite_translator.h b/lite_translator/arm64_to_x86_64/lite_translator.h +index f2cfbba..8a1bb1f 100644 +--- a/lite_translator/arm64_to_x86_64/lite_translator.h ++++ b/lite_translator/arm64_to_x86_64/lite_translator.h +@@ -24,6 +24,7 @@ + #include "berberis/assembler/common.h" + #include "berberis/assembler/x86_64.h" + #include "berberis/base/checks.h" ++#include "berberis/code_gen_lib/code_gen_lib.h" + #include "berberis/base/macros.h" + #include "berberis/decoder/arm64/decoder.h" + #include "berberis/decoder/arm64/semantics_player.h" +diff --git a/lite_translator/arm64_to_x86_64/lite_translator_integer_ctrl.inc b/lite_translator/arm64_to_x86_64/lite_translator_integer_ctrl.inc +index 86691d8..a8a14f0 100644 +--- a/lite_translator/arm64_to_x86_64/lite_translator_integer_ctrl.inc ++++ b/lite_translator/arm64_to_x86_64/lite_translator_integer_ctrl.inc +@@ -484,18 +484,22 @@ + // address register before using it in a host x86 load/store. ARM64 ignores + // the top byte of pointers in load/store; x86 doesn't, so we must clear it + // ourselves. Returns a temp register holding (base & 0x00FFFFFFFFFFFFFF). +- Register ApplyTbi(Register base) { ++ // recoverable: the access has a recovery point, so it may go without the mask ++ // until a tagged pointer faults there (see MaskAddressTopByte). ++ Register ApplyTbi(Register base, bool recoverable = false) { + Register tbi = AllocTempReg(); + as_.Movq(tbi, base); +- as_.Shlq(tbi, static_cast(8)); +- as_.Shrq(tbi, static_cast(8)); ++ if (!recoverable || MaskAddressTopByte(GetInsnAddr())) { ++ as_.Shlq(tbi, static_cast(8)); ++ as_.Shrq(tbi, static_cast(8)); ++ } + return tbi; + } + + Register Load(Decoder::LoadStoreSize size, bool is_signed, bool is_64bit_target, + Register base, int32_t offset) { + // apply TBI mask before using base as memory operand. +- base = ApplyTbi(base); ++ base = ApplyTbi(base, /*recoverable=*/true); + AssemblerBase::Label* recovery_label = as_.MakeLabel(); + as_.SetRecoveryPoint(recovery_label); + +@@ -548,7 +552,7 @@ + + void Store(Decoder::LoadStoreSize size, Register base, int32_t offset, Register data) { + // apply TBI mask before using base as memory operand. +- base = ApplyTbi(base); ++ base = ApplyTbi(base, /*recoverable=*/true); + AssemblerBase::Label* recovery_label = as_.MakeLabel(); + as_.SetRecoveryPoint(recovery_label); + +diff --git a/heavy_optimizer/arm64/frontend.cc b/heavy_optimizer/arm64/frontend.cc +index c978f8b..b371996 100644 +--- a/heavy_optimizer/arm64/frontend.cc ++++ b/heavy_optimizer/arm64/frontend.cc +@@ -2875,7 +2926,7 @@ Register HeavyOptimizerFrontend::Load(Decoder::LoadStoreSize size, + if (!success()) { + return AllocTempReg(); + } +- Register masked = ApplyTbi(base); ++ Register masked = ApplyTbi(base, /*recoverable=*/true); + Register res; + switch (size) { + case Decoder::LoadStoreSize::k64bit: +@@ -2927,7 +2978,7 @@ void HeavyOptimizerFrontend::Store(Decoder::LoadStoreSize size, Register base, i + if (!success()) { + return; + } +- Register masked = ApplyTbi(base); ++ Register masked = ApplyTbi(base, /*recoverable=*/true); + switch (size) { + case Decoder::LoadStoreSize::k64bit: + Gen({.base = masked, .disp = offset}, data); +@@ -5467,7 +5518,7 @@ void HeavyOptimizerFrontend::SimdLoadStoreImm(const Decoder::SimdLoadStoreImmArg + UndefinedReturningVoid(BailReason::kUnsupportedSize); + return; + } +- Register masked = ApplyTbi(base); ++ Register masked = ApplyTbi(base, /*recoverable=*/true); + const int32_t off = static_cast(args.offset); + const int32_t vreg_off = GetVRegOffset(args.rt); + FpRegister xmm = AllocTempSimdReg(); +@@ -5524,7 +5575,7 @@ void HeavyOptimizerFrontend::SimdLoadStorePair(const Decoder::SimdLoadStorePairA + UndefinedReturningVoid(); + return; + } +- Register masked = ApplyTbi(addr); ++ Register masked = ApplyTbi(addr, /*recoverable=*/true); + const int32_t v1_off = GetVRegOffset(args.rt1); + const int32_t v2_off = GetVRegOffset(args.rt2); + if (args.is_store) { +@@ -5606,7 +5657,7 @@ void HeavyOptimizerFrontend::SimdLoadStoreReg(const Decoder::SimdLoadStoreRegArg + return; + } + Register addr = EmitRegOffsetAddr(base, offset, args.extend_type, args.shift_amount); +- Register masked = ApplyTbi(addr); ++ Register masked = ApplyTbi(addr, /*recoverable=*/true); + const int32_t vreg_off = GetVRegOffset(args.rt); + FpRegister xmm = AllocTempSimdReg(); + if (args.is_store) { diff --git a/direct/patches/digitalis-cond-branch.patch b/direct/patches/digitalis-cond-branch.patch new file mode 100644 index 0000000..67ae018 --- /dev/null +++ b/direct/patches/digitalis-cond-branch.patch @@ -0,0 +1,75 @@ +diff --git a/heavy_optimizer/arm64/frontend.cc b/heavy_optimizer/arm64/frontend.cc +index c978f8b..b371996 100644 +--- a/heavy_optimizer/arm64/frontend.cc ++++ b/heavy_optimizer/arm64/frontend.cc +@@ -306,14 +306,65 @@ Register HeavyOptimizerFrontend::EmitArmCondPredicate(Decoder::Condition cond) { + // Branch to then_bb when `cond` is met, else_bb otherwise. The predicate is a + // 0/1 value; TestlRegReg sets ZF=1 when it is 0 (not taken) and ZF=0 when it is + // 1 (taken), so the then_bb is selected on kNotZero. ++// ++// AXRB: the NZCV word is tested in place rather than first reduced to a 0/1 ++// predicate (EmitArmCondPredicate: up to a dozen instructions per branch). ++// One flag is a TEST of its bit; HI/LS compare the C and Z bits with "C set, ++// Z clear"; the signed conditions fold N into V's position first. + void HeavyOptimizerFrontend::EmitCondBranch(Decoder::Condition cond, + MachineBasicBlock* then_bb, + MachineBasicBlock* else_bb) { +- Register pred = EmitArmCondPredicate(cond); +- builder_.Gen(x86_64::Assembler::Condition::kNotZero, +- then_bb, +- else_bb, +- std::get<0>(Gen(pred, pred))); ++ using HostCondition = x86_64::Assembler::Condition; ++ const int32_t flags_disp = static_cast(offsetof(ThreadState, cpu.flags)); ++ const int32_t n = int32_t{1} << kFlagNegativeBit, z = int32_t{1} << kFlagZeroBit; ++ const int32_t c = int32_t{1} << kFlagCarryBit, v = int32_t{1} << kFlagOverflowBit; ++ // The 16-bit load is the form RemoveLoopGuestContextAccesses recognizes (see ++ // EmitArmCondPredicate); it leaves the register's upper bits undefined. ++ Register flags = ++ std::get<0>(Gen({.base = x86_64::kMachineRegRBP, .disp = flags_disp})); ++ auto branch = [&](HostCondition taken, Register host_flags) { ++ builder_.Gen(taken, then_bb, else_bb, host_flags); ++ }; ++ auto test = [&](Register value, int32_t mask, bool taken_when_set) { ++ branch(taken_when_set ? HostCondition::kNotZero : HostCondition::kZero, ++ std::get<0>(Gen(value, mask))); ++ }; ++ switch (cond) { ++ case Decoder::Condition::kEq: return test(flags, z, true); ++ case Decoder::Condition::kNe: return test(flags, z, false); ++ case Decoder::Condition::kCs: return test(flags, c, true); ++ case Decoder::Condition::kCc: return test(flags, c, false); ++ case Decoder::Condition::kMi: return test(flags, n, true); ++ case Decoder::Condition::kPl: return test(flags, n, false); ++ case Decoder::Condition::kVs: return test(flags, v, true); ++ case Decoder::Condition::kVc: return test(flags, v, false); ++ case Decoder::Condition::kHi: // C==1 && Z==0 ++ case Decoder::Condition::kLs: { // C==0 || Z==1 ++ Register bits = std::get<0>(Gen(flags)); ++ bits = std::get<0>(Gen(bits, c | z)); ++ return branch(cond == Decoder::Condition::kHi ? HostCondition::kEqual : HostCondition::kNotEqual, ++ std::get<0>(Gen(bits, c))); ++ } ++ case Decoder::Condition::kGe: // N==V ++ case Decoder::Condition::kLt: // N!=V ++ case Decoder::Condition::kGt: // Z==0 && N==V ++ case Decoder::Condition::kLe: { // Z==1 || N!=V ++ static_assert(kFlagNegativeBit == 15 && kFlagOverflowBit == 0 && kFlagZeroBit == 14); ++ Register clean = std::get<0>(Gen(flags)); ++ clean = std::get<0>(Gen(clean, int32_t{0xFFFF})); ++ // Bit 0 becomes N^V; bit 14 stays Z (the shifted copy is zero there). ++ Register folded = std::get<0>(Gen(clean)); ++ folded = std::get<0>(Gen(folded, int8_t{15})); ++ folded = std::get<0>(Gen(folded, clean)); ++ const bool with_zero = cond == Decoder::Condition::kGt || cond == Decoder::Condition::kLe; ++ return test(folded, with_zero ? (v | z) : v, ++ cond == Decoder::Condition::kLt || cond == Decoder::Condition::kLe); ++ } ++ case Decoder::Condition::kAl: ++ case Decoder::Condition::kNv: ++ break; // Unconditional: handled by the caller. ++ } ++ CHECK(false); + } + + // B.cond (conditional). AL/NV are unconditional. Otherwise split into a taken diff --git a/direct/patches/digitalis-pairwise-fp-minmax.patch b/direct/patches/digitalis-pairwise-fp-minmax.patch new file mode 100644 index 0000000..dc3c3d2 --- /dev/null +++ b/direct/patches/digitalis-pairwise-fp-minmax.patch @@ -0,0 +1,81 @@ +diff --git a/lite_translator/arm64_to_x86_64/lite_translator_simd_three_same.inc b/lite_translator/arm64_to_x86_64/lite_translator_simd_three_same.inc +index ac1d177..9c29525 100644 +--- a/lite_translator/arm64_to_x86_64/lite_translator_simd_three_same.inc ++++ b/lite_translator/arm64_to_x86_64/lite_translator_simd_three_same.inc +@@ -1776,7 +1776,11 @@ + case Decoder::AdvSimdThreeSameOpcode::kFmaxV: + case Decoder::AdvSimdThreeSameOpcode::kFminV: + case Decoder::AdvSimdThreeSameOpcode::kFmaxnmV: +- case Decoder::AdvSimdThreeSameOpcode::kFminnmV: { ++ case Decoder::AdvSimdThreeSameOpcode::kFminnmV: ++ case Decoder::AdvSimdThreeSameOpcode::kFmaxpV: ++ case Decoder::AdvSimdThreeSameOpcode::kFminpV: ++ case Decoder::AdvSimdThreeSameOpcode::kFmaxnmpV: ++ case Decoder::AdvSimdThreeSameOpcode::kFminnmpV: { + // FP vector FMAX / FMIN / FMAXNM / FMINNM. + // + // ARM and x86 disagree on NaN handling for MAX/MIN: +@@ -1805,10 +1809,23 @@ + // mantissa strictly contains FP16's. NaN-result bit patterns may + // differ from a canonical FP16 qNaN (0x7E00), but are still valid + // NaNs per ARM ARM default-NaN propagation rules. ++ // Pairwise forms (FMAXP/FMINP/FMAXNMP/FMINNMP) combine adjacent lanes ++ // of the concatenation Vm:Vn. De-interleaving it into its even and ++ // odd elements turns them into the element-wise operation below, with ++ // the even (lower) element as the first operand of each pair. ++ const bool pairwise = (args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxpV || ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFminpV || ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxnmpV || ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFminnmpV); + const bool is_max = (args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxV || +- args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxnmV); ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxnmV || ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxpV || ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxnmpV); + const bool is_nm = (args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxnmV || +- args.opcode == Decoder::AdvSimdThreeSameOpcode::kFminnmV); ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFminnmV || ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFmaxnmpV || ++ args.opcode == Decoder::AdvSimdThreeSameOpcode::kFminnmpV); ++ if (pairwise && args.is_fp16) { Undefined(); return; } + if (!args.is_fp16) { + // FP32 (.2S/.4S, args.size=0) and FP64 (.2D, args.size=1). The + // decoder filters sz=1 && !Q (the reserved .1D shape) to Undefined, +@@ -1821,6 +1838,37 @@ + if (!success()) { return; } + LoadVReg(xn, args.rn); + LoadVReg(xm, args.rm); ++ if (pairwise) { ++ if (is_double) { ++ // .2D: [n0, m0] op [n1, m1]. ++ SimdRegister odd = AllocTempSimdReg(); ++ if (!success()) { return; } ++ as_.Movdqa(odd, xn); ++ as_.Punpcklqdq(xn, xm); ++ as_.Punpckhqdq(odd, xm); ++ as_.Movdqa(xm, odd); ++ } else if (args.q) { ++ // .4S: [n0, n2, m0, m2] op [n1, n3, m1, m3]. ++ SimdRegister odd = AllocTempSimdReg(); ++ if (!success()) { return; } ++ as_.Pshufd(xn, xn, static_cast(0xD8)); ++ as_.Pshufd(xm, xm, static_cast(0xD8)); ++ as_.Movdqa(odd, xn); ++ as_.Punpcklqdq(xn, xm); ++ as_.Punpckhqdq(odd, xm); ++ as_.Movdqa(xm, odd); ++ } else { ++ // .2S: [n0, m0] op [n1, m1] in the low half. ++ as_.Punpcklqdq(xn, xm); ++ as_.Pshufd(xm, xn, static_cast(0xDD)); ++ as_.Pshufd(xn, xn, static_cast(0x88)); ++ } ++ } else if (!args.q) { ++ // .2S: stale upper lanes must not take part; a NaN there would ++ // raise a spurious invalid-operation flag. ++ MaskLow64(xn); ++ MaskLow64(xm); ++ } + // ARM FMAX/FMAXNM are computed as -FMIN(-a,-b). x86 MAXPS/MINPS return + // the SECOND source on a +-0 tie (OR-of-signs), which is correct only + // for FMIN (most-negative). Negating inputs+output makes FMAX reuse the diff --git a/direct/patches/dynarmic-hint-hooks.patch b/direct/patches/dynarmic-hint-hooks.patch new file mode 100644 index 0000000..93014b3 --- /dev/null +++ b/direct/patches/dynarmic-hint-hooks.patch @@ -0,0 +1,12 @@ +diff --git a/src/dynarmic/backend/x64/a64_interface.cpp b/src/dynarmic/backend/x64/a64_interface.cpp +--- a/src/dynarmic/backend/x64/a64_interface.cpp ++++ b/src/dynarmic/backend/x64/a64_interface.cpp +@@ -276,7 +276,7 @@ + // JIT Compile + const auto get_code = [this](u64 vaddr) { return conf.callbacks->MemoryReadCode(vaddr); }; + IR::Block ir_block = A64::Translate(A64::LocationDescriptor{current_location}, get_code, +- {conf.define_unpredictable_behaviour, conf.wall_clock_cntpct}); ++ {conf.define_unpredictable_behaviour, conf.wall_clock_cntpct, conf.hook_hint_instructions}); + Optimization::PolyfillPass(ir_block, polyfill_options); + Optimization::A64CallbackConfigPass(ir_block, conf); + Optimization::NamingPass(ir_block); diff --git a/direct/patches/dynarmic-host-code-lookup.patch b/direct/patches/dynarmic-host-code-lookup.patch new file mode 100644 index 0000000..9618737 --- /dev/null +++ b/direct/patches/dynarmic-host-code-lookup.patch @@ -0,0 +1,63 @@ +diff --git a/src/dynarmic/backend/x64/a64_interface.cpp b/src/dynarmic/backend/x64/a64_interface.cpp +index 7c0fa8d2..24906846 100644 +--- a/src/dynarmic/backend/x64/a64_interface.cpp ++++ b/src/dynarmic/backend/x64/a64_interface.cpp +@@ -151,6 +151,11 @@ public: + return jit_state.pc; + } + ++ u64 GuestPCForHostCode(const void* host) const { ++ const auto location = emitter.LocationForHostCode(static_cast(host)); ++ return location ? A64::LocationDescriptor{*location}.PC() : ~u64{0}; ++ } ++ + void SetPC(u64 value) { + jit_state.pc = value; + } +@@ -370,6 +375,10 @@ u64 Jit::GetPC() const { + return impl->GetPC(); + } + ++u64 Jit::GuestPCForHostCode(const void* host) const { ++ return impl->GuestPCForHostCode(host); ++} ++ + void Jit::SetPC(u64 value) { + impl->SetPC(value); + } +diff --git a/src/dynarmic/backend/x64/emit_x64.h b/src/dynarmic/backend/x64/emit_x64.h +index 5a46f04e..d043c10a 100644 +--- a/src/dynarmic/backend/x64/emit_x64.h ++++ b/src/dynarmic/backend/x64/emit_x64.h +@@ -78,6 +78,16 @@ public: + size_t size; // Length in bytes of emitted code + }; + ++ /// The block whose emitted code contains `host` (linear search; profilers). ++ std::optional LocationForHostCode(CodePtr host) const { ++ for (const auto& [location, block] : block_descriptors) { ++ const auto* begin = static_cast(block.entrypoint); ++ const auto* at = static_cast(host); ++ if (at >= begin && at < begin + block.size) return location; ++ } ++ return std::nullopt; ++ } ++ + explicit EmitX64(BlockOfCode& code); + virtual ~EmitX64(); + +diff --git a/src/dynarmic/interface/A64/a64.h b/src/dynarmic/interface/A64/a64.h +index a150da84..31d75986 100644 +--- a/src/dynarmic/interface/A64/a64.h ++++ b/src/dynarmic/interface/A64/a64.h +@@ -72,6 +72,10 @@ public: + + /// Read Program Counter + std::uint64_t GetPC() const; ++ /// Guest address of the translated block containing host code address ++ /// `host`, or ~0 when it is not code of this JIT. For profilers: call only ++ /// while the thread running this JIT is stopped. ++ std::uint64_t GuestPCForHostCode(const void* host) const; + /// Modify Program Counter + void SetPC(std::uint64_t value); + diff --git a/direct/patches/dynarmic-round-exact.patch b/direct/patches/dynarmic-round-exact.patch new file mode 100644 index 0000000..b09cfbf --- /dev/null +++ b/direct/patches/dynarmic-round-exact.patch @@ -0,0 +1,41 @@ +diff --git a/src/dynarmic/backend/x64/emit_x64_floating_point.cpp b/src/dynarmic/backend/x64/emit_x64_floating_point.cpp +--- a/src/dynarmic/backend/x64/emit_x64_floating_point.cpp ++++ b/src/dynarmic/backend/x64/emit_x64_floating_point.cpp +@@ -1074,14 +1074,18 @@ + const bool exact = inst->GetArg(2).GetU1(); + const auto round_imm = ConvertRoundingModeToX64Immediate(rounding_mode); + +- if (fsize != 16 && code.HasHostFeature(HostFeature::SSE41) && round_imm && !exact) { ++ if (fsize != 16 && code.HasHostFeature(HostFeature::SSE41) && round_imm) { ++ // AXRB: FRINTX (exact) is the same rounding with the inexact exception ++ // reported, which the host instruction does unless bit 3 suppresses it. ++ // It was a call into the software implementation for every value. ++ const u8 imm = static_cast(*round_imm | (exact ? 0 : 0b1000)); + if (fsize == 64) { + FPTwoOp<64>(code, ctx, inst, [&](Xbyak::Xmm result) { +- code.roundsd(result, result, *round_imm); ++ code.roundsd(result, result, imm); + }); + } else { + FPTwoOp<32>(code, ctx, inst, [&](Xbyak::Xmm result) { +- code.roundss(result, result, *round_imm); ++ code.roundss(result, result, imm); + }); + } + +diff --git a/src/dynarmic/backend/x64/emit_x64_vector_floating_point.cpp b/src/dynarmic/backend/x64/emit_x64_vector_floating_point.cpp +--- a/src/dynarmic/backend/x64/emit_x64_vector_floating_point.cpp ++++ b/src/dynarmic/backend/x64/emit_x64_vector_floating_point.cpp +@@ -1684,8 +1684,10 @@ + const bool exact = inst->GetArg(2).GetU1(); + + if constexpr (fsize != 16) { +- if (code.HasHostFeature(HostFeature::SSE41) && rounding != FP::RoundingMode::ToNearest_TieAwayFromZero && !exact) { +- const u8 round_imm = [&]() -> u8 { ++ if (code.HasHostFeature(HostFeature::SSE41) && rounding != FP::RoundingMode::ToNearest_TieAwayFromZero) { ++ // AXRB: also FRINTX (exact), which reports the inexact exception: ++ // the host instruction does unless bit 3 suppresses it. ++ const u8 round_imm = (exact ? 0 : 0b1000) | [&]() -> u8 { + switch (rounding) { + case FP::RoundingMode::ToNearest_TieEven: + return 0b00; diff --git a/direct/patches/dynarmic-tbi.patch b/direct/patches/dynarmic-tbi.patch new file mode 100644 index 0000000..5bb66b7 --- /dev/null +++ b/direct/patches/dynarmic-tbi.patch @@ -0,0 +1,130 @@ +diff --git a/src/dynarmic/frontend/A64/a64_ir_emitter.cpp b/src/dynarmic/frontend/A64/a64_ir_emitter.cpp +index 3f5a70bd..47d3c976 100644 +--- a/src/dynarmic/frontend/A64/a64_ir_emitter.cpp ++++ b/src/dynarmic/frontend/A64/a64_ir_emitter.cpp +@@ -102,84 +102,93 @@ void IREmitter::ClearExclusive() { + Inst(Opcode::A64ClearExclusive); + } + ++// Top Byte Ignore: Linux runs arm64 user space with TBI enabled for data ++// accesses, and Android's allocator hands out pointers tagged in bits 56-63. ++IR::U64 IREmitter::UntagAddress(const IR::U64& vaddr) { ++ if (vaddr.IsImmediate()) { ++ return Imm64(vaddr.GetImmediateAsU64() & 0x00FF'FFFF'FFFF'FFFFull); ++ } ++ return And(vaddr, Imm64(0x00FF'FFFF'FFFF'FFFFull)); ++} ++ + IR::U8 IREmitter::ReadMemory8(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ReadMemory8, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ReadMemory8, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U16 IREmitter::ReadMemory16(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ReadMemory16, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ReadMemory16, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U32 IREmitter::ReadMemory32(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ReadMemory32, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ReadMemory32, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U64 IREmitter::ReadMemory64(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ReadMemory64, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ReadMemory64, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U128 IREmitter::ReadMemory128(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ReadMemory128, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ReadMemory128, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U8 IREmitter::ExclusiveReadMemory8(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveReadMemory8, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveReadMemory8, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U16 IREmitter::ExclusiveReadMemory16(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveReadMemory16, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveReadMemory16, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U32 IREmitter::ExclusiveReadMemory32(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveReadMemory32, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveReadMemory32, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U64 IREmitter::ExclusiveReadMemory64(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveReadMemory64, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveReadMemory64, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + IR::U128 IREmitter::ExclusiveReadMemory128(const IR::U64& vaddr, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveReadMemory128, ImmCurrentLocationDescriptor(), vaddr, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveReadMemory128, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), IR::Value{acc_type}); + } + + void IREmitter::WriteMemory8(const IR::U64& vaddr, const IR::U8& value, IR::AccType acc_type) { +- Inst(Opcode::A64WriteMemory8, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ Inst(Opcode::A64WriteMemory8, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + void IREmitter::WriteMemory16(const IR::U64& vaddr, const IR::U16& value, IR::AccType acc_type) { +- Inst(Opcode::A64WriteMemory16, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ Inst(Opcode::A64WriteMemory16, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + void IREmitter::WriteMemory32(const IR::U64& vaddr, const IR::U32& value, IR::AccType acc_type) { +- Inst(Opcode::A64WriteMemory32, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ Inst(Opcode::A64WriteMemory32, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + void IREmitter::WriteMemory64(const IR::U64& vaddr, const IR::U64& value, IR::AccType acc_type) { +- Inst(Opcode::A64WriteMemory64, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ Inst(Opcode::A64WriteMemory64, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + void IREmitter::WriteMemory128(const IR::U64& vaddr, const IR::U128& value, IR::AccType acc_type) { +- Inst(Opcode::A64WriteMemory128, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ Inst(Opcode::A64WriteMemory128, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + IR::U32 IREmitter::ExclusiveWriteMemory8(const IR::U64& vaddr, const IR::U8& value, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveWriteMemory8, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveWriteMemory8, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + IR::U32 IREmitter::ExclusiveWriteMemory16(const IR::U64& vaddr, const IR::U16& value, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveWriteMemory16, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveWriteMemory16, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + IR::U32 IREmitter::ExclusiveWriteMemory32(const IR::U64& vaddr, const IR::U32& value, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveWriteMemory32, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveWriteMemory32, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + IR::U32 IREmitter::ExclusiveWriteMemory64(const IR::U64& vaddr, const IR::U64& value, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveWriteMemory64, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveWriteMemory64, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + IR::U32 IREmitter::ExclusiveWriteMemory128(const IR::U64& vaddr, const IR::U128& value, IR::AccType acc_type) { +- return Inst(Opcode::A64ExclusiveWriteMemory128, ImmCurrentLocationDescriptor(), vaddr, value, IR::Value{acc_type}); ++ return Inst(Opcode::A64ExclusiveWriteMemory128, ImmCurrentLocationDescriptor(), UntagAddress(vaddr), value, IR::Value{acc_type}); + } + + IR::U32 IREmitter::GetW(Reg reg) { +diff --git a/src/dynarmic/frontend/A64/a64_ir_emitter.h b/src/dynarmic/frontend/A64/a64_ir_emitter.h +index 7fc8bea7..da701fcd 100644 +--- a/src/dynarmic/frontend/A64/a64_ir_emitter.h ++++ b/src/dynarmic/frontend/A64/a64_ir_emitter.h +@@ -56,6 +56,7 @@ public: + void SetTPIDR(const IR::U64& value); + + void ClearExclusive(); ++ IR::U64 UntagAddress(const IR::U64& vaddr); + IR::U8 ReadMemory8(const IR::U64& vaddr, IR::AccType acc_type); + IR::U16 ReadMemory16(const IR::U64& vaddr, IR::AccType acc_type); + IR::U32 ReadMemory32(const IR::U64& vaddr, IR::AccType acc_type); diff --git a/direct/src/app.cpp b/direct/src/app.cpp new file mode 100644 index 0000000..d8aac93 --- /dev/null +++ b/direct/src/app.cpp @@ -0,0 +1,512 @@ +// An Android application session. +// +// Host side of the launcher (direct/guest/axrb_app.c): the guest main thread +// enters "axrb.app_main", which creates the managed runtime on that thread and +// runs ActivityThread.main -- so the app's main thread, its Java threads and +// every thread its native code starts are all guest threads, and Java code, +// JNI and native code interleave on them exactly as on a device. +// +// Package layout in the guest, as the package manager would install it: +// /data/app/~~axrb==/-1/base.apk the APK, read in place +// /data/app/~~axrb==/-1/lib/arm64/ its native libraries (extracted once) +// /data/data/ (= /data/user/0/) private storage, persistent +// /sdcard/Android/obb/ the game's downloaded files, read-only +// /sdcard/Android/data/ external app storage, persistent + +#include "app.h" + +#include "audio.h" +#include "vk_host.h" +#include "ndk.h" + +#include "cpu.h" +#include "files.h" +#include "jni.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" +#include "natives.h" +#include "sysroot.h" +#include "vfs.h" + +#include "arun/framework_host.h" +#include "arun/manifest.h" +#include "arun/vm/class_linker.h" +#include "arun/vm/heap.h" +#include "arun/vm/natives.h" +#include "arun/vm/runtime.h" +#include "arun/vm/thread.h" +#include "arun/vfs.h" +#include "arun/zip_reader.h" + +#include + +#include +#include +#include +#include + +namespace axrb { +namespace vm = arun::vm; + +namespace { + +struct Session { + AppOptions options; + arun::manifest::Manifest manifest; + std::shared_ptr apk; + std::string package; + std::string activity; + std::string install_dir; // guest + std::string guest_apk; + std::string lib_dir; + std::string data_dir; + std::wstring boot_dir; // host directory of arun-boot.dex and arun-framework-res.apk +}; + +Session g_session; + +// Session end: one manual-reset event, named when the launcher asked for it +// (AXRB_DIRECT_STOP_EVENT) so it can be set from outside the process. +HANDLE stop_event() { + static HANDLE event = [] { + wchar_t name[256]; + DWORD n = GetEnvironmentVariableW(L"AXRB_DIRECT_STOP_EVENT", name, static_cast(std::size(name))); + return CreateEventW(nullptr, TRUE, FALSE, n && n < std::size(name) ? name : nullptr); + }(); + return event; +} + +// Runs on a Java daemon thread for the whole session; returns once the +// session should end, and ActivityThread then finishes the activity. +void ActivityThread_nativeWaitForStopRequest(vm::Thread* self, vm::Frame*, vm::JValue*) { + HANDLE event = stop_event(); + { + vm::ScopedBlocking blocking(self); + if (event) WaitForSingleObject(event, INFINITE); + else Sleep(INFINITE); // no stop request can arrive + } + request_session_end("stop requested"); +} +std::unique_ptr g_runtime; +std::unique_ptr g_framework; +arun::vfs::FileSystem g_java_fs; + +std::wstring exe_directory() { + wchar_t path[MAX_PATH * 4]; + DWORD n = GetModuleFileNameW(nullptr, path, static_cast(std::size(path))); + std::wstring dir(path, n); + return dir.substr(0, dir.find_last_of(L"\\/")); +} + +std::wstring find_beside_exe(const std::wstring& relative_candidates) { + std::wstring dir = exe_directory(); + for (const wchar_t* prefix : {L"\\", L"\\..\\", L"\\..\\..\\"}) { + std::wstring candidate = dir + prefix + relative_candidates; + if (GetFileAttributesW(candidate.c_str()) != INVALID_FILE_ATTRIBUTES) { + wchar_t full[MAX_PATH * 4]; + GetFullPathNameW(candidate.c_str(), static_cast(std::size(full)), full, nullptr); + return full; + } + } + return {}; +} + +bool ensure_dir(const std::wstring& path) { + std::error_code ec; + std::filesystem::create_directories(path, ec); + return !ec; +} + +// Native libraries are compressed in most APKs; the linker needs files. +bool extract_libraries(const arun::pkg::ZipReader& zip, const std::wstring& out_dir) { + ensure_dir(out_dir); + int extracted = 0, kept = 0; + for (const auto& entry : zip.entries()) { + const std::string prefix = "lib/arm64-v8a/"; + if (entry.is_directory || entry.name.rfind(prefix, 0) != 0) continue; + std::string name = entry.name.substr(prefix.size()); + if (name.empty() || name.find('/') != std::string::npos) continue; + std::wstring target = out_dir + L"\\" + widen(name); + WIN32_FILE_ATTRIBUTE_DATA data{}; + std::wstring stamp = target + L".crc"; + if (GetFileAttributesExW(target.c_str(), GetFileExInfoStandard, &data) && + ((static_cast(data.nFileSizeHigh) << 32) | data.nFileSizeLow) == entry.uncompressed_size) { + std::ifstream in(stamp); + uint32_t crc = 0; + if (in >> crc && crc == entry.crc32) { + ++kept; + continue; + } + } + auto bytes = zip.Extract(entry); + if (!bytes.ok()) { + LOGE("app", "cannot extract %s: %s", entry.name.c_str(), bytes.status().ToString().c_str()); + return false; + } + std::ofstream out(target, std::ios::binary | std::ios::trunc); + out.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); + if (!out) { + LOGE("app", "cannot write %s", narrow(target).c_str()); + return false; + } + std::ofstream(stamp) << entry.crc32; + ++extracted; + } + LOGI("app", "native libraries: %d extracted, %d up to date", extracted, kept); + return true; +} + +std::vector read_host_file(const std::wstring& path) { + std::ifstream in(path, std::ios::binary); + return std::vector((std::istreambuf_iterator(in)), std::istreambuf_iterator()); +} + +// --- the VM ---------------------------------------------------------------------- + +void log_sink(int priority, std::string_view tag, std::string_view message) { + static const char kLevels[] = "??VDIWEF"; + std::string line; + line += priority >= 0 && priority < 8 ? kLevels[priority] : '?'; + line += '/'; + line += tag; + line += "(java): "; + line += message; + if (line.empty() || line.back() != '\n') line += '\n'; + log_raw(line.data(), line.size()); +} + +[[noreturn]] void fail_with_exception(vm::Runtime* rt, vm::Thread* self, const char* what) { + std::string text = self->IsExceptionPending() ? rt->DescribeException(self, self->exception()) : "(no exception)"; + fatal("%s failed: %s", what, text.c_str()); +} + +struct ThreadStart { + void (*entry)(void*); + void* arg; + HANDLE started; // set once the new thread runs: Thread.start() waits for it +}; + +bool spawn_java_thread(void (*entry)(void*), void* arg) { + GuestThread* guest = current_thread(); + if (!guest) return false; + HANDLE started = CreateEventW(nullptr, TRUE, FALSE, nullptr); + auto* start = new ThreadStart{entry, arg, started}; + vm::Thread* self = vm::Thread::Current(); + bool runnable = self && self->state() == vm::ThreadState::kRunnable; + if (runnable) self->TransitionFromRunnable(vm::ThreadState::kNative); + uint64_t r = call_guest(*guest, guest_api().spawn_thread, {reinterpret_cast(start), 0}); + // Like a device, where the new thread is scheduled right away: applications + // hand work to a thread they just started and expect it to be listening + // within the frame (Unity's render thread and the first surface). + if (r == 0) WaitForSingleObject(started, 2000); + if (runnable) self->TransitionToRunnable(); + if (r != 0) { // otherwise the new thread owns (and closes) the event + CloseHandle(started); + delete start; + return false; + } + return true; +} + +// AXRB_DIRECT_DUMP=: periodically log every thread (guest threads and +// what they block in, Java stacks) -- for finding where an app is stuck. +void start_dump_thread() { + const char* every = std::getenv("AXRB_DIRECT_DUMP"); + if (!every || std::atoi(every) <= 0) return; + const int seconds = std::atoi(every); + std::thread([seconds] { + for (;;) { + Sleep(static_cast(seconds) * 1000); + std::string out = "thread dump:\n"; + { + std::lock_guard held(process().threads_lock); + for (auto& [tid, t] : process().threads) { + const char* why = t->blocked_in.load(); + out += " guest " + std::to_string(tid) + " \"" + t->name + "\"" + + (why ? std::string(" blocked in ") + why : std::string(" running")) + "\n"; + } + } + if (g_runtime) out += g_runtime->DumpThreads(); + log_raw(out.data(), out.size()); + } + }).detach(); +} + +int run_app(GuestThread& main_thread) { + Session& s = g_session; + std::vector boot = read_host_file(s.boot_dir + L"\\arun-boot.dex"); + if (boot.empty()) fatal("missing %s\\arun-boot.dex", narrow(s.boot_dir).c_str()); + + vm::RuntimeOptions ro; + ro.boot_class_path.push_back(vm::BootDex{"arun-boot.dex", std::move(boot)}); + ro.log_sink = log_sink; + ro.file_system = &g_java_fs; + ro.properties.emplace_back("user.language", "en"); + ro.properties.emplace_back("user.region", "US"); + ro.properties.emplace_back("java.library.path", s.lib_dir); + ro.properties.emplace_back("os.arch", "aarch64"); + // AXRB's OpenXR runtime, offered by the runtime broker (ContentResolver). + if (!find_beside_exe(L"guest\\lib64\\libopenxr_runtime.so").empty()) { + ro.properties.emplace_back("axrb.openxr.runtime", "/apex/com.axrb.direct/lib64/libopenxr_runtime.so"); + } + ro.properties.emplace_back("os.version", "5.10.226-android12-9-axrb"); + ro.properties.emplace_back("http.agent", "Dalvik/2.1.0 (Linux; U; Android 16; sdk_gphone64_arm64 Build/BP22.250325.006)"); + ro.exit_hook = [](int status) { + LOGI("app", "System.exit(%d)", status); + log_flush(); + TerminateProcess(GetCurrentProcess(), static_cast(status & 0xff)); + }; + ro.fatal_hook = [](std::string_view description) { + LOGE("app", "the application crashed: %.*s", static_cast(description.size()), description.data()); + log_flush(); + TerminateProcess(GetCurrentProcess(), 10); + }; + ro.spawn_thread = spawn_java_thread; + jni::configure(ro); + + auto runtime = vm::Runtime::Create(std::move(ro)); + if (!runtime.ok()) fatal("managed runtime: %s", runtime.status().ToString().c_str()); + g_runtime = std::move(*runtime); + vm::Runtime* rt = g_runtime.get(); + start_dump_thread(); + vm::Thread* self = rt->main_thread(); + + arun::fw::HostConfig hc; + hc.headless = true; + hc.width_px = s.options.width; + hc.height_px = s.options.height; + hc.density_dpi = s.options.density_dpi; + hc.guest_pid = process().pid; + hc.apk = s.apk; + auto framework_res = arun::pkg::ZipReader::Open(s.boot_dir + L"\\arun-framework-res.apk"); + if (!framework_res.ok()) fatal("framework resources: %s", framework_res.status().ToString().c_str()); + hc.framework_res = std::make_shared(std::move(*framework_res)); + auto host = arun::fw::FrameworkHost::Create(std::move(hc)); + if (!host.ok()) fatal("framework host: %s", host.status().ToString().c_str()); + g_framework = std::move(*host); + g_framework->RegisterNatives(rt); + register_direct_natives(rt, s.apk, s.guest_apk); + rt->RegisterNative("Landroid/app/ActivityThread;.nativeWaitForStopRequest()V", ActivityThread_nativeWaitForStopRequest); + NdkConfig ndk; + ndk.apk = s.apk; + ndk.guest_apk = s.guest_apk; + ndk.width = s.options.width; + ndk.height = s.options.height; + ndk.density_dpi = s.options.density_dpi; + ndk_init(ndk); + audio_init(); + vulkan_init(); + g_framework->BindUiThread(); + jni::install(rt); + + vm::RootScope roots(self); + // The application's class loader: a PathClassLoader over the APK. + vm::RtClass* pcl = rt->class_linker().FindClass(self, "Ldalvik/system/PathClassLoader;", nullptr); + if (!pcl || !rt->class_linker().EnsureInitialized(self, pcl)) fail_with_exception(rt, self, "PathClassLoader"); + vm::JValue result{}; + vm::RtMethod* get_system = rt->FindMethod(rt->wk.class_loader, "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); + if (!get_system || !rt->Invoke(self, get_system, nullptr, {}, &result)) fail_with_exception(rt, self, "getSystemClassLoader"); + auto parent = roots.Add(result.l); + auto loader = roots.Add(rt->heap().AllocObject(self, pcl)); + auto apk_path = roots.Add(rt->NewStringUtf8(self, s.guest_apk)); + auto lib_path = roots.Add(rt->NewStringUtf8(self, s.lib_dir)); + vm::RtMethod* ctor = + rt->FindMethod(pcl, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V"); + vm::JValue ctor_args[3]; + ctor_args[0].l = apk_path.Get(); + ctor_args[1].l = lib_path.Get(); + ctor_args[2].l = parent.Get(); + if (!ctor || !rt->Invoke(self, ctor, loader.Get(), ctor_args, &result)) fail_with_exception(rt, self, "opening the APK's DEX files"); + g_framework->SetAppClassLoader(loader.Get()); + jni::set_app_loader(vm::natives::LoaderFromJava(rt, loader.Get())); + if (vm::RtMethod* set_ccl = rt->FindMethod(rt->wk.thread, "setContextClassLoader", "(Ljava/lang/ClassLoader;)V")) { + vm::JValue arg; + arg.l = loader.Get(); + if (!rt->Invoke(self, set_ccl, self->peer, std::span(&arg, 1), &result)) + fail_with_exception(rt, self, "setContextClassLoader"); + } + + const auto& m = s.manifest; + std::string label = m.package; + if (m.application_label.literal) label = *m.application_label.literal; + const int target_sdk = m.target_sdk ? m.target_sdk : m.min_sdk; + const std::vector args = {m.package, m.application_class, s.activity, + label, s.guest_apk, s.data_dir, + std::to_string(target_sdk), std::to_string(m.min_sdk)}; + auto array = roots.Add(rt->heap().AllocArray(self, rt->wk.string_array, static_cast(args.size()))); + for (size_t i = 0; i < args.size(); ++i) + static_cast(array.Get())->elements()[i] = rt->NewStringUtf8(self, args[i]); + vm::RtClass* at = rt->class_linker().FindClass(self, "Landroid/app/ActivityThread;", nullptr); + vm::RtMethod* main = at ? rt->FindMethod(at, "main", "([Ljava/lang/String;)V") : nullptr; + if (!main) fail_with_exception(rt, self, "ActivityThread.main"); + LOGI("app", "starting %s/%s", m.package.c_str(), s.activity.c_str()); + vm::JValue main_arg; + main_arg.l = array.Get(); + if (!rt->Invoke(self, main, nullptr, std::span(&main_arg, 1), &result)) rt->HandleUncaughtException(self); + LOGI("app", "main thread finished"); + (void)main_thread; + return 0; +} + +} // namespace + +bool session_stop_requested() { + HANDLE event = stop_event(); + return event && WaitForSingleObject(event, 0) == WAIT_OBJECT_0; +} + +void request_session_end(const char* why) { + static std::atomic requested{false}; + if (HANDLE event = stop_event()) SetEvent(event); + if (requested.exchange(true)) return; + LOGI("app", "ending the session (%s)", why); + // The activity finishes on its main thread and the process exits when + // ActivityThread.main returns; a game that hangs on the way is cut off. + std::thread([] { + Sleep(20000); + LOGW("app", "the application did not finish within 20 s; terminating"); + log_flush(); + TerminateProcess(GetCurrentProcess(), 0); + }).detach(); +} + +bool app_prepare(const AppOptions& options, std::string& guest_program, std::vector& env) { + Session& s = g_session; + s.options = options; + auto zip = arun::pkg::ZipReader::Open(options.apk); + if (!zip.ok()) { + LOGE("app", "cannot open %s: %s", narrow(options.apk).c_str(), zip.status().ToString().c_str()); + return false; + } + s.apk = std::make_shared(std::move(*zip)); + const auto* manifest_entry = s.apk->Find("AndroidManifest.xml"); + if (!manifest_entry) { + LOGE("app", "%s has no AndroidManifest.xml", narrow(options.apk).c_str()); + return false; + } + auto manifest_bytes = s.apk->Extract(*manifest_entry); + if (!manifest_bytes.ok()) { + LOGE("app", "manifest: %s", manifest_bytes.status().ToString().c_str()); + return false; + } + auto manifest = arun::manifest::Parse(*manifest_bytes); + if (!manifest.ok()) { + LOGE("app", "manifest: %s", manifest.status().ToString().c_str()); + return false; + } + s.manifest = std::move(*manifest); + s.package = s.manifest.package; + if (!options.activity.empty()) { + s.activity = arun::manifest::QualifyClassName(s.package, options.activity); + } else { + auto launcher = s.manifest.LauncherActivity(); + if (!launcher.ok()) { + LOGE("app", "no launcher activity: %s", launcher.status().ToString().c_str()); + return false; + } + s.activity = *launcher; + } + + s.boot_dir = find_beside_exe(L"vm\\boot"); + if (s.boot_dir.empty()) s.boot_dir = find_beside_exe(L"boot"); + std::wstring guest_bin = find_beside_exe(L"guest\\axrb_app"); + if (s.boot_dir.empty() || guest_bin.empty()) { + LOGE("app", "AXRB Direct is incomplete: boot class path or guest launcher missing next to the executable"); + return false; + } + guest_bin = guest_bin.substr(0, guest_bin.find_last_of(L'\\')); + + // Missing proxies otherwise fall back to AOSP libraries: Unity then waits + // forever for a native window from services that Direct mode does not run. + for (const wchar_t* name : {L"libandroid.so", L"libvulkan.so", L"libEGL.so", L"libGLESv1_CM.so", + L"libGLESv2.so", L"libGLESv3.so", L"libaaudio.so", L"libOpenSLES.so", + L"libopenxr_runtime.so"}) { + const auto file = guest_bin + L"\\lib64\\" + name; + std::error_code error; + if (!std::filesystem::is_regular_file(file, error) || std::filesystem::file_size(file, error) == 0 || error) { + LOGE("app", "Direct mode installation is incomplete: missing or empty %s. Reinstall the AXRB launcher.", + narrow(file).c_str()); + return false; + } + } + + // Install layout. + s.install_dir = "/data/app/~~axrb==/" + s.package + "-1"; + s.guest_apk = s.install_dir + "/base.apk"; + s.lib_dir = s.install_dir + "/lib/arm64"; + s.data_dir = "/data/data/" + s.package; + std::wstring app_root = options.data_root + L"\\apps\\" + widen(s.package); + std::wstring lib_host = app_root + L"\\lib\\arm64"; + if (!extract_libraries(*s.apk, lib_host)) return false; + vfs().add_mount(s.guest_apk, options.apk, true, true); + vfs().add_mount(s.lib_dir, lib_host, true); + vfs().add_mount("/apex/com.axrb.direct/bin", guest_bin, true); + // The NDK libraries AXRB provides in place of the system ones (libandroid, + // libvulkan, libEGL, audio): first on the library path. + vfs().add_mount("/apex/com.axrb.direct/lib64", guest_bin + L"\\lib64", true); + + std::wstring data_host = options.data_root + L"\\data\\data\\" + widen(s.package); + for (const wchar_t* sub : {L"files", L"cache", L"code_cache", L"shared_prefs", L"databases"}) ensure_dir(data_host + L"\\" + sub); + vfs().add_symlink("/data/user/0", "/data/data"); + std::wstring external = options.data_root + L"\\sdcard\\Android\\data\\" + widen(s.package); + ensure_dir(external + L"\\files"); + ensure_dir(external + L"\\cache"); + if (!options.obb_dir.empty()) { + vfs().add_mount("/storage/emulated/0/Android/obb/" + s.package, options.obb_dir, true); + LOGI("app", "obb: %s", narrow(options.obb_dir).c_str()); + } else { + ensure_dir(options.data_root + L"\\sdcard\\Android\\obb\\" + widen(s.package)); + } + // Content files used where the launcher keeps them: one "guesthost" + // line per file, guest relative to shared storage (Android/obb//...). + if (!options.asset_list.empty()) { + std::vector list = read_host_file(options.asset_list); + std::string text(list.begin(), list.end()); + const std::string own_obb = "Android/obb/" + s.package + "/", own_data = "Android/data/" + s.package + "/"; + size_t mounted = 0; + for (size_t at = 0; at < text.size();) { + size_t end = text.find('\n', at); + if (end == std::string::npos) end = text.size(); + std::string line = text.substr(at, end - at); + at = end + 1; + if (!line.empty() && line.back() == '\r') line.pop_back(); + size_t tab = line.find('\t'); + if (tab == std::string::npos) continue; + std::string guest = line.substr(0, tab); + if ((guest.rfind(own_obb, 0) != 0 && guest.rfind(own_data, 0) != 0) || guest.find("..") != std::string::npos) { + LOGW("app", "ignoring content file outside the game's storage: %s", guest.c_str()); + continue; + } + vfs().add_mount("/storage/emulated/0/" + guest, widen(line.substr(tab + 1)), true, true); + ++mounted; + } + LOGI("app", "%zu content files from %s", mounted, narrow(options.asset_list).c_str()); + } + + register_named_function("axrb.app_main", [](HostCall& c) { + GuestApi api{}; + guest_read(c.x(0), &api, sizeof api); + if (api.version != 1) fatal("guest launcher API version %u", api.version); + guest_api() = api; + c.ret(static_cast(run_app(c.thread()))); + }); + register_named_function("axrb.thread_main", [](HostCall& c) { + auto* start = reinterpret_cast(c.x(0)); + ThreadStart copy = *start; + delete start; + SetEvent(copy.started); + CloseHandle(copy.started); + copy.entry(copy.arg); + }); + + guest_program = "/apex/com.axrb.direct/bin/axrb_app"; + env.push_back("LD_LIBRARY_PATH=/apex/com.axrb.direct/lib64:" + s.lib_dir); + env.push_back("ANDROID_APP_PACKAGE=" + s.package); + process().package = s.package; + process().uid = process().gid = 10123; + LOGI("app", "%s (%s), activity %s", s.package.c_str(), narrow(options.apk).c_str(), s.activity.c_str()); + return true; +} + +} // namespace axrb diff --git a/direct/src/app.h b/direct/src/app.h new file mode 100644 index 0000000..8124f96 --- /dev/null +++ b/direct/src/app.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace axrb { + +// An Android application session: what main.cpp knows from the command line. +struct AppOptions { + std::wstring apk; // host path of the (patched) APK, used in place + std::wstring obb_dir; // host directory with the game's downloaded files (optional) + std::wstring asset_list; // file of "guesthost" content files to mount (optional) + std::string activity; // launcher activity override (optional) + std::wstring data_root; // persistent guest storage + int width = 1920, height = 1080, density_dpi = 480; +}; + +// Mounts the package into the guest file system, extracts its native +// libraries and registers the host side of the launcher. Returns false (after +// logging why) if the APK cannot be used. `guest_program` and `env` receive +// what to exec (the launcher) and extra environment variables. +bool app_prepare(const AppOptions& options, std::string& guest_program, std::vector& env); + +// Ends the session like leaving the app on a device: the activity is paused, +// stopped and destroyed on its main thread (so the game can save), then the +// process exits. Forced after a grace period if the app does not finish. +// Also triggered by the event named in AXRB_DIRECT_STOP_EVENT. +void request_session_end(const char* why); +bool session_stop_requested(); + +} // namespace axrb diff --git a/direct/src/audio.cpp b/direct/src/audio.cpp new file mode 100644 index 0000000..0f1b6ce --- /dev/null +++ b/direct/src/audio.cpp @@ -0,0 +1,270 @@ +// Host audio output for the guest's libaaudio.so (direct/guest/proxy/libaaudio.c): +// one WASAPI shared-mode stream on the default render device per AAudio +// stream. WASAPI converts format and rate (AUTOCONVERTPCM), so the guest's +// samples are copied as they are. + +#include "audio.h" + +#include "cpu.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace axrb { + +namespace { + +constexpr int32_t kErrorUnavailable = -889, kErrorInvalidFormat = -883, kErrorInvalidHandle = -892, + kErrorDisconnected = -899; + +struct Stream { + IAudioClient* client = nullptr; + IAudioRenderClient* render = nullptr; + IAudioClock* clock = nullptr; + HANDLE event = nullptr; + UINT32 capacity = 0; + int32_t frame_bytes = 0; + int32_t rate = 0; + int32_t burst = 0; + bool started = false; + std::mutex lock; +}; + +std::mutex g_streams_lock; +std::unordered_map g_streams; +int64_t g_next_handle = 1; + +void ensure_com() { + thread_local bool initialized = false; + if (!initialized) { + CoInitializeEx(nullptr, COINIT_MULTITHREADED); + initialized = true; + } +} + +Stream* stream_of(int64_t handle) { + std::lock_guard held(g_streams_lock); + auto it = g_streams.find(handle); + return it == g_streams.end() ? nullptr : it->second; +} + +void destroy(Stream* s) { + if (s->client && s->started) s->client->Stop(); + if (s->clock) s->clock->Release(); + if (s->render) s->render->Release(); + if (s->client) s->client->Release(); + if (s->event) CloseHandle(s->event); + delete s; +} + +int64_t open_stream(int32_t rate, int32_t channels, int32_t format, int32_t* out_rate, int32_t* out_burst, + int32_t* out_capacity) { + ensure_com(); + IMMDeviceEnumerator* enumerator = nullptr; + if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + reinterpret_cast(&enumerator)))) + return kErrorUnavailable; + IMMDevice* device = nullptr; + HRESULT hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); + enumerator->Release(); + if (FAILED(hr)) { + LOGW("audio", "no default output device (0x%08lx)", hr); + return kErrorUnavailable; + } + auto* s = new Stream; + hr = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(&s->client)); + device->Release(); + if (FAILED(hr)) { + destroy(s); + return kErrorUnavailable; + } + WAVEFORMATEX* mix = nullptr; + s->client->GetMixFormat(&mix); + if (rate <= 0) rate = mix ? static_cast(mix->nSamplesPerSec) : 48000; + if (mix) CoTaskMemFree(mix); + if (channels <= 0 || channels > 8) channels = 2; + + WAVEFORMATEXTENSIBLE fmt{}; + fmt.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + fmt.Format.nChannels = static_cast(channels); + fmt.Format.nSamplesPerSec = static_cast(rate); + fmt.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); + switch (format) { + case 1: fmt.Format.wBitsPerSample = 16; fmt.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; break; + case 2: fmt.Format.wBitsPerSample = 32; fmt.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; break; + case 3: fmt.Format.wBitsPerSample = 24; fmt.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; break; + case 4: fmt.Format.wBitsPerSample = 32; fmt.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; break; + default: destroy(s); return kErrorInvalidFormat; + } + fmt.Samples.wValidBitsPerSample = fmt.Format.wBitsPerSample; + fmt.Format.nBlockAlign = static_cast(channels * fmt.Format.wBitsPerSample / 8); + fmt.Format.nAvgBytesPerSec = fmt.Format.nSamplesPerSec * fmt.Format.nBlockAlign; + static const DWORD masks[] = {0, SPEAKER_FRONT_CENTER, KSAUDIO_SPEAKER_STEREO, KSAUDIO_SPEAKER_STEREO | SPEAKER_FRONT_CENTER, + KSAUDIO_SPEAKER_QUAD, KSAUDIO_SPEAKER_QUAD | SPEAKER_FRONT_CENTER, KSAUDIO_SPEAKER_5POINT1, + KSAUDIO_SPEAKER_5POINT1 | SPEAKER_BACK_CENTER, KSAUDIO_SPEAKER_7POINT1_SURROUND}; + fmt.dwChannelMask = masks[channels]; + + const REFERENCE_TIME duration = 400000; // 40 ms + hr = s->client->Initialize(AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | + AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, + duration, 0, &fmt.Format, nullptr); + if (FAILED(hr)) { + LOGW("audio", "IAudioClient::Initialize(%d Hz, %d ch, format %d) failed: 0x%08lx", rate, channels, format, hr); + destroy(s); + return kErrorInvalidFormat; + } + s->event = CreateEventW(nullptr, FALSE, FALSE, nullptr); + s->client->SetEventHandle(s->event); + s->client->GetBufferSize(&s->capacity); + s->client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast(&s->render)); + s->client->GetService(__uuidof(IAudioClock), reinterpret_cast(&s->clock)); + REFERENCE_TIME period = 0; + s->client->GetDevicePeriod(&period, nullptr); + s->rate = rate; + s->frame_bytes = fmt.Format.nBlockAlign; + s->burst = std::max(64, static_cast(period * rate / 10000000)); + if (!s->render) { + destroy(s); + return kErrorUnavailable; + } + *out_rate = rate; + *out_burst = s->burst; + *out_capacity = static_cast(s->capacity); + std::lock_guard held(g_streams_lock); + int64_t handle = g_next_handle++; + g_streams[handle] = s; + LOGI("audio", "output stream %lld: %d Hz, %d channels, format %d, burst %d, capacity %u", (long long)handle, rate, + channels, format, s->burst, s->capacity); + return handle; +} + +int32_t room(Stream* s) { + UINT32 padding = 0; + if (FAILED(s->client->GetCurrentPadding(&padding))) return kErrorDisconnected; + return static_cast(s->capacity - padding); +} + +} // namespace + +bool audio_active() { + std::lock_guard guard(g_streams_lock); + for (const auto& [id, stream] : g_streams) { + std::lock_guard streamGuard(stream->lock); + if (stream->started) return true; + } + return false; +} + +void audio_init() { + register_named_function("audio.open", [](HostCall& c) { + int32_t rate = 0, burst = 0, capacity = 0; + int64_t h = open_stream(static_cast(c.x(0)), static_cast(c.x(1)), + static_cast(c.x(2)), &rate, &burst, &capacity); + if (h > 0) { + guest_write(c.x(3), &rate, 4); + guest_write(c.x(4), &burst, 4); + guest_write(c.x(5), &capacity, 4); + } + c.ret(static_cast(h)); + }); + register_named_function("audio.start", [](HostCall& c) { + Stream* s = stream_of(static_cast(c.x(0))); + if (!s) return c.ret(static_cast(static_cast(kErrorInvalidHandle))); + ensure_com(); + std::lock_guard held(s->lock); + if (!s->started) { + // Start with a primed buffer of silence so the first callbacks are not an underrun. + s->started = SUCCEEDED(s->client->Start()); + } + c.ret(s->started ? 0 : static_cast(static_cast(kErrorDisconnected))); + }); + register_named_function("audio.stop", [](HostCall& c) { + Stream* s = stream_of(static_cast(c.x(0))); + if (!s) return c.ret(static_cast(static_cast(kErrorInvalidHandle))); + ensure_com(); + std::lock_guard held(s->lock); + if (s->started) s->client->Stop(); + s->started = false; + SetEvent(s->event); + c.ret(0); + }); + register_named_function("audio.flush", [](HostCall& c) { + Stream* s = stream_of(static_cast(c.x(0))); + if (!s) return c.ret(static_cast(static_cast(kErrorInvalidHandle))); + ensure_com(); + std::lock_guard held(s->lock); + if (!s->started) s->client->Reset(); + c.ret(0); + }); + register_named_function("audio.wait", [](HostCall& c) { + Stream* s = stream_of(static_cast(c.x(0))); + if (!s) return c.ret(static_cast(static_cast(kErrorInvalidHandle))); + ensure_com(); + const int64_t timeout_ns = static_cast(c.x(1)); + int32_t r = room(s); + if (r < 0 || r >= s->burst || !s->started || timeout_ns <= 0) return c.ret(static_cast(static_cast(r))); + DWORD ms = static_cast(std::min((timeout_ns + 999999) / 1000000, 1000)); + WaitForSingleObject(s->event, ms); + c.ret(static_cast(static_cast(room(s)))); + }); + register_named_function("audio.write", [](HostCall& c) { + Stream* s = stream_of(static_cast(c.x(0))); + if (!s) return c.ret(static_cast(static_cast(kErrorInvalidHandle))); + ensure_com(); + int32_t frames = std::min(static_cast(c.x(2)), room(s)); + if (frames <= 0) return c.ret(0); + BYTE* buffer = nullptr; + if (FAILED(s->render->GetBuffer(static_cast(frames), &buffer))) + return c.ret(static_cast(static_cast(kErrorDisconnected))); + guest_read(c.x(1), buffer, static_cast(frames) * s->frame_bytes); + s->render->ReleaseBuffer(static_cast(frames), 0); + c.ret(static_cast(frames)); + }); + register_named_function("audio.position", [](HostCall& c) { + Stream* s = stream_of(static_cast(c.x(0))); + if (!s || !s->clock) return c.ret(static_cast(static_cast(kErrorInvalidHandle))); + ensure_com(); + UINT64 freq = 0, pos = 0, qpc = 0; + s->clock->GetFrequency(&freq); + s->clock->GetPosition(&pos, &qpc); + int64_t frames = freq ? static_cast(pos * static_cast(s->rate) / freq) : 0; + int64_t now = monotonic_ns(); + LARGE_INTEGER counter, frequency; + QueryPerformanceCounter(&counter); + QueryPerformanceFrequency(&frequency); + const int64_t qpc_now_100ns = counter.QuadPart * 10000000 / frequency.QuadPart; + int64_t time_ns = now - (qpc_now_100ns - static_cast(qpc)) * 100; + guest_write(c.x(1), &frames, 8); + guest_write(c.x(2), &time_ns, 8); + c.ret(0); + }); + register_named_function("audio.close", [](HostCall& c) { + Stream* s = nullptr; + { + std::lock_guard held(g_streams_lock); + auto it = g_streams.find(static_cast(c.x(0))); + if (it != g_streams.end()) { + s = it->second; + g_streams.erase(it); + } + } + if (s) { + ensure_com(); + destroy(s); + } + }); +} + +} // namespace axrb diff --git a/direct/src/audio.h b/direct/src/audio.h new file mode 100644 index 0000000..ea88299 --- /dev/null +++ b/direct/src/audio.h @@ -0,0 +1,9 @@ +#pragma once + +namespace axrb { + +// Registers the host functions behind the guest's libaaudio.so (WASAPI output). +void audio_init(); +bool audio_active(); + +} // namespace axrb diff --git a/direct/src/binder.cpp b/direct/src/binder.cpp new file mode 100644 index 0000000..b7cf2d6 --- /dev/null +++ b/direct/src/binder.cpp @@ -0,0 +1,398 @@ +#include "binder.h" +#include "files.h" +#include "memory.h" +#include "log.h" + +#include +#include +#include + +namespace axrb { +namespace { +using namespace linux_abi; + +// Linux arm64 Binder UAPI. Keep these layouts independent of Windows ABI. +constexpr uint32_t ioc(uint32_t dir, char type, uint32_t nr, uint32_t size = 0) { + return (dir << 30) | (size << 16) | (uint32_t(type) << 8) | nr; +} +constexpr uint32_t br(uint32_t nr, uint32_t size = 0) { return ioc(size ? 2 : 0, 'r', nr, size); } +struct WriteRead { uint64_t write_size, write_consumed, write_buffer, read_size, read_consumed, read_buffer; }; +struct Transaction { + uint64_t target, cookie; + uint32_t code, flags; + int32_t pid; + uint32_t uid; + uint64_t data_size, offsets_size, data, offsets; +}; +struct Object { uint32_t type, flags; uint64_t value, cookie; }; +struct PtrCookie { uint64_t ptr, cookie; }; +static_assert(sizeof(Transaction) == 64 && sizeof(WriteRead) == 48 && sizeof(Object) == 24); +constexpr uint32_t kBinder = 0x73622a85, kHandle = 0x73682a85, kFd = 0x66642a85; + +class BinderFile; +struct Node { + std::weak_ptr owner; + uint64_t ptr = 0, cookie = 0; +}; +struct Message { + uint32_t command = 0; + std::vector payload; + Transaction transaction{}; + std::vector data; + std::vector offsets; + std::weak_ptr sender; + int sender_tid = 0; +}; +struct Domain { + std::mutex lock; + std::weak_ptr manager; + std::shared_ptr manager_node; +}; +struct ThreadState { + std::deque inbox; + std::vector, int>> callers; + bool looper = false; +}; + +class BinderFile final : public OpenFile { +public: + explicit BinderFile(std::shared_ptr domain) : domain_(std::move(domain)) {} + ~BinderFile() override { + // Buffers belong to this open description, not a particular fd. + if (base_) address_space().munmap(base_, size_); + } + bool has_device_mapping() const override { return true; } + void on_close() override { + std::lock_guard held(domain_->lock); + ++flush_epoch_; + waiters_.wake_all(); + } + int64_t map_device(uint64_t length, uint32_t prot, uint32_t flags, uint64_t offset) override { + std::lock_guard held(domain_->lock); + if (base_) return -EBUSY_; + if (offset || !length || length > (4 << 20) || (prot & PROT_WRITE_) || (flags & MAP_FIXED_)) return -EINVAL_; + int64_t result = address_space().mmap(0, length, PROT_READ_, MAP_PRIVATE_ | MAP_ANONYMOUS_, nullptr, 0, "/dev/binder"); + if (result < 0) return result; + base_ = result; + size_ = page_up(length); + pages_.resize(size_ / kPageSize); + return result; + } + int64_t stat(kstat& st) override { + st = {}; + st.st_mode = S_IFCHR_ | 0666; + st.st_nlink = 1; + st.st_blksize = 4096; + return 0; + } + WaitQueue* wait_queue() override { return &waiters_; } + uint32_t poll_mask() override { + std::lock_guard held(domain_->lock); + auto* t = current_thread(); + return (!pending_.empty() || (t && !threads_[t->tid].inbox.empty())) ? POLLIN_ : 0; + } + int64_t ioctl(GuestThread& t, uint64_t request, uint64_t arg) override; + +private: + std::shared_ptr self() { return std::static_pointer_cast(shared_from_this()); } + void send(int tid, Message message) { + if (tid) threads_[tid].inbox.push_back(std::move(message)); + else pending_.push_back(std::move(message)); + waiters_.wake_all(); + } + void code(int tid, uint32_t command) { Message m; m.command = command; send(tid, std::move(m)); } + void reference(uint32_t command, const Node& node) { + Message m; + m.command = command; + PtrCookie pc{node.ptr, node.cookie}; + m.payload.resize(sizeof pc); + std::memcpy(m.payload.data(), &pc, sizeof pc); + send(0, std::move(m)); + } + std::shared_ptr node(uint32_t handle) { + if (!handle) return domain_->manager_node; + auto found = handles_.find(handle); + return found == handles_.end() ? nullptr : found->second; + } + bool translate(Message& message, BinderFile& target); + int64_t transact(int tid, const Transaction& transaction, bool reply); + int64_t write_commands(int tid, WriteRead& wr); + int64_t read_commands(int tid, WriteRead& wr); + bool materialize(Message& message); + void free_buffer(uint64_t address); + + std::shared_ptr domain_; + std::map threads_; + std::deque pending_; + std::map> nodes_; + std::map> handles_; + uint32_t next_handle_ = 1, max_threads_ = 15; + uint64_t flush_epoch_ = 0; + uint64_t base_ = 0, size_ = 0; + std::vector pages_; + std::map buffers_; + WaitQueue waiters_; +}; + +bool BinderFile::translate(Message& message, BinderFile& target) { + uint64_t previous_end = 0; + for (uint64_t offset : message.offsets) { + if (offset < previous_end || offset % 4 || offset > message.data.size() || message.data.size() - offset < sizeof(Object)) return false; + previous_end = offset + sizeof(Object); + Object object; + std::memcpy(&object, message.data.data() + offset, sizeof object); + std::shared_ptr exported; + if (object.type == kBinder) { + if (!object.value) continue; // null Binder + auto& n = nodes_[object.value]; + if (!n) { + n = std::make_shared(Node{self(), object.value, object.cookie}); + reference(br(7, 16), *n); + reference(br(8, 16), *n); + } + exported = n; + } else if (object.type == kHandle) { + exported = node(static_cast(object.value)); + if (!exported || exported->owner.expired()) return false; + } else if (object.type == kFd) { + auto file = fds().get(static_cast(object.value)); + if (!file) return false; + // Endpoints currently share the Direct process's descriptor table. + object.value = fds().install(file, true); + } else { + LOGW("binder", "unsupported object type %08x", object.type); + return false; + } + if (exported) { + if (exported->owner.lock().get() == &target) { + object.type = kBinder; + object.value = exported->ptr; + object.cookie = exported->cookie; + } else { + uint32_t handle = 0; + for (const auto& [id, n] : target.handles_) if (n == exported) { handle = id; break; } + if (!handle) { handle = target.next_handle_++; target.handles_[handle] = exported; } + object.type = kHandle; + object.value = handle; + object.cookie = 0; + } + } + std::memcpy(message.data.data() + offset, &object, sizeof object); + } + return true; +} + +int64_t BinderFile::transact(int tid, const Transaction& tr, bool reply) { + if (tr.data_size > (1 << 20) || tr.offsets_size > (1 << 20) || tr.offsets_size % 8) return -EINVAL_; + Message m; + m.transaction = tr; + m.transaction.pid = process().pid; + m.transaction.uid = process().uid; + m.data.resize(tr.data_size); + m.offsets.resize(tr.offsets_size / 8); + if (!guest_read(tr.data, m.data.data(), m.data.size()) || + !guest_read(tr.offsets, m.offsets.data(), tr.offsets_size)) return -EFAULT_; + std::shared_ptr target; + int target_tid = 0; + if (reply) { + auto& stack = threads_[tid].callers; + if (stack.empty()) { code(tid, br(17)); return 0; } + target = stack.back().first.lock(); + target_tid = stack.back().second; + stack.pop_back(); + m.command = br(3, sizeof(Transaction)); + m.transaction.target = m.transaction.cookie = 0; + } else { + auto n = node(static_cast(tr.target)); + if (n) { + target = n->owner.lock(); + m.transaction.target = n->ptr; + m.transaction.cookie = n->cookie; + } + m.command = br(2, sizeof(Transaction)); + } + if (!target) { code(tid, br(5)); return 0; } + if (!translate(m, *target)) { code(tid, br(17)); return 0; } + m.sender = self(); + m.sender_tid = tid; + target->send(target_tid, std::move(m)); + code(tid, br(6)); + return 0; +} + +bool BinderFile::materialize(Message& m) { + uint64_t aligned_data = (m.data.size() + 7) & ~7ull; + size_t count = static_cast(page_up(std::max(1, aligned_data + m.offsets.size() * 8)) / kPageSize); + size_t start = 0, run = 0; + for (size_t i = 0; i < pages_.size(); ++i) { + if (pages_[i]) run = 0; + else if (++run == count) { start = i + 1 - count; break; } + } + if (run != count) return false; + uint64_t address = base_ + start * kPageSize; + uint64_t length = count * kPageSize; + if (address_space().mprotect(address, length, PROT_READ_ | PROT_WRITE_) < 0) return false; + bool ok = guest_write(address, m.data.data(), m.data.size()) && + guest_write(address + aligned_data, m.offsets.data(), m.offsets.size() * 8); + address_space().mprotect(address, length, PROT_READ_); + if (!ok) return false; + for (size_t i = start; i < start + count; ++i) pages_[i] = true; + buffers_[address] = count; + m.transaction.data = address; + m.transaction.offsets = address + aligned_data; + return true; +} + +void BinderFile::free_buffer(uint64_t address) { + auto found = buffers_.find(address); + if (found == buffers_.end()) return; + size_t start = static_cast((address - base_) / kPageSize); + for (size_t i = start; i < start + found->second; ++i) pages_[i] = false; + address_space().madvise(address, found->second * kPageSize, MADV_DONTNEED_); + buffers_.erase(found); +} + +int64_t BinderFile::write_commands(int tid, WriteRead& wr) { + while (wr.write_consumed < wr.write_size) { + uint32_t command; + uint64_t at = wr.write_buffer + wr.write_consumed; + if (wr.write_size - wr.write_consumed < 4 || !guest_read(at, &command, 4)) return -EFAULT_; + uint32_t length = (command >> 16) & 0x3fff; + if (((command >> 8) & 0xff) != 'c' || length > 72 || wr.write_size - wr.write_consumed < 4 + length) return -EINVAL_; + uint8_t payload[72]{}; + if (length && !guest_read(at + 4, payload, length)) return -EFAULT_; + uint64_t value = 0; + std::memcpy(&value, payload, std::min(length, sizeof value)); + switch (command & 0xff) { + case 0: case 1: case 17: case 18: { + bool sg = (command & 0xff) >= 17; + if (length != (sg ? 72 : 64)) return -EINVAL_; + uint64_t extra = 0; + if (sg) std::memcpy(&extra, payload + 64, 8); + if (extra) { code(tid, br(17)); break; } + Transaction tr; + std::memcpy(&tr, payload, sizeof tr); + int64_t error = transact(tid, tr, (command & 0xff) == 1 || (command & 0xff) == 18); + if (error) return error; + break; + } + case 3: + if (length != 8) return -EINVAL_; + free_buffer(value); + break; + case 4: case 5: case 6: case 7: + if (length != 4) return -EINVAL_; + // Handle ownership is retained for this open description. + break; + case 8: case 9: + if (length != 16) return -EINVAL_; + break; + case 11: case 12: threads_[tid].looper = true; break; + case 13: threads_[tid].looper = false; break; + default: + LOGW("binder", "unsupported command %08x", command); + return -EINVAL_; + } + wr.write_consumed += 4 + length; + } + return 0; +} + +int64_t BinderFile::read_commands(int tid, WriteRead& wr) { + auto& state = threads_[tid]; + while (wr.read_size - wr.read_consumed >= 4) { + std::deque* queue = !state.inbox.empty() ? &state.inbox : state.looper && !pending_.empty() ? &pending_ : nullptr; + if (!queue) break; + Message& m = queue->front(); + bool transaction = m.command == br(2, 64) || m.command == br(3, 64); + uint64_t size = 4 + (transaction ? sizeof(Transaction) : m.payload.size()); + if (size > wr.read_size - wr.read_consumed) break; + if (transaction && !materialize(m)) { + if (auto sender = m.sender.lock()) sender->code(m.sender_tid, br(17)); + queue->pop_front(); + continue; + } + uint64_t at = wr.read_buffer + wr.read_consumed; + if (!guest_write(at, &m.command, 4) || + !guest_write(at + 4, transaction ? static_cast(&m.transaction) : m.payload.data(), size - 4)) { + if (transaction) free_buffer(m.transaction.data); + return -EFAULT_; + } + if (m.command == br(2, 64) && !(m.transaction.flags & 1)) state.callers.emplace_back(m.sender, m.sender_tid); + wr.read_consumed += size; + queue->pop_front(); + } + return 0; +} + +int64_t BinderFile::ioctl(GuestThread& t, uint64_t request, uint64_t arg) { + if (request == ioc(3, 'b', 9, 4)) { + int32_t version = 8; + return guest_write(arg, &version, sizeof version) ? 0 : -EFAULT_; + } + if (request == ioc(1, 'b', 5, 4)) { + uint32_t value; + if (!guest_read(arg, &value, 4)) return -EFAULT_; + std::lock_guard held(domain_->lock); + max_threads_ = value; + return 0; + } + if (request == ioc(1, 'b', 16, 4)) return 0; // optional oneway diagnostics + if (request == ioc(1, 'b', 7, 4) || request == ioc(1, 'b', 13, 24)) { + Object object{}; + if (request == ioc(1, 'b', 13, 24) && !guest_read(arg, &object, sizeof object)) return -EFAULT_; + std::lock_guard held(domain_->lock); + if (!domain_->manager.expired()) return -EBUSY_; + domain_->manager = self(); + domain_->manager_node = std::make_shared(Node{self(), object.value, object.cookie}); + return 0; + } + if (request == ioc(1, 'b', 8, 4)) { + std::lock_guard held(domain_->lock); + for (auto& caller : threads_[t.tid].callers) if (auto file = caller.first.lock()) file->code(caller.second, br(5)); + threads_.erase(t.tid); + return 0; + } + if (request != ioc(3, 'b', 1, sizeof(WriteRead))) { + LOGW("binder", "unsupported ioctl %llx", static_cast(request)); + return -ENOTTY_; + } + WriteRead wr; + if (!guest_read(arg, &wr, sizeof wr)) return -EFAULT_; + if (wr.write_consumed > wr.write_size || wr.read_consumed > wr.read_size || wr.write_size > (4 << 20) || wr.read_size > (4 << 20)) return -EINVAL_; + uint64_t epoch; + { + std::lock_guard held(domain_->lock); + epoch = flush_epoch_; + int64_t error = write_commands(t.tid, wr); + if (error) { guest_write(arg, &wr, sizeof wr); return error; } + } + int64_t result = 0; + if (wr.read_size) { + auto ready = [&] { + std::lock_guard held(domain_->lock); + auto& state = threads_[t.tid]; + return epoch != flush_epoch_ || !state.inbox.empty() || (state.looper && !pending_.empty()); + }; + waiters_.add(&t); + if (nonblocking() && !ready()) result = -EAGAIN_; + else result = block_until(t, ready, 0, "binder"); + waiters_.remove(&t); + if (!result) { + std::lock_guard held(domain_->lock); + result = read_commands(t.tid, wr); + } + } + if (!guest_write(arg, &wr, sizeof wr)) return -EFAULT_; + return result; +} +} // namespace + +void init_binder() { + for (const char* name : {"binder", "hwbinder", "vndbinder"}) { + auto domain = std::make_shared(); + register_device(name, [domain] { return std::make_shared(domain); }); + vfs().add_device(std::string("/dev/") + name, name); + } +} +} // namespace axrb diff --git a/direct/src/binder.h b/direct/src/binder.h new file mode 100644 index 0000000..b63eac8 --- /dev/null +++ b/direct/src/binder.h @@ -0,0 +1,6 @@ +#pragma once + +namespace axrb { +// Register userspace Binder devices with the virtual filesystem. +void init_binder(); +} diff --git a/direct/src/compat/android/log.h b/direct/src/compat/android/log.h new file mode 100644 index 0000000..260474e --- /dev/null +++ b/direct/src/compat/android/log.h @@ -0,0 +1,6 @@ +#pragma once +// Host build of shared Android-side code: android/log.h onto the runner's log. + +enum { ANDROID_LOG_VERBOSE = 2, ANDROID_LOG_DEBUG, ANDROID_LOG_INFO, ANDROID_LOG_WARN, ANDROID_LOG_ERROR, ANDROID_LOG_FATAL }; + +int __android_log_print(int priority, const char* tag, const char* format, ...); diff --git a/direct/src/compat/sys/system_properties.h b/direct/src/compat/sys/system_properties.h new file mode 100644 index 0000000..f850418 --- /dev/null +++ b/direct/src/compat/sys/system_properties.h @@ -0,0 +1,6 @@ +#pragma once +// Host build of shared Android-side code: the runner's system properties. + +#define PROP_VALUE_MAX 92 + +int __system_property_get(const char* name, char* value); diff --git a/direct/src/cpu.cpp b/direct/src/cpu.cpp new file mode 100644 index 0000000..553d1be --- /dev/null +++ b/direct/src/cpu.cpp @@ -0,0 +1,729 @@ +#include "cpu.h" +#include "cpu_engine.h" +#include "kernel.h" + +#include "log.h" +#include "memory.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { +using namespace linux_abi; +using Dynarmic::HaltReason; + +namespace { + +constexpr HaltReason kHaltSvc = HaltReason::UserDefined1; +constexpr HaltReason kHaltKick = HaltReason::UserDefined2; +constexpr HaltReason kHaltException = HaltReason::UserDefined3; +constexpr HaltReason kHaltInvalidate = HaltReason::UserDefined4; + +constexpr uint64_t kCntFrq = 19200000; // Qualcomm-class counter frequency + +thread_local GuestThread* t_current = nullptr; + +// Host functions and their trampolines. +std::shared_mutex g_functions_lock; +std::vector g_function_names; +std::vector g_functions; +uint64_t g_trampolines = 0; +constexpr uint32_t kMaxHostFunctions = 1 << 16; +uint64_t g_return_trap = 0; + +std::unordered_map g_named; // name -> function id +std::unordered_map g_named_by_address; // stub string address -> id + +std::mutex g_cpus_lock; +std::set g_cpus; + +std::function g_describer; + +uint64_t qpc_frequency() { + static uint64_t frequency = [] { + LARGE_INTEGER f; + QueryPerformanceFrequency(&f); + return static_cast(f.QuadPart); + }(); + return frequency; +} + +std::mutex g_processor_lock; +std::vector g_processor_used; +constexpr size_t kProcessors = 512; + +int allocate_processor() { + std::lock_guard held(g_processor_lock); + if (g_processor_used.empty()) g_processor_used.resize(kProcessors); + for (size_t i = 0; i < kProcessors; ++i) + if (!g_processor_used[i]) { + g_processor_used[i] = true; + return static_cast(i); + } + fatal("more than %zu guest threads", kProcessors); +} + +void release_processor(int id) { + std::lock_guard held(g_processor_lock); + if (id >= 0 && static_cast(id) < g_processor_used.size()) g_processor_used[id] = false; +} + +Dynarmic::ExclusiveMonitor& monitor() { + static Dynarmic::ExclusiveMonitor instance(kProcessors); + return instance; +} + +} // namespace + +uint64_t cntfrq() { return kCntFrq; } +uint64_t cntpct() { + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + uint64_t f = qpc_frequency(); + uint64_t ticks = static_cast(now.QuadPart); + return (ticks / f) * kCntFrq + (ticks % f) * kCntFrq / f; +} + +GuestThread* current_thread() { return t_current; } +void set_current_thread(GuestThread* thread) { t_current = thread; } + +// --------------------------------------------------------------------------- +// One thread's processor. + +class GuestCpu final : public Dynarmic::A64::UserCallbacks { +public: + explicit GuestCpu(GuestThread& thread) : thread_(thread) { + Dynarmic::A64::UserConfig config; + config.callbacks = this; + config.processor_id = static_cast(thread.processor_id); + config.global_monitor = &monitor(); + config.fastmem_pointer = 0; // identity mapping: host address == guest address + config.fastmem_address_space_bits = 48; + config.silently_mirror_fastmem = false; + config.recompile_on_fastmem_failure = true; + config.fastmem_exclusive_access = true; + config.recompile_on_exclusive_fastmem_failure = true; + config.tpidr_el0 = &thread.tpidr; + config.tpidrro_el0 = &tpidrro_; + config.cntfrq_el0 = static_cast(kCntFrq); + config.ctr_el0 = 0x8444c004; // 64-byte lines, IDC/DIC clear + config.dczid_el0 = 0x14; // DZP: DC ZVA prohibited, so memset avoids it + config.hook_data_cache_operations = true; + // YIELD is a no-op, as on the processor itself. Engines put it in the + // spin loops of their worker threads; as a callback that gave up the + // time slice, every iteration was a Windows system call and a spin + // meant for microseconds kept a core busy for far longer. + // AXRB_DIRECT_HOOK_HINTS=1 restores the callbacks. + static const bool hook_hints = [] { + const char* v = std::getenv("AXRB_DIRECT_HOOK_HINTS"); + return v && v[0] == '1'; + }(); + config.hook_hint_instructions = hook_hints; + config.enable_cycle_counting = false; + config.wall_clock_cntpct = true; + config.define_unpredictable_behaviour = true; + config.code_cache_size = 256ull << 20; + // Exclusive stores are a host compare-and-swap against the value the + // paired exclusive load saw (as QEMU's user mode does): atomic, without + // the global monitor's process-wide lock and its check of every one of + // kProcessors slots on each store. Only an ABA change between the two + // goes unnoticed. AXRB_DIRECT_STRICT_EXCLUSIVE=1 restores the monitor. + static const bool strict_exclusive = [] { + const char* v = std::getenv("AXRB_DIRECT_STRICT_EXCLUSIVE"); + return v && v[0] == '1'; + }(); + if (!strict_exclusive) { + config.unsafe_optimizations = true; + config.optimizations |= Dynarmic::OptimizationFlag::Unsafe_IgnoreGlobalMonitor; + } + // AXRB_DIRECT_FAST_FP=1: host floating-point semantics for NaN payloads, + // estimate instructions and standard-FPCR vector code. + static const bool fast_fp = [] { + const char* v = std::getenv("AXRB_DIRECT_FAST_FP"); + return v && v[0] == '1'; + }(); + if (fast_fp) { + config.unsafe_optimizations = true; + config.optimizations |= Dynarmic::OptimizationFlag::Unsafe_ReducedErrorFP | + Dynarmic::OptimizationFlag::Unsafe_InaccurateNaN | + Dynarmic::OptimizationFlag::Unsafe_IgnoreStandardFPCRValue; + } + jit_ = std::make_unique(config, thread); + std::lock_guard held(g_cpus_lock); + g_cpus.insert(this); + } + ~GuestCpu() override { + std::lock_guard held(g_cpus_lock); + g_cpus.erase(this); + } + + CpuEngine& jit() { return *jit_; } + + // Why the last Run stopped, beyond the halt reason. + uint32_t svc_imm = 0; + struct PendingException { + int signal = 0; + int code = 0; + uint64_t address = 0; + uint64_t pc = 0; + } exception; + + void queue_invalidation(uint64_t addr, uint64_t len) { + std::lock_guard held(invalidate_lock_); + invalidations_.push_back({addr, len}); + jit_->HaltExecution(kHaltInvalidate); + } + void apply_invalidations() { + std::vector> ranges; + { + std::lock_guard held(invalidate_lock_); + ranges.swap(invalidations_); + } + for (auto& [addr, len] : ranges) jit_->InvalidateCacheRange(addr, len); + } + + // --- Dynarmic callbacks --------------------------------------------------- + std::optional MemoryReadCode(Dynarmic::A64::VAddr vaddr) override { + uint32_t word = 0; + // File-backed pages become committed before their contents have been + // copied. Synchronize with the loader instead of decoding transient zeros. + if (!address_space().populate(vaddr, 4) || !guest_read(vaddr, &word, 4)) return std::nullopt; + return word; + } + uint8_t MemoryRead8(Dynarmic::A64::VAddr vaddr) override { return read(vaddr); } + uint16_t MemoryRead16(Dynarmic::A64::VAddr vaddr) override { return read(vaddr); } + uint32_t MemoryRead32(Dynarmic::A64::VAddr vaddr) override { return read(vaddr); } + uint64_t MemoryRead64(Dynarmic::A64::VAddr vaddr) override { return read(vaddr); } + Dynarmic::A64::Vector MemoryRead128(Dynarmic::A64::VAddr vaddr) override { + Dynarmic::A64::Vector v{}; + if (!guest_read(vaddr, v.data(), 16)) fault(vaddr, false); + return v; + } + void MemoryWrite8(Dynarmic::A64::VAddr vaddr, uint8_t value) override { write(vaddr, value); } + void MemoryWrite16(Dynarmic::A64::VAddr vaddr, uint16_t value) override { write(vaddr, value); } + void MemoryWrite32(Dynarmic::A64::VAddr vaddr, uint32_t value) override { write(vaddr, value); } + void MemoryWrite64(Dynarmic::A64::VAddr vaddr, uint64_t value) override { write(vaddr, value); } + void MemoryWrite128(Dynarmic::A64::VAddr vaddr, Dynarmic::A64::Vector value) override { + if (!guest_write(vaddr, value.data(), 16)) fault(vaddr, true); + } + bool MemoryWriteExclusive8(Dynarmic::A64::VAddr vaddr, uint8_t value, uint8_t expected) override { + return cas(reinterpret_cast(vaddr), static_cast(value), static_cast(expected)); + } + bool MemoryWriteExclusive16(Dynarmic::A64::VAddr vaddr, uint16_t value, uint16_t expected) override { + return cas(reinterpret_cast(vaddr), static_cast(value), static_cast(expected)); + } + bool MemoryWriteExclusive32(Dynarmic::A64::VAddr vaddr, uint32_t value, uint32_t expected) override { + return cas(reinterpret_cast(vaddr), static_cast(value), static_cast(expected)); + } + bool MemoryWriteExclusive64(Dynarmic::A64::VAddr vaddr, uint64_t value, uint64_t expected) override { + return cas(reinterpret_cast(vaddr), static_cast(value), + static_cast(expected)); + } + bool MemoryWriteExclusive128(Dynarmic::A64::VAddr vaddr, Dynarmic::A64::Vector value, + Dynarmic::A64::Vector expected) override { + __try { + long long compare[2] = {static_cast(expected[0]), static_cast(expected[1])}; + return _InterlockedCompareExchange128(reinterpret_cast(vaddr), + static_cast(value[1]), static_cast(value[0]), + compare) != 0; + } __except (EXCEPTION_EXECUTE_HANDLER) { + fault(vaddr, true); + return false; + } + } + + void InterpreterFallback(Dynarmic::A64::VAddr pc, size_t num_instructions) override { + for (size_t i = 0; i < num_instructions; ++i) { + uint32_t instruction = 0; + if (!guest_read(pc, &instruction, 4)) { + raise(SIGSEGV_, SEGV_MAPERR_, pc, pc); + return; + } + if (!interpret_instruction(thread_, pc, instruction)) { + raise(SIGILL_, ILL_ILLOPC_, pc, pc); + return; + } + if (exception.signal) return; // the interpreter faulted + pc = jit_->GetPC(); + } + } + + void CallSVC(uint32_t swi) override { + svc_imm = swi; + jit_->HaltExecution(kHaltSvc); + } + + void ExceptionRaised(Dynarmic::A64::VAddr pc, Dynarmic::A64::Exception kind) override { + using E = Dynarmic::A64::Exception; + switch (kind) { + case E::Yield: + case E::WaitForEvent: + SwitchToThread(); + return; + case E::WaitForInterrupt: + Sleep(0); + return; + case E::SendEvent: + case E::SendEventLocal: + return; + case E::Breakpoint: + raise(SIGTRAP_, TRAP_BRKPT_, pc, pc); + return; + case E::NoExecuteFault: + raise(SIGSEGV_, SEGV_ACCERR_, pc, pc); + return; + default: { + // Dynarmic knows the encoding but not the semantics: the + // interpreter may still (system register reads, for example). + uint32_t instruction = 0; + if (guest_read(pc, &instruction, 4) && interpret_instruction(thread_, pc, instruction)) return; + raise(SIGILL_, ILL_ILLOPC_, pc, pc); + return; + } + } + } + + void DataCacheOperationRaised(Dynarmic::A64::DataCacheOperation op, Dynarmic::A64::VAddr value) override { + if (op == Dynarmic::A64::DataCacheOperation::ZeroByVA) { + static const uint8_t zeros[64] = {}; + if (!guest_write(untag(value) & ~63ull, zeros, 64)) fault(value, true); + } + } + void InstructionCacheOperationRaised(Dynarmic::A64::InstructionCacheOperation op, + Dynarmic::A64::VAddr value) override { + if (op == Dynarmic::A64::InstructionCacheOperation::InvalidateByVAToPoU) + invalidate_code(value & ~63ull, 64); + else { + invalidate_code(address_space().arena_base(), address_space().arena_end() - address_space().arena_base()); + if (address_space().overlaps_guest(0, 0x100000000ull)) invalidate_code(0, 0x100000000ull); + } + } + + void AddTicks(uint64_t) override {} + uint64_t GetTicksRemaining() override { return ~0ull >> 2; } + uint64_t GetCNTPCT() override { return cntpct(); } + + void raise(int signal, int code, uint64_t address, uint64_t pc) { + exception.signal = signal; + exception.code = code; + exception.address = address; + exception.pc = pc; + jit_->HaltExecution(kHaltException); + } + +private: + template + T read(uint64_t vaddr) { + T value{}; + if (!guest_read(vaddr, &value, sizeof(T))) fault(vaddr, false); + return value; + } + template + void write(uint64_t vaddr, T value) { + if (!guest_write(vaddr, &value, sizeof(T))) fault(vaddr, true); + } + template + bool cas(volatile T* p, T value, T expected) { + __try { + if constexpr (sizeof(T) == 1) return _InterlockedCompareExchange8(p, value, expected) == expected; + else if constexpr (sizeof(T) == 2) return _InterlockedCompareExchange16(p, value, expected) == expected; + else if constexpr (sizeof(T) == 4) return _InterlockedCompareExchange(p, value, expected) == expected; + else return _InterlockedCompareExchange64(p, value, expected) == expected; + } __except (EXCEPTION_EXECUTE_HANDLER) { + fault(reinterpret_cast(p), true); + return false; + } + } + void fault(uint64_t address, bool write) { + (void)write; + if (exception.signal) return; + // The precise instruction is not known here (precise aborts would cost + // GetSetElimination); the block's pc is close enough for a report. + raise(SIGSEGV_, address_space().check(address, 1, 0) ? SEGV_ACCERR_ : SEGV_MAPERR_, address, jit_->GetPC()); + } + + GuestThread& thread_; + std::unique_ptr jit_; + uint64_t tpidrro_ = 0; + std::mutex invalidate_lock_; + std::vector> invalidations_; +}; + +// --------------------------------------------------------------------------- + +void GuestCpuDeleter::operator()(GuestCpu* cpu) const { delete cpu; } + +void attach_cpu(GuestThread& thread) { + thread.processor_id = allocate_processor(); + thread.cpu.reset(new GuestCpu(thread)); + t_current = &thread; +} + +void detach_cpu(GuestThread& thread) { + monitor().ClearProcessor(static_cast(thread.processor_id)); + thread.cpu.reset(); + release_processor(thread.processor_id); + if (t_current == &thread) t_current = nullptr; +} + +void load_context(GuestThread& thread, const CpuContext& c) { + auto& jit = thread.cpu->jit(); + std::array regs; + std::copy(std::begin(c.x), std::end(c.x), regs.begin()); + jit.SetRegisters(regs); + jit.SetSP(c.sp); + jit.SetPC(c.pc); + jit.SetPstate(c.pstate); + jit.SetFpcr(c.fpcr); + jit.SetFpsr(c.fpsr); + std::array vectors; + for (int i = 0; i < 32; ++i) vectors[i] = c.v[i]; + jit.SetVectors(vectors); +} + +void save_context(GuestThread& thread, CpuContext& c) { + auto& jit = thread.cpu->jit(); + auto regs = jit.GetRegisters(); + std::copy(regs.begin(), regs.end(), std::begin(c.x)); + c.sp = jit.GetSP(); + c.pc = jit.GetPC(); + c.pstate = jit.GetPstate(); + c.fpcr = jit.GetFpcr(); + c.fpsr = jit.GetFpsr(); + auto vectors = jit.GetVectors(); + for (int i = 0; i < 32; ++i) c.v[i] = vectors[i]; +} + +uint64_t get_reg(GuestThread& t, int i) { return t.cpu->jit().GetRegister(static_cast(i)); } +void set_reg(GuestThread& t, int i, uint64_t v) { t.cpu->jit().SetRegister(static_cast(i), v); } +uint64_t get_pc(GuestThread& t) { return t.cpu->jit().GetPC(); } +uint64_t guest_pc_for_host_code(GuestThread& t, uint64_t host_ip) { + return t.cpu ? t.cpu->jit().GuestPCForHostCode(reinterpret_cast(host_ip)) : ~uint64_t{0}; +} +uint64_t guest_pc_for_host_code_resumed(GuestThread& t, uint64_t host_ip, bool* optimized, uint64_t* region_start, + uint32_t* region_bytes) { + return t.cpu ? t.cpu->jit().GuestPCForHostCodeResumed(reinterpret_cast(host_ip), optimized, region_start, + region_bytes) : ~uint64_t{0}; +} +void set_pc(GuestThread& t, uint64_t pc) { t.cpu->jit().SetPC(pc); } +uint64_t get_sp(GuestThread& t) { return t.cpu->jit().GetSP(); } +void set_sp(GuestThread& t, uint64_t sp) { t.cpu->jit().SetSP(sp); } +Vec128 get_vec(GuestThread& t, int i) { return t.cpu->jit().GetVector(static_cast(i)); } +void set_vec(GuestThread& t, int i, Vec128 v) { t.cpu->jit().SetVector(static_cast(i), v); } + +uint64_t HostCall::x(int i) const { return cpu_.jit().GetRegister(static_cast(i)); } +float HostCall::s(int i) const { + auto v = cpu_.jit().GetVector(static_cast(i)); + float f; + uint32_t bits = static_cast(v[0]); + std::memcpy(&f, &bits, 4); + return f; +} +double HostCall::d(int i) const { + auto v = cpu_.jit().GetVector(static_cast(i)); + double f; + std::memcpy(&f, &v[0], 8); + return f; +} +Vec128 HostCall::v(int i) const { return cpu_.jit().GetVector(static_cast(i)); } +uint64_t HostCall::sp() const { return cpu_.jit().GetSP(); } +uint64_t HostCall::lr() const { return cpu_.jit().GetRegister(30); } +uint64_t HostCall::stack(int slot) const { + uint64_t value = 0; + guest_read(sp() + 8ull * static_cast(slot), &value, 8); + return value; +} +void HostCall::ret(uint64_t value) { cpu_.jit().SetRegister(0, value); } +void HostCall::ret_float(float value) { + uint32_t bits; + std::memcpy(&bits, &value, 4); + cpu_.jit().SetVector(0, {bits, 0}); +} +void HostCall::ret_double(double value) { + uint64_t bits; + std::memcpy(&bits, &value, 8); + cpu_.jit().SetVector(0, {bits, 0}); +} +void HostCall::set_x(int i, uint64_t value) { cpu_.jit().SetRegister(static_cast(i), value); } +void HostCall::set_v(int i, Vec128 value) { cpu_.jit().SetVector(static_cast(i), value); } + +// --------------------------------------------------------------------------- +// Trampolines: 16 bytes each in one read-only, executable guest page run. +// movz x16, #lo ; movk x16, #hi, lsl #16 ; svc #1 ; ret + +static void ensure_trampolines() { + if (g_trampolines) return; + uint64_t bytes = 16ull * kMaxHostFunctions + kPageSize; + int64_t at = address_space().mmap(0, bytes, PROT_READ_ | PROT_WRITE_, MAP_PRIVATE_ | MAP_ANONYMOUS_, nullptr, 0, + "[axrb-host-calls]"); + if (at < 0) fatal("cannot map host trampolines"); + address_space().populate(static_cast(at), bytes); + uint32_t* words = reinterpret_cast(at); + for (uint32_t id = 0; id < kMaxHostFunctions; ++id) { + words[id * 4 + 0] = 0xd2800010u | ((id & 0xffff) << 5); // movz x16, #lo + words[id * 4 + 1] = 0xf2a00010u | (((id >> 16) & 0xffff) << 5); // movk x16, #hi, lsl #16 + words[id * 4 + 2] = 0xd4000001u | (kSvcHostCall << 5); // svc #1 + words[id * 4 + 3] = 0xd65f03c0u; // ret + } + uint64_t trap = static_cast(at) + 16ull * kMaxHostFunctions; + reinterpret_cast(trap)[0] = 0xd4000001u | (kSvcReturnTrap << 5); // svc #2 + reinterpret_cast(trap)[1] = 0xd4200000u; // brk #0 (never reached) + address_space().mprotect(static_cast(at), bytes, PROT_READ_ | PROT_EXEC_); + g_return_trap = trap; + g_trampolines = static_cast(at); +} + +uint32_t register_host_function(const std::string& name, HostFunction fn) { + std::unique_lock held(g_functions_lock); + ensure_trampolines(); + if (g_function_names.size() >= kMaxHostFunctions) fatal("host function table full"); + g_function_names.push_back(name); + g_functions.push_back(std::move(fn)); + return static_cast(g_functions.size() - 1); +} + +void register_named_function(const std::string& name, HostFunction fn) { + std::unique_lock held(g_functions_lock); + auto it = g_named.find(name); + if (it != g_named.end()) { + g_functions[it->second] = std::move(fn); + return; + } + ensure_trampolines(); + g_function_names.push_back(name); + g_functions.push_back(std::move(fn)); + g_named[name] = static_cast(g_functions.size() - 1); +} + +uint64_t host_trampoline(uint32_t id) { return g_trampolines + 16ull * id; } +uint64_t host_trampoline(const std::string& name, HostFunction fn) { + return host_trampoline(register_host_function(name, std::move(fn))); +} +const std::string& host_function_name(uint32_t id) { + std::shared_lock held(g_functions_lock); + static const std::string unknown = "?"; + return id < g_function_names.size() ? g_function_names[id] : unknown; +} +uint64_t return_trap_address() { + std::unique_lock held(g_functions_lock); + ensure_trampolines(); + return g_return_trap; +} + +void set_address_describer(std::function describer) { g_describer = std::move(describer); } + +std::string describe_address(uint64_t address) { + if (g_describer) return g_describer(address); + std::string result; + char text[64]; + std::snprintf(text, sizeof text, "%llx", (unsigned long long)address); + result = text; + std::string name; + uint64_t base = 0; + address_space().for_each([&](const Vma& vma) { + if (address >= vma.start && address < vma.end) name = vma.name; + }); + if (!name.empty()) { + address_space().for_each([&](const Vma& vma) { + if (vma.name == name && (!base || vma.start < base)) base = vma.start; + }); + size_t slash = name.rfind('/'); + std::snprintf(text, sizeof text, "+%llx", (unsigned long long)(address - base)); + result = name.substr(slash == std::string::npos ? 0 : slash + 1) + text; + } + return result; +} + +void dump_thread_state(GuestThread& thread, const char* why) { + CpuContext c; + save_context(thread, c); + LOGE("cpu", "thread %d (%s): %s", thread.tid, thread.name.c_str(), why); + LOGE("cpu", " pc %s lr %s sp %llx", describe_address(c.pc).c_str(), describe_address(c.x[30]).c_str(), + (unsigned long long)c.sp); + for (int r = 0; r < 31; r += 4) { + char line[256]; + int n = std::snprintf(line, sizeof line, " x%-2d", r); + for (int k = r; k < r + 4 && k < 31; ++k) n += std::snprintf(line + n, sizeof line - n, " %016llx", (unsigned long long)c.x[k]); + LOGE("cpu", "%s", line); + } + // Frame records: [fp] = caller fp, [fp+8] = return address. + uint64_t fp = c.x[29]; + for (int depth = 0; depth < 32 && fp; ++depth) { + uint64_t record[2]; + if (!guest_read(fp, record, 16) || (fp & 7)) break; + if (!record[1]) break; + LOGE("cpu", " #%02d %s", depth, describe_address(record[1]).c_str()); + if (record[0] <= fp) break; + fp = record[0]; + } +} + +void cpu_raise(GuestThread& thread, int signal, int code, uint64_t address, uint64_t pc) { + thread.cpu->raise(signal, code, address, pc); +} + +void kick_thread(GuestThread& thread) { + if (thread.cpu) thread.cpu->jit().HaltExecution(kHaltKick); + if (thread.event) SetEvent(thread.event); +} + +void invalidate_code(uint64_t addr, uint64_t len) { + g_syscall_counts[503].fetch_add(1, std::memory_order_relaxed); + g_syscall_counts[504].fetch_add((len + 4095) >> 12, std::memory_order_relaxed); + std::lock_guard held(g_cpus_lock); + for (GuestCpu* cpu : g_cpus) cpu->queue_invalidation(addr, len); +} + +// --------------------------------------------------------------------------- +// The run loop. + +static uint32_t named_function_id(GuestThread& thread) { + uint64_t address = untag(get_reg(thread, 17)); + { + std::shared_lock held(g_functions_lock); + auto it = g_named_by_address.find(address); + if (it != g_named_by_address.end()) return it->second; + } + std::string name; + if (!guest_string(address, name, 256)) return ~0u; + std::unique_lock held(g_functions_lock); + auto it = g_named.find(name); + if (it == g_named.end()) { + LOGE("cpu", "guest called unknown host function '%s' from %s", name.c_str(), + describe_address(get_reg(thread, 30)).c_str()); + return ~0u; + } + g_named_by_address[address] = it->second; + return it->second; +} + +static void handle_host_call(GuestThread& thread, uint32_t id) { + HostFunction* fn = nullptr; + { + std::shared_lock held(g_functions_lock); + if (id < g_functions.size()) fn = &g_functions[id]; + } + if (!fn) { + LOGE("cpu", "call to unregistered host function %u from %s", id, describe_address(get_reg(thread, 30)).c_str()); + set_reg(thread, 0, 0); + return; + } + HostCall call(thread, *thread.cpu, id); + (*fn)(call); +} + +#ifdef AXRB_ARM_SERVICE_EMBEDDED +void arm_service_trap(GuestThread&,uint32_t,uint64_t); +bool arm_service_named(GuestThread& thread,const std::string& name) { + uint32_t id; + {std::shared_lock held(g_functions_lock);auto it=g_named.find(name);if(it==g_named.end())return false;id=it->second;} + handle_host_call(thread,id);return true; +} +#endif +void run_guest(GuestThread& thread, bool nested) { + GuestCpu& cpu = *thread.cpu; + auto& jit = cpu.jit(); + for (;;) { + if (thread.pending.load(std::memory_order_relaxed)) deliver_signals(thread); + HaltReason reason = jit.Run(); + if (Dynarmic::Has(reason, kHaltInvalidate)) cpu.apply_invalidations(); + if (Dynarmic::Has(reason, kHaltException)) { + auto ex = cpu.exception; + cpu.exception = {}; + jit.SetPC(ex.pc); + raise_fault(thread, ex.signal, ex.code, ex.address); + continue; + } + if (Dynarmic::Has(reason, kHaltSvc)) { + uint32_t imm = cpu.svc_imm; +#ifdef AXRB_ARM_SERVICE_EMBEDDED + arm_service_trap(thread,imm,jit.GetPC()-4); +#endif + if (imm == kSvcSyscall) { + uint64_t nr = jit.GetRegister(8); + if (nr == 113 || nr == 169 || nr == 98) { + // The calls engines make thousands of times a second + // (clocks, futex wakes) skip the general dispatch. + uint64_t x[9]; + for (size_t i = 0; i < 6; ++i) x[i] = jit.GetRegister(i); + x[8] = nr; + if (fast_syscall(x)) { + jit.SetRegister(0, x[0]); + continue; + } + } + int64_t result = do_syscall(thread, nr, jit.GetRegister(0), jit.GetRegister(1), jit.GetRegister(2), + jit.GetRegister(3), jit.GetRegister(4), jit.GetRegister(5)); + // rt_sigreturn replaced the whole state; everything else returns in x0. + if (nr != 139) jit.SetRegister(0, static_cast(result)); + } else if (imm == kSvcHostCall) { + handle_host_call(thread, static_cast(jit.GetRegister(16))); + } else if (imm == kSvcNamedCall) { + uint32_t id = named_function_id(thread); + if (id == ~0u) jit.SetRegister(0, 0); + else handle_host_call(thread, id); + } else if (imm == kSvcReturnTrap) { + if (nested) return; + LOGE("cpu", "thread %d returned to the host-call trap with no call in progress", thread.tid); + throw ThreadExit{0}; + } else { + LOGW("cpu", "svc #%u at %s", imm, describe_address(jit.GetPC() - 4).c_str()); + jit.SetRegister(0, static_cast(-ENOSYS_)); + } + } + } +} + +uint64_t call_guest_args(GuestThread& thread, uint64_t function, const GuestArgs& args, Vec128* fp_result) { + if (!thread.cpu) fatal("call_guest on a thread without a cpu"); + CpuContext saved; + save_context(thread, saved); + auto& jit = thread.cpu->jit(); + for (size_t i = 0; i < args.ints.size() && i < 8; ++i) jit.SetRegister(i, args.ints[i]); + for (size_t i = 0; i < args.fps.size() && i < 8; ++i) jit.SetVector(i, args.fps[i]); + // Below the caller's frame, with room for stack arguments, 16-aligned. + uint64_t sp = (saved.sp - 512 - 8 * args.stack.size()) & ~15ull; + if (!args.stack.empty()) guest_write(sp, args.stack.data(), args.stack.size() * 8); + jit.SetSP(sp); + jit.SetRegister(30, return_trap_address()); + jit.SetPC(function); + ++thread.call_depth; + struct Restore { + GuestThread& thread; + CpuContext& saved; + bool active = true; + ~Restore() { + --thread.call_depth; + if (active && thread.cpu) load_context(thread, saved); + } + } restore{thread, saved}; + run_guest(thread, true); + uint64_t result = jit.GetRegister(0); + if (fp_result) *fp_result = jit.GetVector(0); + return result; +} + +uint64_t call_guest(GuestThread& thread, uint64_t function, std::initializer_list args) { + GuestArgs a; + a.ints.assign(args.begin(), args.end()); + if (a.ints.size() > 8) { + a.stack.assign(a.ints.begin() + 8, a.ints.end()); + a.ints.resize(8); + } + return call_guest_args(thread, function, a, nullptr); +} + +} // namespace axrb diff --git a/direct/src/cpu.h b/direct/src/cpu.h new file mode 100644 index 0000000..70366d5 --- /dev/null +++ b/direct/src/cpu.h @@ -0,0 +1,110 @@ +#pragma once + +#include "kernel.h" + +#include +#include +#include +#include +#include +#include + +namespace axrb { + +// Supervisor-call immediates used by guest code. 0 is the Linux system call. +constexpr uint32_t kSvcSyscall = 0; +constexpr uint32_t kSvcHostCall = 1; // x16 = host function id +constexpr uint32_t kSvcReturnTrap = 2; // end of a host -> guest call +constexpr uint32_t kSvcNamedCall = 3; // x17 = NUL-terminated host function name + +// Arguments of a host function called from guest code, in AAPCS64 terms. +class HostCall { +public: + HostCall(GuestThread& thread, GuestCpu& cpu, uint32_t id) : thread_(thread), cpu_(cpu), id_(id) {} + GuestThread& thread() { return thread_; } + uint32_t id() const { return id_; } + uint64_t x(int i) const; // general-purpose argument registers x0-x7 (and beyond: any register) + float s(int i) const; // float arguments in v0-v7 + double d(int i) const; + Vec128 v(int i) const; + uint64_t sp() const; + uint64_t lr() const; + uint64_t stack(int slot) const; // the slot-th 8-byte stack argument + void ret(uint64_t value); + void ret_float(float value); + void ret_double(double value); + void set_x(int i, uint64_t value); + void set_v(int i, Vec128 value); + +private: + GuestThread& thread_; + GuestCpu& cpu_; + uint32_t id_; +}; + +using HostFunction = std::function; + +// Host functions reachable from guest code through a 16-byte trampoline. +uint32_t register_host_function(const std::string& name, HostFunction fn); +uint64_t host_trampoline(uint32_t id); +uint64_t host_trampoline(const std::string& name, HostFunction fn); +const std::string& host_function_name(uint32_t id); +// Host functions guest libraries reach by name (svc #3 stubs, see +// direct/guest/axrb_guest.h). Registering a name again replaces it. +void register_named_function(const std::string& name, HostFunction fn); +uint64_t return_trap_address(); + +// Creates the Jit for a thread (on the host thread that will run it). +void attach_cpu(GuestThread& thread); +void detach_cpu(GuestThread& thread); + +// Register access for the current state of a thread's cpu. +void load_context(GuestThread& thread, const CpuContext& context); +void save_context(GuestThread& thread, CpuContext& context); +uint64_t get_reg(GuestThread& thread, int index); +void set_reg(GuestThread& thread, int index, uint64_t value); +uint64_t get_pc(GuestThread& thread); +// Guest block address of translated host code (~0 if none); thread must be stopped. +uint64_t guest_pc_for_host_code(GuestThread& thread, uint64_t host_ip); +// The same for a translator whose lookup needs no stopped thread but may allocate +// (so the thread must be running again); ~0 otherwise. +uint64_t guest_pc_for_host_code_resumed(GuestThread& thread, uint64_t host_ip, bool* optimized = nullptr, + uint64_t* region_start = nullptr, uint32_t* region_bytes = nullptr); +void set_pc(GuestThread& thread, uint64_t pc); +uint64_t get_sp(GuestThread& thread); +void set_sp(GuestThread& thread, uint64_t sp); +Vec128 get_vec(GuestThread& thread, int index); +void set_vec(GuestThread& thread, int index, Vec128 value); + +// Runs the thread's guest code from its current state until it exits (throws +// ThreadExit) or, for a nested call, until the return trap is reached. +void run_guest(GuestThread& thread, bool nested); + +// Calls a guest function on the current thread, preserving its state. +struct GuestArgs { + std::vector ints; // x0.. (beyond 8 they spill to `stack` — the caller lays it out) + std::vector fps; // v0..v7: a float in the low 32 bits, a double in the low 64 + std::vector stack; // stack argument words, lowest address first +}; +uint64_t call_guest(GuestThread& thread, uint64_t function, std::initializer_list args); +uint64_t call_guest_args(GuestThread& thread, uint64_t function, const GuestArgs& args, Vec128* fp_result = nullptr); + +// Interrupts the thread's guest execution so pending work (signals) is seen. +void kick_thread(GuestThread& thread); +// Discards translations of [addr, addr+len) in every thread. +void invalidate_code(uint64_t addr, uint64_t len); + +// The fallback interpreter for instructions Dynarmic does not implement (LSE +// atomics, RCPC loads, ID register reads). Returns false for an undefined one. +bool interpret_instruction(GuestThread& thread, uint64_t pc, uint32_t instruction); +// Stops the thread's current Run with a synchronous signal at `pc`. +void cpu_raise(GuestThread& thread, int signal, int code, uint64_t address, uint64_t pc); + +std::string describe_address(uint64_t address); +void set_address_describer(std::function describer); +void dump_thread_state(GuestThread& thread, const char* why); + +uint64_t cntfrq(); +uint64_t cntpct(); + +} // namespace axrb diff --git a/direct/src/cpu_engine.cpp b/direct/src/cpu_engine.cpp new file mode 100644 index 0000000..abdc7bf --- /dev/null +++ b/direct/src/cpu_engine.cpp @@ -0,0 +1,104 @@ +#include "cpu_engine.h" +#include "cpu.h" +#include "memory.h" +#include "log.h" +#include "kernel.h" +#include +#include +#include +#include +#include + +namespace axrb { +CpuEngine::CpuEngine(const Dynarmic::A64::UserConfig& config, GuestThread& thread) + : thread_(thread), callbacks_(config.callbacks) { + const char* translator = std::getenv("AXRB_DIRECT_TRANSLATOR"); + if (!translator || !*translator || !std::strcmp(translator, "dynarmic")) { + dynarmic_ = std::make_unique(config); + } else if (!std::strcmp(translator, "digitalis")) { + digitalis::Callbacks callbacks; + callbacks.read = [](void*, uint64_t address, void* out, size_t size) { return guest_read(address, out, size); }; + callbacks.write = [](void*, uint64_t address, const void* data, size_t size) { return guest_write(address, data, size); }; + callbacks.fault = [](void*, uint64_t address, bool write) { return address_space().handle_fault(address, write, false); }; + callbacks.invalidate = [](void*, uint64_t address, uint64_t size) { invalidate_code(address, size); }; + callbacks.syscall = [](void*, uint64_t* x) { return fast_syscall(x); }; + // Wait for lazy file-backed pages to finish populating before decoding. + // A concurrent commit exposes zeroed memory until its content is copied. + callbacks.fetch = [](void*, uint64_t address, void* out, size_t size) { + g_syscall_counts[505].fetch_add(1, std::memory_order_relaxed); + return address_space().read_complete(address, out, size); + }; + digitalis_ = digitalis::create(callbacks); + } else { + throw std::runtime_error(std::string("Unknown Direct translator: ") + translator); + } + static std::once_flag logged; + std::call_once(logged, [&] { LOGI("cpu", "ARM64 translator: %s", digitalis_ ? "Digitalis (optimizing JIT + lite JIT + interpreter)" : "Dynarmic"); }); +} +CpuEngine::~CpuEngine() { digitalis::destroy(digitalis_); } +Dynarmic::HaltReason CpuEngine::Run() { + if (dynarmic_) return dynarmic_->Run(); + auto pending = halt_.exchange(0, std::memory_order_acq_rel); + if (pending) { + digitalis::clear_interrupt(digitalis_); + return static_cast(pending); + } + registers_.tls = thread_.tpidr; + const auto result = digitalis::run(digitalis_, ®isters_, 4096); + thread_.tpidr = registers_.tls; + using E = Dynarmic::A64::Exception; + switch (result.reason) { + case digitalis::Stop::Svc: callbacks_->CallSVC(result.immediate); break; + case digitalis::Stop::Undefined: callbacks_->ExceptionRaised(registers_.pc, E::UnallocatedEncoding); break; + case digitalis::Stop::Breakpoint: callbacks_->ExceptionRaised(registers_.pc, E::Breakpoint); break; + case digitalis::Stop::MemoryFault: + cpu_raise(thread_, linux_abi::SIGSEGV_, address_space().check(result.address, 1, 0) ? + linux_abi::SEGV_ACCERR_ : linux_abi::SEGV_MAPERR_, result.address, registers_.pc); + break; + case digitalis::Stop::Yield: break; + } + const auto reason = halt_.exchange(0, std::memory_order_acq_rel); + // Every pending halt is returned now; a halt requested after the exchange + // is still in halt_ and returns at the top of the next Run. + digitalis::clear_interrupt(digitalis_); + return static_cast(reason); +} +void CpuEngine::HaltExecution(Dynarmic::HaltReason reason) { + if (dynarmic_) dynarmic_->HaltExecution(reason); + else { halt_.fetch_or(static_cast(reason), std::memory_order_release); digitalis::interrupt(digitalis_); } +} +void CpuEngine::InvalidateCacheRange(uint64_t address, size_t length) { + if (dynarmic_) dynarmic_->InvalidateCacheRange(address, length); + else digitalis::invalidate_cache(digitalis_, address, length); +} +uint64_t CpuEngine::GuestPCForHostCode(const void* code) const { + return dynarmic_ ? dynarmic_->GuestPCForHostCode(code) : ~uint64_t{0}; +} +uint64_t CpuEngine::GuestPCForHostCodeResumed(const void* code, bool* optimized, uint64_t* start, uint32_t* bytes) const { + return digitalis_ ? digitalis::guest_pc_for_host_code(digitalis_, code, optimized, start, bytes) : ~uint64_t{0}; +} +std::array CpuEngine::GetRegisters() const { + if (dynarmic_) return dynarmic_->GetRegisters(); + std::array result; std::copy_n(registers_.x, 31, result.begin()); return result; +} +void CpuEngine::SetRegisters(const std::array& values) { + if (dynarmic_) dynarmic_->SetRegisters(values); else std::copy(values.begin(), values.end(), registers_.x); +} +Vec128 CpuEngine::GetVector(size_t i) const { + return dynarmic_ ? dynarmic_->GetVector(i) : Vec128{registers_.v[i][0], registers_.v[i][1]}; +} +void CpuEngine::SetVector(size_t i, Vec128 value) { + if (dynarmic_) dynarmic_->SetVector(i, value); + else { registers_.v[i][0] = value[0]; registers_.v[i][1] = value[1]; } +} +std::array CpuEngine::GetVectors() const { + if (dynarmic_) return dynarmic_->GetVectors(); + std::array result; + for (size_t i = 0; i < 32; ++i) result[i] = GetVector(i); + return result; +} +void CpuEngine::SetVectors(const std::array& values) { + if (dynarmic_) dynarmic_->SetVectors(values); + else for (size_t i = 0; i < 32; ++i) SetVector(i, values[i]); +} +} diff --git a/direct/src/cpu_engine.h b/direct/src/cpu_engine.h new file mode 100644 index 0000000..77b9825 --- /dev/null +++ b/direct/src/cpu_engine.h @@ -0,0 +1,44 @@ +#pragma once +#include "kernel.h" +#include "../digitalis/digitalis.h" +#include +#include + +namespace axrb { +// Common execution interface; Android services and host bridges do not depend +// on the selected instruction translator. +class CpuEngine { +public: + CpuEngine(const Dynarmic::A64::UserConfig& config, GuestThread& thread); + ~CpuEngine(); + Dynarmic::HaltReason Run(); + void HaltExecution(Dynarmic::HaltReason reason); + void InvalidateCacheRange(uint64_t address, size_t length); + uint64_t GuestPCForHostCode(const void* code) const; + uint64_t GuestPCForHostCodeResumed(const void* code, bool* optimized, uint64_t* start = nullptr, uint32_t* bytes = nullptr) const; + uint64_t GetRegister(size_t i) const { return dynarmic_ ? dynarmic_->GetRegister(i) : registers_.x[i]; } + void SetRegister(size_t i, uint64_t v) { if (dynarmic_) dynarmic_->SetRegister(i, v); else registers_.x[i] = v; } + std::array GetRegisters() const; + void SetRegisters(const std::array& values); + Vec128 GetVector(size_t i) const; + void SetVector(size_t i, Vec128 value); + std::array GetVectors() const; + void SetVectors(const std::array& values); +#define AXRB_CPU_FIELD(Name, Field, Type) \ + Type Get##Name() const { return dynarmic_ ? dynarmic_->Get##Name() : registers_.Field; } \ + void Set##Name(Type value) { if (dynarmic_) dynarmic_->Set##Name(value); else registers_.Field = value; } + AXRB_CPU_FIELD(PC, pc, uint64_t) + AXRB_CPU_FIELD(SP, sp, uint64_t) + AXRB_CPU_FIELD(Pstate, pstate, uint32_t) + AXRB_CPU_FIELD(Fpcr, fpcr, uint32_t) + AXRB_CPU_FIELD(Fpsr, fpsr, uint32_t) +#undef AXRB_CPU_FIELD +private: + GuestThread& thread_; + Dynarmic::A64::UserCallbacks* callbacks_; + std::unique_ptr dynarmic_; + digitalis::Engine* digitalis_{}; + digitalis::Registers registers_{}; + std::atomic halt_{}; +}; +} diff --git a/direct/src/exec.cpp b/direct/src/exec.cpp new file mode 100644 index 0000000..8e29c59 --- /dev/null +++ b/direct/src/exec.cpp @@ -0,0 +1,242 @@ +#include "exec.h" + +#include "cpu.h" +#include "files.h" +#include "log.h" +#include "memory.h" + +#include +#include + +#include + +namespace axrb { +using namespace linux_abi; + +namespace { + +#pragma pack(push, 1) +struct Elf64Header { + uint8_t ident[16]; + uint16_t type, machine; + uint32_t version; + uint64_t entry, phoff, shoff; + uint32_t flags; + uint16_t ehsize, phentsize, phnum, shentsize, shnum, shstrndx; +}; +struct Elf64Phdr { + uint32_t type, flags; + uint64_t offset, vaddr, paddr, filesz, memsz, align; +}; +#pragma pack(pop) +static_assert(sizeof(Elf64Header) == 64 && sizeof(Elf64Phdr) == 56); + +constexpr uint32_t PT_LOAD = 1, PT_INTERP = 3, PT_PHDR = 6; +constexpr uint16_t ET_EXEC = 2, ET_DYN = 3, EM_AARCH64 = 183; +constexpr uint64_t kStackSize = 8ull << 20; +// Where Linux puts a PIE on a 39-bit arm64 address space (2/3 of TASK_SIZE). +constexpr uint64_t kPieBase = 0x5555554000ull; + +struct LoadedElf { + uint64_t bias = 0; + uint64_t entry = 0; + uint64_t phdr = 0; + uint16_t phnum = 0; + uint64_t end = 0; // highest mapped address + std::string interpreter; + std::string path; +}; + +uint32_t segment_prot(uint32_t flags) { + uint32_t prot = 0; + if (flags & 4) prot |= PROT_READ_; + if (flags & 2) prot |= PROT_WRITE_; + if (flags & 1) prot |= PROT_EXEC_; + return prot; +} + +int64_t load_elf(GuestThread& t, const std::string& path, uint64_t hint, LoadedElf& out) { + std::shared_ptr file; + if (int64_t e = open_path(t, path, O_RDONLY_ | O_CLOEXEC_, 0, file)) return e; + if (!file->mappable()) return -EACCES_; + Elf64Header eh{}; + if (file->pread_host(&eh, sizeof eh, 0) != sizeof eh) return -ENOEXEC_; + if (std::memcmp(eh.ident, "\x7f" "ELF", 4) != 0 || eh.ident[4] != 2 || eh.ident[5] != 1) return -ENOEXEC_; + if (eh.machine != EM_AARCH64 || (eh.type != ET_EXEC && eh.type != ET_DYN)) return -ENOEXEC_; + if (eh.phentsize != sizeof(Elf64Phdr) || eh.phnum == 0 || eh.phnum > 128) return -ENOEXEC_; + std::vector ph(eh.phnum); + int64_t bytes = static_cast(ph.size() * sizeof(Elf64Phdr)); + if (file->pread_host(ph.data(), static_cast(bytes), eh.phoff) != bytes) return -ENOEXEC_; + + uint64_t lo = ~0ull, hi = 0; + for (auto& p : ph) { + if (p.type == PT_INTERP) { + std::string interp(static_cast(p.filesz), '\0'); + file->pread_host(interp.data(), p.filesz, p.offset); + out.interpreter = interp.c_str(); + } + if (p.type != PT_LOAD) continue; + lo = std::min(lo, page_down(p.vaddr)); + hi = std::max(hi, page_up(p.vaddr + p.memsz)); + } + if (lo >= hi) return -ENOEXEC_; + + // Reserve the whole image, then place segments inside the reservation. + uint64_t bias = 0; + if (eh.type == ET_DYN) { + int64_t at = address_space().mmap(hint, hi - lo, PROT_NONE_, MAP_PRIVATE_ | MAP_ANONYMOUS_, nullptr, 0); + if (at < 0) return at; + bias = static_cast(at) - lo; + } else { + int64_t at = address_space().mmap(lo, hi - lo, PROT_NONE_, MAP_PRIVATE_ | MAP_ANONYMOUS_ | MAP_FIXED_, nullptr, 0); + if (at < 0) return at; + } + + for (auto& p : ph) { + if (p.type != PT_LOAD || p.memsz == 0) continue; + uint32_t prot = segment_prot(p.flags); + uint64_t start = bias + p.vaddr; + uint64_t map_start = page_down(start); + uint64_t file_end = start + p.filesz; + uint64_t mem_end = start + p.memsz; + if (p.filesz) { + uint64_t length = page_up(file_end) - map_start; + uint64_t offset = page_down(p.offset); + if ((p.offset - offset) != (start - map_start)) return -ENOEXEC_; + // Writable while the tail of the last file page is cleared. + int64_t r = address_space().mmap(map_start, length, prot | PROT_WRITE_, MAP_PRIVATE_ | MAP_FIXED_, file, offset); + if (r < 0) return r; + if (mem_end > file_end && page_up(file_end) > file_end) { + uint64_t clear_end = std::min(page_up(file_end), mem_end); + address_space().populate(file_end, clear_end - file_end); + std::memset(reinterpret_cast(file_end), 0, static_cast(clear_end - file_end)); + } + if (!(prot & PROT_WRITE_)) address_space().mprotect(map_start, length, prot); + } + uint64_t anon_start = p.filesz ? page_up(file_end) : map_start; + if (page_up(mem_end) > anon_start) { + int64_t r = address_space().mmap(anon_start, page_up(mem_end) - anon_start, prot, + MAP_PRIVATE_ | MAP_ANONYMOUS_ | MAP_FIXED_, nullptr, 0); + if (r < 0) return r; + address_space().name_range(anon_start, page_up(mem_end) - anon_start, "[anon:.bss]"); + } + } + + out.bias = bias; + out.entry = bias + eh.entry; + out.phnum = eh.phnum; + out.end = bias + hi; + out.path = file->guest_path(); + for (auto& p : ph) + if (p.type == PT_PHDR) out.phdr = bias + p.vaddr; + if (!out.phdr) { + for (auto& p : ph) + if (p.type == PT_LOAD && eh.phoff >= p.offset && eh.phoff < p.offset + p.filesz) + out.phdr = bias + p.vaddr + (eh.phoff - p.offset); + } + TRACE("mmap", "loaded %s at %llx (entry %llx)", path.c_str(), (unsigned long long)bias, (unsigned long long)out.entry); + return 0; +} + +} // namespace + +int64_t exec_program(GuestThread& thread, const std::string& path, const std::vector& argv, + const std::vector& envp, CpuContext& start) { + auto& space = address_space(); + // The stack sits at the top of the arena, like Linux's below TASK_SIZE. + uint64_t stack_top = space.arena_end() - (16ull << 20); + int64_t stack = space.mmap(stack_top - kStackSize, kStackSize, PROT_READ_ | PROT_WRITE_, + MAP_PRIVATE_ | MAP_ANONYMOUS_ | MAP_FIXED_, nullptr, 0, "[stack]"); + if (stack < 0) return stack; + + LoadedElf program; + if (int64_t e = load_elf(thread, path, kPieBase, program)) { + LOGE("exec", "cannot load %s: %lld", path.c_str(), (long long)e); + return e; + } + LoadedElf interpreter; + if (!program.interpreter.empty()) { + if (int64_t e = load_elf(thread, program.interpreter, 0, interpreter)) { + LOGE("exec", "cannot load interpreter %s: %lld", program.interpreter.c_str(), (long long)e); + return e; + } + } + space.set_brk_base(program.end + kPageSize); + + // Strings and the random bytes at the very top, then the vectors. + std::vector blob; + auto put_string = [&](const std::string& s) { + uint64_t offset = blob.size(); + blob.insert(blob.end(), s.begin(), s.end()); + blob.push_back(0); + return offset; + }; + uint64_t execfn = put_string(program.path); + std::vector argv_offsets, envp_offsets; + for (auto& a : argv) argv_offsets.push_back(put_string(a)); + for (auto& e : envp) envp_offsets.push_back(put_string(e)); + uint64_t platform = put_string("aarch64"); + while (blob.size() % 16) blob.push_back(0); + uint64_t random = blob.size(); + blob.resize(blob.size() + 16); + BCryptGenRandom(nullptr, blob.data() + random, 16, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + while (blob.size() % 16) blob.push_back(0); + + uint64_t blob_base = (stack_top - 64 - blob.size()) & ~15ull; + auto at = [&](uint64_t offset) { return blob_base + offset; }; + + const uint64_t hwcap = HWCAP_FP_ | HWCAP_ASIMD_ | HWCAP_EVTSTRM_ | HWCAP_AES_ | HWCAP_PMULL_ | HWCAP_SHA1_ | + HWCAP_SHA2_ | HWCAP_CRC32_; + std::vector auxv = { + AT_PHDR_, program.phdr, + AT_PHENT_, sizeof(Elf64Phdr), + AT_PHNUM_, program.phnum, + AT_PAGESZ_, kPageSize, + AT_BASE_, interpreter.bias, + AT_FLAGS_, 0, + AT_ENTRY_, program.entry, + AT_UID_, process().uid, + AT_EUID_, process().uid, + AT_GID_, process().gid, + AT_EGID_, process().gid, + AT_SECURE_, 0, + AT_RANDOM_, at(random), + AT_HWCAP_, hwcap, + AT_HWCAP2_, 0, + AT_CLKTCK_, 100, + AT_MINSIGSTKSZ_, 5120, + AT_EXECFN_, at(execfn), + AT_PLATFORM_, at(platform), + AT_NULL_, 0, + }; + process().auxv = auxv; + + std::vector vectors; + vectors.push_back(argv.size()); + for (uint64_t o : argv_offsets) vectors.push_back(at(o)); + vectors.push_back(0); + for (uint64_t o : envp_offsets) vectors.push_back(at(o)); + vectors.push_back(0); + vectors.insert(vectors.end(), auxv.begin(), auxv.end()); + uint64_t sp = (blob_base - vectors.size() * 8) & ~15ull; + + space.populate(sp, stack_top - sp); + if (!guest_write(blob_base, blob.data(), blob.size()) || !guest_write(sp, vectors.data(), vectors.size() * 8)) + return -EFAULT_; + + process().exe_path = program.path; + process().start_stack = sp; + process().cmdline.clear(); + for (auto& a : argv) { + process().cmdline += a; + process().cmdline.push_back('\0'); + } + start = CpuContext{}; + start.sp = sp; + start.pc = program.interpreter.empty() ? program.entry : interpreter.entry; + LOGI("exec", "%s: entry %s, sp %llx", program.path.c_str(), describe_address(start.pc).c_str(), + (unsigned long long)sp); + return 0; +} + +} // namespace axrb diff --git a/direct/src/exec.h b/direct/src/exec.h new file mode 100644 index 0000000..d2a8e4b --- /dev/null +++ b/direct/src/exec.h @@ -0,0 +1,16 @@ +#pragma once + +#include "kernel.h" + +#include +#include + +namespace axrb { + +// What the kernel's execve does for an arm64 ELF: maps the program and its +// interpreter (the Android linker), builds the initial stack (argc, argv, +// envp, auxv) and returns the first register state. +int64_t exec_program(GuestThread& thread, const std::string& path, const std::vector& argv, + const std::vector& envp, CpuContext& start); + +} // namespace axrb diff --git a/direct/src/fallback.cpp b/direct/src/fallback.cpp new file mode 100644 index 0000000..fc5bf21 --- /dev/null +++ b/direct/src/fallback.cpp @@ -0,0 +1,258 @@ +// Instructions Dynarmic does not translate, executed one at a time on the +// thread's register state: the ARMv8.1 LSE atomics (CAS, CASP, LD, SWP), +// RCPC loads/stores (LDAPR, LDAPUR, STLUR) and reads of the ID registers that +// Linux emulates for user space. All atomics act on the identity-mapped guest +// memory with host atomic instructions, so they interoperate with Dynarmic's +// exclusive-monitor implementation (which also compare-exchanges memory). + +#include "cpu.h" +#include "linux.h" +#include "log.h" +#include "memory.h" + +#include +#include + +#include +#include + +namespace axrb { +using namespace linux_abi; + +namespace { + +struct Regs { + GuestThread& t; + uint64_t x(int r) const { return r == 31 ? 0 : get_reg(t, r); } + uint64_t xsp(int r) const { return r == 31 ? get_sp(t) : get_reg(t, r); } + void set(int r, uint64_t v) const { + if (r != 31) set_reg(t, r, v); + } +}; + +uint64_t mask_for(int bytes) { return bytes == 8 ? ~0ull : ((1ull << (bytes * 8)) - 1); } + +int64_t sign_extend(uint64_t v, int bytes) { + int shift = 64 - bytes * 8; + return static_cast(v << shift) >> shift; +} + +// Atomic compare-exchange of `bytes` at `address`. Returns the old value. +// Faults propagate as a structured exception to the caller's __try. +uint64_t cmpxchg(uint64_t address, uint64_t expected, uint64_t desired, int bytes) { + switch (bytes) { + case 1: + return static_cast(_InterlockedCompareExchange8(reinterpret_cast(address), + static_cast(desired), static_cast(expected))); + case 2: + return static_cast(_InterlockedCompareExchange16(reinterpret_cast(address), + static_cast(desired), static_cast(expected))); + case 4: + return static_cast(_InterlockedCompareExchange(reinterpret_cast(address), + static_cast(desired), static_cast(expected))); + default: + return static_cast(_InterlockedCompareExchange64(reinterpret_cast(address), + static_cast(desired), + static_cast(expected))); + } +} + +uint64_t load(uint64_t address, int bytes) { + switch (bytes) { + case 1: return *reinterpret_cast(address); + case 2: return *reinterpret_cast(address); + case 4: return *reinterpret_cast(address); + default: return *reinterpret_cast(address); + } +} + +void store(uint64_t address, uint64_t value, int bytes) { + switch (bytes) { + case 1: *reinterpret_cast(address) = static_cast(value); break; + case 2: *reinterpret_cast(address) = static_cast(value); break; + case 4: *reinterpret_cast(address) = static_cast(value); break; + default: *reinterpret_cast(address) = value; break; + } + _ReadWriteBarrier(); +} + +// LD: returns the old value; `op` 0-7 as in the encoding. +uint64_t atomic_rmw(uint64_t address, uint64_t operand, int op, int bytes) { + uint64_t m = mask_for(bytes); + operand &= m; + uint64_t old = load(address, bytes); + for (;;) { + uint64_t next; + switch (op) { + case 0: next = old + operand; break; // ADD + case 1: next = old & ~operand; break; // CLR + case 2: next = old ^ operand; break; // EOR + case 3: next = old | operand; break; // SET + case 4: next = sign_extend(old, bytes) > sign_extend(operand, bytes) ? old : operand; break; // SMAX + case 5: next = sign_extend(old, bytes) < sign_extend(operand, bytes) ? old : operand; break; // SMIN + case 6: next = old > operand ? old : operand; break; // UMAX + default: next = old < operand ? old : operand; break; // UMIN + } + next &= m; + uint64_t seen = cmpxchg(address, old, next, bytes); + if (seen == old) return old; + old = seen; + } +} + +uint64_t swap(uint64_t address, uint64_t value, int bytes) { + switch (bytes) { + case 1: return static_cast(_InterlockedExchange8(reinterpret_cast(address), static_cast(value))); + case 2: return static_cast(_InterlockedExchange16(reinterpret_cast(address), static_cast(value))); + case 4: return static_cast(_InterlockedExchange(reinterpret_cast(address), static_cast(value))); + default: return static_cast(_InterlockedExchange64(reinterpret_cast(address), static_cast(value))); + } +} + +// The sanitized ID register values Linux reports to EL0 (arch/arm64/kernel/cpufeature.c), +// for a Cortex-A78C-class core without LSE (matching AT_HWCAP). +bool id_register(uint32_t sysreg, uint64_t& value) { + uint32_t op0 = 2 + ((sysreg >> 14) & 1), op1 = (sysreg >> 11) & 7, crn = (sysreg >> 7) & 15, crm = (sysreg >> 3) & 15, + op2 = sysreg & 7; + if (op0 != 3 || op1 != 0 || crn != 0) return false; + value = 0; + switch ((crm << 3) | op2) { + case (0 << 3) | 0: value = 0x410fd4b0; return true; // MIDR_EL1: ARM Cortex-A78C r0p0 + case (0 << 3) | 5: value = 0x80000000; return true; // MPIDR_EL1 + case (0 << 3) | 6: value = 0; return true; // REVIDR_EL1 + case (4 << 3) | 0: value = 0x0000000000000011ull; return true; // ID_AA64PFR0: EL0/EL1 AArch64, FP, AdvSIMD + case (6 << 3) | 0: // ID_AA64ISAR0: AES+PMULL, SHA1, SHA2, CRC32 + value = (2ull << 4) | (1ull << 8) | (1ull << 12) | (1ull << 16); + return true; + default: + if (crm >= 1 && crm <= 7) return true; // other ID registers read as zero + return false; + } +} + +bool execute(GuestThread& t, uint64_t pc, uint32_t insn) { + Regs r{t}; + // LD / SWP / LDAPR: size 111 0 00 A R 1 Rs o3 opc 00 Rn Rt + if ((insn & 0x3f200c00u) == 0x38200000u) { + int bytes = 1 << (insn >> 30); + int rs = (insn >> 16) & 31, rn = (insn >> 5) & 31, rt = insn & 31; + int o3 = (insn >> 15) & 1, opc = (insn >> 12) & 7; + uint64_t address = untag(r.xsp(rn)); + uint64_t old; + if (o3 == 0) { + old = atomic_rmw(address, r.x(rs), opc, bytes); + } else if (opc == 0) { + old = swap(address, r.x(rs) & mask_for(bytes), bytes); + } else if (opc == 4 && rs == 31) { // LDAPR + old = load(address, bytes); + _ReadWriteBarrier(); + } else { + return false; + } + r.set(rt, old); + set_pc(t, pc + 4); + return true; + } + // CAS{A}{L}{B,H}: size 001000 1 L 1 Rs o0 11111 Rn Rt + if ((insn & 0x3fa07c00u) == 0x08a07c00u) { + int bytes = 1 << (insn >> 30); + int rs = (insn >> 16) & 31, rn = (insn >> 5) & 31, rt = insn & 31; + uint64_t m = mask_for(bytes); + uint64_t old = cmpxchg(untag(r.xsp(rn)), r.x(rs) & m, r.x(rt) & m, bytes); + r.set(rs, old); + set_pc(t, pc + 4); + return true; + } + // CASP{A}{L}: 0 sz 001000 0 L 1 Rs o0 11111 Rn Rt (register pairs) + if ((insn & 0xbfa07c00u) == 0x08207c00u) { + bool wide = (insn >> 30) & 1; + int rs = (insn >> 16) & 31, rn = (insn >> 5) & 31, rt = insn & 31; + if ((rs & 1) || (rt & 1)) return false; + uint64_t address = untag(r.xsp(rn)); + if (wide) { + long long compare[2] = {static_cast(r.x(rs)), static_cast(r.x(rs + 1))}; + _InterlockedCompareExchange128(reinterpret_cast(address), static_cast(r.x(rt + 1)), + static_cast(r.x(rt)), compare); + r.set(rs, static_cast(compare[0])); + r.set(rs + 1, static_cast(compare[1])); + } else { + uint64_t expected = (r.x(rs) & 0xffffffffull) | (r.x(rs + 1) << 32); + uint64_t desired = (r.x(rt) & 0xffffffffull) | (r.x(rt + 1) << 32); + uint64_t old = cmpxchg(address, expected, desired, 8); + r.set(rs, old & 0xffffffffull); + r.set(rs + 1, old >> 32); + } + set_pc(t, pc + 4); + return true; + } + // LDAPUR / LDAPURS / STLUR: size 011001 opc 0 imm9 00 Rn Rt + if ((insn & 0x3f200c00u) == 0x19000000u) { + int bytes = 1 << (insn >> 30); + int opc = (insn >> 22) & 3, rn = (insn >> 5) & 31, rt = insn & 31; + int64_t imm = static_cast(static_cast(insn << 11) >> 23); + uint64_t address = untag(r.xsp(rn)) + static_cast(imm); + if (opc == 0) { + _ReadWriteBarrier(); + store(address, r.x(rt), bytes); + } else { + uint64_t v = load(address, bytes); + if (opc == 2) v = static_cast(sign_extend(v, bytes)); + else if (opc == 3) v = static_cast(sign_extend(v, bytes)); + r.set(rt, v); + } + set_pc(t, pc + 4); + return true; + } + // MRS of an ID register (Linux traps and emulates these for EL0). + if ((insn & 0xfff00000u) == 0xd5300000u) { + uint64_t value; + const uint32_t sysreg = (insn >> 5) & 0x7fff; + // CNTVCT_EL0 / CNTVCTSS_EL0 / CNTPCTSS_EL0: Linux user space has no + // virtual offset, so all of them are the physical count Dynarmic has. + constexpr uint32_t kCntvct = (1u << 14) | (3u << 11) | (14u << 7) | 2u; + if (sysreg == kCntvct || sysreg == (kCntvct | 4u) || sysreg == ((kCntvct & ~7u) | 5u)) { + value = cntpct(); + } else if (!id_register(sysreg, value)) { + return false; + } + r.set(insn & 31, value); + set_pc(t, pc + 4); + return true; + } + return false; +} + +int access_filter(EXCEPTION_POINTERS* info, uint64_t* address) { + if (info->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) return EXCEPTION_CONTINUE_SEARCH; + *address = info->ExceptionRecord->ExceptionInformation[1]; + return EXCEPTION_EXECUTE_HANDLER; +} + +bool execute_guarded(GuestThread& t, uint64_t pc, uint32_t insn, bool& faulted, uint64_t& fault_address) { + __try { + return execute(t, pc, insn); + } __except (access_filter(GetExceptionInformation(), &fault_address)) { + faulted = true; + return true; + } +} + +} // namespace + +bool interpret_instruction(GuestThread& thread, uint64_t pc, uint32_t instruction) { + bool faulted = false; + uint64_t fault_address = 0; + bool known = execute_guarded(thread, pc, instruction, faulted, fault_address); + if (faulted) { + cpu_raise(thread, SIGSEGV_, SEGV_MAPERR_, fault_address, pc); + return true; + } + if (!known) { + static std::atomic reports{0}; + if (reports.fetch_add(1) < 50) + LOGW("cpu", "undefined instruction %08x at %s", instruction, describe_address(pc).c_str()); + } + return known; +} + +} // namespace axrb diff --git a/direct/src/files.cpp b/direct/src/files.cpp new file mode 100644 index 0000000..f14438d --- /dev/null +++ b/direct/src/files.cpp @@ -0,0 +1,1688 @@ +#include "files.h" + +#include "cpu.h" +#include "log.h" +#include "memory.h" + +#include +#include +#include + +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +// --------------------------------------------------------------------------- +// Helpers + +int64_t errno_from_win32(DWORD error) { + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_INVALID_DRIVE: return -ENOENT_; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: return -EACCES_; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: return -EEXIST_; + case ERROR_DIR_NOT_EMPTY: return -ENOTEMPTY_; + case ERROR_DIRECTORY: return -ENOTDIR_; + case ERROR_DISK_FULL: + case ERROR_HANDLE_DISK_FULL: return -ENOSPC_; + case ERROR_INVALID_HANDLE: return -EBADF_; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: return -ENOMEM_; + case ERROR_FILENAME_EXCED_RANGE: return -ENAMETOOLONG_; + case ERROR_NOT_SAME_DEVICE: return -EXDEV_; + case ERROR_WRITE_PROTECT: return -EROFS_; + case ERROR_NOACCESS: return -EFAULT_; + case ERROR_BROKEN_PIPE: return -EPIPE_; + default: return -EIO_; + } +} + +uint64_t inode_for(const std::string& key) { + // FNV-1a: stable across opens, distinct enough for directory listings. + uint64_t h = 1469598103934665603ull; + for (unsigned char c : key) h = (h ^ c) * 1099511628211ull; + return (h & 0x7fffffffffffull) | 1; +} + +void OpenFile::assign_inode(const std::string& key) { inode_ = inode_for(key); } + +static int64_t filetime_to_unix_ns(const FILETIME& ft) { + uint64_t t = (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + return static_cast((t - 116444736000000000ull) * 100); +} + +void fill_stat_from_attributes(kstat& st, const WIN32_FILE_ATTRIBUTE_DATA& data, const std::string& key) { + std::memset(&st, 0, sizeof st); + bool dir = (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + st.st_dev = 0xfd01; + st.st_ino = inode_for(key); + st.st_mode = dir ? (S_IFDIR_ | 0771) : (S_IFREG_ | 0660); + st.st_nlink = dir ? 2 : 1; + st.st_uid = process().uid; + st.st_gid = process().gid; + st.st_size = dir ? 4096 : static_cast((static_cast(data.nFileSizeHigh) << 32) | data.nFileSizeLow); + st.st_blksize = 4096; + st.st_blocks = (st.st_size + 511) / 512; + int64_t m = filetime_to_unix_ns(data.ftLastWriteTime), a = filetime_to_unix_ns(data.ftLastAccessTime), + c = filetime_to_unix_ns(data.ftCreationTime); + st.st_mtime_sec = m / 1000000000; + st.st_mtime_nsec = static_cast(m % 1000000000); + st.st_atime_sec = a / 1000000000; + st.st_atime_nsec = static_cast(a % 1000000000); + st.st_ctime_sec = std::max(m, c) / 1000000000; + st.st_ctime_nsec = static_cast(std::max(m, c) % 1000000000); +} + +static void fill_simple_stat(kstat& st, uint32_t mode, int64_t size, uint64_t ino) { + std::memset(&st, 0, sizeof st); + st.st_dev = 0x3; + st.st_ino = ino; + st.st_mode = mode; + st.st_nlink = (mode & S_IFMT_) == S_IFDIR_ ? 2 : 1; + st.st_uid = (mode & S_IFMT_) == S_IFCHR_ ? 0 : process().uid; + st.st_gid = (mode & S_IFMT_) == S_IFCHR_ ? 0 : process().gid; + st.st_size = size; + st.st_blksize = 4096; + st.st_blocks = (size + 511) / 512; + int64_t now = realtime_ns(); + st.st_mtime_sec = st.st_atime_sec = st.st_ctime_sec = now / 1000000000; +} + +static bool guest_buffer_ready(uint64_t buf, uint64_t len, bool write) { + if (!len) return true; + if (!address_space().populate(buf, len)) return false; + return address_space().check(buf, len, write ? PROT_WRITE_ : PROT_READ_) || + !address_space().overlaps_guest(buf, len); +} + +// --------------------------------------------------------------------------- +// OpenFile defaults + +int64_t OpenFile::read(GuestThread&, uint64_t, uint64_t) { return -EINVAL_; } +int64_t OpenFile::write(GuestThread&, uint64_t, uint64_t) { return -EINVAL_; } +int64_t OpenFile::pread(GuestThread&, uint64_t, uint64_t, int64_t) { return -ESPIPE_; } +int64_t OpenFile::pwrite(GuestThread&, uint64_t, uint64_t, int64_t) { return -ESPIPE_; } +int64_t OpenFile::lseek(int64_t, int) { return -ESPIPE_; } +int64_t OpenFile::stat(kstat& st) { + fill_simple_stat(st, S_IFREG_ | 0600, 0, inode_); + return 0; +} +int64_t OpenFile::ioctl(GuestThread&, uint64_t, uint64_t) { return -ENOTTY_; } +int64_t OpenFile::getdents64(uint64_t, uint64_t) { return -ENOTDIR_; } +int64_t OpenFile::truncate(int64_t) { return -EINVAL_; } +int64_t OpenFile::fallocate(int, int64_t, int64_t) { return -EOPNOTSUPP_; } +int64_t OpenFile::pread_host(void*, uint64_t, uint64_t) { return -ENODEV_; } +int64_t OpenFile::pwrite_host(const void*, uint64_t, uint64_t) { return -ENODEV_; } + +// --------------------------------------------------------------------------- +// HostFile + +HostFile::HostFile(HANDLE handle, std::wstring host_path, bool directory, bool read_only) + : handle_(handle), host_path_(std::move(host_path)), directory_(directory), read_only_(read_only) {} + +HostFile::~HostFile() { + if (handle_ != INVALID_HANDLE_VALUE) CloseHandle(handle_); +} + +// Regular files are opened for overlapped I/O: Windows serializes every +// read and write on a synchronous handle, and engines read one package file +// from several threads at once. Each call still waits for its own transfer. +static int64_t transfer(HANDLE handle, bool write, void* buf, uint64_t len, uint64_t offset) { + struct Event { + HANDLE handle = CreateEventW(nullptr, TRUE, FALSE, nullptr); + ~Event() { if (handle) CloseHandle(handle); } + }; + thread_local Event event; + uint64_t done = 0; + while (done < len) { + OVERLAPPED ov{}; + uint64_t at = offset + done; + ov.Offset = static_cast(at); + ov.OffsetHigh = static_cast(at >> 32); + ov.hEvent = event.handle; + DWORD chunk = static_cast(std::min(len - done, 64ull << 20)), moved = 0; + uint8_t* data = static_cast(buf) + done; + BOOL ok = write ? WriteFile(handle, data, chunk, nullptr, &ov) : ReadFile(handle, data, chunk, nullptr, &ov); + if (ok || GetLastError() == ERROR_IO_PENDING) ok = GetOverlappedResult(handle, &ov, &moved, TRUE); + if (!ok) { + DWORD error = GetLastError(); + if (!write && error == ERROR_HANDLE_EOF) break; + return done ? static_cast(done) : errno_from_win32(error); + } + done += moved; + if (write ? !moved : moved < chunk) break; + } + return static_cast(done); +} + +int64_t HostFile::pread_host(void* buf, uint64_t len, uint64_t offset) { + if (directory_) return -EISDIR_; + return transfer(handle_, false, buf, len, offset); +} + +int64_t HostFile::pwrite_host(const void* buf, uint64_t len, uint64_t offset) { + if (directory_) return -EISDIR_; + if (read_only_) return -EBADF_; + return transfer(handle_, true, const_cast(buf), len, offset); +} + +int64_t HostFile::read(GuestThread& t, uint64_t buf, uint64_t len) { + std::lock_guard held(lock_); + int64_t got = pread(t, buf, len, position_); + if (got > 0) position_ += got; + return got; +} + +int64_t HostFile::write(GuestThread& t, uint64_t buf, uint64_t len) { + std::lock_guard held(lock_); + if (status_flags & O_APPEND_) { + LARGE_INTEGER size{}; + GetFileSizeEx(handle_, &size); + position_ = size.QuadPart; + } + int64_t wrote = pwrite(t, buf, len, position_); + if (wrote > 0) position_ += wrote; + return wrote; +} + +int64_t HostFile::pread(GuestThread&, uint64_t buf, uint64_t len, int64_t offset) { + if (directory_) return -EISDIR_; + if ((status_flags & O_ACCMODE_) == O_WRONLY_) return -EBADF_; + if (offset < 0) return -EINVAL_; + if (!guest_buffer_ready(buf, len, true)) return -EFAULT_; + return pread_host(reinterpret_cast(buf), len, static_cast(offset)); +} + +int64_t HostFile::pwrite(GuestThread&, uint64_t buf, uint64_t len, int64_t offset) { + if (directory_) return -EISDIR_; + if ((status_flags & O_ACCMODE_) == O_RDONLY_) return -EBADF_; + if (offset < 0) return -EINVAL_; + if (!guest_buffer_ready(buf, len, false)) return -EFAULT_; + return pwrite_host(reinterpret_cast(buf), len, static_cast(offset)); +} + +int64_t HostFile::lseek(int64_t offset, int whence) { + std::lock_guard held(lock_); + int64_t base; + switch (whence) { + case 0: base = 0; break; + case 1: base = position_; break; + case 2: { + if (directory_) { + base = 0; + break; + } + LARGE_INTEGER size{}; + GetFileSizeEx(handle_, &size); + base = size.QuadPart; + break; + } + case 3: // SEEK_DATA + case 4: { // SEEK_HOLE: no sparse files, data everywhere + LARGE_INTEGER size{}; + GetFileSizeEx(handle_, &size); + if (offset >= size.QuadPart) return -ENXIO_; + position_ = whence == 3 ? offset : size.QuadPart; + return position_; + } + default: return -EINVAL_; + } + if (base + offset < 0) return -EINVAL_; + position_ = base + offset; + if (directory_ && position_ == 0) listed_ = false; + return position_; +} + +int64_t HostFile::stat(kstat& st) { + BY_HANDLE_FILE_INFORMATION info{}; + if (!GetFileInformationByHandle(handle_, &info)) return errno_from_win32(GetLastError()); + WIN32_FILE_ATTRIBUTE_DATA data{}; + data.dwFileAttributes = info.dwFileAttributes; + data.ftCreationTime = info.ftCreationTime; + data.ftLastAccessTime = info.ftLastAccessTime; + data.ftLastWriteTime = info.ftLastWriteTime; + data.nFileSizeHigh = info.nFileSizeHigh; + data.nFileSizeLow = info.nFileSizeLow; + fill_stat_from_attributes(st, data, path_); + st.st_nlink = info.nNumberOfLinks; + if (read_only_) { + st.st_uid = st.st_gid = 0; + st.st_mode = directory_ ? (S_IFDIR_ | 0755) : (S_IFREG_ | 0644); + } + return 0; +} + +void HostFile::load_directory() { + entries_.clear(); + entries_.push_back({".", DT_DIR_, inode_for(path_)}); + entries_.push_back({"..", DT_DIR_, inode_for(Vfs::normalize(path_, ".."))}); + std::set seen; + WIN32_FIND_DATAW found{}; + std::wstring pattern = host_path_ + (host_path_.back() == L'\\' ? L"*" : L"\\*"); + HANDLE search = FindFirstFileExW(pattern.c_str(), FindExInfoBasic, &found, FindExSearchNameMatch, nullptr, + FIND_FIRST_EX_LARGE_FETCH); + if (search != INVALID_HANDLE_VALUE) { + do { + std::wstring wname = found.cFileName; + if (wname == L"." || wname == L"..") continue; + std::string name = unescape_component(wname); + uint8_t type = (found.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? DT_DIR_ : DT_REG_; + entries_.push_back({name, type, inode_for(path_ + "/" + name)}); + seen.insert(name); + } while (FindNextFileW(search, &found)); + FindClose(search); + } + for (auto& [name, type] : vfs().virtual_children(path_)) + if (!seen.count(name)) entries_.push_back({name, type, inode_for(path_ + "/" + name)}); + listed_ = true; +} + +static int64_t emit_dirents(uint64_t buf, uint64_t len, int64_t& position, + const std::function& entry_at) { + uint64_t used = 0; + std::vector out; + for (;;) { + std::string name; + uint8_t type; + uint64_t ino; + if (!entry_at(static_cast(position), name, type, ino)) break; + size_t reclen = (19 + name.size() + 1 + 7) & ~size_t(7); + if (used + reclen > len) { + if (!used) return -EINVAL_; + break; + } + size_t at = out.size(); + out.resize(at + reclen, 0); + int64_t next = position + 1; + uint16_t rl = static_cast(reclen); + std::memcpy(&out[at], &ino, 8); + std::memcpy(&out[at + 8], &next, 8); + std::memcpy(&out[at + 16], &rl, 2); + out[at + 18] = type; + std::memcpy(&out[at + 19], name.data(), name.size()); + used += reclen; + ++position; + } + if (used && !guest_write(buf, out.data(), used)) return -EFAULT_; + return static_cast(used); +} + +int64_t HostFile::getdents64(uint64_t buf, uint64_t len) { + if (!directory_) return -ENOTDIR_; + std::lock_guard held(lock_); + if (!listed_ || position_ == 0) load_directory(); + return emit_dirents(buf, len, position_, [&](size_t i, std::string& name, uint8_t& type, uint64_t& ino) { + if (i >= entries_.size()) return false; + name = entries_[i].name; + type = entries_[i].type; + ino = entries_[i].inode; + return true; + }); +} + +int64_t HostFile::truncate(int64_t length) { + if (directory_) return -EISDIR_; + if (read_only_) return -EROFS_; + FILE_END_OF_FILE_INFO info{}; + info.EndOfFile.QuadPart = length; + if (!SetFileInformationByHandle(handle_, FileEndOfFileInfo, &info, sizeof info)) return errno_from_win32(GetLastError()); + return 0; +} + +int64_t HostFile::sync() { + if (!directory_ && !read_only_) FlushFileBuffers(handle_); + return 0; +} + +int64_t HostFile::fallocate(int mode, int64_t offset, int64_t length) { + if (mode != 0 && mode != 1) return -EOPNOTSUPP_; // only plain and KEEP_SIZE + if (read_only_) return -EBADF_; + if (mode == 1) return 0; + LARGE_INTEGER size{}; + GetFileSizeEx(handle_, &size); + if (offset + length > size.QuadPart) return truncate(offset + length); + return 0; +} + +// --------------------------------------------------------------------------- +// MemoryFile + +MemoryFile::MemoryFile(std::string content, bool directory, std::vector> children) + : content_(std::move(content)), directory_(directory), children_(std::move(children)) {} + +int64_t MemoryFile::pread_host(void* buf, uint64_t len, uint64_t offset) { + if (offset >= content_.size()) return 0; + uint64_t n = std::min(len, content_.size() - offset); + std::memcpy(buf, content_.data() + offset, n); + return static_cast(n); +} + +int64_t MemoryFile::pread(GuestThread&, uint64_t buf, uint64_t len, int64_t offset) { + if (directory_) return -EISDIR_; + if (offset < 0) return -EINVAL_; + if (static_cast(offset) >= content_.size()) return 0; + uint64_t n = std::min(len, content_.size() - offset); + if (!guest_write(buf, content_.data() + offset, n)) return -EFAULT_; + return static_cast(n); +} + +int64_t MemoryFile::read(GuestThread& t, uint64_t buf, uint64_t len) { + std::lock_guard held(lock_); + int64_t got = pread(t, buf, len, position_); + if (got > 0) position_ += got; + return got; +} + +int64_t MemoryFile::write(GuestThread&, uint64_t, uint64_t len) { + // /proc/self/... writes (oom_score_adj, timerslack_ns) are accepted and ignored. + return static_cast(len); +} + +int64_t MemoryFile::lseek(int64_t offset, int whence) { + std::lock_guard held(lock_); + int64_t base = whence == 0 ? 0 : whence == 1 ? position_ : whence == 2 ? static_cast(content_.size()) : -1; + if (base < 0 || base + offset < 0) return -EINVAL_; + position_ = base + offset; + return position_; +} + +int64_t MemoryFile::stat(kstat& st) { + if (directory_) fill_simple_stat(st, S_IFDIR_ | 0555, 0, inode_); + else fill_simple_stat(st, S_IFREG_ | 0444, path_.rfind("/proc/", 0) == 0 ? 0 : static_cast(content_.size()), inode_); + return 0; +} + +int64_t MemoryFile::getdents64(uint64_t buf, uint64_t len) { + if (!directory_) return -ENOTDIR_; + std::lock_guard held(lock_); + return emit_dirents(buf, len, position_, [&](size_t i, std::string& name, uint8_t& type, uint64_t& ino) { + if (i == 0) { + name = "."; + type = DT_DIR_; + } else if (i == 1) { + name = ".."; + type = DT_DIR_; + } else if (i - 2 < children_.size()) { + name = children_[i - 2].first; + type = children_[i - 2].second; + } else { + return false; + } + ino = inode_for(path_ + "/" + name); + return true; + }); +} + +// --------------------------------------------------------------------------- +// Devices and the console + +int64_t DeviceFile::pread_host(void* buf, uint64_t len, uint64_t) { + std::memset(buf, 0, len); + return static_cast(len); +} + +int64_t DeviceFile::read(GuestThread&, uint64_t buf, uint64_t len) { + if (kind_ == "null") return 0; + if (!guest_buffer_ready(buf, len, true)) return -EFAULT_; + if (kind_ == "zero") { + std::vector zeros(static_cast(std::min(len, 1 << 20))); + uint64_t done = 0; + while (done < len) { + uint64_t n = std::min(zeros.size(), len - done); + if (!guest_write(buf + done, zeros.data(), n)) return -EFAULT_; + done += n; + } + return static_cast(len); + } + if (kind_ == "urandom" || kind_ == "random") { + std::vector bytes(static_cast(std::min(len, 1 << 20))); + BCryptGenRandom(nullptr, bytes.data(), static_cast(bytes.size()), BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (!guest_write(buf, bytes.data(), bytes.size())) return -EFAULT_; + return static_cast(bytes.size()); + } + return 0; +} + +int64_t DeviceFile::write(GuestThread&, uint64_t, uint64_t len) { return static_cast(len); } + +int64_t DeviceFile::stat(kstat& st) { + uint64_t rdev = kind_ == "null" ? 0x103 : kind_ == "zero" ? 0x105 : kind_ == "random" ? 0x108 : 0x109; + fill_simple_stat(st, S_IFCHR_ | 0666, 0, inode_); + st.st_rdev = rdev; + return 0; +} + +int64_t DeviceFile::ioctl(GuestThread&, uint64_t request, uint64_t) { + if (request == FIONBIO_ || request == FIOCLEX_ || request == FIONCLEX_) return 0; + return -ENOTTY_; +} + +int64_t ConsoleFile::read(GuestThread&, uint64_t, uint64_t) { return 0; } + +int64_t ConsoleFile::write(GuestThread&, uint64_t buf, uint64_t len) { + std::string text(static_cast(std::min(len, 1 << 20)), '\0'); + if (!guest_read(buf, text.data(), text.size())) return -EFAULT_; + log_raw(text.data(), text.size()); + return static_cast(len); +} + +int64_t ConsoleFile::stat(kstat& st) { + fill_simple_stat(st, S_IFIFO_ | 0600, 0, 0x100 + which_); + return 0; +} + +int64_t ConsoleFile::ioctl(GuestThread&, uint64_t, uint64_t) { return -ENOTTY_; } + +// --------------------------------------------------------------------------- +// Pipes + +PipeFile::PipeFile(std::shared_ptr in, std::shared_ptr out, bool socket) + : in_(std::move(in)), out_(std::move(out)), socket_(socket) {} + +PipeFile::~PipeFile() { + if (out_) { + std::lock_guard held(out_->lock); + out_->writer_closed = true; + out_->waiters.wake_all(); + } + if (in_) { + std::lock_guard held(in_->lock); + in_->reader_closed = true; + in_->waiters.wake_all(); + } +} + +WaitQueue* PipeFile::wait_queue() { return in_ ? &in_->waiters : (out_ ? &out_->waiters : nullptr); } + +uint32_t PipeFile::poll_mask() { + uint32_t mask = 0; + if (in_) { + std::lock_guard held(in_->lock); + if (!in_->bytes.empty()) mask |= POLLIN_ | POLLRDNORM_; + if (in_->writer_closed) mask |= POLLHUP_ | (socket_ ? POLLIN_ | POLLRDHUP_ : 0); + } + if (out_) { + std::lock_guard held(out_->lock); + if (out_->reader_closed) mask |= POLLERR_ | (socket_ ? POLLHUP_ : 0); + else if (out_->bytes.size() < out_->capacity) mask |= POLLOUT_ | POLLWRNORM_; + } + return mask; +} + +int64_t PipeFile::read(GuestThread& t, uint64_t buf, uint64_t len) { + if (!in_) return -EBADF_; + if (!len) return 0; + for (;;) { + { + std::lock_guard held(in_->lock); + if (!in_->bytes.empty()) { + size_t n = static_cast(std::min(len, in_->bytes.size())); + std::vector chunk(in_->bytes.begin(), in_->bytes.begin() + static_cast(n)); + if (!guest_write(buf, chunk.data(), n)) return -EFAULT_; + in_->bytes.erase(in_->bytes.begin(), in_->bytes.begin() + static_cast(n)); + in_->waiters.wake_all(); // writers waiting for space + return static_cast(n); + } + if (in_->writer_closed) return 0; + } + if (nonblocking()) return -EAGAIN_; + in_->waiters.add(&t); + int64_t r = block_until(t, [&] { + std::lock_guard held(in_->lock); + return !in_->bytes.empty() || in_->writer_closed; + }, 0, "pipe read"); + in_->waiters.remove(&t); + if (r < 0) return r; + } +} + +int64_t PipeFile::write(GuestThread& t, uint64_t buf, uint64_t len) { + if (!out_) return -EBADF_; + std::vector data(static_cast(len)); + if (len && !guest_read(buf, data.data(), data.size())) return -EFAULT_; + uint64_t done = 0; + while (done < len) { + { + std::lock_guard held(out_->lock); + if (out_->reader_closed) { + if (done) return static_cast(done); + ksiginfo info{}; + info.si_signo = SIGPIPE_; + info.si_code = SI_USER_; + send_signal(t, SIGPIPE_, info); + return -EPIPE_; + } + size_t space = out_->capacity > out_->bytes.size() ? out_->capacity - out_->bytes.size() : 0; + if (space) { + size_t n = static_cast(std::min(space, len - done)); + out_->bytes.insert(out_->bytes.end(), data.begin() + static_cast(done), + data.begin() + static_cast(done + n)); + done += n; + out_->waiters.wake_all(); + continue; + } + } + if (nonblocking()) return done ? static_cast(done) : -EAGAIN_; + out_->waiters.add(&t); + int64_t r = block_until(t, [&] { + std::lock_guard held(out_->lock); + return out_->reader_closed || out_->bytes.size() < out_->capacity; + }, 0, "pipe write"); + out_->waiters.remove(&t); + if (r < 0) return done ? static_cast(done) : r; + } + return static_cast(done); +} + +void PipeFile::shutdown(int how) { + if ((how == SHUT_WR_ || how == SHUT_RDWR_) && out_) { + std::lock_guard held(out_->lock); + out_->writer_closed = true; + out_->waiters.wake_all(); + } + if ((how == SHUT_RD_ || how == SHUT_RDWR_) && in_) { + std::lock_guard held(in_->lock); + in_->reader_closed = true; + in_->waiters.wake_all(); + } +} + +int64_t PipeFile::stat(kstat& st) { + fill_simple_stat(st, (socket_ ? S_IFSOCK_ : S_IFIFO_) | 0600, 0, inode_ ? inode_ : reinterpret_cast(this) >> 4); + return 0; +} + +int64_t PipeFile::ioctl(GuestThread&, uint64_t request, uint64_t arg) { + if (request == FIONREAD_ && in_) { + std::lock_guard held(in_->lock); + int32_t n = static_cast(in_->bytes.size()); + return guest_write(arg, &n, 4) ? 0 : -EFAULT_; + } + if (request == FIONBIO_) { + int32_t on = 0; + guest_read(arg, &on, 4); + if (on) status_flags |= O_NONBLOCK_; + else status_flags &= ~O_NONBLOCK_; + return 0; + } + return -ENOTTY_; +} + +// --------------------------------------------------------------------------- +// eventfd + +uint32_t EventFile::poll_mask() { + std::lock_guard held(lock_); + uint32_t mask = POLLOUT_ | POLLWRNORM_; + if (counter_) mask |= POLLIN_ | POLLRDNORM_; + return mask; +} + +int64_t EventFile::read(GuestThread& t, uint64_t buf, uint64_t len) { + if (len < 8) return -EINVAL_; + for (;;) { + { + std::lock_guard held(lock_); + if (counter_) { + uint64_t value = semaphore_ ? 1 : counter_; + counter_ -= value; + if (!guest_write(buf, &value, 8)) return -EFAULT_; + waiters_.wake_all(); + return 8; + } + } + if (nonblocking()) return -EAGAIN_; + waiters_.add(&t); + int64_t r = block_until(t, [&] { + std::lock_guard held(lock_); + return counter_ != 0; + }, 0, "eventfd read"); + waiters_.remove(&t); + if (r < 0) return r; + } +} + +int64_t EventFile::write(GuestThread&, uint64_t buf, uint64_t len) { + if (len < 8) return -EINVAL_; + uint64_t value = 0; + if (!guest_read(buf, &value, 8)) return -EFAULT_; + if (value == ~0ull) return -EINVAL_; + std::lock_guard held(lock_); + if (counter_ + value < counter_ || counter_ + value == ~0ull) return -EAGAIN_; + counter_ += value; + waiters_.wake_all(); + return 8; +} + +int64_t EventFile::stat(kstat& st) { + fill_simple_stat(st, 0600, 0, reinterpret_cast(this) >> 4); + return 0; +} + +// --------------------------------------------------------------------------- +// timerfd + +uint64_t TimerFile::expirations_locked() { + if (!next_ns_) return 0; + int64_t now = clock_ == CLOCK_REALTIME_ ? realtime_ns() : monotonic_ns(); + if (now < next_ns_) return 0; + if (!interval_ns_) { + next_ns_ = 0; + return 1; + } + uint64_t count = static_cast((now - next_ns_) / interval_ns_) + 1; + next_ns_ += static_cast(count) * interval_ns_; + return count; +} + +uint32_t TimerFile::poll_mask() { + std::lock_guard held(lock_); + if (!next_ns_) return 0; + int64_t now = clock_ == CLOCK_REALTIME_ ? realtime_ns() : monotonic_ns(); + return now >= next_ns_ ? (POLLIN_ | POLLRDNORM_) : 0; +} + +int64_t TimerFile::read(GuestThread& t, uint64_t buf, uint64_t len) { + if (len < 8) return -EINVAL_; + for (;;) { + int64_t wait_ns = -1; + { + std::lock_guard held(lock_); + uint64_t count = expirations_locked(); + if (count) return guest_write(buf, &count, 8) ? 8 : -EFAULT_; + if (next_ns_) wait_ns = next_ns_ - (clock_ == CLOCK_REALTIME_ ? realtime_ns() : monotonic_ns()); + } + if (nonblocking()) return -EAGAIN_; + int64_t deadline = wait_ns < 0 ? 0 : ticks_now() + std::max(1, wait_ns * ticks_per_second() / 1000000000); + int64_t r = block_until(t, [&] { return poll_mask() != 0; }, deadline, "timerfd read"); + if (r == -EINTR_) return r; + } +} + +int64_t TimerFile::settime(int flags, const kitimerspec& value, kitimerspec* old) { + std::lock_guard held(lock_); + if (old) { + int64_t now = clock_ == CLOCK_REALTIME_ ? realtime_ns() : monotonic_ns(); + int64_t left = next_ns_ ? std::max(0, next_ns_ - now) : 0; + old->it_value = {left / 1000000000, left % 1000000000}; + old->it_interval = {interval_ns_ / 1000000000, interval_ns_ % 1000000000}; + } + int64_t v = value.it_value.tv_sec * 1000000000 + value.it_value.tv_nsec; + interval_ns_ = value.it_interval.tv_sec * 1000000000 + value.it_interval.tv_nsec; + if (!v) next_ns_ = 0; + else if (flags & TIMER_ABSTIME_) next_ns_ = v; + else next_ns_ = (clock_ == CLOCK_REALTIME_ ? realtime_ns() : monotonic_ns()) + v; + waiters_.wake_all(); + return 0; +} + +int64_t TimerFile::gettime(kitimerspec& out) { + std::lock_guard held(lock_); + int64_t now = clock_ == CLOCK_REALTIME_ ? realtime_ns() : monotonic_ns(); + int64_t left = next_ns_ ? std::max(0, next_ns_ - now) : 0; + out.it_value = {left / 1000000000, left % 1000000000}; + out.it_interval = {interval_ns_ / 1000000000, interval_ns_ % 1000000000}; + return 0; +} + +int64_t TimerFile::stat(kstat& st) { + fill_simple_stat(st, 0600, 0, reinterpret_cast(this) >> 4); + return 0; +} + +// --------------------------------------------------------------------------- +// epoll + +int64_t EpollFile::ctl(int op, int fd, const kepoll_event* event) { + auto file = fds().get(fd); + if (!file) return -EBADF_; + if (file.get() == this) return -EINVAL_; + std::lock_guard held(lock_); + auto it = interests_.find(fd); + switch (op) { + case EPOLL_CTL_ADD_: + if (it != interests_.end() && !it->second.file.expired()) return -EEXIST_; + interests_[fd] = {file, event->events, event->data, false}; + return 0; + case EPOLL_CTL_MOD_: + if (it == interests_.end()) return -ENOENT_; + it->second.events = event->events; + it->second.data = event->data; + it->second.disabled = false; + return 0; + case EPOLL_CTL_DEL_: + if (it == interests_.end()) return -ENOENT_; + interests_.erase(it); + return 0; + default: return -EINVAL_; + } +} + +uint32_t EpollFile::poll_mask() { + std::lock_guard held(lock_); + for (auto& [fd, interest] : interests_) { + auto file = interest.file.lock(); + if (file && !interest.disabled && (file->poll_mask() & (interest.events | POLLERR_ | POLLHUP_))) return POLLIN_; + } + return 0; +} + +int64_t EpollFile::wait(GuestThread& t, uint64_t events, int max, int64_t deadline) { + if (max <= 0) return -EINVAL_; + std::vector ready; + auto collect = [&]() { + ready.clear(); + std::lock_guard held(lock_); + for (auto it = interests_.begin(); it != interests_.end();) { + auto file = it->second.file.lock(); + if (!file) { + it = interests_.erase(it); + continue; + } + Interest& in = it->second; + uint32_t mask = file->poll_mask() & (in.events | EPOLLERR_ | EPOLLHUP_); + if (mask && !in.disabled && static_cast(ready.size()) < max) { + ready.push_back({mask, 0, in.data}); + if (in.events & EPOLLONESHOT_) in.disabled = true; + } + ++it; + } + return !ready.empty(); + }; + // Register with everything watched so readiness changes wake us. + std::vector> watched; + std::vector handles; + { + std::lock_guard held(lock_); + for (auto& [fd, in] : interests_) + if (auto file = in.file.lock()) watched.push_back(file); + } + for (auto& file : watched) { + if (auto* q = file->wait_queue()) q->add(&t); + if (HANDLE h = file->readiness_event()) handles.push_back(h); + } + int64_t r = 0; + if (!collect()) { + if (deadline == -1) r = -ETIMEDOUT_; + else r = block_until(t, collect, deadline, "epoll_wait"); + } + for (auto& file : watched) + if (auto* q = file->wait_queue()) q->remove(&t); + (void)handles; + if (r == -EINTR_) return r; + if (ready.empty()) return 0; + if (!guest_write(events, ready.data(), ready.size() * sizeof(kepoll_event))) return -EFAULT_; + return static_cast(ready.size()); +} + +int64_t EpollFile::stat(kstat& st) { + fill_simple_stat(st, 0600, 0, reinterpret_cast(this) >> 4); + return 0; +} + +// --------------------------------------------------------------------------- +// Descriptor table + +FdTable& fds() { + static FdTable table; + return table; +} + +int FdTable::install(std::shared_ptr file, bool cloexec, int minimum) { + std::lock_guard held(lock_); + for (size_t fd = static_cast(std::max(0, minimum));; ++fd) { + if (fd >= 65536) return static_cast(-EMFILE_); + if (fd >= files_.size()) { + files_.resize(fd + 1); + cloexec_.resize(fd + 1); + } + if (!files_[fd]) { + files_[fd] = std::move(file); + cloexec_[fd] = cloexec; + return static_cast(fd); + } + } +} + +int FdTable::install_at(int fd, std::shared_ptr file, bool cloexec) { + if (fd < 0 || fd >= 65536) return static_cast(-EBADF_); + std::shared_ptr old; + { + std::lock_guard held(lock_); + if (static_cast(fd) >= files_.size()) { + files_.resize(static_cast(fd) + 1); + cloexec_.resize(static_cast(fd) + 1); + } + old = std::move(files_[fd]); + files_[fd] = std::move(file); + cloexec_[fd] = cloexec; + } + if (old) old->on_close(); + return fd; // `old` closes outside the lock +} + +std::shared_ptr FdTable::get(int fd) { + std::lock_guard held(lock_); + if (fd < 0 || static_cast(fd) >= files_.size()) return nullptr; + return files_[fd]; +} + +int FdTable::close(int fd) { + std::shared_ptr old; + { + std::lock_guard held(lock_); + if (fd < 0 || static_cast(fd) >= files_.size() || !files_[fd]) return static_cast(-EBADF_); + old = std::move(files_[fd]); + files_[fd].reset(); + cloexec_[fd] = false; + } + old->on_close(); + return 0; +} + +bool FdTable::cloexec(int fd) { + std::lock_guard held(lock_); + return fd >= 0 && static_cast(fd) < cloexec_.size() && cloexec_[fd]; +} + +void FdTable::set_cloexec(int fd, bool value) { + std::lock_guard held(lock_); + if (fd >= 0 && static_cast(fd) < cloexec_.size()) cloexec_[fd] = value; +} + +std::vector FdTable::list() { + std::lock_guard held(lock_); + std::vector out; + for (size_t i = 0; i < files_.size(); ++i) + if (files_[i]) out.push_back(static_cast(i)); + return out; +} + +// --------------------------------------------------------------------------- +// Paths + +int64_t path_at(int dirfd, uint64_t path_addr, std::string& out, bool allow_empty) { + std::string path; + if (!guest_string(path_addr, path, 4096)) return -EFAULT_; + if (path.empty() && !allow_empty) return -ENOENT_; + if (!path.empty() && path[0] == '/') { + out = Vfs::normalize("/", path); + return 0; + } + std::string base; + if (dirfd == AT_FDCWD_) { + base = process().cwd; + } else { + auto dir = fds().get(dirfd); + if (!dir) return -EBADF_; + base = dir->guest_path(); + if (base.empty()) return -ENOTDIR_; + } + out = path.empty() ? base : Vfs::normalize(base, path); + return 0; +} + +namespace { +std::mutex g_devices_lock; +std::map()>> g_devices; +} // namespace + +void register_device(const std::string& name, std::function()> make) { + std::lock_guard held(g_devices_lock); + g_devices[name] = std::move(make); +} + +std::shared_ptr make_device(const std::string& name) { + { + std::lock_guard held(g_devices_lock); + if (auto it = g_devices.find(name); it != g_devices.end()) return it->second(); + } + return std::make_shared(name); +} + +int64_t stat_path(const std::string& path, bool follow, kstat& st) { + Resolved r; + std::string link; + if (!follow && vfs().readlink(path, link)) { + fill_simple_stat(st, S_IFLNK_ | 0777, static_cast(link.size()), inode_for(path)); + return 0; + } + int e = vfs().resolve(path, follow, r); + if (e) return e; + switch (r.kind) { + case Resolved::Host: { + WIN32_FILE_ATTRIBUTE_DATA data{}; + if (!GetFileAttributesExW(r.host.c_str(), GetFileExInfoStandard, &data)) return errno_from_win32(GetLastError()); + fill_stat_from_attributes(st, data, r.guest); + if (r.read_only) { + // Read-only mounts are the system image: root-owned, as on a device. + st.st_uid = st.st_gid = 0; + st.st_mode = (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? (S_IFDIR_ | 0755) : (S_IFREG_ | 0644); + } + return 0; + } + case Resolved::Synthetic: { + bool proc = r.guest.rfind("/proc/", 0) == 0; + fill_simple_stat(st, S_IFREG_ | 0444, proc ? 0 : static_cast(r.reader().size()), inode_for(r.guest)); + return 0; + } + case Resolved::SyntheticDir: + fill_simple_stat(st, S_IFDIR_ | 0555, 0, inode_for(r.guest)); + return 0; + case Resolved::Device: + return make_device(r.device)->stat(st); + default: return -ENOENT_; + } +} + +int64_t open_path(GuestThread&, const std::string& path, uint32_t flags, uint32_t mode, std::shared_ptr& out) { + (void)mode; + Resolved r; + std::string link; + if ((flags & O_NOFOLLOW_) && vfs().readlink(path, link) && !(flags & O_PATH_)) return -ELOOP_; + int e = vfs().resolve(path, !(flags & O_NOFOLLOW_), r); + uint32_t access = flags & O_ACCMODE_; + bool wants_write = access != O_RDONLY_ || (flags & (O_CREAT_ | O_TRUNC_)); + if (e) { + // Creating a file under a missing parent is ENOENT too. + return e; + } + switch (r.kind) { + case Resolved::Device: + out = make_device(r.device); + break; + case Resolved::Synthetic: + if (access != O_RDONLY_ && r.guest.rfind("/proc/", 0) != 0) return -EACCES_; + out = std::make_shared(r.reader()); + break; + case Resolved::SyntheticDir: { + if (wants_write) return -EISDIR_; + auto children = vfs().virtual_children(r.guest); + for (auto& name : r.children) + if (std::none_of(children.begin(), children.end(), [&](auto& c) { return c.first == name; })) + children.push_back({name, DT_UNKNOWN_}); + out = std::make_shared("", true, std::move(children)); + break; + } + case Resolved::Host: { + DWORD attributes = GetFileAttributesW(r.host.c_str()); + bool exists = attributes != INVALID_FILE_ATTRIBUTES; + bool directory = exists && (attributes & FILE_ATTRIBUTE_DIRECTORY); + if ((flags & O_DIRECTORY_) && exists && !directory) return -ENOTDIR_; + if (directory && access != O_RDONLY_) return -EISDIR_; + if (r.read_only && (wants_write || (!exists && (flags & O_CREAT_)))) return exists ? -EROFS_ : (flags & O_CREAT_) ? -EROFS_ : -ENOENT_; + if (!exists && !(flags & O_CREAT_)) return -ENOENT_; + if (exists && (flags & O_CREAT_) && (flags & O_EXCL_)) return -EEXIST_; + DWORD desired = FILE_READ_ATTRIBUTES | SYNCHRONIZE; + if (access == O_RDONLY_ || access == O_RDWR_) desired |= GENERIC_READ; + if (access == O_WRONLY_ || access == O_RDWR_) desired |= GENERIC_WRITE; + if (flags & O_PATH_) desired = FILE_READ_ATTRIBUTES | SYNCHRONIZE; + DWORD disposition = OPEN_EXISTING; + if (flags & O_CREAT_) disposition = (flags & O_EXCL_) ? CREATE_NEW : (flags & O_TRUNC_) ? OPEN_ALWAYS : OPEN_ALWAYS; + else if ((flags & O_TRUNC_) && access != O_RDONLY_) disposition = TRUNCATE_EXISTING; + DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + DWORD flags_and_attributes = FILE_ATTRIBUTE_NORMAL | (directory ? FILE_FLAG_BACKUP_SEMANTICS : FILE_FLAG_OVERLAPPED); + HANDLE h = CreateFileW(r.host.c_str(), desired, share, nullptr, disposition, flags_and_attributes, nullptr); + if (h == INVALID_HANDLE_VALUE) { + DWORD error = GetLastError(); + if (error == ERROR_PATH_NOT_FOUND) return -ENOENT_; + return errno_from_win32(error); + } + if ((flags & O_CREAT_) && (flags & O_TRUNC_) && access != O_RDONLY_ && exists) { + FILE_END_OF_FILE_INFO eof{}; + SetFileInformationByHandle(h, FileEndOfFileInfo, &eof, sizeof eof); + } + auto file = std::make_shared(h, r.host, directory, r.read_only); + out = file; + break; + } + default: return -ENOENT_; + } + out->set_guest_path(r.guest); + out->status_flags = flags & (O_ACCMODE_ | O_APPEND_ | O_NONBLOCK_ | O_DIRECT_ | O_DSYNC_); + return 0; +} + +// --------------------------------------------------------------------------- +// System calls + +int64_t OpenFile::writev(GuestThread& t, const std::vector& vecs) { + int64_t total = 0; + for (auto& v : vecs) { + if (!v.len) continue; + int64_t n = write(t, v.base, v.len); + if (n < 0) return total ? total : n; + total += n; + if (static_cast(n) < v.len) break; + } + return total; +} + +static int64_t do_readv(GuestThread& t, OpenFile& file, uint64_t iov, int64_t count, int64_t offset, bool write) { + if (count < 0 || count > 1024) return -EINVAL_; + std::vector vecs(static_cast(count)); + if (count && !guest_read(iov, vecs.data(), vecs.size() * sizeof(kiovec))) return -EFAULT_; + if (write && offset < 0) return file.writev(t, vecs); + int64_t total = 0; + for (auto& v : vecs) { + if (!v.len) continue; + int64_t n; + if (offset >= 0) n = write ? file.pwrite(t, v.base, v.len, offset + total) : file.pread(t, v.base, v.len, offset + total); + else n = write ? file.write(t, v.base, v.len) : file.read(t, v.base, v.len); + if (n < 0) return total ? total : n; + total += n; + if (static_cast(n) < v.len) break; + } + return total; +} + +static int64_t remove_path(const std::string& path, bool directory) { + Resolved r; + int e = vfs().resolve(path, false, r); + if (e) return e; + if (r.kind != Resolved::Host) return -EACCES_; + if (r.read_only) return -EROFS_; + DWORD attributes = GetFileAttributesW(r.host.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) return -ENOENT_; + bool is_dir = attributes & FILE_ATTRIBUTE_DIRECTORY; + if (directory && !is_dir) return -ENOTDIR_; + if (!directory && is_dir) return -EISDIR_; + if (is_dir) { + if (!RemoveDirectoryW(r.host.c_str())) return errno_from_win32(GetLastError()); + return 0; + } + // POSIX semantics: the name goes away now, open handles keep working. + HANDLE h = CreateFileW(r.host.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + if (h == INVALID_HANDLE_VALUE) return errno_from_win32(GetLastError()); + FILE_DISPOSITION_INFO_EX info{FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS | + FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE}; + BOOL ok = SetFileInformationByHandle(h, FileDispositionInfoEx, &info, sizeof info); + if (!ok) { + FILE_DISPOSITION_INFO legacy{TRUE}; + ok = SetFileInformationByHandle(h, FileDispositionInfo, &legacy, sizeof legacy); + } + DWORD error = GetLastError(); + CloseHandle(h); + return ok ? 0 : errno_from_win32(error); +} + +static int64_t rename_path(const std::string& from, const std::string& to, uint32_t flags) { + Resolved a, b; + int e = vfs().resolve(from, false, a); + if (e) return e; + e = vfs().resolve(to, false, b); + if (e) return e; + if (a.kind != Resolved::Host || b.kind != Resolved::Host) return -EXDEV_; + if (a.read_only || b.read_only) return -EROFS_; + if (a.mount != b.mount) return -EXDEV_; + const uint32_t RENAME_NOREPLACE = 1; + DWORD attributes = GetFileAttributesW(a.host.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) return -ENOENT_; + DWORD target_attributes = GetFileAttributesW(b.host.c_str()); + if (target_attributes != INVALID_FILE_ATTRIBUTES && (flags & RENAME_NOREPLACE)) return -EEXIST_; + if (target_attributes != INVALID_FILE_ATTRIBUTES && (target_attributes & FILE_ATTRIBUTE_DIRECTORY) && + (attributes & FILE_ATTRIBUTE_DIRECTORY)) { + RemoveDirectoryW(b.host.c_str()); // only succeeds when empty, as Linux requires + } + HANDLE h = CreateFileW(a.host.c_str(), DELETE | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + if (h == INVALID_HANDLE_VALUE) return errno_from_win32(GetLastError()); + std::vector buffer(sizeof(FILE_RENAME_INFO) + b.host.size() * sizeof(wchar_t) + 16); + auto* info = reinterpret_cast(buffer.data()); + info->Flags = FILE_RENAME_FLAG_POSIX_SEMANTICS | ((flags & RENAME_NOREPLACE) ? 0 : FILE_RENAME_FLAG_REPLACE_IF_EXISTS); + info->RootDirectory = nullptr; + info->FileNameLength = static_cast(b.host.size() * sizeof(wchar_t)); + std::memcpy(info->FileName, b.host.data(), info->FileNameLength); + BOOL ok = SetFileInformationByHandle(h, FileRenameInfoEx, info, static_cast(buffer.size())); + DWORD error = GetLastError(); + CloseHandle(h); + if (!ok) { + if (MoveFileExW(a.host.c_str(), b.host.c_str(), (flags & RENAME_NOREPLACE) ? 0 : MOVEFILE_REPLACE_EXISTING)) return 0; + return errno_from_win32(error); + } + return 0; +} + +static int64_t poll_files(GuestThread& t, uint64_t fds_addr, uint64_t count, int64_t deadline) { + if (count > 4096) return -EINVAL_; + std::vector polls(static_cast(count)); + if (count && !guest_read(fds_addr, polls.data(), polls.size() * sizeof(kpollfd))) return -EFAULT_; + std::vector> files(polls.size()); + for (size_t i = 0; i < polls.size(); ++i) + if (polls[i].fd >= 0) files[i] = fds().get(polls[i].fd); + int ready_count = 0; + auto check = [&]() { + ready_count = 0; + for (size_t i = 0; i < polls.size(); ++i) { + polls[i].revents = 0; + if (polls[i].fd < 0) continue; + if (!files[i]) { + polls[i].revents = POLLNVAL_; + ++ready_count; + continue; + } + uint32_t mask = files[i]->poll_mask(); + polls[i].revents = static_cast(mask & (static_cast(polls[i].events) | POLLERR_ | POLLHUP_ | POLLNVAL_)); + if (polls[i].revents) ++ready_count; + } + return ready_count > 0; + }; + for (auto& f : files) + if (f) + if (auto* q = f->wait_queue()) q->add(&t); + int64_t r = 0; + if (!check() && deadline != -1) r = block_until(t, check, deadline, "poll"); + for (auto& f : files) + if (f) + if (auto* q = f->wait_queue()) q->remove(&t); + if (r == -EINTR_) return r; + if (count && !guest_write(fds_addr, polls.data(), polls.size() * sizeof(kpollfd))) return -EFAULT_; + return ready_count; +} + +int64_t sys_files(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled) { + handled = true; + auto fd_arg = [&](int i) { return static_cast(static_cast(a[i])); }; + switch (nr) { + case 17: { // getcwd + std::string cwd = process().cwd; + if (cwd.size() + 1 > a[1]) return -ERANGE_; + if (!guest_write(a[0], cwd.c_str(), cwd.size() + 1)) return -EFAULT_; + return static_cast(cwd.size() + 1); + } + case 23: { // dup + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return fds().install(file, false); + } + case 24: { // dup3 + int oldfd = fd_arg(0), newfd = fd_arg(1); + if (oldfd == newfd) return -EINVAL_; + auto file = fds().get(oldfd); + if (!file) return -EBADF_; + return fds().install_at(newfd, file, (a[2] & O_CLOEXEC_) != 0); + } + case 25: { // fcntl + int fd = fd_arg(0); + auto file = fds().get(fd); + if (!file) return -EBADF_; + switch (static_cast(a[1])) { + case F_DUPFD_: return fds().install(file, false, static_cast(a[2])); + case F_DUPFD_CLOEXEC_: return fds().install(file, true, static_cast(a[2])); + case F_GETFD_: return fds().cloexec(fd) ? FD_CLOEXEC_ : 0; + case F_SETFD_: fds().set_cloexec(fd, (a[2] & FD_CLOEXEC_) != 0); return 0; + case F_GETFL_: return file->status_flags | O_LARGEFILE_; + case F_SETFL_: + file->status_flags = (file->status_flags & ~(O_APPEND_ | O_NONBLOCK_)) | + (static_cast(a[2]) & (O_APPEND_ | O_NONBLOCK_)); + return 0; + case F_GETLK_: + case F_OFD_GETLK_: { + kflock lock{}; + if (!guest_read(a[2], &lock, sizeof lock)) return -EFAULT_; + lock.l_type = 2; // F_UNLCK: nobody else holds locks in this process model + return guest_write(a[2], &lock, sizeof lock) ? 0 : -EFAULT_; + } + case F_SETLK_: + case F_SETLKW_: + case F_OFD_SETLK_: + case F_OFD_SETLKW_: return 0; + case F_GETPIPE_SZ_: return 65536; + case F_SETPIPE_SZ_: return static_cast(a[2]); + case F_ADD_SEALS_: return 0; + case F_GET_SEALS_: return 0; + case F_SETOWN_: case F_GETOWN_: case F_SETSIG_: case F_GETSIG_: return 0; + default: + LOGW("files", "fcntl cmd %llu unsupported", (unsigned long long)a[1]); + return -EINVAL_; + } + } + case 29: { // ioctl + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + if (a[1] == FIOCLEX_) { fds().set_cloexec(fd_arg(0), true); return 0; } + if (a[1] == FIONCLEX_) { fds().set_cloexec(fd_arg(0), false); return 0; } + return file->ioctl(t, a[1], a[2]); + } + case 32: return 0; // flock + case 33: return -EPERM_; // mknodat + case 34: { // mkdirat + std::string path; + if (int64_t e = path_at(fd_arg(0), a[1], path)) return e; + Resolved r; + if (int e = vfs().resolve(path, true, r)) return e; + if (r.kind != Resolved::Host) return -EEXIST_; + if (r.read_only) return GetFileAttributesW(r.host.c_str()) != INVALID_FILE_ATTRIBUTES ? -EEXIST_ : -EROFS_; + if (!CreateDirectoryW(r.host.c_str(), nullptr)) return errno_from_win32(GetLastError()); + return 0; + } + case 35: { // unlinkat + std::string path; + if (int64_t e = path_at(fd_arg(0), a[1], path)) return e; + return remove_path(path, (a[2] & AT_REMOVEDIR_) != 0); + } + case 36: // symlinkat + case 37: // linkat + return -EPERM_; + case 38: // renameat + case 276: { // renameat2 + std::string from, to; + if (int64_t e = path_at(fd_arg(0), a[1], from)) return e; + if (int64_t e = path_at(fd_arg(2), a[3], to)) return e; + return rename_path(from, to, nr == 276 ? static_cast(a[4]) : 0); + } + case 43: // statfs + case 44: { // fstatfs + kstatfs st{}; + st.f_type = 0xef53; // ext4 + st.f_bsize = 4096; + st.f_frsize = 4096; + ULARGE_INTEGER avail{}, total{}, free_bytes{}; + GetDiskFreeSpaceExW(nullptr, &avail, &total, &free_bytes); + st.f_blocks = total.QuadPart / 4096; + st.f_bfree = free_bytes.QuadPart / 4096; + st.f_bavail = avail.QuadPart / 4096; + st.f_files = 1 << 22; + st.f_ffree = 1 << 21; + st.f_namelen = 255; + return guest_write(a[1], &st, sizeof st) ? 0 : -EFAULT_; + } + case 45: { // truncate + std::string path; + if (int64_t e = path_at(AT_FDCWD_, a[0], path)) return e; + std::shared_ptr file; + if (int64_t e = open_path(t, path, O_WRONLY_, 0, file)) return e; + return file->truncate(static_cast(a[1])); + } + case 46: { // ftruncate + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->truncate(static_cast(a[1])); + } + case 47: { // fallocate + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->fallocate(static_cast(a[1]), static_cast(a[2]), static_cast(a[3])); + } + case 48: // faccessat + case 439: { // faccessat2 + std::string path; + if (int64_t e = path_at(fd_arg(0), a[1], path, nr == 439 && (a[3] & AT_EMPTY_PATH_))) return e; + kstat st; + if (int64_t e = stat_path(path, !(nr == 439 && (a[3] & AT_SYMLINK_NOFOLLOW_)), st)) return e; + uint32_t mode = static_cast(a[2]); + if (mode & ~7u) return -EINVAL_; + if (mode & 2) { + Resolved resolved; + if (int64_t e = vfs().resolve(path, !(nr == 439 && (a[3] & AT_SYMLINK_NOFOLLOW_)), resolved)) return e; + if (resolved.read_only) return -EROFS_; + } + uint32_t allowed = (st.st_mode >> (st.st_uid == process().uid ? 6 : st.st_gid == process().gid ? 3 : 0)) & 7; + if (process().uid == 0) allowed = 6 | ((st.st_mode & 0111) ? 1 : 0); + if (mode & ~allowed) return -EACCES_; + return 0; + } + case 49: { // chdir + std::string path; + if (int64_t e = path_at(AT_FDCWD_, a[0], path)) return e; + kstat st; + if (int64_t e = stat_path(path, true, st)) return e; + if ((st.st_mode & S_IFMT_) != S_IFDIR_) return -ENOTDIR_; + process().cwd = path; + return 0; + } + case 50: { // fchdir + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + process().cwd = file->guest_path(); + return 0; + } + case 52: case 53: case 54: case 55: return 0; // fchmod, fchmodat, fchownat, fchown + case 56: { // openat + std::string path; + if (int64_t e = path_at(fd_arg(0), a[1], path)) return e; + std::shared_ptr file; + uint32_t flags = static_cast(a[2]); + int64_t e = open_path(t, path, flags, static_cast(a[3]), file); + TRACE("files", "openat %s flags %o -> %lld", path.c_str(), flags, (long long)e); + if (e) return e; + return fds().install(file, (flags & O_CLOEXEC_) != 0); + } + case 57: return fds().close(fd_arg(0)); // close + case 59: { // pipe2 + auto buffer = std::make_shared(); + auto reader = std::make_shared(buffer, nullptr, false); + auto writer = std::make_shared(nullptr, buffer, false); + bool cloexec = (a[1] & O_CLOEXEC_) != 0; + if (a[1] & O_NONBLOCK_) { + reader->status_flags |= O_NONBLOCK_; + writer->status_flags |= O_NONBLOCK_; + } + writer->status_flags |= O_WRONLY_; + int pair[2] = {fds().install(reader, cloexec), fds().install(writer, cloexec)}; + if (!guest_write(a[0], pair, sizeof pair)) { + fds().close(pair[0]); + fds().close(pair[1]); + return -EFAULT_; + } + return 0; + } + case 61: { // getdents64 + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->getdents64(a[1], a[2]); + } + case 62: { // lseek + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->lseek(static_cast(a[1]), static_cast(a[2])); + } + case 63: { // read + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->read(t, a[1], a[2]); + } + case 64: { // write + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->write(t, a[1], a[2]); + } + case 65: case 66: { // readv, writev + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return do_readv(t, *file, a[1], static_cast(a[2]), -1, nr == 66); + } + case 67: { // pread64 + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->pread(t, a[1], a[2], static_cast(a[3])); + } + case 68: { // pwrite64 + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->pwrite(t, a[1], a[2], static_cast(a[3])); + } + case 69: case 70: { // preadv, pwritev + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return do_readv(t, *file, a[1], static_cast(a[2]), static_cast(a[3]), nr == 70); + } + case 71: { // sendfile + auto out = fds().get(fd_arg(0)); + auto in = fds().get(fd_arg(1)); + if (!out || !in) return -EBADF_; + int64_t offset = -1; + if (a[2] && !guest_read(a[2], &offset, 8)) return -EFAULT_; + uint64_t count = std::min(a[3], 1 << 24); + std::vector data(static_cast(count)); + int64_t got = offset >= 0 ? in->pread_host(data.data(), count, static_cast(offset)) : -1; + if (offset < 0) { + // Through a scratch guest buffer would be needed for stream reads; files are the common case. + kstat st; + in->stat(st); + int64_t position = in->lseek(0, 1); + got = in->pread_host(data.data(), count, static_cast(position)); + if (got > 0) in->lseek(position + got, 0); + } + if (got <= 0) return got; + // Write via a host-side path when possible. + int64_t written = out->pwrite_host(data.data(), static_cast(got), static_cast(out->lseek(0, 1))); + if (written > 0) out->lseek(written, 1); + if (offset >= 0 && a[2]) { + offset += got; + guest_write(a[2], &offset, 8); + } + return written; + } + case 72: { // pselect6 + int n = fd_arg(0); + uint64_t sets[3] = {a[1], a[2], a[3]}; + std::vector bits[3]; + size_t words = static_cast((n + 63) / 64); + for (int s = 0; s < 3; ++s) { + bits[s].assign(words, 0); + if (sets[s] && words && !guest_read(sets[s], bits[s].data(), words * 8)) return -EFAULT_; + } + int64_t deadline = 0; + if (a[4]) { + ktimespec ts; + if (!guest_read(a[4], &ts, sizeof ts)) return -EFAULT_; + deadline = (ts.tv_sec == 0 && ts.tv_nsec == 0) ? -1 : ticks_now() + ticks_from_timespec_relative(ts); + } + std::vector> files(static_cast(n)); + for (int fd = 0; fd < n; ++fd) + for (int s = 0; s < 3; ++s) + if (bits[s][fd / 64] & (1ull << (fd % 64))) files[fd] = fds().get(fd); + std::vector out[3]; + int count = 0; + auto check = [&] { + count = 0; + for (int s = 0; s < 3; ++s) out[s].assign(words, 0); + for (int fd = 0; fd < n; ++fd) { + if (!files[fd]) continue; + uint32_t mask = files[fd]->poll_mask(); + bool want[3] = {(bits[0][fd / 64] >> (fd % 64)) & 1, (bits[1][fd / 64] >> (fd % 64)) & 1, + (bits[2][fd / 64] >> (fd % 64)) & 1}; + bool got[3] = {(mask & (POLLIN_ | POLLHUP_ | POLLERR_)) != 0, (mask & (POLLOUT_ | POLLERR_)) != 0, + (mask & POLLPRI_) != 0}; + for (int s = 0; s < 3; ++s) + if (want[s] && got[s]) { + out[s][fd / 64] |= 1ull << (fd % 64); + ++count; + } + } + return count > 0; + }; + for (auto& f : files) + if (f) + if (auto* q = f->wait_queue()) q->add(&t); + int64_t r = 0; + if (!check() && deadline != -1) r = block_until(t, check, deadline, "select"); + for (auto& f : files) + if (f) + if (auto* q = f->wait_queue()) q->remove(&t); + if (r == -EINTR_) return r; + for (int s = 0; s < 3; ++s) + if (sets[s] && words) guest_write(sets[s], out[s].data(), words * 8); + return count; + } + case 73: { // ppoll + int64_t deadline = 0; + if (a[2]) { + ktimespec ts; + if (!guest_read(a[2], &ts, sizeof ts)) return -EFAULT_; + deadline = (ts.tv_sec == 0 && ts.tv_nsec == 0) ? -1 : ticks_now() + ticks_from_timespec_relative(ts); + } + uint64_t old_mask = t.sigmask; + if (a[3]) { + uint64_t mask; + if (!guest_read(a[3], &mask, 8)) return -EFAULT_; + t.sigmask = mask & ~((1ull << (SIGKILL_ - 1)) | (1ull << (SIGSTOP_ - 1))); + } + int64_t r = poll_files(t, a[0], a[1], deadline); + if (a[3] && r != -EINTR_) t.sigmask = old_mask; + return r; + } + case 78: { // readlinkat + std::string path; + if (int64_t e = path_at(fd_arg(0), a[1], path)) return e; + std::string target; + if (!vfs().readlink(path, target)) { + kstat st; + if (int64_t e = stat_path(path, false, st)) return e; + return -EINVAL_; + } + uint64_t n = std::min(target.size(), a[3]); + if (!guest_write(a[2], target.data(), n)) return -EFAULT_; + return static_cast(n); + } + case 79: { // newfstatat + uint32_t flags = static_cast(a[3]); + kstat st; + std::string path; + if (int64_t e = path_at(fd_arg(0), a[1], path, (flags & AT_EMPTY_PATH_) != 0)) return e; + std::string raw; + guest_string(a[1], raw); + if (raw.empty() && (flags & AT_EMPTY_PATH_)) { + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + if (int64_t e = file->stat(st)) return e; + } else if (int64_t e = stat_path(path, !(flags & AT_SYMLINK_NOFOLLOW_), st)) { + TRACE("files", "stat %s -> %lld", path.c_str(), (long long)e); + return e; + } + return guest_write(a[2], &st, sizeof st) ? 0 : -EFAULT_; + } + case 80: { // fstat + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + kstat st; + if (int64_t e = file->stat(st)) return e; + return guest_write(a[1], &st, sizeof st) ? 0 : -EFAULT_; + } + case 81: return 0; // sync + case 82: case 83: { // fsync, fdatasync + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + return file->sync(); + } + case 84: return 0; // sync_file_range + case 85: { // timerfd_create + auto file = std::make_shared(static_cast(a[0])); + if (a[1] & O_NONBLOCK_) file->status_flags |= O_NONBLOCK_; + return fds().install(file, (a[1] & O_CLOEXEC_) != 0); + } + case 86: { // timerfd_settime + auto timer = std::dynamic_pointer_cast(fds().get(fd_arg(0))); + if (!timer) return -EINVAL_; + kitimerspec value{}, old{}; + if (!guest_read(a[2], &value, sizeof value)) return -EFAULT_; + timer->settime(static_cast(a[1]), value, a[3] ? &old : nullptr); + if (a[3]) guest_write(a[3], &old, sizeof old); + return 0; + } + case 87: { // timerfd_gettime + auto timer = std::dynamic_pointer_cast(fds().get(fd_arg(0))); + if (!timer) return -EINVAL_; + kitimerspec value{}; + timer->gettime(value); + return guest_write(a[1], &value, sizeof value) ? 0 : -EFAULT_; + } + case 88: return 0; // utimensat + case 19: { // eventfd2 + auto file = std::make_shared(a[0] & 0xffffffffull, (a[1] & 1) != 0); + if (a[1] & O_NONBLOCK_) file->status_flags |= O_NONBLOCK_; + return fds().install(file, (a[1] & O_CLOEXEC_) != 0); + } + case 20: { // epoll_create1 + auto file = std::make_shared(); + return fds().install(file, (a[0] & O_CLOEXEC_) != 0); + } + case 21: { // epoll_ctl + auto ep = std::dynamic_pointer_cast(fds().get(fd_arg(0))); + if (!ep) return -EINVAL_; + kepoll_event event{}; + if (static_cast(a[1]) != EPOLL_CTL_DEL_ && !guest_read(a[3], &event, sizeof event)) return -EFAULT_; + return ep->ctl(static_cast(a[1]), fd_arg(2), &event); + } + case 22: { // epoll_pwait + auto ep = std::dynamic_pointer_cast(fds().get(fd_arg(0))); + if (!ep) return -EINVAL_; + int timeout = static_cast(a[3]); + int64_t deadline = timeout < 0 ? 0 : timeout == 0 ? -1 : ticks_now() + static_cast(timeout) * ticks_per_second() / 1000; + uint64_t old_mask = t.sigmask; + if (a[4]) { + uint64_t mask; + if (guest_read(a[4], &mask, 8)) t.sigmask = mask; + } + int64_t r = ep->wait(t, a[1], static_cast(a[2]), deadline); + if (a[4] && r != -EINTR_) t.sigmask = old_mask; + return r; + } + case 291: { // statx + std::string path; + uint32_t flags = static_cast(a[2]); + if (int64_t e = path_at(fd_arg(0), a[1], path, (flags & AT_EMPTY_PATH_) != 0)) return e; + kstat st; + std::string raw; + guest_string(a[1], raw); + if (raw.empty() && (flags & AT_EMPTY_PATH_)) { + auto file = fds().get(fd_arg(0)); + if (!file) return -EBADF_; + if (int64_t e = file->stat(st)) return e; + } else if (int64_t e = stat_path(path, !(flags & AT_SYMLINK_NOFOLLOW_), st)) { + return e; + } + kstatx sx{}; + sx.stx_mask = 0x7ff; + sx.stx_blksize = static_cast(st.st_blksize); + sx.stx_nlink = st.st_nlink; + sx.stx_uid = st.st_uid; + sx.stx_gid = st.st_gid; + sx.stx_mode = static_cast(st.st_mode); + sx.stx_ino = st.st_ino; + sx.stx_size = static_cast(st.st_size); + sx.stx_blocks = static_cast(st.st_blocks); + sx.stx_atime = {st.st_atime_sec, static_cast(st.st_atime_nsec), 0}; + sx.stx_mtime = {st.st_mtime_sec, static_cast(st.st_mtime_nsec), 0}; + sx.stx_ctime = {st.st_ctime_sec, static_cast(st.st_ctime_nsec), 0}; + sx.stx_btime = sx.stx_ctime; + sx.stx_dev_major = 253; + sx.stx_dev_minor = 1; + return guest_write(a[4], &sx, sizeof sx) ? 0 : -EFAULT_; + } + case 279: { // memfd_create + std::string name; + guest_string(a[0], name, 256); + wchar_t dir[MAX_PATH], file_name[MAX_PATH]; + GetTempPathW(MAX_PATH, dir); + GetTempFileNameW(dir, L"amf", 0, file_name); + HANDLE h = CreateFileW(file_name, GENERIC_READ | GENERIC_WRITE | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_OVERLAPPED, nullptr); + if (h == INVALID_HANDLE_VALUE) return -ENOMEM_; + auto file = std::make_shared(h, file_name, false, false); + file->set_guest_path("/memfd:" + name + " (deleted)"); + file->status_flags = O_RDWR_; + return fds().install(file, (a[1] & 1) != 0); + } + case 285: return -EXDEV_; // copy_file_range: callers fall back to read/write + case 436: { // close_range + uint32_t first = static_cast(a[0]), last = static_cast(a[1]); + for (int fd : fds().list()) + if (static_cast(fd) >= first && static_cast(fd) <= last) { + if (a[2] & 4) fds().set_cloexec(fd, true); // CLOSE_RANGE_CLOEXEC + else fds().close(fd); + } + return 0; + } + case 26: { // inotify_init1: a descriptor that never reports changes + auto file = std::make_shared(0, false); + if (a[0] & O_NONBLOCK_) file->status_flags |= O_NONBLOCK_; + return fds().install(file, (a[0] & O_CLOEXEC_) != 0); + } + case 27: return 1; // inotify_add_watch + case 28: return 0; // inotify_rm_watch + case 5: case 6: case 7: return -EOPNOTSUPP_; // setxattr family + case 8: case 9: case 10: return -ENODATA_; // getxattr family + case 11: case 12: case 13: return 0; // listxattr family: none + case 14: case 15: case 16: return -ENODATA_; // removexattr family + case 213: return 0; // readahead + case 223: return 0; // fadvise64 + case 74: return -ENOSYS_; // signalfd4 + default: + handled = false; + return 0; + } +} + +} // namespace axrb diff --git a/direct/src/files.h b/direct/src/files.h new file mode 100644 index 0000000..efc7736 --- /dev/null +++ b/direct/src/files.h @@ -0,0 +1,282 @@ +#pragma once + +#include "kernel.h" +#include "linux.h" +#include "vfs.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { + +// An open file description (Linux "struct file"): shared by dup'd descriptors. +class OpenFile : public std::enable_shared_from_this { +public: + virtual ~OpenFile() = default; + // Linux flush is called for each descriptor close, even while a blocked + // ioctl still holds the open file description alive. + virtual void on_close() {} + + // Guest-visible operations; results are byte counts or -errno. + virtual int64_t read(GuestThread& t, uint64_t buf, uint64_t len); + virtual int64_t write(GuestThread& t, uint64_t buf, uint64_t len); + virtual int64_t pread(GuestThread& t, uint64_t buf, uint64_t len, int64_t offset); + virtual int64_t pwrite(GuestThread& t, uint64_t buf, uint64_t len, int64_t offset); + // Gathering write: datagram sockets need the whole vector in one message; + // by default the pieces are written one after another. + virtual int64_t writev(GuestThread& t, const std::vector& vecs); + virtual int64_t lseek(int64_t offset, int whence); + virtual int64_t stat(linux_abi::kstat& st); + virtual int64_t ioctl(GuestThread& t, uint64_t request, uint64_t arg); + virtual int64_t getdents64(uint64_t buf, uint64_t len); + virtual int64_t truncate(int64_t length); + virtual int64_t sync() { return 0; } + virtual int64_t fallocate(int mode, int64_t offset, int64_t length); + // Readiness for poll/epoll (POLLIN, POLLOUT, ...). + virtual uint32_t poll_mask() { return linux_abi::POLLIN_ | linux_abi::POLLOUT_; } + // Where to register to be woken when readiness may change (null: never changes). + virtual WaitQueue* wait_queue() { return nullptr; } + // A Windows event signalled on readiness change, for sockets. + virtual HANDLE readiness_event() { return nullptr; } + + // Host-level access for mmap. + virtual int64_t pread_host(void* buf, uint64_t len, uint64_t offset); + virtual int64_t pwrite_host(const void* buf, uint64_t len, uint64_t offset); + virtual bool mappable() const { return false; } + // Objects that live in guest memory already (the property area): mmap + // returns this address instead of making a copy. + virtual uint64_t fixed_mapping() const { return 0; } + // Device-managed mappings (e.g. Binder's receive arena). + virtual bool has_device_mapping() const { return false; } + virtual int64_t map_device(uint64_t length, uint32_t prot, uint32_t flags, uint64_t offset) { + return -linux_abi::ENODEV_; + } + + const std::string& guest_path() const { return path_; } + void set_guest_path(std::string path) { path_ = std::move(path); } + uint64_t inode() const { return inode_; } + uint32_t status_flags = 0; // O_APPEND, O_NONBLOCK, access mode + bool nonblocking() const { return (status_flags & linux_abi::O_NONBLOCK_) != 0; } + +protected: + std::string path_; + uint64_t inode_ = 0; + void assign_inode(const std::string& key); +}; + +// A host file (or directory) reached through a mount. +class HostFile : public OpenFile { +public: + HostFile(HANDLE handle, std::wstring host_path, bool directory, bool read_only); + ~HostFile() override; + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t pread(GuestThread& t, uint64_t buf, uint64_t len, int64_t offset) override; + int64_t pwrite(GuestThread& t, uint64_t buf, uint64_t len, int64_t offset) override; + int64_t lseek(int64_t offset, int whence) override; + int64_t stat(linux_abi::kstat& st) override; + int64_t getdents64(uint64_t buf, uint64_t len) override; + int64_t truncate(int64_t length) override; + int64_t sync() override; + int64_t fallocate(int mode, int64_t offset, int64_t length) override; + int64_t pread_host(void* buf, uint64_t len, uint64_t offset) override; + int64_t pwrite_host(const void* buf, uint64_t len, uint64_t offset) override; + bool mappable() const override { return !directory_; } + HANDLE handle() const { return handle_; } + const std::wstring& host_path() const { return host_path_; } + +private: + void load_directory(); + HANDLE handle_; + std::wstring host_path_; + bool directory_; + bool read_only_; + std::mutex lock_; + int64_t position_ = 0; + // Directory listing state. + struct Entry { + std::string name; + uint8_t type; + uint64_t inode; + }; + std::vector entries_; + bool listed_ = false; +}; + +// Read-only content generated at open time (/proc, /sys, synthetic dirs). +class MemoryFile : public OpenFile { +public: + MemoryFile(std::string content, bool directory = false, std::vector> children = {}); + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t pread(GuestThread& t, uint64_t buf, uint64_t len, int64_t offset) override; + int64_t lseek(int64_t offset, int whence) override; + int64_t stat(linux_abi::kstat& st) override; + int64_t getdents64(uint64_t buf, uint64_t len) override; + int64_t pread_host(void* buf, uint64_t len, uint64_t offset) override; + bool mappable() const override { return !directory_; } + +private: + std::mutex lock_; + std::string content_; + bool directory_; + std::vector> children_; + int64_t position_ = 0; +}; + +// /dev/null, /dev/zero, /dev/urandom, /dev/random, /dev/tty... +class DeviceFile : public OpenFile { +public: + explicit DeviceFile(std::string kind) : kind_(std::move(kind)) {} + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t pread(GuestThread& t, uint64_t buf, uint64_t len, int64_t) override { return read(t, buf, len); } + int64_t stat(linux_abi::kstat& st) override; + int64_t lseek(int64_t, int) override { return 0; } + int64_t ioctl(GuestThread& t, uint64_t request, uint64_t arg) override; + int64_t pread_host(void* buf, uint64_t len, uint64_t) override; + bool mappable() const override { return kind_ == "zero"; } + +private: + std::string kind_; +}; + +// stdout/stderr: lines go to the log. +class ConsoleFile : public OpenFile { +public: + explicit ConsoleFile(int which) : which_(which) {} + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t stat(linux_abi::kstat& st) override; + int64_t ioctl(GuestThread& t, uint64_t request, uint64_t arg) override; + +private: + int which_; +}; + +// Byte pipe shared by a read end and a write end (also socketpair halves). +struct PipeBuffer { + std::mutex lock; + std::deque bytes; + bool writer_closed = false; + bool reader_closed = false; + size_t capacity = 65536; + WaitQueue waiters; +}; + +class PipeFile : public OpenFile { +public: + PipeFile(std::shared_ptr in, std::shared_ptr out, bool socket); + ~PipeFile() override; + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t stat(linux_abi::kstat& st) override; + int64_t ioctl(GuestThread& t, uint64_t request, uint64_t arg) override; + uint32_t poll_mask() override; + WaitQueue* wait_queue() override; + void shutdown(int how); + bool is_socket() const { return socket_; } + +private: + std::shared_ptr in_; // what we read + std::shared_ptr out_; // what we write + bool socket_; + // A combined queue: waiters on either direction. + WaitQueue queue_; +}; + +class EventFile : public OpenFile { +public: + EventFile(uint64_t initial, bool semaphore) : counter_(initial), semaphore_(semaphore) {} + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t stat(linux_abi::kstat& st) override; + uint32_t poll_mask() override; + WaitQueue* wait_queue() override { return &waiters_; } + +private: + std::mutex lock_; + uint64_t counter_; + bool semaphore_; + WaitQueue waiters_; +}; + +class TimerFile : public OpenFile { +public: + explicit TimerFile(int clock) : clock_(clock) {} + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t stat(linux_abi::kstat& st) override; + uint32_t poll_mask() override; + WaitQueue* wait_queue() override { return &waiters_; } + int64_t settime(int flags, const linux_abi::kitimerspec& value, linux_abi::kitimerspec* old); + int64_t gettime(linux_abi::kitimerspec& out); + +private: + uint64_t expirations_locked(); + std::mutex lock_; + int clock_; + int64_t next_ns_ = 0; // absolute monotonic ns of the next expiry (0 = disarmed) + int64_t interval_ns_ = 0; + WaitQueue waiters_; +}; + +class EpollFile : public OpenFile { +public: + struct Interest { + std::weak_ptr file; + uint32_t events; + uint64_t data; + bool disabled = false; // EPOLLONESHOT fired + }; + int64_t ctl(int op, int fd, const linux_abi::kepoll_event* event); + int64_t wait(GuestThread& t, uint64_t events, int max, int64_t deadline_ticks); + int64_t stat(linux_abi::kstat& st) override; + uint32_t poll_mask() override; + +private: + std::mutex lock_; + std::map interests_; +}; + +// The process's descriptor table. +class FdTable { +public: + int install(std::shared_ptr file, bool cloexec, int minimum = 0); + int install_at(int fd, std::shared_ptr file, bool cloexec); + std::shared_ptr get(int fd); + int close(int fd); + bool cloexec(int fd); + void set_cloexec(int fd, bool value); + std::vector list(); + +private: + std::mutex lock_; + std::vector> files_; + std::vector cloexec_; +}; + +FdTable& fds(); + +// Device nodes with a host implementation beyond DeviceFile's. +void register_device(const std::string& name, std::function()> make); +std::shared_ptr make_device(const std::string& name); + +// Opens a guest path (absolute) the way openat would. +int64_t open_path(GuestThread& t, const std::string& path, uint32_t flags, uint32_t mode, std::shared_ptr& out); +// Resolves a path argument relative to a directory descriptor. +int64_t path_at(int dirfd, uint64_t path_addr, std::string& out, bool allow_empty = false); +int64_t stat_path(const std::string& path, bool follow, linux_abi::kstat& st); +void fill_stat_from_attributes(linux_abi::kstat& st, const WIN32_FILE_ATTRIBUTE_DATA& data, const std::string& key); +int64_t errno_from_win32(DWORD error); +uint64_t inode_for(const std::string& key); + +} // namespace axrb diff --git a/direct/src/java_vfs.cpp b/direct/src/java_vfs.cpp new file mode 100644 index 0000000..0f1e6ce --- /dev/null +++ b/direct/src/java_vfs.cpp @@ -0,0 +1,192 @@ +// java.io for the managed runtime: Androitainer's vfs::FileSystem interface, +// implemented on the guest's file system. Handles are guest file descriptors, +// so a FileDescriptor that Java code hands to native code (AssetFileDescriptor, +// ParcelFileDescriptor) is a real descriptor there too. + +#include "arun/vfs.h" + +#include "files.h" +#include "kernel.h" +#include "log.h" +#include "linux.h" +#include "memory.h" + +#include + +namespace arun::vfs { +using namespace axrb::linux_abi; + +namespace { + +axrb::GuestThread& self() { + axrb::GuestThread* t = axrb::current_thread(); + if (!t) axrb::fatal("java.io used from a thread that is not a guest thread"); + return *t; +} + +int64_t sys(uint64_t nr, uint64_t a0 = 0, uint64_t a1 = 0, uint64_t a2 = 0, uint64_t a3 = 0, uint64_t a4 = 0) { + return axrb::do_syscall(self(), nr, a0, a1, a2, a3, a4, 0); +} + +uint64_t ptr(const void* p) { return reinterpret_cast(p); } + +constexpr uint64_t kOpenat = 56, kClose = 57, kLseek = 62, kMkdirat = 34, kUnlinkat = 35, kRenameat = 38, + kFaccessat = 48, kNewfstatat = 79, kGetdents64 = 61, kIoctl = 29; +const uint64_t kCwd = static_cast(static_cast(AT_FDCWD_)); + +} // namespace + +void FileSystem::AddMount(std::string, std::filesystem::path, bool) {} + +int FileSystem::Resolve(std::string_view, bool, std::filesystem::path*) const { return kENOENT; } + +int FileSystem::Open(std::string_view guest, const OpenFlags& flags, int* handle) { + std::string path(guest); + uint32_t mode = flags.write ? O_WRONLY_ : O_RDONLY_; + if (flags.write) { + if (flags.append) mode |= O_APPEND_; + else if (flags.truncate) mode |= O_TRUNC_; + if (flags.create) mode |= O_CREAT_; + if (flags.exclusive) mode |= O_EXCL_; + } + int64_t fd = sys(kOpenat, kCwd, ptr(path.c_str()), mode | O_CLOEXEC_, 0666); + if (fd < 0) return static_cast(-fd); + *handle = static_cast(fd); + return 0; +} + +int FileSystem::Read(int handle, std::span out, std::size_t* n) { + auto file = axrb::fds().get(handle); + if (!file) return kEBADF; + int64_t r = file->read(self(), ptr(out.data()), out.size()); + if (r < 0) return static_cast(-r); + *n = static_cast(r); + return 0; +} + +int FileSystem::Write(int handle, std::span data) { + auto file = axrb::fds().get(handle); + if (!file) return kEBADF; + size_t done = 0; + while (done < data.size()) { + int64_t r = file->write(self(), ptr(data.data() + done), data.size() - done); + if (r < 0) return static_cast(-r); + if (r == 0) return kEIO; + done += static_cast(r); + } + return 0; +} + +int FileSystem::Available(int handle, std::int64_t* bytes) { + auto file = axrb::fds().get(handle); + if (!file) return kEBADF; + kstat st{}; + if (file->stat(st) == 0 && (st.st_mode & S_IFMT_) == S_IFREG_) { + int64_t at = file->lseek(0, 1); + *bytes = at >= 0 ? std::max(0, st.st_size - at) : 0; + return 0; + } + int32_t n = 0; + *bytes = sys(kIoctl, static_cast(handle), FIONREAD_, ptr(&n)) == 0 ? n : 0; + return 0; +} + +int FileSystem::Skip(int handle, std::int64_t n, std::int64_t* skipped) { + auto file = axrb::fds().get(handle); + if (!file) return kEBADF; + int64_t at = file->lseek(0, 1); + if (at >= 0) { + int64_t to = file->lseek(n, 1); + if (to < 0) return static_cast(-to); + *skipped = to - at; + return 0; + } + // Not seekable: read and discard. + std::vector sink(static_cast(std::min(n, 65536))); + int64_t total = 0; + while (total < n) { + int64_t r = file->read(self(), ptr(sink.data()), static_cast(std::min(n - total, sink.size()))); + if (r <= 0) break; + total += r; + } + *skipped = total; + return 0; +} + +int FileSystem::Close(int handle) { + int64_t r = sys(kClose, static_cast(handle)); + return r < 0 ? static_cast(-r) : 0; +} + +bool FileSystem::Access(std::string_view guest, int mode) const { + std::string path(guest); + return sys(kFaccessat, kCwd, ptr(path.c_str()), static_cast(mode)) == 0; +} + +int FileSystem::Stat(std::string_view guest, StatInfo* info) const { + std::string path(guest); + kstat st{}; + int64_t r = sys(kNewfstatat, kCwd, ptr(path.c_str()), ptr(&st), 0); + if (r < 0) return static_cast(-r); + info->is_directory = (st.st_mode & S_IFMT_) == S_IFDIR_; + info->size = static_cast(st.st_size); + info->mtime_ms = st.st_mtime_sec * 1000 + static_cast(st.st_mtime_nsec / 1000000); + return 0; +} + +int FileSystem::Mkdir(std::string_view guest) { + std::string path(guest); + int64_t r = sys(kMkdirat, kCwd, ptr(path.c_str()), 0771); + return r < 0 ? static_cast(-r) : 0; +} + +int FileSystem::Remove(std::string_view guest) { + std::string path(guest); + int64_t r = sys(kUnlinkat, kCwd, ptr(path.c_str()), 0); + if (r == -EISDIR_) r = sys(kUnlinkat, kCwd, ptr(path.c_str()), AT_REMOVEDIR_); + return r < 0 ? static_cast(-r) : 0; +} + +int FileSystem::Rename(std::string_view from, std::string_view to) { + std::string a(from), b(to); + int64_t r = sys(kRenameat, kCwd, ptr(a.c_str()), kCwd, ptr(b.c_str())); + return r < 0 ? static_cast(-r) : 0; +} + +int FileSystem::List(std::string_view guest, std::vector* names) const { + std::string path(guest); + int64_t fd = sys(kOpenat, kCwd, ptr(path.c_str()), O_RDONLY_ | O_DIRECTORY_ | O_CLOEXEC_, 0); + if (fd < 0) return static_cast(-fd); + std::vector buffer(32768); + for (;;) { + int64_t n = sys(kGetdents64, static_cast(fd), ptr(buffer.data()), buffer.size()); + if (n <= 0) break; + for (int64_t at = 0; at < n;) { + uint16_t reclen; + std::memcpy(&reclen, buffer.data() + at + 16, 2); + const char* name = reinterpret_cast(buffer.data() + at + 19); + if (std::strcmp(name, ".") != 0 && std::strcmp(name, "..") != 0) names->push_back(name); + at += reclen; + } + } + sys(kClose, static_cast(fd)); + return 0; +} + +int FileSystem::ReadFile(std::string_view guest, std::vector* bytes) const { + std::string path(guest); + int64_t fd = sys(kOpenat, kCwd, ptr(path.c_str()), O_RDONLY_ | O_CLOEXEC_, 0); + if (fd < 0) return static_cast(-fd); + auto file = axrb::fds().get(static_cast(fd)); + bytes->clear(); + std::uint8_t chunk[65536]; + for (;;) { + int64_t r = file->read(self(), ptr(chunk), sizeof chunk); + if (r <= 0) break; + bytes->insert(bytes->end(), chunk, chunk + r); + } + sys(kClose, static_cast(fd)); + return 0; +} + +} // namespace arun::vfs diff --git a/direct/src/jni.cpp b/direct/src/jni.cpp new file mode 100644 index 0000000..e7ca62a --- /dev/null +++ b/direct/src/jni.cpp @@ -0,0 +1,1401 @@ +// The Java Native Interface between guest native code (arm64, under Dynarmic) +// and the managed runtime (Androitainer's DEX interpreter, on the host). +// +// JNIEnv and JavaVM are host structures whose function tables hold host-call +// trampolines, so guest code calls (*env)->FindClass(...) exactly as on +// Android. References are tagged handles: locals index the calling VM +// thread's root list, globals a process table; jmethodID/jfieldID are the +// runtime's method and field records. Java -> native calls go through +// RuntimeOptions::invoke_guest_native: arguments are laid out per AAPCS64 and +// the guest function runs on the same thread (which is always a guest thread). +// +// GC: a VM thread is "native" (kNative) while guest code runs, so collections +// never wait for it; each JNI function makes it runnable for its duration. + +#include "jni.h" + +#include "cpu.h" +#include "log.h" +#include "memory.h" + +#include "arun/vm/class.h" +#include "arun/vm/class_linker.h" +#include "arun/vm/heap.h" +#include "arun/vm/monitor.h" +#include "arun/vm/natives.h" +#include "arun/vm/object.h" +#include "arun/vm/runtime.h" +#include "arun/vm/thread.h" + +#include +#include +#include +#include +#include +#include + +namespace axrb { + +GuestApi& guest_api() { + static GuestApi api{}; + return api; +} + +namespace jni { +using namespace arun::vm; + +namespace { + +constexpr uint64_t kLocal = 1, kGlobal = 2, kWeak = 3; +constexpr uint32_t kEnvMagic = 0x4a4e4945; // 'JNIE' +constexpr int JNI_OK = 0, JNI_ERR = -1, JNI_EDETACHED = -2, JNI_EVERSION = -3; + +Runtime* g_rt = nullptr; +ClassLoaderRt* g_app_loader = nullptr; + +// What guest code sees as JNIEnv*: the table pointer first, as jni.h requires. +struct Env { + uint64_t functions; + uint32_t magic; + Thread* vm; + GuestThread* guest; + std::vector frames; // PushLocalFrame bases +}; + +struct Vm { + uint64_t functions; +}; + +std::vector g_env_table; +std::vector g_vm_table; +Vm g_vm; + +std::mutex g_globals_lock; +std::vector g_globals; +std::vector g_free_globals; + +std::mutex g_libs_lock; +std::vector> g_libraries; // path, dlopen handle + +// --- references ---------------------------------------------------------------- + +uint64_t make_local(Thread* self, Object* o) { + if (!o) return 0; + size_t index = self->PushRoot(o); + return ((index + 1) << 2) | kLocal; +} + +// The JNI function in progress on this thread, for diagnostics. +struct CurrentCall { + const char* name = nullptr; + uint64_t caller = 0; +}; +thread_local CurrentCall t_call; + +std::string current_call() { + if (!t_call.name) return "host code"; + return std::string(t_call.name) + " from " + describe_address(t_call.caller); +} + +Object* decode_ref(Thread* self, uint64_t ref) { + if (!ref) return nullptr; + uint64_t index = (ref >> 2) - 1; + switch (ref & 3) { + case kLocal: + if (index < self->root_count()) return self->GetRoot(index); + LOGE("jni", "stale local reference %llx in %s", (unsigned long long)ref, current_call().c_str()); + return nullptr; + case kGlobal: + case kWeak: { + std::lock_guard held(g_globals_lock); + if (index < g_globals.size()) return g_globals[index]; + LOGE("jni", "stale global reference %llx in %s", (unsigned long long)ref, current_call().c_str()); + return nullptr; + } + default: + LOGE("jni", "invalid reference %llx", (unsigned long long)ref); + return nullptr; + } +} + +uint64_t make_global(Object* o, uint64_t kind = kGlobal) { + if (!o) return 0; + std::lock_guard held(g_globals_lock); + size_t index; + if (!g_free_globals.empty()) { + index = g_free_globals.back(); + g_free_globals.pop_back(); + g_globals[index] = o; + } else { + index = g_globals.size(); + g_globals.push_back(o); + } + return ((index + 1) << 2) | kind; +} + +void delete_global(uint64_t ref) { + if (!ref || (ref & 3) == kLocal) return; + std::lock_guard held(g_globals_lock); + uint64_t index = (ref >> 2) - 1; + if (index < g_globals.size() && g_globals[index]) { + g_globals[index] = nullptr; + g_free_globals.push_back(index); + } +} + +// --- threads --------------------------------------------------------------------- + +Env* new_env(Thread* vm, GuestThread* guest) { + auto* env = new Env(); + env->functions = reinterpret_cast(g_env_table.data()); + env->magic = kEnvMagic; + env->vm = vm; + env->guest = guest; + return env; +} + +// The Env of the calling guest thread, attaching it to the VM if needed. +Env* attach(bool daemon, const char* name) { + GuestThread* guest = current_thread(); + if (!guest) fatal("JNI used from a host thread"); + if (guest->jni_env) return static_cast(guest->jni_env); + Thread* vm = Thread::Current(); + if (!vm) { + std::string thread_name = name ? name : guest->name; + vm = new Thread(g_rt, thread_name); + vm->daemon = daemon; + vm->AttachCurrent(); + if (!g_rt->AttachPeer(vm, thread_name, daemon)) LOGW("jni", "no java.lang.Thread peer for %s", thread_name.c_str()); + vm->TransitionFromRunnable(ThreadState::kNative); + TRACE("jni", "attached thread %d (%s)", guest->tid, thread_name.c_str()); + } + Env* env = new_env(vm, guest); + guest->jni_env = env; + return env; +} + +Env* env_of(HostCall& c) { + auto* env = reinterpret_cast(untag(c.x(0))); + if (!env || env->magic != kEnvMagic) { + GuestThread* guest = current_thread(); + if (guest && guest->jni_env) return static_cast(guest->jni_env); + fatal("JNI call with an invalid JNIEnv* %llx from %s", (unsigned long long)c.x(0), + describe_address(c.lr()).c_str()); + } + return env; +} + +// Runnable for the duration of a JNI function called from native code. +class Runnable { +public: + explicit Runnable(Thread* t) : t_(t), was_native_(t->state() != ThreadState::kRunnable) { + if (was_native_) t_->TransitionToRunnable(); + } + ~Runnable() { + if (was_native_) t_->TransitionFromRunnable(ThreadState::kNative); + } + +private: + Thread* t_; + bool was_native_; +}; + +// --- classes, methods, fields ---------------------------------------------------- + +std::string to_descriptor(const std::string& jni_name) { + if (!jni_name.empty() && jni_name[0] == '[') return jni_name; + if (jni_name.size() > 1 && jni_name[0] == 'L' && jni_name.back() == ';') return jni_name; + return "L" + jni_name + ";"; +} + +ClassLoaderRt* caller_loader(Thread* self) { + for (Frame* f = self->top_frame(); f; f = f->prev) + if (f->method && f->method->declaring) return f->method->declaring->loader; + return nullptr; +} + +RtClass* find_class(Thread* self, const std::string& name) { + std::string descriptor = to_descriptor(name); + auto& linker = g_rt->class_linker(); + ClassLoaderRt* loader = caller_loader(self); + // Native threads see the system class loader on Android; engines that + // cache classes in JNI_OnLoad never notice, the rest are helped by also + // trying the application's loader. + if (RtClass* c = linker.FindClassNoThrow(self, descriptor, loader ? loader : g_app_loader)) return c; + if (g_app_loader && loader != g_app_loader) + if (RtClass* c = linker.FindClassNoThrow(self, descriptor, g_app_loader)) return c; + if (RtClass* c = linker.FindClassNoThrow(self, descriptor, nullptr)) return c; + LOGW("jni", "FindClass(%s): not found", name.c_str()); + linker.NoteMissing(descriptor); + g_rt->ThrowNew(self, "Ljava/lang/NoClassDefFoundError;", name); + return nullptr; +} + +RtClass* class_of(Thread* self, uint64_t ref) { + Object* mirror = decode_ref(self, ref); + return mirror ? g_rt->ClassFromMirror(mirror) : nullptr; +} + +RtMethod* find_method(RtClass* klass, const std::string& name, const std::string& sig, bool is_static) { + for (RtClass* c = klass; c; c = c->super) { + if (RtMethod* m = c->FindDeclaredMethod(name, sig)) + if (m->IsStatic() == is_static) return m; + } + // Interface methods (abstract or default) for calls through an interface type. + for (RtClass* i : klass->all_interfaces) { + if (RtMethod* m = i->FindDeclaredMethod(name, sig)) + if (m->IsStatic() == is_static) return m; + } + return nullptr; +} + +RtField* find_field(RtClass* klass, const std::string& name, const std::string& sig, bool is_static) { + for (RtClass* c = klass; c; c = c->super) { + if (RtField* f = c->FindDeclaredField(name, sig)) + if (f->IsStatic() == is_static) return f; + if (is_static) { + for (RtClass* i : c->interfaces) + if (RtField* f = i->FindDeclaredField(name, sig)) return f; + } + } + return nullptr; +} + +// The implementation a virtual call reaches for `receiver`. +RtMethod* dispatch(Thread* self, Object* receiver, RtMethod* m) { + if (!receiver || m->IsDirect()) return m; + RtClass* k = receiver->klass; + if (m->declaring->IsInterface()) { + RtMethod* target = g_rt->class_linker().FindInterfaceTarget(self, k, m); + return target ? target : m; + } + if (m->vtable_index >= 0 && static_cast(m->vtable_index) < k->vtable.size()) return k->vtable[m->vtable_index]; + for (RtClass* c = k; c; c = c->super) + if (RtMethod* found = c->FindDeclaredMethod(m->name, m->signature)) + if (!found->IsAbstract()) return found; + return m; +} + +// --- argument sources for Call*Method -------------------------------------------- + +// Reads integer and floating-point arguments the way an AAPCS64 callee does. +class ArgSource { +public: + virtual ~ArgSource() = default; + virtual uint64_t next_int() = 0; + virtual uint64_t next_fp(bool is_float_in_jvalue) = 0; // raw bits: double, or float for jvalue arrays + virtual bool fp_is_double() const { return true; } +}; + +// The "..." of CallXMethod: variadic arguments sit in the next free argument +// registers (AAPCS64 for Linux), then on the stack. +class VarArgs : public ArgSource { +public: + VarArgs(HostCall& c, int first_int) : c_(c), x_(first_int) {} + uint64_t next_int() override { + if (x_ < 8) return c_.x(x_++); + return c_.stack(stack_++); + } + uint64_t next_fp(bool) override { + if (v_ < 8) return c_.v(v_++)[0]; + return c_.stack(stack_++); + } + +private: + HostCall& c_; + int x_; + int v_ = 0; + int stack_ = 0; +}; + +// A va_list: { __stack, __gr_top, __vr_top, __gr_offs, __vr_offs }. +class VaList : public ArgSource { +public: + explicit VaList(uint64_t address) { + uint64_t words[4] = {}; + guest_read(address, words, sizeof words); + stack_ = words[0]; + gr_top_ = words[1]; + vr_top_ = words[2]; + int32_t offs[2] = {}; + guest_read(address + 24, offs, sizeof offs); + gr_offs_ = offs[0]; + vr_offs_ = offs[1]; + } + uint64_t next_int() override { + uint64_t value = 0; + if (gr_offs_ < 0) { + guest_read(gr_top_ + gr_offs_, &value, 8); + gr_offs_ += 8; + } else { + guest_read(stack_, &value, 8); + stack_ += 8; + } + return value; + } + uint64_t next_fp(bool) override { + uint64_t value = 0; + if (vr_offs_ < 0) { + guest_read(vr_top_ + vr_offs_, &value, 8); + vr_offs_ += 16; + } else { + guest_read(stack_, &value, 8); + stack_ += 8; + } + return value; + } + +private: + uint64_t stack_, gr_top_, vr_top_; + int32_t gr_offs_, vr_offs_; +}; + +// A jvalue array. +class JValues : public ArgSource { +public: + explicit JValues(uint64_t address) : at_(address) {} + uint64_t next_int() override { return read(); } + uint64_t next_fp(bool) override { return read(); } + bool fp_is_double() const override { return false; } + +private: + uint64_t read() { + uint64_t value = 0; + guest_read(at_, &value, 8); + at_ += 8; + return value; + } + uint64_t at_; +}; + +// Converts guest arguments to JValues per the method's shorty. +std::vector collect_args(Thread* self, RtMethod* m, ArgSource& src) { + std::vector args; + for (size_t i = 1; i < m->shorty.size(); ++i) { + JValue v{}; + switch (m->shorty[i]) { + case 'L': v.l = decode_ref(self, src.next_int()); break; + case 'J': v.j = static_cast(src.next_int()); break; + case 'Z': v.i = static_cast(src.next_int()) != 0; break; + case 'B': v.i = static_cast(src.next_int()); break; + case 'C': v.i = static_cast(src.next_int()); break; + case 'S': v.i = static_cast(src.next_int()); break; + case 'I': v.i = static_cast(src.next_int()); break; + case 'F': { + uint64_t bits = src.next_fp(true); + if (src.fp_is_double()) { // promoted to double through "..."/va_list + double d; + std::memcpy(&d, &bits, 8); + v.f = static_cast(d); + } else { + uint32_t b32 = static_cast(bits); + std::memcpy(&v.f, &b32, 4); + } + break; + } + case 'D': { + uint64_t bits = src.next_fp(false); + std::memcpy(&v.d, &bits, 8); + break; + } + default: break; + } + args.push_back(v); + } + return args; +} + +enum class CallForm { Var, VaList, Array }; + +// CallMethod{,V,A}, CallNonvirtual..., CallStatic... +void do_call(HostCall& c, char ret, bool is_static, bool nonvirtual, CallForm form) { + Env* e = env_of(c); + Thread* self = e->vm; + Runnable runnable(self); + // (env, obj|clazz, [clazz,] methodID, args...) + int mid_reg = nonvirtual ? 3 : 2; + auto* m = reinterpret_cast(c.x(mid_reg)); + Object* receiver = is_static ? nullptr : decode_ref(self, c.x(1)); + if (!m) { + g_rt->ThrowNew(self, "Ljava/lang/NullPointerException;", "jmethodID is null"); + c.ret(0); + return; + } + std::vector args; + if (form == CallForm::Var) { + VarArgs src(c, mid_reg + 1); + args = collect_args(self, m, src); + } else if (form == CallForm::VaList) { + VaList src(c.x(mid_reg + 1)); + args = collect_args(self, m, src); + } else { + JValues src(c.x(mid_reg + 1)); + args = collect_args(self, m, src); + } + if (!is_static && !receiver) { + g_rt->ThrowNew(self, "Ljava/lang/NullPointerException;", "JNI call of " + m->PrettyName() + " on null"); + c.ret(0); + return; + } + RtMethod* target = (is_static || nonvirtual) ? m : dispatch(self, receiver, m); + if (is_static) g_rt->class_linker().EnsureInitialized(self, target->declaring); + JValue result{}; + bool ok; + { + RootScope roots(self); + if (receiver) roots.Add(receiver); + for (auto& a : args) + if (a.l) roots.Add(a.l); + ok = !self->IsExceptionPending() && g_rt->Invoke(self, target, receiver, args, &result); + } + if (!ok) { + c.ret(0); + c.set_v(0, {0, 0}); + return; + } + switch (ret) { + case 'V': break; + case 'L': c.ret(make_local(self, result.l)); break; + case 'Z': c.ret(result.i ? 1 : 0); break; + case 'B': c.ret(static_cast(static_cast(static_cast(result.i)))); break; + case 'C': c.ret(static_cast(result.i)); break; + case 'S': c.ret(static_cast(static_cast(static_cast(result.i)))); break; + case 'I': c.ret(static_cast(static_cast(result.i))); break; + case 'J': c.ret(static_cast(result.j)); break; + case 'F': c.ret_float(result.f); break; + case 'D': c.ret_double(result.d); break; + } +} + +// --- arrays --------------------------------------------------------------------- + +size_t element_size(char kind) { + switch (kind) { + case 'Z': case 'B': return 1; + case 'C': case 'S': return 2; + case 'I': case 'F': return 4; + default: return 8; + } +} + +RtClass* primitive_array_class(Thread* self, char kind) { + char descriptor[3] = {'[', kind, 0}; + return g_rt->class_linker().FindClass(self, descriptor, nullptr); +} + +Array* array_of(Thread* self, uint64_t ref) { return static_cast(decode_ref(self, ref)); } + +// --- the function table ------------------------------------------------------- + +using Impl = std::function; +std::map g_impls; + +void def(const std::string& name, Impl fn) { g_impls[name] = std::move(fn); } + +std::string utf8_of(Thread* self, Object* s) { return s ? g_rt->GetStringUtf8(s) : std::string(); } + +std::string guest_str(uint64_t address) { + std::string s; + guest_string(address, s, 1 << 20); + return s; +} + +void* host_alloc(size_t bytes) { return std::malloc(bytes ? bytes : 1); } + +void define_functions() { + def("GetVersion", [](HostCall& c) { c.ret(0x00010006); }); + + def("DefineClass", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + g_rt->ThrowNew(e->vm, "Ljava/lang/UnsupportedOperationException;", "DefineClass"); + c.ret(0); + }); + + def("FindClass", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + std::string name = guest_str(c.x(1)); + RtClass* k = find_class(e->vm, name); + TRACE("jni", "FindClass(%s) = %s", name.c_str(), k ? "ok" : "missing"); + c.ret(k ? make_local(e->vm, g_rt->ClassMirror(e->vm, k)) : 0); + }); + + def("FromReflectedMethod", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + if (!o) return c.ret(0); + RtField* f = find_field(o->klass, "vmMethod", "J", false); + c.ret(f ? static_cast(FieldSlots(o)[f->slot].j) : 0); + }); + def("FromReflectedField", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + if (!o) return c.ret(0); + RtField* f = find_field(o->klass, "vmField", "J", false); + c.ret(f ? static_cast(FieldSlots(o)[f->slot].j) : 0); + }); + + def("GetSuperclass", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* k = class_of(e->vm, c.x(1)); + c.ret(k && !k->IsInterface() && k->super ? make_local(e->vm, g_rt->ClassMirror(e->vm, k->super)) : 0); + }); + def("IsAssignableFrom", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* from = class_of(e->vm, c.x(1)); + RtClass* to = class_of(e->vm, c.x(2)); + c.ret(from && to && to->IsAssignableFrom(from) ? 1 : 0); + }); + + def("Throw", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* t = decode_ref(e->vm, c.x(1)); + if (t) e->vm->SetException(t); + c.ret(t ? 0 : static_cast(JNI_ERR)); + }); + def("ThrowNew", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* k = class_of(e->vm, c.x(1)); + if (!k) return c.ret(static_cast(JNI_ERR)); + g_rt->ThrowNew(e->vm, k->descriptor, c.x(2) ? guest_str(c.x(2)) : std::string()); + c.ret(0); + }); + def("ExceptionOccurred", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + c.ret(make_local(e->vm, e->vm->exception())); + }); + def("ExceptionDescribe", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + if (Object* t = e->vm->exception()) { + LOGW("jni", "%s", g_rt->DescribeException(e->vm, t).c_str()); + } + }); + def("ExceptionClear", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + if (e->vm->IsExceptionPending()) TRACE("jni", "ExceptionClear"); + e->vm->ClearException(); + }); + def("FatalError", [](HostCall& c) { + fatal("JNI FatalError: %s (from %s)", guest_str(c.x(1)).c_str(), describe_address(c.lr()).c_str()); + }); + def("ExceptionCheck", [](HostCall& c) { + Env* e = env_of(c); + c.ret(e->vm->IsExceptionPending() ? 1 : 0); + }); + + def("PushLocalFrame", [](HostCall& c) { + Env* e = env_of(c); + e->frames.push_back(e->vm->root_count()); + c.ret(0); + }); + def("PopLocalFrame", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* keep = decode_ref(e->vm, c.x(1)); + if (!e->frames.empty()) { + e->vm->TruncateRoots(e->frames.back()); + e->frames.pop_back(); + } + c.ret(make_local(e->vm, keep)); + }); + def("NewGlobalRef", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + c.ret(make_global(decode_ref(e->vm, c.x(1)))); + }); + def("DeleteGlobalRef", [](HostCall& c) { delete_global(c.x(1)); }); + def("DeleteLocalRef", [](HostCall& c) { + Env* e = env_of(c); + uint64_t ref = c.x(1); + if ((ref & 3) != kLocal || !ref) return; + size_t index = (ref >> 2) - 1; + Thread* t = e->vm; + if (index < t->root_count()) { + t->SetRoot(index, nullptr); + size_t floor = e->frames.empty() ? 0 : e->frames.back(); + size_t n = t->root_count(); + while (n > floor && n - 1 >= index && t->GetRoot(n - 1) == nullptr) --n; + if (n < t->root_count() && n >= floor) t->TruncateRoots(n); + } + }); + def("IsSameObject", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + c.ret(decode_ref(e->vm, c.x(1)) == decode_ref(e->vm, c.x(2)) ? 1 : 0); + }); + def("NewLocalRef", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + c.ret(make_local(e->vm, decode_ref(e->vm, c.x(1)))); + }); + def("EnsureLocalCapacity", [](HostCall& c) { c.ret(0); }); + + def("AllocObject", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* k = class_of(e->vm, c.x(1)); + if (!k || !g_rt->class_linker().EnsureInitialized(e->vm, k)) return c.ret(0); + c.ret(make_local(e->vm, g_rt->heap().AllocObject(e->vm, k))); + }); + for (auto [name, form] : {std::pair{"NewObject", CallForm::Var}, std::pair{"NewObjectV", CallForm::VaList}, + std::pair{"NewObjectA", CallForm::Array}}) { + def(name, [form = form](HostCall& c) { + Env* e = env_of(c); + Thread* self = e->vm; + Runnable r(self); + RtClass* k = class_of(self, c.x(1)); + auto* ctor = reinterpret_cast(c.x(2)); + if (!k || !ctor || !g_rt->class_linker().EnsureInitialized(self, k)) return c.ret(0); + std::vector args; + if (form == CallForm::Var) { + VarArgs src(c, 3); + args = collect_args(self, ctor, src); + } else if (form == CallForm::VaList) { + VaList src(c.x(3)); + args = collect_args(self, ctor, src); + } else { + JValues src(c.x(3)); + args = collect_args(self, ctor, src); + } + Object* created = nullptr; + { + RootScope roots(self); + for (auto& a : args) + if (a.l) roots.Add(a.l); + auto object = roots.Add(g_rt->heap().AllocObject(self, k)); + if (!object.Get()) return c.ret(0); + JValue unused{}; + if (!g_rt->Invoke(self, ctor, object.Get(), args, &unused)) return c.ret(0); + created = object.Get(); + } + // After the scope: a local reference made inside it would be truncated with it. + c.ret(make_local(self, created)); + }); + } + + def("GetObjectClass", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + c.ret(o ? make_local(e->vm, g_rt->ClassMirror(e->vm, o->klass)) : 0); + }); + def("IsInstanceOf", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + RtClass* k = class_of(e->vm, c.x(2)); + c.ret(!o || (k && k->IsAssignableFrom(o->klass)) ? 1 : 0); + }); + + for (bool is_static : {false, true}) { + def(is_static ? "GetStaticMethodID" : "GetMethodID", [is_static](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* k = class_of(e->vm, c.x(1)); + std::string name = guest_str(c.x(2)), sig = guest_str(c.x(3)); + if (!k) return c.ret(0); + if (!g_rt->class_linker().EnsureInitialized(e->vm, k)) return c.ret(0); + RtMethod* m = find_method(k, name, sig, is_static); + if (!m) { + LOGW("jni", "Get%sMethodID(%s, %s%s): not found", is_static ? "Static" : "", k->descriptor.c_str(), + name.c_str(), sig.c_str()); + g_rt->ThrowNew(e->vm, "Ljava/lang/NoSuchMethodError;", k->descriptor + "." + name + sig); + } + c.ret(reinterpret_cast(m)); + }); + def(is_static ? "GetStaticFieldID" : "GetFieldID", [is_static](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* k = class_of(e->vm, c.x(1)); + std::string name = guest_str(c.x(2)), sig = guest_str(c.x(3)); + if (!k) return c.ret(0); + if (!g_rt->class_linker().EnsureInitialized(e->vm, k)) return c.ret(0); + RtField* f = find_field(k, name, sig, is_static); + if (!f) { + LOGW("jni", "Get%sFieldID(%s, %s %s): not found", is_static ? "Static" : "", k->descriptor.c_str(), + name.c_str(), sig.c_str()); + g_rt->ThrowNew(e->vm, "Ljava/lang/NoSuchFieldError;", k->descriptor + "." + name); + } + c.ret(reinterpret_cast(f)); + }); + } + + static const std::pair kTypes[] = {{"Object", 'L'}, {"Boolean", 'Z'}, {"Byte", 'B'}, {"Char", 'C'}, + {"Short", 'S'}, {"Int", 'I'}, {"Long", 'J'}, {"Float", 'F'}, + {"Double", 'D'}, {"Void", 'V'}}; + for (auto [type, kind] : kTypes) { + for (auto [suffix, form] : {std::pair{"", CallForm::Var}, std::pair{"V", CallForm::VaList}, + std::pair{"A", CallForm::Array}}) { + std::string t = type; + def("Call" + t + "Method" + suffix, [kind = kind, form = form](HostCall& c) { do_call(c, kind, false, false, form); }); + def("CallNonvirtual" + t + "Method" + suffix, + [kind = kind, form = form](HostCall& c) { do_call(c, kind, false, true, form); }); + def("CallStatic" + t + "Method" + suffix, [kind = kind, form = form](HostCall& c) { do_call(c, kind, true, false, form); }); + } + if (kind == 'V') continue; + std::string t = type; + // Get/SetField and the static variants. + for (bool is_static : {false, true}) { + def(std::string(is_static ? "GetStatic" : "Get") + t + "Field", [kind = kind, is_static](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + auto* f = reinterpret_cast(c.x(2)); + if (!f) return c.ret(0); + Slot* slot; + if (is_static) { + g_rt->class_linker().EnsureInitialized(e->vm, f->declaring); + slot = &f->declaring->statics[f->slot]; + } else { + Object* o = decode_ref(e->vm, c.x(1)); + if (!o) { + g_rt->ThrowNew(e->vm, "Ljava/lang/NullPointerException;", "field read on null"); + return c.ret(0); + } + slot = &FieldSlots(o)[f->slot]; + } + switch (kind) { + case 'L': c.ret(make_local(e->vm, slot->l)); break; + case 'Z': c.ret(slot->i ? 1 : 0); break; + case 'B': c.ret(static_cast(static_cast(static_cast(slot->i)))); break; + case 'C': c.ret(static_cast(slot->i)); break; + case 'S': c.ret(static_cast(static_cast(static_cast(slot->i)))); break; + case 'I': c.ret(static_cast(static_cast(slot->i))); break; + case 'J': c.ret(static_cast(slot->j)); break; + case 'F': c.ret_float(slot->f); break; + case 'D': c.ret_double(slot->d); break; + } + }); + def(std::string(is_static ? "SetStatic" : "Set") + t + "Field", [kind = kind, is_static](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + auto* f = reinterpret_cast(c.x(2)); + if (!f) return; + Slot* slot; + if (is_static) { + g_rt->class_linker().EnsureInitialized(e->vm, f->declaring); + slot = &f->declaring->statics[f->slot]; + } else { + Object* o = decode_ref(e->vm, c.x(1)); + if (!o) { + g_rt->ThrowNew(e->vm, "Ljava/lang/NullPointerException;", "field write on null"); + return; + } + slot = &FieldSlots(o)[f->slot]; + } + uint64_t v = c.x(3); + switch (kind) { + case 'L': slot->l = decode_ref(e->vm, v); break; + case 'Z': slot->i = static_cast(v) != 0; break; + case 'B': slot->i = static_cast(v); break; + case 'C': slot->i = static_cast(v); break; + case 'S': slot->i = static_cast(v); break; + case 'I': slot->i = static_cast(v); break; + case 'J': slot->j = static_cast(v); break; + case 'F': slot->f = c.s(0); break; + case 'D': slot->d = c.d(0); break; + } + }); + } + if (kind == 'L') continue; + // Primitive arrays. The heap does not move objects, so element access + // hands out the array's own storage. + def("New" + t + "Array", [kind = kind](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* k = primitive_array_class(e->vm, kind); + int32_t n = static_cast(c.x(1)); + if (!k || n < 0) return c.ret(0); + c.ret(make_local(e->vm, g_rt->heap().AllocArray(e->vm, k, n))); + }); + def("Get" + t + "ArrayElements", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Array* a = array_of(e->vm, c.x(1)); + if (c.x(2)) { + uint8_t no = 0; + guest_write(c.x(2), &no, 1); + } + c.ret(a ? reinterpret_cast(a->data()) : 0); + }); + def("Release" + t + "ArrayElements", [](HostCall&) {}); + for (bool set : {false, true}) { + def(std::string(set ? "Set" : "Get") + t + "ArrayRegion", [kind = kind, set](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Array* a = array_of(e->vm, c.x(1)); + int32_t start = static_cast(c.x(2)), len = static_cast(c.x(3)); + if (!a || start < 0 || len < 0 || start + len > a->length) { + g_rt->ThrowNew(e->vm, "Ljava/lang/ArrayIndexOutOfBoundsException;", "JNI array region"); + return; + } + size_t es = element_size(kind); + uint8_t* data = a->data() + static_cast(start) * es; + if (set) guest_read(c.x(4), data, static_cast(len) * es); + else guest_write(c.x(4), data, static_cast(len) * es); + }); + } + } + + def("NewString", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + int32_t n = static_cast(c.x(2)); + std::u16string chars(static_cast(std::max(0, n)), u'\0'); + if (n > 0) guest_read(c.x(1), chars.data(), chars.size() * 2); + c.ret(make_local(e->vm, g_rt->NewString(e->vm, chars))); + }); + def("GetStringLength", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + c.ret(g_rt->GetString(decode_ref(e->vm, c.x(1))).size()); + }); + auto string_chars = [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + std::u16string s = g_rt->GetString(decode_ref(e->vm, c.x(1))); + auto* out = static_cast(host_alloc((s.size() + 1) * 2)); + std::memcpy(out, s.c_str(), (s.size() + 1) * 2); + if (c.x(2)) { + uint8_t yes = 1; + guest_write(c.x(2), &yes, 1); + } + c.ret(reinterpret_cast(out)); + }; + def("GetStringChars", string_chars); + def("GetStringCritical", string_chars); + def("ReleaseStringChars", [](HostCall& c) { std::free(reinterpret_cast(c.x(2))); }); + def("ReleaseStringCritical", [](HostCall& c) { std::free(reinterpret_cast(c.x(2))); }); + def("NewStringUTF", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + if (!c.x(1)) return c.ret(0); + c.ret(make_local(e->vm, g_rt->NewStringUtf8(e->vm, guest_str(c.x(1))))); + }); + def("GetStringUTFLength", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + c.ret(utf8_of(e->vm, decode_ref(e->vm, c.x(1))).size()); + }); + def("GetStringUTFChars", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + std::string s = utf8_of(e->vm, decode_ref(e->vm, c.x(1))); + auto* out = static_cast(host_alloc(s.size() + 1)); + std::memcpy(out, s.c_str(), s.size() + 1); + if (c.x(2)) { + uint8_t yes = 1; + guest_write(c.x(2), &yes, 1); + } + c.ret(reinterpret_cast(out)); + }); + def("ReleaseStringUTFChars", [](HostCall& c) { std::free(reinterpret_cast(c.x(2))); }); + def("GetStringRegion", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + std::u16string s = g_rt->GetString(decode_ref(e->vm, c.x(1))); + int32_t start = static_cast(c.x(2)), len = static_cast(c.x(3)); + if (start < 0 || len < 0 || static_cast(start + len) > s.size()) { + g_rt->ThrowNew(e->vm, "Ljava/lang/StringIndexOutOfBoundsException;", "GetStringRegion"); + return; + } + guest_write(c.x(4), s.data() + start, static_cast(len) * 2); + }); + def("GetStringUTFRegion", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + std::u16string s = g_rt->GetString(decode_ref(e->vm, c.x(1))); + int32_t start = static_cast(c.x(2)), len = static_cast(c.x(3)); + if (start < 0 || len < 0 || static_cast(start + len) > s.size()) { + g_rt->ThrowNew(e->vm, "Ljava/lang/StringIndexOutOfBoundsException;", "GetStringUTFRegion"); + return; + } + Object* sub = g_rt->NewString(e->vm, std::u16string_view(s).substr(static_cast(start), static_cast(len))); + std::string u = utf8_of(e->vm, sub); + guest_write(c.x(4), u.c_str(), u.size() + 1); + }); + + def("GetArrayLength", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Array* a = array_of(e->vm, c.x(1)); + c.ret(a ? static_cast(a->length) : 0); + }); + def("NewObjectArray", [](HostCall& c) { + Env* e = env_of(c); + Thread* self = e->vm; + Runnable r(self); + int32_t n = static_cast(c.x(1)); + RtClass* element = class_of(self, c.x(2)); + Object* init = decode_ref(self, c.x(3)); + if (!element || n < 0) return c.ret(0); + RtClass* k = g_rt->class_linker().FindClass(self, "[" + element->descriptor, element->loader); + if (!k) return c.ret(0); + Array* a; + { + RootScope roots(self); + if (init) roots.Add(init); + a = g_rt->heap().AllocArray(self, k, n); + if (a && init) + for (int32_t i = 0; i < n; ++i) a->elements()[i] = init; + } + c.ret(make_local(self, a)); + }); + def("GetObjectArrayElement", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Array* a = array_of(e->vm, c.x(1)); + int32_t i = static_cast(c.x(2)); + if (!a || i < 0 || i >= a->length) { + g_rt->ThrowNew(e->vm, "Ljava/lang/ArrayIndexOutOfBoundsException;", std::to_string(i)); + return c.ret(0); + } + c.ret(make_local(e->vm, a->elements()[i])); + }); + def("SetObjectArrayElement", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Array* a = array_of(e->vm, c.x(1)); + int32_t i = static_cast(c.x(2)); + if (!a || i < 0 || i >= a->length) { + g_rt->ThrowNew(e->vm, "Ljava/lang/ArrayIndexOutOfBoundsException;", std::to_string(i)); + return; + } + a->elements()[i] = decode_ref(e->vm, c.x(3)); + }); + def("GetPrimitiveArrayCritical", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Array* a = array_of(e->vm, c.x(1)); + if (c.x(2)) { + uint8_t no = 0; + guest_write(c.x(2), &no, 1); + } + c.ret(a ? reinterpret_cast(a->data()) : 0); + }); + def("ReleasePrimitiveArrayCritical", [](HostCall&) {}); + + def("RegisterNatives", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + RtClass* k = class_of(e->vm, c.x(1)); + int32_t n = static_cast(c.x(3)); + if (!k) return c.ret(static_cast(JNI_ERR)); + for (int32_t i = 0; i < n; ++i) { + uint64_t entry[3] = {}; + guest_read(c.x(2) + 24ull * static_cast(i), entry, sizeof entry); + std::string name = guest_str(entry[0]), sig = guest_str(entry[1]); + RtMethod* m = k->FindDeclaredMethod(name, sig); + if (!m || !m->IsNative()) { + LOGW("jni", "RegisterNatives: %s.%s%s is not a native method", k->descriptor.c_str(), name.c_str(), sig.c_str()); + g_rt->ThrowNew(e->vm, "Ljava/lang/NoSuchMethodError;", k->descriptor + "." + name + sig); + return c.ret(static_cast(JNI_ERR)); + } + m->jni_entry = reinterpret_cast(entry[2]); + TRACE("jni", "RegisterNatives %s.%s%s -> %s", k->descriptor.c_str(), name.c_str(), sig.c_str(), + describe_address(entry[2]).c_str()); + } + c.ret(0); + }); + def("UnregisterNatives", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + if (RtClass* k = class_of(e->vm, c.x(1))) + for (auto& m : k->methods) + if (m.IsNative()) m.jni_entry = nullptr; + c.ret(0); + }); + + def("MonitorEnter", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + c.ret(o && MonitorEnter(e->vm, o) ? 0 : static_cast(JNI_ERR)); + }); + def("MonitorExit", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + c.ret(o && MonitorExit(e->vm, o) ? 0 : static_cast(JNI_ERR)); + }); + def("GetJavaVM", [](HostCall& c) { + uint64_t vm = reinterpret_cast(&g_vm); + c.ret(guest_write(c.x(1), &vm, 8) ? 0 : static_cast(JNI_ERR)); + }); + + def("NewWeakGlobalRef", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + c.ret(make_global(decode_ref(e->vm, c.x(1)), kWeak)); + }); + def("DeleteWeakGlobalRef", [](HostCall& c) { delete_global(c.x(1)); }); + def("GetObjectRefType", [](HostCall& c) { + uint64_t ref = c.x(1); + c.ret(ref ? (ref & 3) : 0); + }); + + // Direct buffers: java.nio.DirectByteBuffer(long address, int capacity). + def("NewDirectByteBuffer", [](HostCall& c) { + Env* e = env_of(c); + Thread* self = e->vm; + Runnable r(self); + RtClass* k = g_rt->class_linker().FindClass(self, "Ljava/nio/DirectByteBuffer;", nullptr); + if (!k || !g_rt->class_linker().EnsureInitialized(self, k)) return c.ret(0); + RtMethod* ctor = k->FindDeclaredMethod("", "(JI)V"); + if (!ctor) return c.ret(0); + Object* created = nullptr; + { + RootScope roots(self); + auto object = roots.Add(g_rt->heap().AllocObject(self, k)); + JValue args[2]; + args[0].j = static_cast(untag(c.x(1))); + args[1].i = static_cast(c.x(2)); + JValue unused{}; + if (!object.Get() || !g_rt->Invoke(self, ctor, object.Get(), args, &unused)) return c.ret(0); + created = object.Get(); + } + c.ret(make_local(self, created)); + }); + def("GetDirectBufferAddress", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + RtField* f = o ? find_field(o->klass, "address", "J", false) : nullptr; + c.ret(f ? static_cast(FieldSlots(o)[f->slot].j) : 0); + }); + def("GetDirectBufferCapacity", [](HostCall& c) { + Env* e = env_of(c); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, c.x(1)); + RtField* f = o ? find_field(o->klass, "capacity", "I", false) : nullptr; + c.ret(f ? static_cast(static_cast(FieldSlots(o)[f->slot].i)) : static_cast(-1)); + }); +} + +void define_invoke_functions(std::map& impls) { + impls["DestroyJavaVM"] = [](HostCall& c) { c.ret(static_cast(JNI_ERR)); }; + auto attach_fn = [](bool daemon) { + return [daemon](HostCall& c) { + const char* name = nullptr; + std::string name_storage; + if (c.x(2)) { // JavaVMAttachArgs { jint version; const char* name; jobject group; } + uint64_t name_ptr = 0; + guest_read(c.x(2) + 8, &name_ptr, 8); + if (name_ptr && guest_string(name_ptr, name_storage, 256)) name = name_storage.c_str(); + } + Env* env = attach(daemon, name); + uint64_t value = reinterpret_cast(env); + c.ret(guest_write(c.x(1), &value, 8) ? JNI_OK : static_cast(JNI_ERR)); + }; + }; + impls["AttachCurrentThread"] = attach_fn(false); + impls["AttachCurrentThreadAsDaemon"] = attach_fn(true); + impls["DetachCurrentThread"] = [](HostCall& c) { + GuestThread* guest = current_thread(); + if (!guest || !guest->jni_env) return c.ret(0); + auto* env = static_cast(guest->jni_env); + Thread* vm = env->vm; + // Threads the VM started (Java threads) stay attached until their run() ends. + if (vm->top_frame()) return c.ret(static_cast(JNI_ERR)); + guest->jni_env = nullptr; + vm->TransitionToRunnable(); + vm->TruncateRoots(0); + vm->peer = nullptr; + vm->DetachCurrent(); + delete vm; + delete env; + TRACE("jni", "detached thread %d", guest->tid); + c.ret(0); + }; + impls["GetEnv"] = [](HostCall& c) { + GuestThread* guest = current_thread(); + uint64_t value = 0; + int result = JNI_EDETACHED; + if (guest && guest->jni_env) { + value = reinterpret_cast(guest->jni_env); + result = JNI_OK; + } else if (guest && Thread::Current()) { + value = reinterpret_cast(attach(false, nullptr)); + result = JNI_OK; + } + guest_write(c.x(1), &value, 8); + c.ret(static_cast(static_cast(result))); + }; +} + +void build_table(const std::vector& slots, const std::map& impls, const char* prefix, + std::vector& table) { + table.assign(slots.size(), 0); + for (size_t i = 0; i < slots.size(); ++i) { + const std::string& name = slots[i]; + if (name.rfind("reserved", 0) == 0) continue; + auto it = impls.find(name); + Impl fn; + if (it != impls.end()) { + fn = [name, inner = it->second](HostCall& c) { + CurrentCall saved = t_call; + t_call = {name.c_str(), c.lr()}; + inner(c); + t_call = saved; + }; + } else { + fn = [name](HostCall& c) { + LOGE("jni", "unimplemented JNI function %s called from %s", name.c_str(), describe_address(c.lr()).c_str()); + c.ret(0); + }; + } + table[i] = host_trampoline(std::string(prefix) + name, std::move(fn)); + } +} + +// --- Java -> guest native calls ------------------------------------------------ + +std::string mangle(const std::string& s) { + std::string out; + for (unsigned char ch : s) { + if (ch == '/') out += '_'; + else if (ch == '_') out += "_1"; + else if (ch == ';') out += "_2"; + else if (ch == '[') out += "_3"; + else if (std::isalnum(ch)) out += static_cast(ch); + else { + char buf[8]; + std::snprintf(buf, sizeof buf, "_0%04x", ch); + out += buf; + } + } + return out; +} + +uint64_t resolve_native(RtMethod* m) { + std::string cls = m->declaring->descriptor.substr(1, m->declaring->descriptor.size() - 2); + std::string short_name = "Java_" + mangle(cls) + "_" + mangle(m->name); + if (uint64_t f = find_symbol(short_name)) return f; + std::string params = m->signature.substr(1, m->signature.find(')') - 1); + return find_symbol(short_name + "__" + mangle(params)); +} + +bool invoke_guest_native(Thread* self, RtMethod* m, Frame* frame, JValue* result) { + uint64_t entry = reinterpret_cast(m->jni_entry); + if (!entry) { + entry = resolve_native(m); + if (!entry) { + LOGW("jni", "no native implementation for %s", m->PrettyName().c_str()); + return false; + } + m->jni_entry = reinterpret_cast(entry); + } + GuestThread* guest = current_thread(); + if (!guest) fatal("native method %s called on a host thread", m->PrettyName().c_str()); + Env* env = static_cast(guest->jni_env); + if (!env) { + env = new_env(self, guest); + guest->jni_env = env; + } + size_t base = self->root_count(); + GuestArgs args; + std::vector spill; // stack arguments, in order + auto put_int = [&](uint64_t v) { + if (args.ints.size() < 8) args.ints.push_back(v); + else args.stack.push_back(v); + }; + auto put_fp = [&](uint64_t bits) { + if (args.fps.size() < 8) args.fps.push_back({bits, 0}); + else args.stack.push_back(bits); + }; + put_int(reinterpret_cast(env)); + uint32_t reg = 0; + if (m->IsStatic()) { + put_int(make_local(self, g_rt->ClassMirror(self, m->declaring))); + } else { + put_int(make_local(self, frame->refs[0])); + reg = 1; + } + for (size_t i = 1; i < m->shorty.size(); ++i) { + switch (m->shorty[i]) { + case 'L': put_int(make_local(self, frame->refs[reg])); reg += 1; break; + case 'J': put_int(static_cast(frame->regs[reg]) | (static_cast(frame->regs[reg + 1]) << 32)); reg += 2; break; + case 'D': put_fp(static_cast(frame->regs[reg]) | (static_cast(frame->regs[reg + 1]) << 32)); reg += 2; break; + case 'F': put_fp(frame->regs[reg]); reg += 1; break; + case 'Z': put_int(frame->regs[reg] & 0xff ? 1 : 0); reg += 1; break; + case 'B': put_int(static_cast(static_cast(static_cast(frame->regs[reg])))); reg += 1; break; + case 'C': put_int(static_cast(frame->regs[reg])); reg += 1; break; + case 'S': put_int(static_cast(static_cast(static_cast(frame->regs[reg])))); reg += 1; break; + default: put_int(static_cast(static_cast(static_cast(frame->regs[reg])))); reg += 1; break; + } + } + TRACE("jni", "-> %s", m->PrettyName().c_str()); + Vec128 fp{}; + self->TransitionFromRunnable(ThreadState::kNative); + uint64_t r = call_guest_args(*guest, entry, args, &fp); + self->TransitionToRunnable(); + TRACE("jni", "<- %s", m->PrettyName().c_str()); + switch (m->ReturnKind()) { + case 'V': break; + case 'L': result->l = decode_ref(self, r); break; + case 'Z': result->i = (r & 0xff) != 0; break; + case 'B': result->i = static_cast(r); break; + case 'C': result->i = static_cast(r); break; + case 'S': result->i = static_cast(r); break; + case 'I': result->i = static_cast(r); break; + case 'J': result->j = static_cast(r); break; + case 'F': { + uint32_t bits = static_cast(fp[0]); + std::memcpy(&result->f, &bits, 4); + break; + } + case 'D': std::memcpy(&result->d, &fp[0], 8); break; + } + // The returned object must survive the truncation: the interpreter roots + // it through the thread's result_ref once we return. + if (m->ReturnKind() == 'L') self->result_ref = result->l; + self->TruncateRoots(base); + if (env->frames.size() && env->frames.back() > base) env->frames.clear(); + return true; +} + +// Runtime.nativeLoad / nativeLoadLibrary(String, ClassLoader): String error. +void Runtime_nativeLoad(Thread* self, Frame* a, JValue* r) { + std::string name = g_rt->GetStringUtf8(ArgRef(a, 0)); + std::string path = name; + if (path.find('/') == std::string::npos) path = "lib" + name + ".so"; // nativeLoadLibrary: a bare name + std::string error = load_library(self, path); + r->l = error.empty() ? nullptr : g_rt->NewStringUtf8(self, error); +} + +} // namespace + +void configure(RuntimeOptions& options) { + options.invoke_guest_native = invoke_guest_native; + options.extra_roots = [](const std::function& visit) { + std::lock_guard held(g_globals_lock); + for (Object* o : g_globals) + if (o) visit(o); + }; +} + +void install(Runtime* runtime) { + g_rt = runtime; + static const std::vector env_slots = { +#define JNI_SLOT(name) #name, +#include "jni_table.inc" +#undef JNI_SLOT + }; + static const std::vector vm_slots = { +#define JNI_SLOT(name) #name, +#include "jni_invoke_table.inc" +#undef JNI_SLOT + }; + define_functions(); + build_table(env_slots, g_impls, "jni.", g_env_table); + std::map invoke; + define_invoke_functions(invoke); + build_table(vm_slots, invoke, "jvm.", g_vm_table); + g_vm.functions = reinterpret_cast(g_vm_table.data()); + runtime->RegisterNative("Ljava/lang/Runtime;.nativeLoadLibrary(Ljava/lang/String;Ljava/lang/ClassLoader;)Ljava/lang/String;", + Runtime_nativeLoad); + runtime->RegisterNative("Ljava/lang/Runtime;.nativeLoad(Ljava/lang/String;Ljava/lang/ClassLoader;)Ljava/lang/String;", + Runtime_nativeLoad); +} + +void set_app_loader(ClassLoaderRt* loader) { g_app_loader = loader; } + +uint64_t current_env() { return reinterpret_cast(attach(false, nullptr)); } +uint64_t java_vm() { return reinterpret_cast(&g_vm); } +uint64_t local_ref(Thread* self, Object* object) { return make_local(self, object); } +uint64_t global_ref(Object* object) { return make_global(object); } +Object* decode(Thread* self, uint64_t ref) { return decode_ref(self, ref); } + +bool object_field(uint64_t ref, const char* name, const char* type, uint64_t* raw) { + Env* e = attach(false, nullptr); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, ref); + RtField* f = o ? find_field(o->klass, name, type, false) : nullptr; + if (!f) return false; + *raw = FieldSlots(o)[f->slot].raw; + return true; +} + +uint64_t retain(uint64_t ref) { + Env* e = attach(false, nullptr); + Runnable r(e->vm); + Object* o = decode_ref(e->vm, ref); + return o ? make_global(o) : 0; +} + +void release(uint64_t global) { + if (global) delete_global(global); +} + +std::string load_library(Thread* self, const std::string& path) { + { + std::lock_guard held(g_libs_lock); + for (auto& [p, h] : g_libraries) + if (p == path) return {}; + } + GuestThread* guest = current_thread(); + if (!guest) return "loadLibrary on a host thread"; + auto& api = guest_api(); + self->TransitionFromRunnable(ThreadState::kNative); + uint64_t handle = call_guest(*guest, api.dlopen, {reinterpret_cast(path.c_str()), 2 /*RTLD_NOW*/}); + std::string error; + if (!handle) { + uint64_t message = call_guest(*guest, api.dlerror, {}); + error = message ? guest_str(message) : "dlopen failed"; + } + self->TransitionToRunnable(); + if (!handle) { + LOGE("jni", "loadLibrary(%s): %s", path.c_str(), error.c_str()); + return error; + } + { + std::lock_guard held(g_libs_lock); + g_libraries.push_back({path, handle}); + } + LOGI("jni", "loaded %s", path.c_str()); + self->TransitionFromRunnable(ThreadState::kNative); + uint64_t onload = call_guest(*guest, api.dlsym, {handle, reinterpret_cast("JNI_OnLoad")}); + if (onload) { + Env* env = static_cast(guest->jni_env); + if (!env) { + env = new_env(self, guest); + guest->jni_env = env; + } + uint64_t version = call_guest(*guest, onload, {reinterpret_cast(&g_vm), 0}); + LOGI("jni", "%s: JNI_OnLoad returned %llx", path.c_str(), (unsigned long long)version); + } + self->TransitionToRunnable(); + return {}; +} + +uint64_t find_symbol(const std::string& name) { + std::vector handles; + { + std::lock_guard held(g_libs_lock); + for (auto& [p, h] : g_libraries) handles.push_back(h); + } + GuestThread* guest = current_thread(); + if (!guest) return 0; + Thread* vm = Thread::Current(); + bool runnable = vm && vm->state() == ThreadState::kRunnable; + if (runnable) vm->TransitionFromRunnable(ThreadState::kNative); + uint64_t found = 0; + for (uint64_t h : handles) { + found = call_guest(*guest, guest_api().dlsym, {h, reinterpret_cast(name.c_str())}); + if (found) break; + } + if (runnable) vm->TransitionToRunnable(); + return found; +} + +} // namespace jni +} // namespace axrb diff --git a/direct/src/jni.h b/direct/src/jni.h new file mode 100644 index 0000000..95e3a67 --- /dev/null +++ b/direct/src/jni.h @@ -0,0 +1,58 @@ +#pragma once + +#include "kernel.h" + +#include +#include + +namespace arun::vm { +class Runtime; +class Thread; +struct Object; +struct RuntimeOptions; +struct ClassLoaderRt; +} // namespace arun::vm + +namespace axrb { + +// Guest facilities the launcher (direct/guest/axrb_app.c) provides. +struct GuestApi { + uint32_t version; + uint32_t size; + uint64_t dlopen, dlsym, dlerror, dlclose, android_dlopen_ext, spawn_thread, malloc, free, dladdr; +}; +GuestApi& guest_api(); + +namespace jni { + +// Hooks the managed runtime needs before it is created (guest natives, JNI +// global references as GC roots). +void configure(arun::vm::RuntimeOptions& options); +// After creation: the JNI function tables and the natives that load libraries. +void install(arun::vm::Runtime* runtime); +void set_app_loader(arun::vm::ClassLoaderRt* loader); + +// JNIEnv* of the calling guest thread (attaching it to the VM if needed) and +// the process's JavaVM*. +uint64_t current_env(); +uint64_t java_vm(); + +// Local references for host code that hands objects to guest code. +uint64_t local_ref(arun::vm::Thread* self, arun::vm::Object* object); +uint64_t global_ref(arun::vm::Object* object); +arun::vm::Object* decode(arun::vm::Thread* self, uint64_t ref); + +// For host code serving guest native APIs (ANativeWindow_fromSurface...): reads +// an instance field of the object a guest JNI reference names, and keeps +// objects alive across calls as JNI global references. +bool object_field(uint64_t ref, const char* name, const char* type, uint64_t* raw); +uint64_t retain(uint64_t ref); +void release(uint64_t global); + +// dlopen + JNI_OnLoad, as Runtime.nativeLoad does. Empty string on success. +std::string load_library(arun::vm::Thread* self, const std::string& path); +// dlsym across every library loaded so far. +uint64_t find_symbol(const std::string& name); + +} // namespace jni +} // namespace axrb diff --git a/direct/src/jni_invoke_table.inc b/direct/src/jni_invoke_table.inc new file mode 100644 index 0000000..66a8a15 --- /dev/null +++ b/direct/src/jni_invoke_table.inc @@ -0,0 +1,9 @@ +// Generated from the NDK's jni.h: struct JNIInvokeInterface slots, in order. +JNI_SLOT(reserved0) +JNI_SLOT(reserved1) +JNI_SLOT(reserved2) +JNI_SLOT(DestroyJavaVM) +JNI_SLOT(AttachCurrentThread) +JNI_SLOT(DetachCurrentThread) +JNI_SLOT(GetEnv) +JNI_SLOT(AttachCurrentThreadAsDaemon) diff --git a/direct/src/jni_table.inc b/direct/src/jni_table.inc new file mode 100644 index 0000000..833d565 --- /dev/null +++ b/direct/src/jni_table.inc @@ -0,0 +1,234 @@ +// Generated from the NDK's jni.h: struct JNINativeInterface slots, in order. +JNI_SLOT(reserved0) +JNI_SLOT(reserved1) +JNI_SLOT(reserved2) +JNI_SLOT(reserved3) +JNI_SLOT(GetVersion) +JNI_SLOT(DefineClass) +JNI_SLOT(FindClass) +JNI_SLOT(FromReflectedMethod) +JNI_SLOT(FromReflectedField) +JNI_SLOT(ToReflectedMethod) +JNI_SLOT(GetSuperclass) +JNI_SLOT(IsAssignableFrom) +JNI_SLOT(ToReflectedField) +JNI_SLOT(Throw) +JNI_SLOT(ThrowNew) +JNI_SLOT(ExceptionOccurred) +JNI_SLOT(ExceptionDescribe) +JNI_SLOT(ExceptionClear) +JNI_SLOT(FatalError) +JNI_SLOT(PushLocalFrame) +JNI_SLOT(PopLocalFrame) +JNI_SLOT(NewGlobalRef) +JNI_SLOT(DeleteGlobalRef) +JNI_SLOT(DeleteLocalRef) +JNI_SLOT(IsSameObject) +JNI_SLOT(NewLocalRef) +JNI_SLOT(EnsureLocalCapacity) +JNI_SLOT(AllocObject) +JNI_SLOT(NewObject) +JNI_SLOT(NewObjectV) +JNI_SLOT(NewObjectA) +JNI_SLOT(GetObjectClass) +JNI_SLOT(IsInstanceOf) +JNI_SLOT(GetMethodID) +JNI_SLOT(CallObjectMethod) +JNI_SLOT(CallObjectMethodV) +JNI_SLOT(CallObjectMethodA) +JNI_SLOT(CallBooleanMethod) +JNI_SLOT(CallBooleanMethodV) +JNI_SLOT(CallBooleanMethodA) +JNI_SLOT(CallByteMethod) +JNI_SLOT(CallByteMethodV) +JNI_SLOT(CallByteMethodA) +JNI_SLOT(CallCharMethod) +JNI_SLOT(CallCharMethodV) +JNI_SLOT(CallCharMethodA) +JNI_SLOT(CallShortMethod) +JNI_SLOT(CallShortMethodV) +JNI_SLOT(CallShortMethodA) +JNI_SLOT(CallIntMethod) +JNI_SLOT(CallIntMethodV) +JNI_SLOT(CallIntMethodA) +JNI_SLOT(CallLongMethod) +JNI_SLOT(CallLongMethodV) +JNI_SLOT(CallLongMethodA) +JNI_SLOT(CallFloatMethod) +JNI_SLOT(CallFloatMethodV) +JNI_SLOT(CallFloatMethodA) +JNI_SLOT(CallDoubleMethod) +JNI_SLOT(CallDoubleMethodV) +JNI_SLOT(CallDoubleMethodA) +JNI_SLOT(CallVoidMethod) +JNI_SLOT(CallVoidMethodV) +JNI_SLOT(CallVoidMethodA) +JNI_SLOT(CallNonvirtualObjectMethod) +JNI_SLOT(CallNonvirtualObjectMethodV) +JNI_SLOT(CallNonvirtualObjectMethodA) +JNI_SLOT(CallNonvirtualBooleanMethod) +JNI_SLOT(CallNonvirtualBooleanMethodV) +JNI_SLOT(CallNonvirtualBooleanMethodA) +JNI_SLOT(CallNonvirtualByteMethod) +JNI_SLOT(CallNonvirtualByteMethodV) +JNI_SLOT(CallNonvirtualByteMethodA) +JNI_SLOT(CallNonvirtualCharMethod) +JNI_SLOT(CallNonvirtualCharMethodV) +JNI_SLOT(CallNonvirtualCharMethodA) +JNI_SLOT(CallNonvirtualShortMethod) +JNI_SLOT(CallNonvirtualShortMethodV) +JNI_SLOT(CallNonvirtualShortMethodA) +JNI_SLOT(CallNonvirtualIntMethod) +JNI_SLOT(CallNonvirtualIntMethodV) +JNI_SLOT(CallNonvirtualIntMethodA) +JNI_SLOT(CallNonvirtualLongMethod) +JNI_SLOT(CallNonvirtualLongMethodV) +JNI_SLOT(CallNonvirtualLongMethodA) +JNI_SLOT(CallNonvirtualFloatMethod) +JNI_SLOT(CallNonvirtualFloatMethodV) +JNI_SLOT(CallNonvirtualFloatMethodA) +JNI_SLOT(CallNonvirtualDoubleMethod) +JNI_SLOT(CallNonvirtualDoubleMethodV) +JNI_SLOT(CallNonvirtualDoubleMethodA) +JNI_SLOT(CallNonvirtualVoidMethod) +JNI_SLOT(CallNonvirtualVoidMethodV) +JNI_SLOT(CallNonvirtualVoidMethodA) +JNI_SLOT(GetFieldID) +JNI_SLOT(GetObjectField) +JNI_SLOT(GetBooleanField) +JNI_SLOT(GetByteField) +JNI_SLOT(GetCharField) +JNI_SLOT(GetShortField) +JNI_SLOT(GetIntField) +JNI_SLOT(GetLongField) +JNI_SLOT(GetFloatField) +JNI_SLOT(GetDoubleField) +JNI_SLOT(SetObjectField) +JNI_SLOT(SetBooleanField) +JNI_SLOT(SetByteField) +JNI_SLOT(SetCharField) +JNI_SLOT(SetShortField) +JNI_SLOT(SetIntField) +JNI_SLOT(SetLongField) +JNI_SLOT(SetFloatField) +JNI_SLOT(SetDoubleField) +JNI_SLOT(GetStaticMethodID) +JNI_SLOT(CallStaticObjectMethod) +JNI_SLOT(CallStaticObjectMethodV) +JNI_SLOT(CallStaticObjectMethodA) +JNI_SLOT(CallStaticBooleanMethod) +JNI_SLOT(CallStaticBooleanMethodV) +JNI_SLOT(CallStaticBooleanMethodA) +JNI_SLOT(CallStaticByteMethod) +JNI_SLOT(CallStaticByteMethodV) +JNI_SLOT(CallStaticByteMethodA) +JNI_SLOT(CallStaticCharMethod) +JNI_SLOT(CallStaticCharMethodV) +JNI_SLOT(CallStaticCharMethodA) +JNI_SLOT(CallStaticShortMethod) +JNI_SLOT(CallStaticShortMethodV) +JNI_SLOT(CallStaticShortMethodA) +JNI_SLOT(CallStaticIntMethod) +JNI_SLOT(CallStaticIntMethodV) +JNI_SLOT(CallStaticIntMethodA) +JNI_SLOT(CallStaticLongMethod) +JNI_SLOT(CallStaticLongMethodV) +JNI_SLOT(CallStaticLongMethodA) +JNI_SLOT(CallStaticFloatMethod) +JNI_SLOT(CallStaticFloatMethodV) +JNI_SLOT(CallStaticFloatMethodA) +JNI_SLOT(CallStaticDoubleMethod) +JNI_SLOT(CallStaticDoubleMethodV) +JNI_SLOT(CallStaticDoubleMethodA) +JNI_SLOT(CallStaticVoidMethod) +JNI_SLOT(CallStaticVoidMethodV) +JNI_SLOT(CallStaticVoidMethodA) +JNI_SLOT(GetStaticFieldID) +JNI_SLOT(GetStaticObjectField) +JNI_SLOT(GetStaticBooleanField) +JNI_SLOT(GetStaticByteField) +JNI_SLOT(GetStaticCharField) +JNI_SLOT(GetStaticShortField) +JNI_SLOT(GetStaticIntField) +JNI_SLOT(GetStaticLongField) +JNI_SLOT(GetStaticFloatField) +JNI_SLOT(GetStaticDoubleField) +JNI_SLOT(SetStaticObjectField) +JNI_SLOT(SetStaticBooleanField) +JNI_SLOT(SetStaticByteField) +JNI_SLOT(SetStaticCharField) +JNI_SLOT(SetStaticShortField) +JNI_SLOT(SetStaticIntField) +JNI_SLOT(SetStaticLongField) +JNI_SLOT(SetStaticFloatField) +JNI_SLOT(SetStaticDoubleField) +JNI_SLOT(NewString) +JNI_SLOT(GetStringLength) +JNI_SLOT(GetStringChars) +JNI_SLOT(ReleaseStringChars) +JNI_SLOT(NewStringUTF) +JNI_SLOT(GetStringUTFLength) +JNI_SLOT(GetStringUTFChars) +JNI_SLOT(ReleaseStringUTFChars) +JNI_SLOT(GetArrayLength) +JNI_SLOT(NewObjectArray) +JNI_SLOT(GetObjectArrayElement) +JNI_SLOT(SetObjectArrayElement) +JNI_SLOT(NewBooleanArray) +JNI_SLOT(NewByteArray) +JNI_SLOT(NewCharArray) +JNI_SLOT(NewShortArray) +JNI_SLOT(NewIntArray) +JNI_SLOT(NewLongArray) +JNI_SLOT(NewFloatArray) +JNI_SLOT(NewDoubleArray) +JNI_SLOT(GetBooleanArrayElements) +JNI_SLOT(GetByteArrayElements) +JNI_SLOT(GetCharArrayElements) +JNI_SLOT(GetShortArrayElements) +JNI_SLOT(GetIntArrayElements) +JNI_SLOT(GetLongArrayElements) +JNI_SLOT(GetFloatArrayElements) +JNI_SLOT(GetDoubleArrayElements) +JNI_SLOT(ReleaseBooleanArrayElements) +JNI_SLOT(ReleaseByteArrayElements) +JNI_SLOT(ReleaseCharArrayElements) +JNI_SLOT(ReleaseShortArrayElements) +JNI_SLOT(ReleaseIntArrayElements) +JNI_SLOT(ReleaseLongArrayElements) +JNI_SLOT(ReleaseFloatArrayElements) +JNI_SLOT(ReleaseDoubleArrayElements) +JNI_SLOT(GetBooleanArrayRegion) +JNI_SLOT(GetByteArrayRegion) +JNI_SLOT(GetCharArrayRegion) +JNI_SLOT(GetShortArrayRegion) +JNI_SLOT(GetIntArrayRegion) +JNI_SLOT(GetLongArrayRegion) +JNI_SLOT(GetFloatArrayRegion) +JNI_SLOT(GetDoubleArrayRegion) +JNI_SLOT(SetBooleanArrayRegion) +JNI_SLOT(SetByteArrayRegion) +JNI_SLOT(SetCharArrayRegion) +JNI_SLOT(SetShortArrayRegion) +JNI_SLOT(SetIntArrayRegion) +JNI_SLOT(SetLongArrayRegion) +JNI_SLOT(SetFloatArrayRegion) +JNI_SLOT(SetDoubleArrayRegion) +JNI_SLOT(RegisterNatives) +JNI_SLOT(UnregisterNatives) +JNI_SLOT(MonitorEnter) +JNI_SLOT(MonitorExit) +JNI_SLOT(GetJavaVM) +JNI_SLOT(GetStringRegion) +JNI_SLOT(GetStringUTFRegion) +JNI_SLOT(GetPrimitiveArrayCritical) +JNI_SLOT(ReleasePrimitiveArrayCritical) +JNI_SLOT(GetStringCritical) +JNI_SLOT(ReleaseStringCritical) +JNI_SLOT(NewWeakGlobalRef) +JNI_SLOT(DeleteWeakGlobalRef) +JNI_SLOT(ExceptionCheck) +JNI_SLOT(NewDirectByteBuffer) +JNI_SLOT(GetDirectBufferAddress) +JNI_SLOT(GetDirectBufferCapacity) +JNI_SLOT(GetObjectRefType) diff --git a/direct/src/kernel.cpp b/direct/src/kernel.cpp new file mode 100644 index 0000000..c041047 --- /dev/null +++ b/direct/src/kernel.cpp @@ -0,0 +1,547 @@ +#include "kernel.h" + +#include "cpu.h" +#include "files.h" +#include "log.h" +#include "memory.h" + +#include +#include +#include + +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +Process& process() { + static Process instance; + return instance; +} + +// --------------------------------------------------------------------------- +// First-touch commits. + +static LONG CALLBACK fault_handler(EXCEPTION_POINTERS* info) { + const EXCEPTION_RECORD* record = info->ExceptionRecord; + if (record->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) { + void* frames[32]; + USHORT count = CaptureStackBackTrace(0, 32, frames, nullptr); + LOGE("host-fault", "integer division by zero at %p", record->ExceptionAddress); + for (USHORT i = 0; i < count; ++i) { + HMODULE module = nullptr; + char path[MAX_PATH]{}; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(frames[i]), &module); + if (module) GetModuleFileNameA(module, path, MAX_PATH); + LOGE("host-fault", "%s + 0x%llx", path, + static_cast(reinterpret_cast(frames[i]) - reinterpret_cast(module))); + } + } + if (record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || record->NumberParameters < 2) + return EXCEPTION_CONTINUE_SEARCH; + ULONG_PTR kind = record->ExceptionInformation[0]; // 0 read, 1 write, 8 execute + uint64_t address = record->ExceptionInformation[1]; + if (address_space().handle_fault(address, kind == 1, kind == 8)) return EXCEPTION_CONTINUE_EXECUTION; + return EXCEPTION_CONTINUE_SEARCH; +} + +void install_fault_handler() { + // First in line: Dynarmic's fastmem handler (an SEH function-table + // handler) must only see accesses that are genuinely bad. + AddVectoredExceptionHandler(1, fault_handler); +} + +// --------------------------------------------------------------------------- +// Memory system calls. + +int64_t sys_memory(GuestThread&, uint64_t nr, const uint64_t* a, bool& handled) { + handled = true; + auto& space = address_space(); + switch (nr) { + case 222: { // mmap(addr, length, prot, flags, fd, offset) + uint32_t prot = static_cast(a[2]), flags = static_cast(a[3]); + std::shared_ptr file; + if (!(flags & MAP_ANONYMOUS_)) { + file = fds().get(static_cast(a[4])); + if (!file) return -EBADF_; + if (file->has_device_mapping()) return file->map_device(a[1], prot, flags, a[5]); + if (uint64_t at = file->fixed_mapping()) return static_cast(at + a[5]); + if (!file->mappable()) return -ENODEV_; + if ((flags & MAP_SHARED_VALIDATE_) == MAP_SHARED_ && (prot & PROT_WRITE_) && + (file->status_flags & O_ACCMODE_) == O_RDONLY_) + return -EACCES_; + } + bool fixed = (flags & MAP_FIXED_) != 0; + if (fixed && space.any_exec(a[0], a[1])) invalidate_code(a[0], page_up(a[1])); + int64_t r = space.mmap(a[0], a[1], prot, flags, std::move(file), a[5]); + TRACE("mmap", "mmap(%llx, %llx, %x, %x, %d, %llx) = %llx", (unsigned long long)a[0], + (unsigned long long)a[1], prot, flags, static_cast(a[4]), (unsigned long long)a[5], + (unsigned long long)r); + return r; + } + case 215: { // munmap + bool exec = space.any_exec(a[0], a[1]); + int64_t r = space.munmap(a[0], a[1]); + if (exec && r == 0) invalidate_code(a[0], page_up(a[1])); + TRACE("mmap", "munmap(%llx, %llx) = %lld", (unsigned long long)a[0], (unsigned long long)a[1], (long long)r); + return r; + } + case 226: { // mprotect + bool exec_before = space.any_exec(a[0], a[1]); + int64_t r = space.mprotect(a[0], a[1], static_cast(a[2])); + // Code that was writable may have been rewritten (JITs, relocations). + if (r == 0 && (exec_before || (a[2] & PROT_EXEC_))) invalidate_code(a[0], page_up(a[1])); + TRACE("mmap", "mprotect(%llx, %llx, %llx) = %lld", (unsigned long long)a[0], (unsigned long long)a[1], + (unsigned long long)a[2], (long long)r); + return r; + } + case 216: { // mremap + bool exec = space.any_exec(a[0], a[1]); + int64_t r = space.mremap(a[0], a[1], a[2], static_cast(a[3]), a[4]); + if (exec && r >= 0) invalidate_code(a[0], page_up(a[1])); + return r; + } + case 233: // madvise + return space.madvise(a[0], a[1], static_cast(a[2])); + case 214: // brk + return space.brk(a[0]); + case 227: // msync + return space.msync(a[0], a[1], static_cast(a[2])); + case 232: { // mincore + uint64_t pages = page_up(a[1]) / kPageSize; + std::vector out(static_cast(pages)); + if (!space.check(a[0], a[1], 0)) return -ENOMEM_; + space.mincore(a[0], a[1], out.data()); + return guest_write(a[2], out.data(), out.size()) ? 0 : -EFAULT_; + } + case 228: case 229: case 230: case 231: case 284: // mlock, munlock, mlockall, munlockall, mlock2 + return 0; + case 285: case 286: case 287: // pkey_mprotect (as mprotect), pkey_alloc, pkey_free + if (nr == 285) return space.mprotect(a[0], a[1], static_cast(a[2])); + return -ENOSYS_; + case 283: // membarrier + if (a[0] == 0) return 1 | 8 | 16; // QUERY: GLOBAL, PRIVATE_EXPEDITED, REGISTER_PRIVATE_EXPEDITED + FlushProcessWriteBuffers(); + return 0; + case 235: case 236: case 237: case 238: case 239: // mbind, get/set_mempolicy, migrate/move_pages + return 0; + default: + handled = false; + return 0; + } +} + +// --------------------------------------------------------------------------- +// Identity, time, limits and the rest. + +static void filetime_to_timeval(const FILETIME& ft, ktimeval& tv) { + uint64_t t = (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; // 100ns units + tv.tv_sec = static_cast(t / 10000000); + tv.tv_usec = static_cast((t % 10000000) / 10); +} + +static int64_t filetime_ns(const FILETIME& ft) { + uint64_t t = (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + return static_cast(t * 100); +} + +static int64_t clock_ns(int clock, int64_t& out) { + switch (clock) { + case CLOCK_REALTIME_: + case CLOCK_REALTIME_COARSE_: + case CLOCK_REALTIME_ALARM_: + out = realtime_ns(); + return 0; + case CLOCK_MONOTONIC_: + case CLOCK_MONOTONIC_RAW_: + case CLOCK_MONOTONIC_COARSE_: + case CLOCK_BOOTTIME_: + case CLOCK_BOOTTIME_ALARM_: + out = monotonic_ns(); + return 0; + case CLOCK_PROCESS_CPUTIME_ID_: { + FILETIME c, e, k, u; + GetProcessTimes(GetCurrentProcess(), &c, &e, &k, &u); + out = filetime_ns(k) + filetime_ns(u); + return 0; + } + default: + // CLOCK_THREAD_CPUTIME_ID and the per-thread cpu clocks bionic + // builds from a tid (negative ids) both mean "this thread". + if (clock == CLOCK_THREAD_CPUTIME_ID_ || clock < 0) { + FILETIME c, e, k, u; + if (GetThreadTimes(GetCurrentThread(), &c, &e, &k, &u)) { + out = filetime_ns(k) + filetime_ns(u); + return 0; + } + out = monotonic_ns(); + return 0; + } + return -EINVAL_; + } +} + +bool fast_syscall(uint64_t* x) { + const uint64_t nr = x[8]; + // Tagged pointers and every error case take the regular path. + auto plain = [](uint64_t address) { return (address >> 56) == 0; }; + int64_t result = 0; + switch (nr) { + case 113: { // clock_gettime + int64_t ns; + if (!plain(x[1]) || clock_ns(static_cast(x[0]), ns)) return false; + ktimespec ts{ns / 1000000000, ns % 1000000000}; + if (!guest_write(x[1], &ts, sizeof ts)) return false; + break; + } + case 169: { // gettimeofday without a timezone + if (x[1] || !plain(x[0])) return false; + if (x[0]) { + int64_t ns = realtime_ns(); + ktimeval tv{ns / 1000000000, (ns % 1000000000) / 1000}; + if (!guest_write(x[0], &tv, sizeof tv)) return false; + } + break; + } + case 98: { // futex wakes never block + const int cmd = static_cast(x[1]) & 127; + if (!plain(x[0])) return false; + if (cmd == FUTEX_WAKE_) result = wake_futex_bitset(x[0], static_cast(x[2]), ~0u); + else if (cmd == FUTEX_WAKE_BITSET_ && static_cast(x[5])) result = wake_futex_bitset(x[0], static_cast(x[2]), static_cast(x[5])); + else return false; + if (result < 0) return false; + break; + } + default: + return false; + } + g_syscall_counts[nr].fetch_add(1, std::memory_order_relaxed); + x[0] = static_cast(result); + return true; +} + +static uint64_t total_memory() { + MEMORYSTATUSEX status{sizeof status}; + GlobalMemoryStatusEx(&status); + // A Quest 3 has 8 GiB; reporting far more than that changes engine + // quality tiers, far less makes them refuse to start. + return std::min(status.ullTotalPhys, 8ull << 30); +} + +static uint64_t free_memory() { + MEMORYSTATUSEX status{sizeof status}; + GlobalMemoryStatusEx(&status); + return std::min(status.ullAvailPhys, total_memory() - std::min(total_memory(), address_space().mapped_bytes() / 8)); +} + +static void init_rlimits() { + static bool done = false; + if (done) return; + done = true; + auto& r = process().rlimits; + for (auto& limit : r) limit = {~0ull, ~0ull}; + r[3] = {8ull << 20, ~0ull}; // RLIMIT_STACK + r[4] = {0, ~0ull}; // RLIMIT_CORE + r[7] = {32768, 32768}; // RLIMIT_NOFILE + r[6] = {16384, 16384}; // RLIMIT_NPROC + r[8] = {64 << 10, 64 << 10}; // RLIMIT_MEMLOCK +} + +int64_t sys_misc(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled) { + handled = true; + switch (nr) { + case 160: { // uname + kutsname u{}; + std::strcpy(u.sysname, "Linux"); + std::strcpy(u.nodename, "localhost"); + std::strcpy(u.release, "5.10.226-android12-9-axrb"); + std::strcpy(u.version, "#1 SMP PREEMPT"); + std::strcpy(u.machine, "aarch64"); + std::strcpy(u.domainname, "(none)"); + return guest_write(a[0], &u, sizeof u) ? 0 : -EFAULT_; + } + case 179: { // sysinfo + ksysinfo s{}; + s.uptime = monotonic_ns() / 1000000000; + s.totalram = total_memory(); + s.freeram = free_memory(); + s.procs = static_cast(process().threads.size()); + s.mem_unit = 1; + return guest_write(a[0], &s, sizeof s) ? 0 : -EFAULT_; + } + case 163: // getrlimit + case 164: // setrlimit + case 261: { // prlimit64(pid, resource, new, old) + init_rlimits(); + uint64_t resource = nr == 261 ? a[1] : a[0]; + uint64_t new_limit = nr == 261 ? a[2] : (nr == 164 ? a[1] : 0); + uint64_t old_limit = nr == 261 ? a[3] : (nr == 163 ? a[1] : 0); + if (resource >= process().rlimits.size()) return -EINVAL_; + if (old_limit && !guest_write(old_limit, &process().rlimits[resource], sizeof(krlimit))) return -EFAULT_; + if (new_limit) { + krlimit value; + if (!guest_read(new_limit, &value, sizeof value)) return -EFAULT_; + process().rlimits[resource] = value; + } + return 0; + } + case 166: { // umask + uint32_t old = process().umask; + process().umask = static_cast(a[0]) & 0777; + return old; + } + case 167: { // prctl + int option = static_cast(a[0]); + switch (option) { + case PR_SET_NAME_: { + std::string name; + if (!guest_string(a[1], name, 15)) return -EFAULT_; + t.name = name; + SetThreadDescription(GetCurrentThread(), widen(name).c_str()); + TRACE("thread", "tid %d named %s", t.tid, name.c_str()); + return 0; + } + case PR_GET_NAME_: { + char name[16] = {}; + std::strncpy(name, t.name.c_str(), 15); + return guest_write(a[1], name, 16) ? 0 : -EFAULT_; + } + case PR_SET_VMA_: { + // PR_SET_VMA_ANON_NAME: (option, 0, addr, len, name) + std::string name; + if (a[4] && guest_string(a[4], name, 80)) address_space().name_range(a[2], a[3], "[anon:" + name + "]"); + return 0; + } + case PR_GET_DUMPABLE_: return 1; + case PR_GET_NO_NEW_PRIVS_: return 0; + case PR_CAPBSET_READ_: return 0; + case PR_GET_TAGGED_ADDR_CTRL_: return 0; + case PR_SET_TAGGED_ADDR_CTRL_: return -EINVAL_; // no MTE / TBI tagging + case PR_GET_PDEATHSIG_: { + int32_t zero = 0; + return guest_write(a[1], &zero, 4) ? 0 : -EFAULT_; + } + case PR_PAC_RESET_KEYS_: return -EINVAL_; + case 0x41555856: // PR_SET_SYSCALL_USER_DISPATCH and friends + return -EINVAL_; + default: + return 0; + } + } + case 174: case 175: return process().uid; // getuid, geteuid + case 176: case 177: return process().gid; // getgid, getegid + case 148: case 150: { // getresuid, getresgid + uint32_t id = nr == 148 ? process().uid : process().gid; + for (int i = 0; i < 3; ++i) + if (!guest_write(a[i], &id, 4)) return -EFAULT_; + return 0; + } + case 158: { // getgroups + if (a[0] == 0) return 1; + uint32_t gid = process().gid; + return guest_write(a[1], &gid, 4) ? 1 : -EFAULT_; + } + case 143: case 144: case 145: case 146: case 147: case 149: case 151: case 152: case 159: + return 0; // set*id, setfsuid/gid, setgroups: the app is already who it is + case 154: return 0; // setpgid + case 155: case 156: return process().pid; // getpgid, getsid + case 157: return process().pid; // setsid + case 92: return 0; // personality + case 90: { // capget + if (a[1]) { + uint32_t zero[6] = {}; + guest_write(a[1], zero, sizeof zero); + } + return 0; + } + case 91: return -EPERM_; // capset + case 278: { // getrandom + uint64_t len = std::min(a[1], 1 << 25); + std::vector bytes(static_cast(len)); + if (len) BCryptGenRandom(nullptr, bytes.data(), static_cast(len), BCRYPT_USE_SYSTEM_PREFERRED_RNG); + address_space().populate(a[0], len); + return guest_write(a[0], bytes.data(), bytes.size()) ? static_cast(len) : -EFAULT_; + } + case 113: { // clock_gettime + int64_t ns; + if (int64_t e = clock_ns(static_cast(a[0]), ns)) return e; + ktimespec ts{ns / 1000000000, ns % 1000000000}; + return guest_write(a[1], &ts, sizeof ts) ? 0 : -EFAULT_; + } + case 114: { // clock_getres + if (a[1]) { + bool coarse = a[0] == CLOCK_REALTIME_COARSE_ || a[0] == CLOCK_MONOTONIC_COARSE_; + ktimespec ts{0, coarse ? 4000000 : 1}; + if (!guest_write(a[1], &ts, sizeof ts)) return -EFAULT_; + } + return 0; + } + case 112: return -EPERM_; // clock_settime + case 169: { // gettimeofday + int64_t ns = realtime_ns(); + if (a[0]) { + ktimeval tv{ns / 1000000000, (ns % 1000000000) / 1000}; + if (!guest_write(a[0], &tv, sizeof tv)) return -EFAULT_; + } + if (a[1]) { + int32_t tz[2] = {0, 0}; + guest_write(a[1], tz, sizeof tz); + } + return 0; + } + case 153: { // times + FILETIME c, e, k, u; + GetProcessTimes(GetCurrentProcess(), &c, &e, &k, &u); + if (a[0]) { + int64_t tms[4] = {filetime_ns(u) / 10000000, filetime_ns(k) / 10000000, 0, 0}; + guest_write(a[0], tms, sizeof tms); + } + return monotonic_ns() / 10000000; + } + case 165: { // getrusage(who, usage) + krusage r{}; + FILETIME c, e, k, u; + if (static_cast(a[0]) == 1) // RUSAGE_THREAD + GetThreadTimes(GetCurrentThread(), &c, &e, &k, &u); + else + GetProcessTimes(GetCurrentProcess(), &c, &e, &k, &u); + filetime_to_timeval(u, r.ru_utime); + filetime_to_timeval(k, r.ru_stime); + PROCESS_MEMORY_COUNTERS counters{}; + if (GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof counters)) + r.ru_maxrss = static_cast(counters.PeakWorkingSetSize / 1024); + return guest_write(a[1], &r, sizeof r) ? 0 : -EFAULT_; + } + case 260: case 95: return -ECHILD_; // wait4, waitid: never any children + case 221: { // execve + std::string path; + guest_string(a[0], path); + LOGW("process", "execve(%s) refused: one process image only", path.c_str()); + return -EACCES_; + } + case 117: return -EPERM_; // ptrace + case 116: return -EPERM_; // syslog + case 277: return -EINVAL_; // seccomp + case 293: return -ENOSYS_; // rseq: bionic falls back + case 270: case 271: return -ENOSYS_; // process_vm_readv/writev + case 272: return -ENOSYS_; // kcmp + case 282: return -ENOSYS_; // userfaultfd + case 424: case 434: case 438: return -ENOSYS_; // pidfd_* + case 107: case 108: case 109: case 110: case 111: return -ENOSYS_; // timer_* (POSIX timers) + case 0: case 1: case 2: case 3: case 4: // io_setup ... + return -ENOSYS_; + case 425: case 426: case 427: return -ENOSYS_; // io_uring + case 89: return -EPERM_; // acct + case 280: return -ENOSYS_; // bpf + case 241: return -ENOSYS_; // perf_event_open + case 97: return -EPERM_; // unshare + case 268: return -EPERM_; // setns + case 40: case 39: return -EPERM_; // mount, umount2 + case 41: return -EPERM_; // pivot_root + case 51: return -EPERM_; // chroot + case 142: return -EPERM_; // reboot + case 30: case 31: return 0; // ioprio_set, ioprio_get + case 102: case 103: return -ENOSYS_; // getitimer/setitimer (when signals does not) + case 104: return -ENOSYS_; // kexec_load + case 105: case 106: case 273: case 313: return -EPERM_; // init_module, delete_module, finit_module + case 171: return 0; // adjtimex: report TIME_OK + case 266: return -EPERM_; // clock_adjtime + case 161: case 162: return -EPERM_; // sethostname, setdomainname + default: + handled = false; + return 0; + } +} + +// --------------------------------------------------------------------------- +// Dispatch. + +std::atomic g_syscall_counts[512]; +std::atomic g_syscall_stacks_enabled{false}; +std::mutex g_syscall_stacks_lock; +std::map>, uint64_t> g_syscall_stacks; + +// Guest call stack (frame-pointer chain) of a sampled syscall, for the profiler. +static void sample_syscall_stack(GuestThread& thread, uint64_t nr) { + std::vector frames{get_pc(thread), get_reg(thread, 30)}; + uint64_t fp = get_reg(thread, 29); + for (int depth = 0; depth < 10 && fp && (fp & 7) == 0; ++depth) { + uint64_t record[2]; + if (!guest_read(fp, record, sizeof record)) break; + frames.push_back(record[1]); + if (record[0] <= fp) break; + fp = record[0]; + } + std::lock_guard held(g_syscall_stacks_lock); + ++g_syscall_stacks[{nr, frames}]; +} + +int64_t do_syscall(GuestThread& thread, uint64_t nr, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, + uint64_t a4, uint64_t a5) { + if (nr < 512) { + const uint64_t n = g_syscall_counts[nr].fetch_add(1, std::memory_order_relaxed); + if ((n & 4095) == 0 && g_syscall_stacks_enabled.load(std::memory_order_relaxed)) sample_syscall_stack(thread, nr); + } + uint64_t args[6] = {a0, a1, a2, a3, a4, a5}; + // Tagged pointers (TBI) are accepted by the kernel's tagged address ABI; + // strip tags from anything that is a guest address under its tag. + for (uint64_t& a : args) { + uint64_t top = a >> 56; + if (top && top != 0xff && address_space().overlaps_guest(untag(a), 1)) a = untag(a); + } + bool handled = false; + int64_t result = sys_files(thread, nr, args, handled); + if (!handled) result = sys_memory(thread, nr, args, handled); + if (!handled) result = sys_threads(thread, nr, args, handled); + if (!handled) result = sys_signals(thread, nr, args, handled); + if (!handled) result = sys_net(thread, nr, args, handled); + if (!handled) result = sys_misc(thread, nr, args, handled); + if (!handled) { + static std::atomic reported[512]; + if (nr < 512 && reported[nr].fetch_add(1) < 3) + LOGW("syscall", "unimplemented syscall %llu (%s) from %s", (unsigned long long)nr, syscall_name(nr), + describe_address(get_pc(thread)).c_str()); + result = -ENOSYS_; + } + TRACE("syscall", "[%d] %s(%llx, %llx, %llx, %llx) = %lld", thread.tid, syscall_name(nr), (unsigned long long)a0, + (unsigned long long)a1, (unsigned long long)a2, (unsigned long long)a3, (long long)result); + return result; +} + +const char* syscall_name(uint64_t nr) { + switch (nr) { +#define N(n, name) \ + case n: return #name; + N(500, [lazy-commit faults]) N(501, [populate]) N(502, [fault already committed]) N(503, [code invalidations]) N(504, [invalidated pages]) N(505, [translator fetches]) + N(17, getcwd) N(19, eventfd2) N(20, epoll_create1) N(21, epoll_ctl) N(22, epoll_pwait) N(23, dup) N(24, dup3) + N(25, fcntl) N(26, inotify_init1) N(27, inotify_add_watch) N(28, inotify_rm_watch) N(29, ioctl) N(32, flock) + N(33, mknodat) N(34, mkdirat) N(35, unlinkat) N(36, symlinkat) N(37, linkat) N(38, renameat) N(43, statfs) + N(44, fstatfs) N(45, truncate) N(46, ftruncate) N(47, fallocate) N(48, faccessat) N(49, chdir) N(50, fchdir) + N(52, fchmod) N(53, fchmodat) N(54, fchownat) N(55, fchown) N(56, openat) N(57, close) N(59, pipe2) + N(61, getdents64) N(62, lseek) N(63, read) N(64, write) N(65, readv) N(66, writev) N(67, pread64) + N(68, pwrite64) N(69, preadv) N(70, pwritev) N(71, sendfile) N(72, pselect6) N(73, ppoll) N(74, signalfd4) + N(78, readlinkat) N(79, newfstatat) N(80, fstat) N(81, sync) N(82, fsync) N(83, fdatasync) N(84, sync_file_range) + N(85, timerfd_create) N(86, timerfd_settime) N(87, timerfd_gettime) N(88, utimensat) N(90, capget) + N(93, exit) N(94, exit_group) N(96, set_tid_address) N(98, futex) N(99, set_robust_list) N(101, nanosleep) + N(113, clock_gettime) N(114, clock_getres) N(115, clock_nanosleep) N(117, ptrace) N(122, sched_setaffinity) + N(123, sched_getaffinity) N(124, sched_yield) N(129, kill) N(130, tkill) N(131, tgkill) N(132, sigaltstack) + N(133, rt_sigsuspend) N(134, rt_sigaction) N(135, rt_sigprocmask) N(136, rt_sigpending) + N(137, rt_sigtimedwait) N(139, rt_sigreturn) N(140, setpriority) N(141, getpriority) N(153, times) + N(160, uname) N(163, getrlimit) N(165, getrusage) N(166, umask) N(167, prctl) N(168, getcpu) + N(169, gettimeofday) N(172, getpid) N(173, getppid) N(174, getuid) N(175, geteuid) N(176, getgid) + N(177, getegid) N(178, gettid) N(179, sysinfo) N(198, socket) N(199, socketpair) N(200, bind) N(201, listen) + N(202, accept) N(203, connect) N(204, getsockname) N(205, getpeername) N(206, sendto) N(207, recvfrom) + N(208, setsockopt) N(209, getsockopt) N(210, shutdown) N(211, sendmsg) N(212, recvmsg) N(214, brk) + N(215, munmap) N(216, mremap) N(220, clone) N(221, execve) N(222, mmap) N(226, mprotect) N(227, msync) + N(232, mincore) N(233, madvise) N(240, rt_tgsigqueueinfo) N(242, accept4) N(260, wait4) N(261, prlimit64) + N(276, renameat2) N(278, getrandom) N(279, memfd_create) N(283, membarrier) N(291, statx) N(293, rseq) + N(435, clone3) N(436, close_range) N(439, faccessat2) +#undef N + default: return "?"; + } +} + +} // namespace axrb diff --git a/direct/src/kernel.h b/direct/src/kernel.h new file mode 100644 index 0000000..864d839 --- /dev/null +++ b/direct/src/kernel.h @@ -0,0 +1,201 @@ +#pragma once + +#include "linux.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Dynarmic { +class ExclusiveMonitor; +namespace A64 { +class Jit; +} +} // namespace Dynarmic + +namespace axrb { + +class GuestCpu; // cpu.cpp: Dynarmic callbacks + Jit for one thread +class OpenFile; + +using Vec128 = std::array; + +// A complete arm64 user register state, as saved around host calls, signal +// frames and thread creation. +struct CpuContext { + uint64_t x[31] = {}; + uint64_t sp = 0; + uint64_t pc = 0; + uint32_t pstate = 0; + uint32_t fpcr = 0; + uint32_t fpsr = 0; + Vec128 v[32] = {}; +}; + +struct GuestCpuDeleter { + void operator()(GuestCpu* cpu) const; // cpu.cpp +}; + +// One guest thread = one host thread = one Dynarmic Jit. +struct GuestThread { + int tid = 0; + std::string name = "main"; + std::unique_ptr cpu; + uint64_t tpidr = 0; + uint64_t clear_child_tid = 0; + uint64_t robust_list = 0; + uint64_t robust_list_len = 0; + + // Signals. + std::mutex signal_lock; + uint64_t sigmask = 0; + std::atomic pending{0}; + std::deque pending_info; + linux_abi::kstack_t altstack{0, linux_abi::SS_DISABLE_, 0, 0}; + std::atomic in_handler{false}; + // A system call interrupted by a signal: restarted after the handler if + // the handler has SA_RESTART (pc of the svc and the original x0). + uint64_t restart_pc = 0; + uint64_t restart_x0 = 0; + // sigsuspend/ppoll/epoll_pwait run with a temporary mask; the handler's + // frame must record (and sigreturn restore) the original. + bool restore_sigmask = false; + uint64_t saved_sigmask = 0; + + // Blocking: every interruptible wait sleeps on `event`; anything that can + // end the wait (a futex wake, data on a pipe, a signal) sets it. + HANDLE event = nullptr; + std::atomic interrupt{false}; + // Set while the thread is blocked in a host call, for diagnostics. + std::atomic blocked_in{nullptr}; + + // Host side. + HANDLE host_thread = nullptr; + DWORD host_tid = 0; + int processor_id = 0; + int call_depth = 0; // nested host -> guest calls in progress + bool exited = false; + int exit_code = 0; + + // Thread-local scratch for host code acting for this thread. + void* jni_env = nullptr; // guest JNIEnv*, when attached to the VM + void* java_thread = nullptr; + + GuestThread(); + ~GuestThread(); +}; + +GuestThread* current_thread(); +void set_current_thread(GuestThread* thread); + +// Thrown (as a C++ exception) out of the run loop when a thread calls exit: +// unwinds any nested host frames back to the thread's entry. +struct ThreadExit { + int code; +}; + +// Process-wide state. +struct Process { + int pid = 0; + uint32_t uid = 10123, gid = 10123; + std::string cwd = "/"; + uint32_t umask = 022; + std::string cmdline; // argv joined with NULs + std::string exe_path; + uint64_t start_stack = 0; // Initial SP, Linux /proc//stat field 28. + std::vector auxv; + std::string package; + + // Signal dispositions (index = signal number). + std::mutex sigaction_lock; + std::array actions{}; + + std::mutex threads_lock; + std::map threads; + std::atomic next_tid{0}; + + std::array rlimits{}; + Dynarmic::ExclusiveMonitor* monitor = nullptr; + std::atomic exiting{false}; +}; + +Process& process(); + +// Interruptible blocking. Returns 0 if `ready` became true, -EINTR when a +// signal must be handled, -ETIMEDOUT on timeout (deadline in QPC ticks, 0 = +// forever, -1 = poll once). +int64_t block_until(GuestThread& thread, const std::function& ready, int64_t deadline_ticks, + const char* why); +int64_t ticks_now(); +int64_t ticks_per_second(); +int64_t ticks_from_timespec_relative(const linux_abi::ktimespec& ts); +// A list of threads to wake when some object changes. +class WaitQueue { +public: + void add(GuestThread* thread); + void remove(GuestThread* thread); + void wake_all(); + +private: + std::mutex lock_; + std::vector waiters_; +}; + +// Syscall entry from the run loop (x8 = number, x0..x5 = arguments). +int64_t do_syscall(GuestThread& thread, uint64_t nr, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, + uint64_t a4, uint64_t a5); +const char* syscall_name(uint64_t nr); + +// Subsystems (each in its own file). +int64_t sys_files(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled); +int64_t sys_threads(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled); +int64_t sys_signals(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled); +int64_t sys_net(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled); + +int64_t sys_memory(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled); +int64_t sys_misc(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled); + +GuestThread* find_thread(int tid); +// Wakes up to `count` guest threads waiting on the futex word at `address`. +int64_t wake_futex(uint64_t address, int count); +int64_t wake_futex_bitset(uint64_t address, int count, uint32_t bitset); +// Completes a non-blocking syscall (clock reads, futex wakes) for x[8] with +// arguments in x[0..5] directly from translated code. Returns false, without +// side effects, for anything that needs the regular syscall path. +bool fast_syscall(uint64_t* x); +void register_main_thread(GuestThread* thread); +void finish_thread(GuestThread& thread); +int guest_cpu_count(); +// Commits pages of guest mappings on first touch (vectored exception handler). +void install_fault_handler(); + +// Host time sources. +int64_t realtime_ns(); +int64_t monotonic_ns(); +// Calls per syscall number, for the profiler. +extern std::atomic g_syscall_counts[512]; +// Sampled guest stacks of syscalls ((nr, frames) -> samples), while enabled. +extern std::atomic g_syscall_stacks_enabled; +extern std::mutex g_syscall_stacks_lock; +extern std::map>, uint64_t> g_syscall_stacks; +const char* syscall_name(uint64_t nr); + +// Delivers pending, unblocked signals to `thread` (called between guest +// instructions, i.e. from the run loop). Returns true if a frame was pushed. +bool deliver_signals(GuestThread& thread); +// Queues a signal for one thread and wakes it. +int64_t send_signal(GuestThread& target, int signal, const linux_abi::ksiginfo& info); +// A synchronous fault in guest code (bad memory, undefined instruction). +void raise_fault(GuestThread& thread, int signal, int code, uint64_t address); + +} // namespace axrb diff --git a/direct/src/linux.h b/direct/src/linux.h new file mode 100644 index 0000000..1912ae8 --- /dev/null +++ b/direct/src/linux.h @@ -0,0 +1,357 @@ +#pragma once + +// The Linux arm64 user ABI, as the guest's bionic sees it. Values come from +// the kernel's uapi headers (asm-generic layout, which arm64 uses). + +#include + +namespace axrb::linux_abi { + +// errno +enum : int64_t { + EPERM_ = 1, ENOENT_ = 2, ESRCH_ = 3, EINTR_ = 4, EIO_ = 5, ENXIO_ = 6, E2BIG_ = 7, ENOEXEC_ = 8, EBADF_ = 9, + ECHILD_ = 10, EAGAIN_ = 11, ENOMEM_ = 12, EACCES_ = 13, EFAULT_ = 14, EBUSY_ = 16, EEXIST_ = 17, EXDEV_ = 18, + ENODEV_ = 19, ENOTDIR_ = 20, EISDIR_ = 21, EINVAL_ = 22, ENFILE_ = 23, EMFILE_ = 24, ENOTTY_ = 25, EFBIG_ = 27, + ENOSPC_ = 28, ESPIPE_ = 29, EROFS_ = 30, EMLINK_ = 31, EPIPE_ = 32, EDOM_ = 33, ERANGE_ = 34, EDEADLK_ = 35, + ENAMETOOLONG_ = 36, ENOLCK_ = 37, ENOSYS_ = 38, ENOTEMPTY_ = 39, ELOOP_ = 40, ENOMSG_ = 42, ENODATA_ = 61, + ETIME_ = 62, EOVERFLOW_ = 75, EBADFD_ = 77, EILSEQ_ = 84, ENOTSOCK_ = 88, EDESTADDRREQ_ = 89, EMSGSIZE_ = 90, + EPROTOTYPE_ = 91, ENOPROTOOPT_ = 92, EPROTONOSUPPORT_ = 93, EOPNOTSUPP_ = 95, EAFNOSUPPORT_ = 97, + EADDRINUSE_ = 98, EADDRNOTAVAIL_ = 99, ENETDOWN_ = 100, ENETUNREACH_ = 101, ECONNABORTED_ = 103, + ECONNRESET_ = 104, ENOBUFS_ = 105, EISCONN_ = 106, ENOTCONN_ = 107, ETIMEDOUT_ = 110, ECONNREFUSED_ = 111, + EHOSTUNREACH_ = 113, EALREADY_ = 114, EINPROGRESS_ = 115, ECANCELED_ = 125, EOWNERDEAD_ = 130, + ERESTARTSYS_ = 512, +}; + +// open flags (arm64 uses the asm-generic values except O_DIRECTORY/O_NOFOLLOW/O_DIRECT/O_LARGEFILE) +enum : uint32_t { + O_ACCMODE_ = 3, O_RDONLY_ = 0, O_WRONLY_ = 1, O_RDWR_ = 2, + O_CREAT_ = 0100, O_EXCL_ = 0200, O_NOCTTY_ = 0400, O_TRUNC_ = 01000, O_APPEND_ = 02000, O_NONBLOCK_ = 04000, + O_DSYNC_ = 010000, O_DIRECTORY_ = 040000, O_NOFOLLOW_ = 0100000, O_DIRECT_ = 0200000, O_LARGEFILE_ = 0400000, + O_CLOEXEC_ = 02000000, O_PATH_ = 010000000, O_TMPFILE_ = 020040000, +}; +constexpr int AT_FDCWD_ = -100; +constexpr uint32_t AT_SYMLINK_NOFOLLOW_ = 0x100, AT_REMOVEDIR_ = 0x200, AT_SYMLINK_FOLLOW_ = 0x400, + AT_EMPTY_PATH_ = 0x1000, AT_EACCESS_ = 0x200; + +// mode bits +constexpr uint32_t S_IFMT_ = 0170000, S_IFSOCK_ = 0140000, S_IFLNK_ = 0120000, S_IFREG_ = 0100000, + S_IFBLK_ = 0060000, S_IFDIR_ = 0040000, S_IFCHR_ = 0020000, S_IFIFO_ = 0010000; + +// dirent d_type +enum : uint8_t { DT_UNKNOWN_ = 0, DT_FIFO_ = 1, DT_CHR_ = 2, DT_DIR_ = 4, DT_BLK_ = 6, DT_REG_ = 8, DT_LNK_ = 10, DT_SOCK_ = 12 }; + +// mmap +enum : uint32_t { + PROT_NONE_ = 0, PROT_READ_ = 1, PROT_WRITE_ = 2, PROT_EXEC_ = 4, + MAP_SHARED_ = 0x01, MAP_PRIVATE_ = 0x02, MAP_SHARED_VALIDATE_ = 0x03, MAP_FIXED_ = 0x10, MAP_ANONYMOUS_ = 0x20, + MAP_GROWSDOWN_ = 0x0100, MAP_DENYWRITE_ = 0x0800, MAP_EXECUTABLE_ = 0x1000, MAP_LOCKED_ = 0x2000, + MAP_NORESERVE_ = 0x4000, MAP_POPULATE_ = 0x8000, MAP_NONBLOCK_ = 0x10000, MAP_STACK_ = 0x20000, + MAP_HUGETLB_ = 0x40000, MAP_FIXED_NOREPLACE_ = 0x100000, + MREMAP_MAYMOVE_ = 1, MREMAP_FIXED_ = 2, MREMAP_DONTUNMAP_ = 4, +}; +enum : int { MADV_NORMAL_ = 0, MADV_RANDOM_ = 1, MADV_SEQUENTIAL_ = 2, MADV_WILLNEED_ = 3, MADV_DONTNEED_ = 4, + MADV_FREE_ = 8, MADV_REMOVE_ = 9, MADV_DONTFORK_ = 10, MADV_MERGEABLE_ = 12, MADV_HUGEPAGE_ = 14, + MADV_NOHUGEPAGE_ = 15, MADV_DONTDUMP_ = 16, MADV_COLD_ = 20, MADV_PAGEOUT_ = 21 }; + +// clone +enum : uint64_t { + CSIGNAL_ = 0x000000ff, CLONE_VM_ = 0x00000100, CLONE_FS_ = 0x00000200, CLONE_FILES_ = 0x00000400, + CLONE_SIGHAND_ = 0x00000800, CLONE_PIDFD_ = 0x00001000, CLONE_PTRACE_ = 0x00002000, CLONE_VFORK_ = 0x00004000, + CLONE_PARENT_ = 0x00008000, CLONE_THREAD_ = 0x00010000, CLONE_NEWNS_ = 0x00020000, CLONE_SYSVSEM_ = 0x00040000, + CLONE_SETTLS_ = 0x00080000, CLONE_PARENT_SETTID_ = 0x00100000, CLONE_CHILD_CLEARTID_ = 0x00200000, + CLONE_DETACHED_ = 0x00400000, CLONE_UNTRACED_ = 0x00800000, CLONE_CHILD_SETTID_ = 0x01000000, +}; + +// futex +enum : int { + FUTEX_WAIT_ = 0, FUTEX_WAKE_ = 1, FUTEX_FD_ = 2, FUTEX_REQUEUE_ = 3, FUTEX_CMP_REQUEUE_ = 4, FUTEX_WAKE_OP_ = 5, + FUTEX_LOCK_PI_ = 6, FUTEX_UNLOCK_PI_ = 7, FUTEX_TRYLOCK_PI_ = 8, FUTEX_WAIT_BITSET_ = 9, FUTEX_WAKE_BITSET_ = 10, + FUTEX_WAIT_REQUEUE_PI_ = 11, FUTEX_CMP_REQUEUE_PI_ = 12, FUTEX_LOCK_PI2_ = 13, + FUTEX_PRIVATE_FLAG_ = 128, FUTEX_CLOCK_REALTIME_ = 256, +}; +constexpr uint32_t FUTEX_WAITERS_ = 0x80000000u, FUTEX_OWNER_DIED_ = 0x40000000u, FUTEX_TID_MASK_ = 0x3fffffffu; + +// clocks +enum : int { CLOCK_REALTIME_ = 0, CLOCK_MONOTONIC_ = 1, CLOCK_PROCESS_CPUTIME_ID_ = 2, CLOCK_THREAD_CPUTIME_ID_ = 3, + CLOCK_MONOTONIC_RAW_ = 4, CLOCK_REALTIME_COARSE_ = 5, CLOCK_MONOTONIC_COARSE_ = 6, CLOCK_BOOTTIME_ = 7, + CLOCK_REALTIME_ALARM_ = 8, CLOCK_BOOTTIME_ALARM_ = 9 }; +constexpr int TIMER_ABSTIME_ = 1; + +// signals +enum : int { + SIGHUP_ = 1, SIGINT_ = 2, SIGQUIT_ = 3, SIGILL_ = 4, SIGTRAP_ = 5, SIGABRT_ = 6, SIGBUS_ = 7, SIGFPE_ = 8, + SIGKILL_ = 9, SIGUSR1_ = 10, SIGSEGV_ = 11, SIGUSR2_ = 12, SIGPIPE_ = 13, SIGALRM_ = 14, SIGTERM_ = 15, + SIGSTKFLT_ = 16, SIGCHLD_ = 17, SIGCONT_ = 18, SIGSTOP_ = 19, SIGTSTP_ = 20, SIGTTIN_ = 21, SIGTTOU_ = 22, + SIGURG_ = 23, SIGXCPU_ = 24, SIGXFSZ_ = 25, SIGVTALRM_ = 26, SIGPROF_ = 27, SIGWINCH_ = 28, SIGIO_ = 29, + SIGPWR_ = 30, SIGSYS_ = 31, SIGRTMIN_ = 32, NSIG_ = 64, +}; +enum : uint64_t { + SA_NOCLDSTOP_ = 1, SA_NOCLDWAIT_ = 2, SA_SIGINFO_ = 4, SA_ONSTACK_ = 0x08000000, SA_RESTART_ = 0x10000000, + SA_NODEFER_ = 0x40000000, SA_RESETHAND_ = 0x80000000, SA_RESTORER_ = 0x04000000, +}; +constexpr uint64_t SIG_DFL_ = 0, SIG_IGN_ = 1; +enum : int { SIG_BLOCK_ = 0, SIG_UNBLOCK_ = 1, SIG_SETMASK_ = 2 }; +enum : int { SI_USER_ = 0, SI_KERNEL_ = 0x80, SI_QUEUE_ = -1, SI_TIMER_ = -2, SI_TKILL_ = -6, + SEGV_MAPERR_ = 1, SEGV_ACCERR_ = 2, ILL_ILLOPC_ = 1, BUS_ADRALN_ = 1, TRAP_BRKPT_ = 1 }; +constexpr int SS_ONSTACK_ = 1, SS_DISABLE_ = 2; + +// fcntl +enum : int { F_DUPFD_ = 0, F_GETFD_ = 1, F_SETFD_ = 2, F_GETFL_ = 3, F_SETFL_ = 4, F_GETLK_ = 5, F_SETLK_ = 6, + F_SETLKW_ = 7, F_SETOWN_ = 8, F_GETOWN_ = 9, F_SETSIG_ = 10, F_GETSIG_ = 11, F_OFD_GETLK_ = 36, + F_OFD_SETLK_ = 37, F_OFD_SETLKW_ = 38, F_SETLEASE_ = 1024, F_GETLEASE_ = 1025, F_DUPFD_CLOEXEC_ = 1030, + F_SETPIPE_SZ_ = 1031, F_GETPIPE_SZ_ = 1032, F_ADD_SEALS_ = 1033, F_GET_SEALS_ = 1034 }; +constexpr int FD_CLOEXEC_ = 1; + +// poll +enum : int16_t { POLLIN_ = 0x001, POLLPRI_ = 0x002, POLLOUT_ = 0x004, POLLERR_ = 0x008, POLLHUP_ = 0x010, + POLLNVAL_ = 0x020, POLLRDNORM_ = 0x040, POLLRDBAND_ = 0x080, POLLWRNORM_ = 0x100, + POLLWRBAND_ = 0x200, POLLRDHUP_ = 0x2000 }; +enum : uint32_t { EPOLLIN_ = 0x001, EPOLLPRI_ = 0x002, EPOLLOUT_ = 0x004, EPOLLERR_ = 0x008, EPOLLHUP_ = 0x010, + EPOLLRDNORM_ = 0x040, EPOLLWRNORM_ = 0x100, EPOLLRDHUP_ = 0x2000, EPOLLEXCLUSIVE_ = 1u << 28, + EPOLLWAKEUP_ = 1u << 29, EPOLLONESHOT_ = 1u << 30, EPOLLET_ = 1u << 31 }; +enum : int { EPOLL_CTL_ADD_ = 1, EPOLL_CTL_DEL_ = 2, EPOLL_CTL_MOD_ = 3 }; + +// sockets (Linux numbering) +enum : int { AF_UNSPEC_ = 0, AF_UNIX_ = 1, AF_INET_ = 2, AF_INET6_ = 10, AF_NETLINK_ = 16 }; +enum : int { SOCK_STREAM_ = 1, SOCK_DGRAM_ = 2, SOCK_RAW_ = 3, SOCK_SEQPACKET_ = 5, SOCK_NONBLOCK_ = 04000, + SOCK_CLOEXEC_ = 02000000 }; +enum : int { SOL_SOCKET_ = 1, IPPROTO_TCP_ = 6, IPPROTO_UDP_ = 17, IPPROTO_IP_ = 0, IPPROTO_IPV6_ = 41 }; +enum : int { SO_DEBUG_ = 1, SO_REUSEADDR_ = 2, SO_TYPE_ = 3, SO_ERROR_ = 4, SO_DONTROUTE_ = 5, SO_BROADCAST_ = 6, + SO_SNDBUF_ = 7, SO_RCVBUF_ = 8, SO_KEEPALIVE_ = 9, SO_OOBINLINE_ = 10, SO_LINGER_ = 13, + SO_REUSEPORT_ = 15, SO_PASSCRED_ = 16, SO_PEERCRED_ = 17, SO_RCVLOWAT_ = 18, SO_SNDLOWAT_ = 19, + SO_RCVTIMEO_ = 20, SO_SNDTIMEO_ = 21, SO_ACCEPTCONN_ = 30, SO_DOMAIN_ = 39, SO_PROTOCOL_ = 38 }; +enum : int { TCP_NODELAY_ = 1, TCP_KEEPIDLE_ = 4, TCP_KEEPINTVL_ = 5, TCP_KEEPCNT_ = 6 }; +enum : int { MSG_OOB_ = 1, MSG_PEEK_ = 2, MSG_DONTROUTE_ = 4, MSG_TRUNC_ = 0x20, MSG_DONTWAIT_ = 0x40, + MSG_WAITALL_ = 0x100, MSG_NOSIGNAL_ = 0x4000, MSG_CMSG_CLOEXEC_ = 0x40000000 }; +enum : int { SHUT_RD_ = 0, SHUT_WR_ = 1, SHUT_RDWR_ = 2 }; + +// ioctl +enum : uint32_t { TCGETS_ = 0x5401, TIOCGWINSZ_ = 0x5413, FIONREAD_ = 0x541b, FIONBIO_ = 0x5421, FIOCLEX_ = 0x5451, + FIONCLEX_ = 0x5452, FIOASYNC_ = 0x5452 + 0 }; + +// prctl +enum : int { PR_SET_PDEATHSIG_ = 1, PR_GET_PDEATHSIG_ = 2, PR_GET_DUMPABLE_ = 3, PR_SET_DUMPABLE_ = 4, + PR_SET_NAME_ = 15, PR_GET_NAME_ = 16, PR_SET_SECCOMP_ = 22, PR_CAPBSET_READ_ = 23, + PR_SET_TIMERSLACK_ = 29, PR_SET_NO_NEW_PRIVS_ = 38, PR_GET_NO_NEW_PRIVS_ = 39, + PR_SET_THP_DISABLE_ = 41, PR_SET_VMA_ = 0x53564d41, PR_SET_TAGGED_ADDR_CTRL_ = 55, + PR_GET_TAGGED_ADDR_CTRL_ = 56, PR_PAC_RESET_KEYS_ = 54, PR_SET_MM_ = 35, PR_MTE_TCF_SHIFT_ = 1 }; + +// auxv +enum : uint64_t { + AT_NULL_ = 0, AT_IGNORE_ = 1, AT_EXECFD_ = 2, AT_PHDR_ = 3, AT_PHENT_ = 4, AT_PHNUM_ = 5, AT_PAGESZ_ = 6, + AT_BASE_ = 7, AT_FLAGS_ = 8, AT_ENTRY_ = 9, AT_NOTELF_ = 10, AT_UID_ = 11, AT_EUID_ = 12, AT_GID_ = 13, + AT_EGID_ = 14, AT_PLATFORM_ = 15, AT_HWCAP_ = 16, AT_CLKTCK_ = 17, AT_SECURE_ = 23, AT_BASE_PLATFORM_ = 24, + AT_RANDOM_ = 25, AT_HWCAP2_ = 26, AT_EXECFN_ = 31, AT_SYSINFO_EHDR_ = 33, AT_MINSIGSTKSZ_ = 51, +}; + +// arm64 HWCAP bits +enum : uint64_t { + HWCAP_FP_ = 1 << 0, HWCAP_ASIMD_ = 1 << 1, HWCAP_EVTSTRM_ = 1 << 2, HWCAP_AES_ = 1 << 3, HWCAP_PMULL_ = 1 << 4, + HWCAP_SHA1_ = 1 << 5, HWCAP_SHA2_ = 1 << 6, HWCAP_CRC32_ = 1 << 7, HWCAP_ATOMICS_ = 1 << 8, HWCAP_FPHP_ = 1 << 9, + HWCAP_ASIMDHP_ = 1 << 10, HWCAP_CPUID_ = 1 << 11, HWCAP_ASIMDRDM_ = 1 << 12, HWCAP_JSCVT_ = 1 << 13, + HWCAP_FCMA_ = 1 << 14, HWCAP_LRCPC_ = 1 << 15, HWCAP_DCPOP_ = 1 << 16, HWCAP_SHA3_ = 1 << 17, + HWCAP_ASIMDDP_ = 1 << 20, HWCAP_SHA512_ = 1 << 21, +}; + +// Kernel-layout structures (arm64). +#pragma pack(push, 1) +struct kstat { + uint64_t st_dev; + uint64_t st_ino; + uint32_t st_mode; + uint32_t st_nlink; + uint32_t st_uid; + uint32_t st_gid; + uint64_t st_rdev; + uint64_t pad1; + int64_t st_size; + int32_t st_blksize; + int32_t pad2; + int64_t st_blocks; + int64_t st_atime_sec; + uint64_t st_atime_nsec; + int64_t st_mtime_sec; + uint64_t st_mtime_nsec; + int64_t st_ctime_sec; + uint64_t st_ctime_nsec; + uint32_t unused4; + uint32_t unused5; +}; +static_assert(sizeof(kstat) == 128); + +struct kstatx_timestamp { + int64_t tv_sec; + uint32_t tv_nsec; + int32_t reserved; +}; +struct kstatx { + uint32_t stx_mask; + uint32_t stx_blksize; + uint64_t stx_attributes; + uint32_t stx_nlink; + uint32_t stx_uid; + uint32_t stx_gid; + uint16_t stx_mode; + uint16_t spare0; + uint64_t stx_ino; + uint64_t stx_size; + uint64_t stx_blocks; + uint64_t stx_attributes_mask; + kstatx_timestamp stx_atime, stx_btime, stx_ctime, stx_mtime; + uint32_t stx_rdev_major, stx_rdev_minor, stx_dev_major, stx_dev_minor; + uint64_t spare2[14]; +}; +static_assert(sizeof(kstatx) == 256); + +struct kstatfs { + uint64_t f_type; + uint64_t f_bsize; + uint64_t f_blocks; + uint64_t f_bfree; + uint64_t f_bavail; + uint64_t f_files; + uint64_t f_ffree; + int32_t f_fsid[2]; + uint64_t f_namelen; + uint64_t f_frsize; + uint64_t f_flags; + uint64_t f_spare[4]; +}; +static_assert(sizeof(kstatfs) == 120); + +struct ktimespec { + int64_t tv_sec; + int64_t tv_nsec; +}; +struct ktimeval { + int64_t tv_sec; + int64_t tv_usec; +}; +struct kitimerspec { + ktimespec it_interval; + ktimespec it_value; +}; + +struct kutsname { + char sysname[65], nodename[65], release[65], version[65], machine[65], domainname[65]; +}; + +struct ksysinfo { + int64_t uptime; + uint64_t loads[3]; + uint64_t totalram, freeram, sharedram, bufferram, totalswap, freeswap; + uint16_t procs; + uint16_t pad; + uint32_t pad2; + uint64_t totalhigh, freehigh; + uint32_t mem_unit; + char reserved[4]; +}; +static_assert(sizeof(ksysinfo) == 112); + +struct krlimit { + uint64_t cur, max; +}; + +struct krusage { + ktimeval ru_utime, ru_stime; + int64_t ru_maxrss, ru_ixrss, ru_idrss, ru_isrss, ru_minflt, ru_majflt, ru_nswap, ru_inblock, ru_oublock, + ru_msgsnd, ru_msgrcv, ru_nsignals, ru_nvcsw, ru_nivcsw; +}; + +struct kiovec { + uint64_t base; + uint64_t len; +}; + +struct kpollfd { + int32_t fd; + int16_t events; + int16_t revents; +}; + +struct kepoll_event { // not packed on arm64: data is at offset 8 + uint32_t events; + uint32_t pad; + uint64_t data; +}; +static_assert(sizeof(kepoll_event) == 16); + +struct ksigaction { + uint64_t handler; + uint64_t flags; + uint64_t restorer; + uint64_t mask; +}; + +struct kstack_t { + uint64_t ss_sp; + int32_t ss_flags; + int32_t pad; + uint64_t ss_size; +}; + +struct ksiginfo { + int32_t si_signo; + int32_t si_errno; + int32_t si_code; + int32_t pad0; + union { + struct { + int32_t pid; + uint32_t uid; + } kill; + struct { + uint64_t addr; + } fault; + struct { + int32_t tid; + int32_t overrun; + uint64_t value; + } timer; + struct { + int32_t pid; + uint32_t uid; + uint64_t value; + } rt; + uint8_t raw[112]; + } fields; +}; +static_assert(sizeof(ksiginfo) == 128); + +struct kmsghdr { + uint64_t msg_name; + uint32_t msg_namelen; + uint32_t pad0; + uint64_t msg_iov; + uint64_t msg_iovlen; + uint64_t msg_control; + uint64_t msg_controllen; + int32_t msg_flags; + int32_t pad1; +}; +static_assert(sizeof(kmsghdr) == 56); + +struct kcmsghdr { + uint64_t cmsg_len; + int32_t cmsg_level; + int32_t cmsg_type; +}; + +struct klinger { + int32_t l_onoff; + int32_t l_linger; +}; + +struct kflock { + int16_t l_type; + int16_t l_whence; + int32_t pad; + int64_t l_start; + int64_t l_len; + int32_t l_pid; + int32_t pad2; +}; +#pragma pack(pop) + +} // namespace axrb::linux_abi diff --git a/direct/src/log.cpp b/direct/src/log.cpp new file mode 100644 index 0000000..71f75f1 --- /dev/null +++ b/direct/src/log.cpp @@ -0,0 +1,111 @@ +#include "log.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { +namespace { + +std::mutex g_lock; +FILE* g_file = nullptr; +std::set g_trace; +bool g_trace_all = false; +LogLevel g_level = LogLevel::Info; +const auto g_start = std::chrono::steady_clock::now(); + +const char* level_name(LogLevel level) { + switch (level) { + case LogLevel::Error: return "E"; + case LogLevel::Warn: return "W"; + case LogLevel::Info: return "I"; + case LogLevel::Debug: return "D"; + default: return "T"; + } +} + +} // namespace + +void log_init() { + if (const char* path = std::getenv("AXRB_DIRECT_LOG")) g_file = std::fopen(path, "wb"); + if (const char* level = std::getenv("AXRB_DIRECT_LOG_LEVEL")) g_level = static_cast(std::atoi(level)); + if (const char* list = std::getenv("AXRB_DIRECT_TRACE")) { + std::string text = list; + size_t at = 0; + while (at <= text.size()) { + size_t comma = text.find(',', at); + if (comma == std::string::npos) comma = text.size(); + std::string item = text.substr(at, comma - at); + if (item == "all") g_trace_all = true; + if (!item.empty()) g_trace.insert(item); + at = comma + 1; + } + } +} + +bool trace_enabled(const char* category) { + if (g_trace_all) return true; + if (g_trace.empty()) return false; + return g_trace.count(category) != 0; +} + +void log_writev(LogLevel level, const char* tag, const char* format, va_list args) { + if (level > g_level && level != LogLevel::Trace) return; + char body[4096]; + std::vsnprintf(body, sizeof body, format, args); + double seconds = std::chrono::duration(std::chrono::steady_clock::now() - g_start).count(); + char line[4300]; + int n = std::snprintf(line, sizeof line, "%9.3f %5lu %s/%s: %s\n", seconds, GetCurrentThreadId(), level_name(level), + tag, body); + if (n < 0) return; + if (n > static_cast(sizeof line) - 1) n = sizeof line - 1; + std::lock_guard held(g_lock); + std::fwrite(line, 1, n, stderr); + if (g_file) std::fwrite(line, 1, n, g_file); + // Unbuffered in effect: a killed or crashed process keeps its last lines. + std::fflush(stderr); + if (g_file) std::fflush(g_file); +} + +void log_write(LogLevel level, const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + log_writev(level, tag, format, args); + va_end(args); +} + +void log_raw(const char* text, size_t length) { + std::lock_guard held(g_lock); + std::fwrite(text, 1, length, stdout); + std::fflush(stdout); + if (g_file) { + std::fwrite(text, 1, length, g_file); + std::fflush(g_file); + } +} + +void log_flush() { + std::lock_guard held(g_lock); + std::fflush(stdout); + std::fflush(stderr); + if (g_file) std::fflush(g_file); +} + +void fatal(const char* format, ...) { + va_list args; + va_start(args, format); + log_writev(LogLevel::Error, "fatal", format, args); + va_end(args); + log_flush(); + TerminateProcess(GetCurrentProcess(), 70); + for (;;) { + } +} + +} // namespace axrb diff --git a/direct/src/log.h b/direct/src/log.h new file mode 100644 index 0000000..4557abb --- /dev/null +++ b/direct/src/log.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +namespace axrb { + +enum class LogLevel { Error = 0, Warn = 1, Info = 2, Debug = 3, Trace = 4 }; + +// AXRB_DIRECT_LOG= mirrors everything into a file; AXRB_DIRECT_TRACE is a +// comma-separated list of categories (syscall, files, mmap, thread, signal, +// futex, net, jni, vulkan, xr, java, all) that enables trace output for them. +void log_init(); +void log_write(LogLevel level, const char* tag, const char* format, ...); +void log_writev(LogLevel level, const char* tag, const char* format, va_list args); +bool trace_enabled(const char* category); +// Raw text, e.g. guest stdout/logcat lines, written without a prefix. +void log_raw(const char* text, size_t length); +void log_flush(); + +[[noreturn]] void fatal(const char* format, ...); + +} // namespace axrb + +#define LOGE(tag, ...) ::axrb::log_write(::axrb::LogLevel::Error, tag, __VA_ARGS__) +#define LOGW(tag, ...) ::axrb::log_write(::axrb::LogLevel::Warn, tag, __VA_ARGS__) +#define LOGI(tag, ...) ::axrb::log_write(::axrb::LogLevel::Info, tag, __VA_ARGS__) +#define LOGD(tag, ...) ::axrb::log_write(::axrb::LogLevel::Debug, tag, __VA_ARGS__) +#define TRACE(category, ...) \ + do { \ + if (::axrb::trace_enabled(category)) \ + ::axrb::log_write(::axrb::LogLevel::Trace, category, __VA_ARGS__); \ + } while (0) diff --git a/direct/src/main.cpp b/direct/src/main.cpp new file mode 100644 index 0000000..e56437e --- /dev/null +++ b/direct/src/main.cpp @@ -0,0 +1,260 @@ +// AXRB Direct: runs Android arm64 programs on Windows without an emulator. +// +// The guest's own bionic, linker and system libraries (from the Android SDK +// system image) run under Dynarmic; this process plays the Linux kernel +// underneath them and the Android services around them. + +#include "app.h" +#include "audio.h" +#include "ndk.h" +#include "cpu.h" +#include "exec.h" +#include "files.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" +#include "media.h" +#include "net.h" +#include "props.h" +#include "vk_host.h" +#include "profiler.h" +#include "sysroot.h" +#include "vfs.h" + +#include +#include + +#include +#include +#include +#include + +using namespace axrb; + +namespace { + +struct Options { + std::wstring sysroot; + std::wstring data; + std::vector env; + std::map props; + std::string program; + std::vector args; + AppOptions app; + bool app_mode = false; + bool low_memory = false; + bool host_graphics = false; + std::vector> read_only_mounts; +}; + +void usage() { + std::fprintf(stderr, + "usage: axrb-direct [--sysroot DIR] [--data DIR] [--env K=V]... [--prop K=V]... PROGRAM [ARGS...]\n" + " axrb-direct [--sysroot DIR] [--data DIR] --apk APK [--obb DIR] [--assets LIST] [--activity NAME]\n" + " PROGRAM is a guest path (/system/bin/...) or a host file, which is mounted read-only at\n" + " /data/local/tmp/axrb-host.\n"); + std::fprintf(stderr, " --low-memory reserves low guest address ranges for ART's compressed Java references.\n"); + std::fprintf(stderr, " --mount-ro GUEST=HOST mounts a host file or directory read-only.\n"); +} + +bool parse(int argc, wchar_t** argv, Options& o) { + int i = 1; + for (; i < argc; ++i) { + std::wstring a = argv[i]; + auto next = [&]() -> std::wstring { return i + 1 < argc ? argv[++i] : L""; }; + if (a == L"--sysroot") o.sysroot = next(); + else if (a == L"--data") o.data = next(); + else if (a == L"--env") o.env.push_back(narrow(next())); + else if (a == L"--low-memory") o.low_memory = true; + else if (a == L"--host-graphics") o.host_graphics = true; + else if (a == L"--mount-ro") { + std::wstring mapping = next(); + size_t eq = mapping.find(L'='); + if (eq == std::wstring::npos || eq == 0 || mapping[0] != L'/' || eq + 1 == mapping.size()) return false; + o.read_only_mounts.emplace_back(narrow(mapping.substr(0, eq)), mapping.substr(eq + 1)); + } + else if (a == L"--apk") { + o.app.apk = next(); + o.app_mode = true; + } else if (a == L"--obb") o.app.obb_dir = next(); + else if (a == L"--assets") o.app.asset_list = next(); + else if (a == L"--activity") o.app.activity = narrow(next()); + else if (a == L"--prop") { + std::string kv = narrow(next()); + size_t eq = kv.find('='); + if (eq != std::string::npos) o.props[kv.substr(0, eq)] = kv.substr(eq + 1); + } else if (a == L"--help" || a == L"-h") { + return false; + } else { + break; + } + } + if (o.app_mode) return i >= argc; + if (i >= argc) return false; + o.program = narrow(argv[i]); + for (++i; i < argc; ++i) o.args.push_back(narrow(argv[i])); + return true; +} + +void ensure_directory(const std::wstring& path) { + // CreateDirectory on each component; existing ones are fine. + for (size_t at = path.find(L'\\', 3); ; at = path.find(L'\\', at + 1)) { + CreateDirectoryW(path.substr(0, at).c_str(), nullptr); + if (at == std::wstring::npos) break; + } +} + +std::vector default_environment() { + return { + "PATH=/product/bin:/apex/com.android.runtime/bin:/system/bin:/system/xbin", + "ANDROID_ROOT=/system", + "ANDROID_DATA=/data", + "ANDROID_STORAGE=/storage", + "EXTERNAL_STORAGE=/sdcard", + "ANDROID_ART_ROOT=/apex/com.android.art", + "ANDROID_I18N_ROOT=/apex/com.android.i18n", + "ANDROID_TZDATA_ROOT=/apex/com.android.tzdata", + "HOME=/data/local/tmp", + "TMPDIR=/data/local/tmp", + "LANG=en_US.UTF-8", + }; +} + +struct MainStart { + GuestThread* thread; + CpuContext context; + int exit_code = 0; +}; + +DWORD WINAPI main_thread(void* param) { + auto* start = static_cast(param); + GuestThread& thread = *start->thread; + set_current_thread(&thread); + attach_cpu(thread); + load_context(thread, start->context); + try { + run_guest(thread, false); + } catch (const ThreadExit& exit) { + start->exit_code = exit.code; + } + finish_thread(thread); + detach_cpu(thread); + return 0; +} + +} // namespace + +int wmain(int argc, wchar_t** argv) { + Options options; + if (!parse(argc, argv, options)) { + usage(); + return 2; + } + log_init(); + profiler_start(); + timeBeginPeriod(1); + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + + if (!address_space().init(options.low_memory)) return 3; + install_fault_handler(); + net_init(); + + std::wstring sysroot = options.sysroot.empty() ? default_sysroot() : options.sysroot; + if (!sysroot_init(sysroot)) return 4; + proc_init(); + + // Persistent guest storage: /data and the shared /storage/emulated/0. + std::wstring data = options.data.empty() ? data_root() : options.data; + ensure_directory(data + L"\\data\\local\\tmp"); + ensure_directory(data + L"\\sdcard"); + vfs().add_mount("/data", data + L"\\data", false); + vfs().add_mount("/storage/emulated/0", data + L"\\sdcard", false); + for (const auto& [guest, host] : options.read_only_mounts) { + DWORD attributes = GetFileAttributesW(host.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { + LOGE("main", "read-only mount does not exist: %s", narrow(host).c_str()); + return 4; + } + vfs().add_mount(guest, host, true, !(attributes & FILE_ATTRIBUTE_DIRECTORY)); + } + LOGI("main", "guest data in %s", narrow(data).c_str()); + + // AXRB's runtime hands eye images to the host bridge through the GPU-share + // layer when it is active in our Vulkan instance. + if (vulkan_prepare_gpu_share()) options.props.emplace("debug.axrb.gpu_share", "1"); + props_init(sysroot, options.props); + if (options.host_graphics) { + register_named_function("art.stop_requested", [](HostCall& call) { + call.ret(session_stop_requested()); + }); + art_windows_init(); + art_media_init(); + art_media_codec_init(); + audio_init(); + vulkan_init(); + } + + Process& proc = process(); + proc.pid = 4242; + proc.next_tid = proc.pid + 1; + proc.cwd = "/"; + + std::vector env = default_environment(); + std::string guest_program = options.program; + if (options.app_mode) { + options.app.data_root = data; + if (!app_prepare(options.app, guest_program, env)) return 7; + } + + // A host file is mounted next to the guest's scratch space. + std::wstring host_program = widen(options.program); + DWORD attributes = GetFileAttributesW(host_program.c_str()); + if (!options.app_mode && attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY) && + options.program[0] != '/') { + wchar_t full[MAX_PATH * 4]; + GetFullPathNameW(host_program.c_str(), static_cast(std::size(full)), full, nullptr); + std::wstring path = full; + size_t slash = path.find_last_of(L"\\/"); + vfs().add_mount("/data/local/tmp/axrb-host", path.substr(0, slash), true); + guest_program = "/data/local/tmp/axrb-host/" + narrow(path.substr(slash + 1)); + proc.cwd = "/data/local/tmp"; + } + + auto* thread = new GuestThread(); + thread->tid = proc.pid; + std::string main_name = options.app_mode ? process().package : guest_program.substr(guest_program.rfind('/') + 1); + thread->name = main_name.size() > 15 ? main_name.substr(main_name.size() - 15) : main_name; + register_main_thread(thread); + + fds().install_at(0, std::make_shared(0), false); + fds().install_at(1, std::make_shared(1), false); + fds().install_at(2, std::make_shared(2), false); + + for (auto& e : options.env) env.push_back(e); + std::vector args = {options.app_mode ? process().package : guest_program}; + args.insert(args.end(), options.args.begin(), options.args.end()); + + MainStart start{thread, {}, 0}; + // exec maps the program; it needs a cpu-less thread context only for + // file access, so it runs here before the guest thread starts. + set_current_thread(thread); + if (int64_t e = exec_program(*thread, guest_program, args, env, start.context)) { + LOGE("main", "exec %s failed: %lld", guest_program.c_str(), (long long)e); + return 5; + } + set_current_thread(nullptr); + + // The guest's main thread gets the same large host stack as every other + // guest thread. + HANDLE h = CreateThread(nullptr, 64ull << 20, main_thread, &start, STACK_SIZE_PARAM_IS_A_RESERVATION, &thread->host_tid); + if (!h) return 6; + DuplicateHandle(GetCurrentProcess(), h, GetCurrentProcess(), &thread->host_thread, 0, FALSE, DUPLICATE_SAME_ACCESS); + WaitForSingleObject(h, INFINITE); + CloseHandle(h); + // The main thread called exit() without exit_group: Linux keeps the + // process while other threads live; Android programs never rely on it. + LOGI("main", "main thread exited (%d)", start.exit_code); + log_flush(); + TerminateProcess(GetCurrentProcess(), static_cast(start.exit_code & 0xff)); + return start.exit_code; +} diff --git a/direct/src/media.cpp b/direct/src/media.cpp new file mode 100644 index 0000000..d731daf --- /dev/null +++ b/direct/src/media.cpp @@ -0,0 +1,604 @@ +// android.media.MediaPlayer on Media Foundation. Video is decoded on the host +// into RGBA frames held per android.view.Surface handle; whoever consumes the +// surface (the OpenXR runtime's Android-surface swapchains) pulls the newest +// frame with Surface.nativeReadFrame. Audio tracks are not played. + +#include "media.h" +#include "cpu.h" +#include "audio.h" + +#include "kernel.h" +#include "linux.h" +#include "log.h" +#include "memory.h" + +#include "arun/vm/runtime.h" +#include "arun/vm/thread.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfreadwrite.lib") +#pragma comment(lib, "mfuuid.lib") +#pragma comment(lib, "shlwapi.lib") + +namespace axrb { +namespace vm = arun::vm; + +namespace { + +template +struct Com { + T* p = nullptr; + Com() = default; + Com(const Com&) = delete; + Com& operator=(const Com&) = delete; + ~Com() { reset(); } + void reset() { + if (p) p->Release(); + p = nullptr; + } + T** put() { + reset(); + return &p; + } + T* operator->() const { return p; } + explicit operator bool() const { return p != nullptr; } +}; + +// Newest decoded frame of one Surface, top-down RGBA. +struct FrameSlot { + std::mutex lock; + uint32_t width = 0, height = 0; + std::vector rgba; + uint64_t serial = 0; +}; + +std::mutex g_slots_lock; +std::map> g_slots; + +std::shared_ptr slot_for(int64_t surface) { + std::lock_guard guard(g_slots_lock); + auto& slot = g_slots[surface]; + if (!slot) slot = std::make_shared(); + return slot; +} + +std::shared_ptr find_slot(int64_t surface) { + std::lock_guard guard(g_slots_lock); + auto it = g_slots.find(surface); + return it == g_slots.end() ? nullptr : it->second; +} + +bool media_foundation() { + static const bool ready = SUCCEEDED(MFStartup(MF_VERSION, MFSTARTUP_LITE)); + return ready; +} + +class Player { +public: + ~Player() { stop_thread(); } + + void set_source(std::vector bytes) { + stop_thread(); + source_ = std::move(bytes); + prepared_ = false; + } + void set_surface(int64_t surface) { + std::lock_guard guard(lock_); + slot_ = surface ? slot_for(surface) : nullptr; + } + void set_looping(bool looping) { looping_ = looping; } + bool looping() const { return looping_; } + + bool prepare() { + stop_thread(); + reader_.reset(); + if (source_.empty() || !media_foundation()) return false; + Com stream; + stream.p = SHCreateMemStream(source_.data(), static_cast(source_.size())); + if (!stream) return false; + Com bytes; + if (FAILED(MFCreateMFByteStreamOnStream(stream.p, bytes.put()))) return false; + Com attributes; + if (FAILED(MFCreateAttributes(attributes.put(), 2))) return false; + attributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE); + attributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); + HRESULT hr = MFCreateSourceReaderFromByteStream(bytes.p, attributes.p, reader_.put()); + if (FAILED(hr)) { + LOGW("media", "MediaPlayer: source reader failed 0x%08lx", static_cast(hr)); + return false; + } + reader_->SetStreamSelection(static_cast(MF_SOURCE_READER_ALL_STREAMS), FALSE); + reader_->SetStreamSelection(static_cast(MF_SOURCE_READER_FIRST_VIDEO_STREAM), TRUE); + Com type; + if (FAILED(MFCreateMediaType(type.put()))) return false; + type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); + hr = reader_->SetCurrentMediaType(static_cast(MF_SOURCE_READER_FIRST_VIDEO_STREAM), nullptr, type.p); + if (FAILED(hr)) { + LOGW("media", "MediaPlayer: no RGB32 output 0x%08lx", static_cast(hr)); + return false; + } + Com actual; + if (FAILED(reader_->GetCurrentMediaType(static_cast(MF_SOURCE_READER_FIRST_VIDEO_STREAM), actual.put()))) + return false; + UINT32 w = 0, h = 0; + MFGetAttributeSize(actual.p, MF_MT_FRAME_SIZE, &w, &h); + width_ = w; + height_ = h; + PROPVARIANT duration; + PropVariantInit(&duration); + if (SUCCEEDED(reader_->GetPresentationAttribute(static_cast(MF_SOURCE_READER_MEDIASOURCE), + MF_PD_DURATION, &duration)) && + duration.vt == VT_UI8) + duration_ms_ = static_cast(duration.uhVal.QuadPart / 10000); + PropVariantClear(&duration); + prepared_ = true; + position_100ns_ = 0; + LOGI("media", "MediaPlayer: prepared %ux%u, %d ms", width_, height_, duration_ms_); + return true; + } + + void start() { + if (!prepared_) return; + std::unique_lock guard(lock_); + playing_ = true; + completed_ = false; + if (!thread_.joinable()) { + quit_ = false; + thread_ = std::thread([this] { decode_loop(); }); + } + wake_.notify_all(); + } + void pause() { + std::lock_guard guard(lock_); + playing_ = false; + wake_.notify_all(); + } + void stop() { + stop_thread(); + playing_ = false; + } + void seek(int32_t ms) { + std::lock_guard guard(lock_); + seek_100ns_ = static_cast(ms) * 10000; + wake_.notify_all(); + } + bool playing() const { return playing_ && !completed_; } + int32_t position_ms() const { return static_cast(position_100ns_ / 10000); } + int32_t duration_ms() const { return duration_ms_; } + uint32_t width() const { return width_; } + uint32_t height() const { return height_; } + +private: + void stop_thread() { + { + std::lock_guard guard(lock_); + quit_ = true; + wake_.notify_all(); + } + if (thread_.joinable()) thread_.join(); + } + + void rewind() { + PROPVARIANT start; + PropVariantInit(&start); + start.vt = VT_I8; + start.hVal.QuadPart = 0; + reader_->SetCurrentPosition(GUID_NULL, start); + PropVariantClear(&start); + } + + void decode_loop() { + using Clock = std::chrono::steady_clock; + CoInitializeEx(nullptr, COINIT_MULTITHREADED); + Clock::time_point origin = Clock::now(); + int64_t origin_pts = -1; + for (;;) { + { + std::unique_lock guard(lock_); + wake_.wait(guard, [&] { return quit_ || playing_; }); + if (quit_) break; + if (seek_100ns_ >= 0) { + PROPVARIANT at; + PropVariantInit(&at); + at.vt = VT_I8; + at.hVal.QuadPart = seek_100ns_; + reader_->SetCurrentPosition(GUID_NULL, at); + PropVariantClear(&at); + seek_100ns_ = -1; + origin_pts = -1; + } + } + DWORD flags = 0; + LONGLONG pts = 0; + Com sample; + HRESULT hr = reader_->ReadSample(static_cast(MF_SOURCE_READER_FIRST_VIDEO_STREAM), 0, nullptr, + &flags, &pts, sample.put()); + if (FAILED(hr) || (flags & MF_SOURCE_READERF_ERROR)) { + LOGW("media", "MediaPlayer: decode failed 0x%08lx", static_cast(hr)); + completed_ = true; + break; + } + if (flags & MF_SOURCE_READERF_ENDOFSTREAM) { + if (!looping_) { + completed_ = true; + break; + } + rewind(); + origin_pts = -1; + continue; + } + if (!sample) continue; + // Pace frames by presentation time from the first frame after a + // (re)start, so pauses and loops do not make playback race. + if (origin_pts < 0) { + origin_pts = pts; + origin = Clock::now(); + } + const auto due = origin + std::chrono::microseconds((pts - origin_pts) / 10); + { + std::unique_lock guard(lock_); + const auto paused_at = Clock::now(); + wake_.wait_until(guard, due, [&] { return quit_ || !playing_ || seek_100ns_ >= 0; }); + if (quit_) break; + if (!playing_) { + wake_.wait(guard, [&] { return quit_ || playing_; }); + if (quit_) break; + origin += Clock::now() - paused_at; + } + } + position_100ns_ = pts; + publish(sample.p); + } + CoUninitialize(); + } + + void publish(IMFSample* sample) { + std::shared_ptr slot; + { + std::lock_guard guard(lock_); + slot = slot_; + } + if (!slot || !width_ || !height_) return; + Com buffer; + if (FAILED(sample->ConvertToContiguousBuffer(buffer.put()))) return; + Com planar; + BYTE* scan0 = nullptr; + LONG pitch = 0; + BYTE* raw = nullptr; + DWORD length = 0; + bool locked2d = SUCCEEDED(buffer->QueryInterface(IID_PPV_ARGS(planar.put()))) && + SUCCEEDED(planar->Lock2D(&scan0, &pitch)); + if (!locked2d) { + if (FAILED(buffer->Lock(&raw, nullptr, &length))) return; + scan0 = raw; + pitch = static_cast(width_ * 4); + if (length < width_ * height_ * 4) { + buffer->Unlock(); + return; + } + } + { + std::lock_guard guard(slot->lock); + slot->width = width_; + slot->height = height_; + slot->rgba.resize(static_cast(width_) * height_ * 4); + for (uint32_t y = 0; y < height_; ++y) { + const uint8_t* in = scan0 + static_cast(pitch) * y; + uint8_t* out = slot->rgba.data() + static_cast(y) * width_ * 4; + for (uint32_t x = 0; x < width_; ++x) { // BGRX -> RGBA + out[x * 4 + 0] = in[x * 4 + 2]; + out[x * 4 + 1] = in[x * 4 + 1]; + out[x * 4 + 2] = in[x * 4 + 0]; + out[x * 4 + 3] = 255; + } + } + ++slot->serial; + // AXRB_MEDIA_DUMP=: the 30th frame of each video as a PPM. + if (slot->serial == 30) { + if (const char* dir = std::getenv("AXRB_MEDIA_DUMP")) { + std::string path = std::string(dir) + "/video-" + std::to_string(width_) + "x" + + std::to_string(height_) + "-" + std::to_string(GetTickCount64()) + ".ppm"; + if (FILE* f = std::fopen(path.c_str(), "wb")) { + std::fprintf(f, "P6 %u %u 255\n", width_, height_); + for (size_t i = 0; i < slot->rgba.size(); i += 4) std::fwrite(&slot->rgba[i], 1, 3, f); + std::fclose(f); + } + } + } + } + if (locked2d) + planar->Unlock2D(); + else + buffer->Unlock(); + } + + std::vector source_; + Com reader_; + std::shared_ptr slot_; + std::thread thread_; + std::mutex lock_; + std::condition_variable wake_; + bool quit_ = false; + std::atomic playing_{false}, completed_{false}, looping_{false}; + bool prepared_ = false; + int64_t seek_100ns_ = -1; + std::atomic position_100ns_{0}; + int32_t duration_ms_ = 0; + uint32_t width_ = 0, height_ = 0; +}; + +std::mutex g_players_lock; +std::map> g_players; +int64_t g_next_player = 1; + +std::shared_ptr player(vm::Frame* a) { + std::lock_guard guard(g_players_lock); + auto it = g_players.find(vm::ArgLong(a, 0)); + return it == g_players.end() ? nullptr : it->second; +} + +int64_t sys(uint64_t nr, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3) { + GuestThread* t = current_thread(); + if (!t) return -linux_abi::EBADF_; + return do_syscall(*t, nr, a0, a1, a2, a3, 0, 0); +} + +// --- android.media.MediaPlayer --------------------------------------------------------- + +void MediaPlayer_nativeCreate(vm::Thread*, vm::Frame*, vm::JValue* r) { + std::lock_guard guard(g_players_lock); + const int64_t id = g_next_player++; + g_players[id] = std::make_shared(); + r->j = id; +} + +// (J player, I fd, J offset, J length) -> Z +void MediaPlayer_nativeSetSource(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + r->i = 0; + auto p = player(a); + const int32_t fd = vm::ArgInt(a, 2); + const int64_t offset = vm::ArgLong(a, 3); + int64_t length = vm::ArgLong(a, 5); + if (!p || fd < 0 || offset < 0) return; + constexpr uint64_t kPread64 = 67, kFstat = 80; + vm::ScopedBlocking blocking(self); + if (length < 0) { + linux_abi::kstat st{}; + if (sys(kFstat, static_cast(fd), reinterpret_cast(&st), 0, 0) != 0) return; + length = st.st_size - offset; + } + if (length <= 0 || length > (int64_t{1} << 31)) return; + std::vector bytes(static_cast(length)); + int64_t done = 0; + while (done < length) { + int64_t n = sys(kPread64, static_cast(fd), reinterpret_cast(bytes.data() + done), + static_cast(length - done), static_cast(offset + done)); + if (n <= 0) return; + done += n; + } + p->set_source(std::move(bytes)); + r->i = 1; +} + +void MediaPlayer_nativeSetSurface(vm::Thread*, vm::Frame* a, vm::JValue*) { + if (auto p = player(a)) p->set_surface(vm::ArgLong(a, 2)); +} + +// -> [I {width, height, durationMs}] or null +void MediaPlayer_nativePrepare(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + r->l = nullptr; + auto p = player(a); + bool ok; + { + vm::ScopedBlocking blocking(self); + ok = p && p->prepare(); + } + if (!ok) return; + vm::Runtime* rt = self->runtime(); + vm::RtClass* int_array = rt->class_linker().FindClass(self, "[I", nullptr); + if (!int_array) return; + vm::Array* out = rt->heap().AllocArray(self, int_array, 3); + if (!out) return; + const int32_t values[3] = {static_cast(p->width()), static_cast(p->height()), p->duration_ms()}; + std::memcpy(out->data(), values, sizeof values); + r->l = out; +} + +void MediaPlayer_nativeStart(vm::Thread*, vm::Frame* a, vm::JValue*) { + if (auto p = player(a)) p->start(); +} +void MediaPlayer_nativePause(vm::Thread*, vm::Frame* a, vm::JValue*) { + if (auto p = player(a)) p->pause(); +} +void MediaPlayer_nativeStop(vm::Thread* self, vm::Frame* a, vm::JValue*) { + vm::ScopedBlocking blocking(self); + if (auto p = player(a)) p->stop(); +} +void MediaPlayer_nativeSeek(vm::Thread*, vm::Frame* a, vm::JValue*) { + if (auto p = player(a)) p->seek(vm::ArgInt(a, 2)); +} +void MediaPlayer_nativeSetLooping(vm::Thread*, vm::Frame* a, vm::JValue*) { + if (auto p = player(a)) p->set_looping(vm::ArgInt(a, 2) != 0); +} +void MediaPlayer_nativeIsPlaying(vm::Thread*, vm::Frame* a, vm::JValue* r) { + auto p = player(a); + r->i = p && p->playing(); +} +void MediaPlayer_nativePosition(vm::Thread*, vm::Frame* a, vm::JValue* r) { + auto p = player(a); + r->i = p ? p->position_ms() : 0; +} +void MediaPlayer_nativeRelease(vm::Thread* self, vm::Frame* a, vm::JValue*) { + std::shared_ptr p; + { + std::lock_guard guard(g_players_lock); + auto it = g_players.find(vm::ArgLong(a, 0)); + if (it == g_players.end()) return; + p = std::move(it->second); + g_players.erase(it); + } + vm::ScopedBlocking blocking(self); + p.reset(); +} + +// --- android.view.Surface ---------------------------------------------------------------- + +// (J handle, J dst, I width, I height, J serial) -> J newest serial. Writes the +// newest frame, scaled to width x height, to dst when it is newer than serial. +void Surface_nativeReadFrame(vm::Thread*, vm::Frame* a, vm::JValue* r) { + const int64_t handle = vm::ArgLong(a, 0); + const uint64_t dst = static_cast(vm::ArgLong(a, 2)); + const int32_t width = vm::ArgInt(a, 4), height = vm::ArgInt(a, 5); + const int64_t seen = vm::ArgLong(a, 6); + r->j = seen; + auto slot = find_slot(handle); + if (!slot || !dst || width <= 0 || height <= 0) return; + std::lock_guard guard(slot->lock); + if (static_cast(slot->serial) == seen || slot->rgba.empty()) return; + auto* out = reinterpret_cast(dst); + if (slot->width == static_cast(width) && slot->height == static_cast(height)) { + std::memcpy(out, slot->rgba.data(), slot->rgba.size()); + } else { + for (int32_t y = 0; y < height; ++y) { + const uint32_t sy = static_cast((static_cast(y) * slot->height) / height); + const uint32_t* row = reinterpret_cast(slot->rgba.data()) + sy * slot->width; + uint32_t* line = reinterpret_cast(out) + static_cast(y) * width; + for (int32_t x = 0; x < width; ++x) + line[x] = row[(static_cast(x) * slot->width) / width]; + } + } + r->j = static_cast(slot->serial); +} + +void Surface_nativeRelease(vm::Thread*, vm::Frame* a, vm::JValue*) { + std::lock_guard guard(g_slots_lock); + g_slots.erase(vm::ArgLong(a, 0)); +} + +} // namespace + +void media_publish_frame(int64_t surface, uint32_t width, uint32_t height, const uint8_t* rgba) { + auto slot = slot_for(surface); + std::lock_guard guard(slot->lock); + slot->width = width; + slot->height = height; + slot->rgba.assign(rgba, rgba + static_cast(width) * height * 4); + ++slot->serial; +} + +void art_media_init() { + register_named_function("art.media", [](HostCall& call) { + const uint64_t op = call.x(0); + const int64_t id = static_cast(call.x(1)); + if (op == 17) return call.ret(audio_active()); + if (op == 16) { std::lock_guard guard(g_slots_lock); g_slots.erase(id); return call.ret(0); } + if (op == 0) { + std::lock_guard guard(g_players_lock); + const int64_t created = g_next_player++; + g_players[created] = std::make_shared(); + return call.ret(created); + } + std::shared_ptr p; + { + std::lock_guard guard(g_players_lock); + auto it = g_players.find(id); + if (it != g_players.end()) { + p = it->second; + if (op == 12) g_players.erase(it); + } + } + if (!p) return call.ret(static_cast(-1)); + switch (op) { + case 1: { + int64_t length = static_cast(call.x(4)); + const int64_t offset = static_cast(call.x(3)); + linux_abi::kstat st{}; + if (offset < 0 || sys(80, call.x(2), reinterpret_cast(&st), 0, 0) != 0 || offset > st.st_size) + return call.ret(-1); + length = std::min(length, st.st_size - offset); + if (length <= 0 || length > (int64_t{1} << 31)) return call.ret(-1); + std::vector bytes(static_cast(length)); + int64_t done = 0; + while (done < length) { + int64_t n = sys(67, call.x(2), reinterpret_cast(bytes.data() + done), length - done, offset + done); + if (n <= 0) return call.ret(-1); + done += n; + } + p->set_source(std::move(bytes)); return call.ret(0); + } + case 2: p->set_surface(call.x(2)); break; + case 3: return call.ret(p->prepare() ? 0 : -1); + case 4: p->start(); break; + case 5: p->pause(); break; + case 6: p->stop(); break; + case 7: p->seek(static_cast(call.x(2))); break; + case 8: p->set_looping(call.x(2) != 0); break; + case 9: return call.ret(p->playing()); + case 10: return call.ret(p->position_ms()); + case 11: return call.ret(p->duration_ms()); + case 12: p.reset(); break; + case 13: return call.ret(p->width()); + case 14: return call.ret(p->height()); + case 15: return call.ret(p->looping()); + default: return call.ret(-1); + } + call.ret(0); + }); + register_named_function("art.media_frame", [](HostCall& call) { + auto slot = find_slot(call.x(0)); + uint64_t seen = call.x(4); + if (!slot) return call.ret(0); + std::lock_guard guard(slot->lock); + uint32_t w = static_cast(call.x(2)), h = static_cast(call.x(3)); + if (slot->rgba.empty()) return call.ret(0); + if (seen == slot->serial || !w || !h || + !address_space().check(call.x(1), uint64_t(w) * h * 4, linux_abi::PROT_WRITE_) || + !address_space().populate(call.x(1), uint64_t(w) * h * 4)) return call.ret(seen); + auto* out = reinterpret_cast(call.x(1)); + const auto* source = reinterpret_cast(slot->rgba.data()); + for (uint32_t y = 0; y < h; ++y) + for (uint32_t x = 0; x < w; ++x) + out[size_t(y) * w + x] = source[(uint64_t(y) * slot->height / h) * slot->width + uint64_t(x) * slot->width / w]; + call.ret(slot->serial); + }); +} + +void register_media_natives(vm::Runtime* rt) { + auto reg = [rt](const char* key, vm::NativeFn fn) { rt->RegisterNative(key, fn); }; + reg("Landroid/media/MediaPlayer;.nativeCreate()J", MediaPlayer_nativeCreate); + reg("Landroid/media/MediaPlayer;.nativeSetSource(JIJJ)Z", MediaPlayer_nativeSetSource); + reg("Landroid/media/MediaPlayer;.nativeSetSurface(JJ)V", MediaPlayer_nativeSetSurface); + reg("Landroid/media/MediaPlayer;.nativePrepare(J)[I", MediaPlayer_nativePrepare); + reg("Landroid/media/MediaPlayer;.nativeStart(J)V", MediaPlayer_nativeStart); + reg("Landroid/media/MediaPlayer;.nativePause(J)V", MediaPlayer_nativePause); + reg("Landroid/media/MediaPlayer;.nativeStop(J)V", MediaPlayer_nativeStop); + reg("Landroid/media/MediaPlayer;.nativeSeek(JI)V", MediaPlayer_nativeSeek); + reg("Landroid/media/MediaPlayer;.nativeSetLooping(JZ)V", MediaPlayer_nativeSetLooping); + reg("Landroid/media/MediaPlayer;.nativeIsPlaying(J)Z", MediaPlayer_nativeIsPlaying); + reg("Landroid/media/MediaPlayer;.nativePosition(J)I", MediaPlayer_nativePosition); + reg("Landroid/media/MediaPlayer;.nativeRelease(J)V", MediaPlayer_nativeRelease); + reg("Landroid/view/Surface;.nativeReadFrame(JJIIJ)J", Surface_nativeReadFrame); + reg("Landroid/view/Surface;.nativeRelease(J)V", Surface_nativeRelease); +} + +} // namespace axrb diff --git a/direct/src/media.h b/direct/src/media.h new file mode 100644 index 0000000..e74a6f0 --- /dev/null +++ b/direct/src/media.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace arun::vm { +class Runtime; +} + +namespace axrb { + +// MediaPlayer (Media Foundation video decoding) and the frame exchange behind +// android.view.Surface. +void register_media_natives(arun::vm::Runtime* rt); +void art_media_init(); +// AMediaExtractor and AMediaCodec on Media Foundation (media_codec.cpp). +void art_media_codec_init(); +// Makes a top-down RGBA frame the newest frame of the Surface with this media +// handle, as MediaPlayer playback does. +void media_publish_frame(int64_t surface, uint32_t width, uint32_t height, const uint8_t* rgba); + +} // namespace axrb diff --git a/direct/src/media_codec.cpp b/direct/src/media_codec.cpp new file mode 100644 index 0000000..8cbf702 --- /dev/null +++ b/direct/src/media_codec.cpp @@ -0,0 +1,1100 @@ +// AMediaExtractor and AMediaCodec (libmediandk) on Media Foundation. AOSP's +// implementations need the media.extractor and media.codec services, which +// Direct mode does not run. The guest proxy (direct/art/ndk_media.cpp) calls +// "art.mediandk" instead: a Source Reader demuxes compressed samples and +// decoder MFTs turn them into PCM, NV12 buffers or Surface frames. + +#include "media.h" +#include "cpu.h" +#include "kernel.h" +#include "linux.h" +#include "log.h" +#include "memory.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfreadwrite.lib") +#pragma comment(lib, "mfuuid.lib") +#pragma comment(lib, "shlwapi.lib") + +namespace axrb { +namespace { + +// Shared with direct/art/ndk_media.cpp; plain fixed-size layouts. +enum Op : uint64_t { + kExtractorNew = 0, kExtractorSourceFd, kExtractorTrackCount, kExtractorTrack, kExtractorCsd, + kExtractorSelect, kExtractorSeek, kExtractorSample, kExtractorRead, kExtractorAdvance, + kExtractorDelete, kExtractorFileFormat, + kCodecNew = 20, kCodecConfigure, kCodecStart, kCodecStop, kCodecFlush, kCodecDelete, + kCodecInput, kCodecOutput, kCodecRelease, kCodecFormat, kCodecSurface, + kWindowHandle = 40, kWindowRegister, +}; +struct TrackInfo { + char mime[64]; + int64_t duration_us; + int32_t width, height, sample_rate, channels, max_input_size, frame_rate, bit_rate, is_adts; + int32_t csd_count, reserved; +}; +struct SampleInfo { + int64_t time_us; + int32_t track, flags, size, reserved; +}; +struct CodecConfig { + int32_t width, height, sample_rate, channels, is_adts, reserved; + uint64_t csd[2]; + int32_t csd_size[2]; +}; +struct OutputInfo { + int64_t time_us; + uint64_t sequence; + int32_t size, flags; +}; +struct OutputFormat { + char mime[64]; + int32_t width, height, stride, slice_height, color_format; + int32_t crop_left, crop_top, crop_right, crop_bottom; + int32_t sample_rate, channels, pcm_encoding; +}; +// media_status_t values and buffer flags of the NDK. +constexpr int64_t kErrorUnknown = -10000, kErrorMalformed = -10001, kErrorUnsupported = -10002, + kErrorInvalidOperation = -10006; +constexpr int32_t kFlagCodecConfig = 2, kFlagEndOfStream = 4; +constexpr int32_t kColorYuv420SemiPlanar = 21; + +template +struct Com { + T* p = nullptr; + Com() = default; + Com(const Com&) = delete; + Com& operator=(const Com&) = delete; + ~Com() { reset(); } + void reset() { + if (p) p->Release(); + p = nullptr; + } + T** put() { + reset(); + return &p; + } + T* operator->() const { return p; } + explicit operator bool() const { return p != nullptr; } +}; + +bool media_foundation() { + static const bool ready = [] { + CoInitializeEx(nullptr, COINIT_MULTITHREADED); + return SUCCEEDED(MFStartup(MF_VERSION, MFSTARTUP_LITE)); + }(); + return ready; +} + +int64_t sys(uint64_t nr, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3) { + GuestThread* t = current_thread(); + if (!t) return -linux_abi::EBADF_; + return do_syscall(*t, nr, a0, a1, a2, a3, 0, 0); +} + +bool guest_writable(uint64_t address, uint64_t size) { + return address && address_space().check(address, size, linux_abi::PROT_WRITE_) && address_space().populate(address, size); +} +bool guest_readable(uint64_t address, uint64_t size) { + return address && address_space().check(address, size, linux_abi::PROT_READ_) && address_space().populate(address, size); +} + +std::vector buffer_bytes(IMFSample* sample) { + std::vector bytes; + Com buffer; + if (!sample || FAILED(sample->ConvertToContiguousBuffer(buffer.put()))) return bytes; + BYTE* data = nullptr; + DWORD length = 0; + if (FAILED(buffer->Lock(&data, nullptr, &length))) return bytes; + bytes.assign(data, data + length); + buffer->Unlock(); + return bytes; +} + +std::vector blob(IMFAttributes* attributes, const GUID& key) { + UINT32 size = 0; + std::vector bytes; + if (FAILED(attributes->GetBlobSize(key, &size)) || !size) return bytes; + bytes.resize(size); + if (FAILED(attributes->GetBlob(key, bytes.data(), size, nullptr))) bytes.clear(); + return bytes; +} + +// --- H.264/HEVC bitstream helpers --------------------------------------------------------- + +const uint8_t kStartCode[4] = {0, 0, 0, 1}; + +bool has_start_code(const std::vector& b) { + return (b.size() >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0 && b[3] == 1) || + (b.size() >= 3 && b[0] == 0 && b[1] == 0 && b[2] == 1); +} + +// 4-byte length-prefixed NAL units (MP4 sample layout) to Annex B, which +// Android extractors produce and decoders accept. Annex B input is unchanged. +std::vector to_annex_b(std::vector bytes) { + if (has_start_code(bytes)) return bytes; + std::vector out; + size_t at = 0; + while (at + 4 <= bytes.size()) { + const uint32_t n = (uint32_t(bytes[at]) << 24) | (uint32_t(bytes[at + 1]) << 16) | (uint32_t(bytes[at + 2]) << 8) | bytes[at + 3]; + at += 4; + if (n == 0 || n > bytes.size() - at) return bytes; + out.insert(out.end(), kStartCode, kStartCode + 4); + out.insert(out.end(), bytes.begin() + at, bytes.begin() + at + n); + at += n; + } + return at == bytes.size() ? out : bytes; +} + +// Annex B NAL units of a byte string, without start codes. +std::vector> nal_units(const std::vector& b) { + std::vector> units; + size_t i = 0, begin = std::string::npos; + while (i + 3 <= b.size()) { + if (b[i] == 0 && b[i + 1] == 0 && b[i + 2] == 1) { + if (begin != std::string::npos) { + size_t end = i; + while (end > begin && b[end - 1] == 0) --end; + units.push_back({begin, end - begin}); + } + i += 3; + begin = i; + } else { + ++i; + } + } + if (begin != std::string::npos && begin < b.size()) units.push_back({begin, b.size() - begin}); + return units; +} + +// avcC / hvcC configuration records to Annex B parameter sets. +std::vector avcc_to_annex_b(const std::vector& c) { + std::vector out; + if (c.size() < 7 || c[0] != 1) return out; + size_t at = 5; + for (int list = 0; list < 2 && at < c.size(); ++list) { + int count = list == 0 ? (c[at] & 0x1f) : c[at]; + ++at; + for (int i = 0; i < count && at + 2 <= c.size(); ++i) { + const size_t n = (size_t(c[at]) << 8) | c[at + 1]; + at += 2; + if (at + n > c.size()) return {}; + out.insert(out.end(), kStartCode, kStartCode + 4); + out.insert(out.end(), c.begin() + at, c.begin() + at + n); + at += n; + } + } + return out; +} +std::vector hvcc_to_annex_b(const std::vector& c) { + std::vector out; + if (c.size() < 23 || c[0] != 1) return out; + size_t at = 22; + const int arrays = c[at++]; + for (int a = 0; a < arrays && at + 3 <= c.size(); ++a) { + at += 1; + const int count = (c[at] << 8) | c[at + 1]; + at += 2; + for (int i = 0; i < count && at + 2 <= c.size(); ++i) { + const size_t n = (size_t(c[at]) << 8) | c[at + 1]; + at += 2; + if (at + n > c.size()) return {}; + out.insert(out.end(), kStartCode, kStartCode + 4); + out.insert(out.end(), c.begin() + at, c.begin() + at + n); + at += n; + } + } + return out; +} + +// MPEG-4 AudioSpecificConfig for AAC-LC. +std::vector aac_config(uint32_t rate, uint32_t channels) { + static const uint32_t rates[] = {96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350}; + uint8_t index = 4; + for (uint8_t i = 0; i < 13; ++i) + if (rates[i] == rate) index = i; + return {static_cast((2 << 3) | (index >> 1)), static_cast(((index & 1) << 7) | ((channels & 0xf) << 3))}; +} + +struct Mime { + const GUID* subtype; + const char* mime; + bool video; +}; +const Mime kMimes[] = { + {&MFVideoFormat_H264, "video/avc", true}, {&MFVideoFormat_HEVC, "video/hevc", true}, + {&MFVideoFormat_VP90, "video/x-vnd.on2.vp9", true}, {&MFVideoFormat_VP80, "video/x-vnd.on2.vp8", true}, + {&MFVideoFormat_AV1, "video/av01", true}, {&MFVideoFormat_MP4V, "video/mp4v-es", true}, + {&MFVideoFormat_M4S2, "video/mp4v-es", true}, {&MFAudioFormat_AAC, "audio/mp4a-latm", false}, + {&MFAudioFormat_MP3, "audio/mpeg", false}, {&MFAudioFormat_MPEG, "audio/mpeg", false}, + {&MFAudioFormat_FLAC, "audio/flac", false}, {&MFAudioFormat_Opus, "audio/opus", false}, + {&MFAudioFormat_ALAC, "audio/alac", false}, {&MFAudioFormat_PCM, "audio/raw", false}, +}; +const Mime* mime_for(const GUID& subtype) { + for (auto& m : kMimes) + if (*m.subtype == subtype) return &m; + return nullptr; +} +const Mime* mime_named(const std::string& name) { + for (auto& m : kMimes) + if (name == m.mime && *m.subtype != MFVideoFormat_M4S2 && *m.subtype != MFAudioFormat_MPEG) return &m; + return nullptr; +} + +void copy_string(char (&out)[64], const std::string& value) { + std::memset(out, 0, sizeof out); + std::memcpy(out, value.data(), std::min(value.size(), sizeof out - 1)); +} + +// --- Extractor ------------------------------------------------------------------------------ + +class Extractor { +public: + std::mutex calls; // held by the dispatcher for each call + struct Track { + DWORD stream; + TrackInfo info{}; + std::vector> csd; + bool selected = false, ended = false, annex_b = false; + }; + + int64_t open(std::vector bytes) { + if (!media_foundation()) return kErrorUnknown; + source_ = std::move(bytes); + Com stream; + stream.p = SHCreateMemStream(source_.data(), static_cast(source_.size())); + if (!stream) return kErrorUnknown; + Com byte_stream; + if (FAILED(MFCreateMFByteStreamOnStream(stream.p, byte_stream.put()))) return kErrorUnknown; + // The scheme resolver picks a byte-stream handler by content type. + const wchar_t* type = sniff(); + Com stream_attributes; + if (type && SUCCEEDED(byte_stream->QueryInterface(IID_PPV_ARGS(stream_attributes.put())))) + stream_attributes->SetString(MF_BYTESTREAM_CONTENT_TYPE, type); + HRESULT hr = MFCreateSourceReaderFromByteStream(byte_stream.p, nullptr, reader_.put()); + if (FAILED(hr)) { + LOGW("media", "AMediaExtractor: unsupported container (0x%08lx)", static_cast(hr)); + return kErrorUnsupported; + } + int64_t duration_us = 0; + PROPVARIANT value; + PropVariantInit(&value); + if (SUCCEEDED(reader_->GetPresentationAttribute(static_cast(MF_SOURCE_READER_MEDIASOURCE), MF_PD_DURATION, &value)) && + value.vt == VT_UI8) + duration_us = static_cast(value.uhVal.QuadPart / 10); + PropVariantClear(&value); + for (DWORD i = 0;; ++i) { + Com native; + hr = reader_->GetNativeMediaType(i, 0, native.put()); + if (hr == MF_E_INVALIDSTREAMNUMBER) break; + reader_->SetStreamSelection(i, FALSE); + if (FAILED(hr)) continue; + Track track; + track.stream = i; + if (!describe(native.p, track)) continue; + track.info.duration_us = duration_us; + tracks_.push_back(std::move(track)); + } + LOGI("media", "AMediaExtractor: %s, %zu track(s), %lld ms", type ? "container" : "unknown container", + tracks_.size(), static_cast(duration_us / 1000)); + for (auto& t : tracks_) + LOGI("media", "AMediaExtractor: track %s %dx%d %d Hz %d ch", t.info.mime, t.info.width, t.info.height, + t.info.sample_rate, t.info.channels); + return tracks_.empty() ? kErrorUnsupported : 0; + } + + size_t track_count() const { return tracks_.size(); } + const Track* track(uint64_t index) const { return index < tracks_.size() ? &tracks_[index] : nullptr; } + std::string file_format() const { return format_; } + + int64_t select(uint64_t index, bool on) { + if (index >= tracks_.size()) return kErrorInvalidOperation; + auto& t = tracks_[index]; + if (t.selected == on) return 0; + t.selected = on; + t.ended = false; + reader_->SetStreamSelection(t.stream, on); + if (!on && has_sample_ && sample_track_ == static_cast(index)) has_sample_ = false; + if (on && !has_sample_ && !ended_) loaded_ = false; + if (on) ended_ = false; + return 0; + } + + int64_t seek(int64_t time_us) { + PROPVARIANT at; + PropVariantInit(&at); + at.vt = VT_I8; + at.hVal.QuadPart = std::max(time_us, 0) * 10; + const HRESULT hr = reader_->SetCurrentPosition(GUID_NULL, at); + PropVariantClear(&at); + for (auto& t : tracks_) t.ended = false; + ended_ = false; + has_sample_ = false; + loaded_ = false; + return SUCCEEDED(hr) ? 0 : kErrorUnknown; + } + + // The current sample of the selected tracks, reading it on first use. + bool current() { + if (!loaded_) load(); + return has_sample_; + } + bool advance() { + if (!current()) return false; + has_sample_ = false; + loaded_ = false; + return current(); + } + int64_t time_us() const { return sample_time_us_; } + int32_t sample_track() const { return sample_track_; } + int32_t sample_flags() const { return sample_flags_; } + const std::vector& sample() const { return sample_; } + +private: + const wchar_t* sniff() { + const auto& b = source_; + auto at = [&](size_t i, const char* text) { return b.size() >= i + std::strlen(text) && !std::memcmp(&b[i], text, std::strlen(text)); }; + if (at(4, "ftyp")) { format_ = "video/mp4"; return L"video/mp4"; } + if (b.size() >= 4 && b[0] == 0x1a && b[1] == 0x45 && b[2] == 0xdf && b[3] == 0xa3) { format_ = "video/webm"; return L"video/webm"; } + if (at(0, "RIFF") && at(8, "WAVE")) { format_ = "audio/wav"; return L"audio/wav"; } + if (at(0, "ID3") || (b.size() >= 2 && b[0] == 0xff && (b[1] & 0xe0) == 0xe0)) { + const bool adts = b.size() >= 2 && b[0] == 0xff && (b[1] & 0xf6) == 0xf0; + format_ = adts ? "audio/aac-adts" : "audio/mpeg"; + return adts ? L"audio/aac" : L"audio/mpeg"; + } + if (at(0, "fLaC")) { format_ = "audio/flac"; return L"audio/flac"; } + if (b.size() >= 4 && b[0] == 0x47 && b.size() > 188 && b[188] == 0x47) { format_ = "video/mp2ts"; return L"video/mp2t"; } + return nullptr; + } + + bool describe(IMFMediaType* type, Track& track) { + GUID major{}, subtype{}; + type->GetGUID(MF_MT_MAJOR_TYPE, &major); + type->GetGUID(MF_MT_SUBTYPE, &subtype); + const Mime* mime = mime_for(subtype); + if (!mime) return false; + auto& info = track.info; + copy_string(info.mime, mime->mime); + if (major == MFMediaType_Video) { + UINT32 w = 0, h = 0, num = 0, den = 0; + MFGetAttributeSize(type, MF_MT_FRAME_SIZE, &w, &h); + MFGetAttributeRatio(type, MF_MT_FRAME_RATE, &num, &den); + info.width = static_cast(w); + info.height = static_cast(h); + info.frame_rate = den ? static_cast((num + den / 2) / den) : 0; + info.bit_rate = static_cast(MFGetAttributeUINT32(type, MF_MT_AVG_BITRATE, 0)); + info.max_input_size = std::max(1 << 20, info.width * info.height * 3 / 2); + auto header = blob(type, MF_MT_MPEG_SEQUENCE_HEADER); + if (subtype == MFVideoFormat_H264) { + track.annex_b = true; + if (!header.empty() && header[0] == 1) header = avcc_to_annex_b(header); + std::vector sps, pps; + for (auto [at, n] : nal_units(header)) { + auto& into = (header[at] & 0x1f) == 7 ? sps : pps; + into.insert(into.end(), kStartCode, kStartCode + 4); + into.insert(into.end(), header.begin() + at, header.begin() + at + n); + } + if (!sps.empty()) track.csd.push_back(sps); + if (!pps.empty()) track.csd.push_back(pps); + } else if (subtype == MFVideoFormat_HEVC) { + track.annex_b = true; + if (!header.empty() && header[0] == 1) header = hvcc_to_annex_b(header); + if (!header.empty()) track.csd.push_back(header); + } else if (!header.empty()) { + track.csd.push_back(header); + } + } else if (major == MFMediaType_Audio) { + info.sample_rate = static_cast(MFGetAttributeUINT32(type, MF_MT_AUDIO_SAMPLES_PER_SECOND, 0)); + info.channels = static_cast(MFGetAttributeUINT32(type, MF_MT_AUDIO_NUM_CHANNELS, 0)); + info.bit_rate = static_cast(MFGetAttributeUINT32(type, MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 0) * 8); + info.max_input_size = 64 * 1024; + if (subtype == MFAudioFormat_AAC) { + info.is_adts = MFGetAttributeUINT32(type, MF_MT_AAC_PAYLOAD_TYPE, 0) == 1; + // MF_MT_USER_DATA is HEAACWAVEINFO after WAVEFORMATEX: 12 bytes, then the AudioSpecificConfig. + auto user = blob(type, MF_MT_USER_DATA); + if (user.size() > 12) + track.csd.push_back(std::vector(user.begin() + 12, user.end())); + else if (!info.is_adts) + track.csd.push_back(aac_config(info.sample_rate, info.channels)); + } + } else { + return false; + } + info.csd_count = static_cast(track.csd.size()); + return true; + } + + void load() { + loaded_ = true; + has_sample_ = false; + if (ended_) return; + if (std::none_of(tracks_.begin(), tracks_.end(), [](const Track& t) { return t.selected && !t.ended; })) return; + for (int guard = 0; guard < 100000; ++guard) { + DWORD stream = 0, flags = 0; + LONGLONG time = 0; + Com sample; + const HRESULT hr = reader_->ReadSample(static_cast(MF_SOURCE_READER_ANY_STREAM), 0, &stream, &flags, &time, sample.put()); + if (FAILED(hr) || (flags & MF_SOURCE_READERF_ERROR)) { + LOGW("media", "AMediaExtractor: read failed 0x%08lx", static_cast(hr)); + ended_ = true; + return; + } + auto it = std::find_if(tracks_.begin(), tracks_.end(), [&](const Track& t) { return t.stream == stream; }); + if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) && it != tracks_.end()) it->ended = true; + if (!sample || it == tracks_.end() || !it->selected) { + if (std::none_of(tracks_.begin(), tracks_.end(), [](const Track& t) { return t.selected && !t.ended; })) { + ended_ = true; + return; + } + continue; + } + sample_ = buffer_bytes(sample.p); + if (it->annex_b) sample_ = to_annex_b(std::move(sample_)); + sample_time_us_ = time / 10; + sample_track_ = static_cast(it - tracks_.begin()); + const bool video = std::strncmp(it->info.mime, "video/", 6) == 0; + sample_flags_ = !video || MFGetAttributeUINT32(sample.p, MFSampleExtension_CleanPoint, 0) ? 1 : 0; + has_sample_ = true; + return; + } + ended_ = true; + } + + std::vector source_; + std::string format_ = "application/octet-stream"; + Com reader_; + std::vector tracks_; + bool loaded_ = false, has_sample_ = false, ended_ = false; + std::vector sample_; + int64_t sample_time_us_ = 0; + int32_t sample_track_ = -1, sample_flags_ = 0; +}; + +// --- Codec ---------------------------------------------------------------------------------- + +class Codec { +public: + std::mutex calls; // held by the dispatcher for each call + explicit Codec(const Mime* mime) : mime_(mime) {} + ~Codec() { shutdown(); } + + const char* mime() const { return mime_->mime; } + + int64_t configure(const CodecConfig& config, int64_t surface) { + shutdown(); + config_ = config; + surface_ = surface; + csd_.clear(); + for (int i = 0; i < 2; ++i) + if (config.csd[i] && config.csd_size[i] > 0) + csd_.push_back(std::vector(reinterpret_cast(config.csd[i]), + reinterpret_cast(config.csd[i]) + config.csd_size[i])); + if (!create()) return kErrorUnsupported; + return 0; + } + + int64_t start() { + if (!transform_) return kErrorInvalidOperation; + transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0); + transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0); + prefix_pending_ = true; + return 0; + } + void stop() { + if (transform_) { + transform_->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0); + transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_END_STREAMING, 0); + } + reset_state(); + } + void flush() { + if (transform_) transform_->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0); + reset_state(); + prefix_pending_ = true; + } + void set_surface(int64_t surface) { surface_ = surface; } + + int64_t input(const uint8_t* data, size_t size, int64_t time_us, int32_t flags) { + if (!transform_) return kErrorInvalidOperation; + if (flags & kFlagCodecConfig) { + // Codec-specific data sent in band: keep it for the next frame. + if (video()) { + extra_.insert(extra_.end(), data, data + size); + } else if (std::string(mime_->mime) == "audio/mp4a-latm") { + csd_.assign(1, std::vector(data, data + size)); + if (!create()) return kErrorUnsupported; + start(); + } + return 0; + } + std::vector bytes; + if (video()) { + if (prefix_pending_) { + for (auto& c : csd_) bytes.insert(bytes.end(), c.begin(), c.end()); + prefix_pending_ = false; + } + bytes.insert(bytes.end(), extra_.begin(), extra_.end()); + extra_.clear(); + std::vector frame(data, data + size); + if (annex_b()) frame = to_annex_b(std::move(frame)); + bytes.insert(bytes.end(), frame.begin(), frame.end()); + } else { + bytes.assign(data, data + size); + } + if (!bytes.empty()) { + Com sample; + Com buffer; + if (FAILED(MFCreateMemoryBuffer(static_cast(bytes.size()), buffer.put())) || FAILED(MFCreateSample(sample.put()))) + return kErrorUnknown; + BYTE* target = nullptr; + buffer->Lock(&target, nullptr, nullptr); + std::memcpy(target, bytes.data(), bytes.size()); + buffer->Unlock(); + buffer->SetCurrentLength(static_cast(bytes.size())); + sample->AddBuffer(buffer.p); + sample->SetSampleTime(time_us * 10); + last_input_us_ = time_us; + for (int attempt = 0;; ++attempt) { + const HRESULT hr = transform_->ProcessInput(0, sample.p, 0); + if (hr == MF_E_NOTACCEPTING) { + // Decoded output must be collected first; never drop input. + if (!pull() && attempt >= 8) { + LOGW("media", "AMediaCodec %s: decoder accepts no input", mime_->mime); + return kErrorUnknown; + } + continue; + } + if (FAILED(hr)) { + LOGW("media", "AMediaCodec %s: input rejected 0x%08lx", mime_->mime, static_cast(hr)); + return kErrorMalformed; + } + break; + } + } + if (flags & kFlagEndOfStream) { + transform_->ProcessMessage(MFT_MESSAGE_NOTIFY_END_OF_STREAM, 0); + transform_->ProcessMessage(MFT_MESSAGE_COMMAND_DRAIN, 0); + draining_ = true; + } + return 0; + } + + // 1: output format changed; 0: a buffer is described in info (and copied + // to dst when it fits); -1: nothing yet; -3: dst is smaller than info->size. + int64_t output(OutputInfo& info, uint8_t* dst, size_t capacity) { + if (!transform_) return kErrorInvalidOperation; + if (frames_.empty()) pull(); + if (format_pending_ && (!frames_.empty() || format_ready_)) { + format_pending_ = false; + return 1; + } + if (frames_.empty()) { + if (draining_ && !eos_sent_) { + eos_sent_ = true; + info = {last_output_us_, ++sequence_, 0, kFlagEndOfStream}; + return 0; + } + return -1; + } + Frame& frame = frames_.front(); + info = {frame.time_us, frame.sequence, static_cast(frame.bytes.size()), 0}; + if (draining_ && frames_.size() == 1 && !more_output()) { + info.flags |= kFlagEndOfStream; + eos_sent_ = true; + } + if (!surface_ && !frame.bytes.empty()) { + if (!dst || capacity < frame.bytes.size()) return -3; + std::memcpy(dst, frame.bytes.data(), frame.bytes.size()); + } + if (surface_ && video()) held_[frame.sequence] = std::move(frame); + frames_.pop_front(); + return 0; + } + + void release(uint64_t sequence, bool render) { + auto it = held_.find(sequence); + if (it == held_.end()) return; + if (render && surface_) publish(it->second); + held_.erase(it); + // A Surface keeps only the newest frame: drop older unreleased ones. + while (held_.size() > 8) held_.erase(held_.begin()); + } + + void format(OutputFormat& out) const { + std::memset(&out, 0, sizeof out); + if (video()) { + copy_string(out.mime, "video/raw"); + out.width = static_cast(width_); + out.height = static_cast(height_); + out.stride = static_cast(width_); + out.slice_height = static_cast(height_); + out.color_format = kColorYuv420SemiPlanar; + out.crop_left = crop_.left; + out.crop_top = crop_.top; + out.crop_right = crop_.right - 1; + out.crop_bottom = crop_.bottom - 1; + } else { + copy_string(out.mime, "audio/raw"); + out.sample_rate = static_cast(rate_); + out.channels = static_cast(channels_); + out.pcm_encoding = 2; // ENCODING_PCM_16BIT + } + } + +private: + struct Frame { + int64_t time_us = 0; + uint64_t sequence = 0; + std::vector bytes; + }; + + bool video() const { return mime_->video; } + bool annex_b() const { return *mime_->subtype == MFVideoFormat_H264 || *mime_->subtype == MFVideoFormat_HEVC; } + + void reset_state() { + frames_.clear(); + held_.clear(); + extra_.clear(); + draining_ = eos_sent_ = false; + } + void shutdown() { + reset_state(); + transform_.reset(); + } + + bool create() { + if (!media_foundation()) return false; + transform_.reset(); + MFT_REGISTER_TYPE_INFO input{video() ? MFMediaType_Video : MFMediaType_Audio, *mime_->subtype}; + IMFActivate** activates = nullptr; + UINT32 count = 0; + const UINT32 flags = MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT | MFT_ENUM_FLAG_SORTANDFILTER; + HRESULT hr = MFTEnumEx(video() ? MFT_CATEGORY_VIDEO_DECODER : MFT_CATEGORY_AUDIO_DECODER, flags, &input, nullptr, &activates, &count); + for (UINT32 i = 0; i < count; ++i) { + if (!transform_ && SUCCEEDED(hr)) { + IMFTransform* t = nullptr; + if (SUCCEEDED(activates[i]->ActivateObject(IID_PPV_ARGS(&t)))) { + transform_.p = t; + if (!set_types()) transform_.reset(); + } + } + activates[i]->Release(); + } + CoTaskMemFree(activates); + if (!transform_) { + LOGW("media", "AMediaCodec: no Windows decoder for %s", mime_->mime); + return false; + } + LOGI("media", "AMediaCodec: %s decoder on Media Foundation", mime_->mime); + return true; + } + + bool set_types() { + Com attributes; + if (SUCCEEDED(transform_->GetAttributes(attributes.put())) && attributes) attributes->SetUINT32(MF_LOW_LATENCY, TRUE); + Com in; + if (FAILED(MFCreateMediaType(in.put()))) return false; + if (video()) { + in->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + in->SetGUID(MF_MT_SUBTYPE, *mime_->subtype); + in->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + if (config_.width > 0 && config_.height > 0) MFSetAttributeSize(in.p, MF_MT_FRAME_SIZE, config_.width, config_.height); + } else { + in->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); + in->SetGUID(MF_MT_SUBTYPE, *mime_->subtype); + if (config_.sample_rate > 0) in->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, config_.sample_rate); + if (config_.channels > 0) in->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, config_.channels); + if (*mime_->subtype == MFAudioFormat_AAC) { + const bool adts = config_.is_adts != 0; + std::vector user(12, 0); + user[0] = adts ? 1 : 0; // wPayloadType + user[2] = 0xfe; // wAudioProfileLevelIndication: unknown + if (!adts) { + auto asc = !csd_.empty() ? csd_[0] : aac_config(config_.sample_rate, config_.channels); + user.insert(user.end(), asc.begin(), asc.end()); + } + in->SetUINT32(MF_MT_AAC_PAYLOAD_TYPE, adts ? 1 : 0); + in->SetBlob(MF_MT_USER_DATA, user.data(), static_cast(user.size())); + } + } + HRESULT hr = transform_->SetInputType(0, in.p, 0); + if (FAILED(hr)) { + LOGW("media", "AMediaCodec %s: input type rejected 0x%08lx", mime_->mime, static_cast(hr)); + return false; + } + return set_output_type(); + } + + bool set_output_type() { + for (DWORD i = 0;; ++i) { + Com type; + const HRESULT hr = transform_->GetOutputAvailableType(0, i, type.put()); + if (FAILED(hr)) break; + GUID subtype{}; + type->GetGUID(MF_MT_SUBTYPE, &subtype); + const bool wanted = video() ? subtype == MFVideoFormat_NV12 + : subtype == MFAudioFormat_PCM && MFGetAttributeUINT32(type.p, MF_MT_AUDIO_BITS_PER_SAMPLE, 16) == 16; + if (!wanted) continue; + if (FAILED(transform_->SetOutputType(0, type.p, 0))) continue; + Com actual; + if (FAILED(transform_->GetOutputCurrentType(0, actual.put()))) return false; + if (video()) { + UINT32 w = 0, h = 0; + MFGetAttributeSize(actual.p, MF_MT_FRAME_SIZE, &w, &h); + width_ = w; + height_ = h; + MFVideoArea area{}; + if (SUCCEEDED(actual->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, reinterpret_cast(&area), sizeof area, nullptr)) && + area.Area.cx > 0 && area.Area.cy > 0) + crop_ = {area.OffsetX.value, area.OffsetY.value, area.OffsetX.value + area.Area.cx, area.OffsetY.value + area.Area.cy}; + else if (config_.width > 0 && config_.height > 0 && static_cast(config_.width) <= w && static_cast(config_.height) <= h) + crop_ = {0, 0, config_.width, config_.height}; + else + crop_ = {0, 0, static_cast(w), static_cast(h)}; + } else { + rate_ = MFGetAttributeUINT32(actual.p, MF_MT_AUDIO_SAMPLES_PER_SECOND, 0); + channels_ = MFGetAttributeUINT32(actual.p, MF_MT_AUDIO_NUM_CHANNELS, 0); + } + format_pending_ = true; + format_ready_ = !video() || (width_ && height_); + return true; + } + // Video decoders offer output types only once they have parsed a frame. + if (video()) { + format_ready_ = false; + return true; + } + return false; + } + + bool more_output() { + pull(); + return !frames_.empty(); + } + + // Collects every frame the decoder can produce now. False if none. + bool pull() { + bool any = false; + for (int guard = 0; guard < 256; ++guard) { + MFT_OUTPUT_STREAM_INFO stream{}; + if (FAILED(transform_->GetOutputStreamInfo(0, &stream))) return any; + Com own; + if (!(stream.dwFlags & (MFT_OUTPUT_STREAM_PROVIDES_SAMPLES | MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES))) { + Com buffer; + const DWORD size = std::max(stream.cbSize, video() ? width_ * height_ * 2 + 4096 : 64 * 1024); + if (FAILED(MFCreateMemoryBuffer(size, buffer.put())) || FAILED(MFCreateSample(own.put()))) return any; + own->AddBuffer(buffer.p); + } + MFT_OUTPUT_DATA_BUFFER out{}; + out.pSample = own.p; + DWORD status = 0; + const HRESULT hr = transform_->ProcessOutput(0, 1, &out, &status); + if (out.pEvents) out.pEvents->Release(); + Com produced; + if (out.pSample && out.pSample != own.p) produced.p = out.pSample; + if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) return any; + if (hr == MF_E_TRANSFORM_STREAM_CHANGE) { + if (!set_output_type()) return any; + format_pending_ = true; + continue; + } + if (FAILED(hr)) { + LOGW("media", "AMediaCodec %s: decode failed 0x%08lx", mime_->mime, static_cast(hr)); + return any; + } + IMFSample* sample = produced ? produced.p : own.p; + Frame frame; + LONGLONG time = 0; + frame.time_us = SUCCEEDED(sample->GetSampleTime(&time)) ? time / 10 : last_input_us_; + frame.sequence = ++sequence_; + frame.bytes = video() ? planar(sample) : buffer_bytes(sample); + if (!video() && frame.bytes.empty()) continue; + last_output_us_ = frame.time_us; + frames_.push_back(std::move(frame)); + any = true; + } + return any; + } + + // Tightly packed NV12 (stride = width, slice height = height). + std::vector planar(IMFSample* sample) { + std::vector out; + if (!width_ || !height_) return out; + Com buffer; + if (FAILED(sample->GetBufferByIndex(0, buffer.put()))) return out; + out.resize(static_cast(width_) * height_ * 3 / 2); + Com two; + BYTE* scan = nullptr; + LONG pitch = 0; + if (SUCCEEDED(buffer->QueryInterface(IID_PPV_ARGS(two.put()))) && SUCCEEDED(two->Lock2D(&scan, &pitch))) { + for (UINT32 y = 0; y < height_; ++y) std::memcpy(&out[size_t(y) * width_], scan + ptrdiff_t(pitch) * y, width_); + const BYTE* chroma = scan + ptrdiff_t(pitch) * height_; + for (UINT32 y = 0; y < height_ / 2; ++y) + std::memcpy(&out[size_t(width_) * height_ + size_t(y) * width_], chroma + ptrdiff_t(pitch) * y, width_); + two->Unlock2D(); + return out; + } + BYTE* data = nullptr; + DWORD length = 0; + if (FAILED(buffer->Lock(&data, nullptr, &length))) return {}; + std::memcpy(out.data(), data, std::min(length, out.size())); + buffer->Unlock(); + return out; + } + + // NV12 to the Surface's RGBA frame, cropped, with BT.601/709 limited range. + void publish(const Frame& frame) { + const LONG w = crop_.right - crop_.left, h = crop_.bottom - crop_.top; + if (w <= 0 || h <= 0 || frame.bytes.size() < size_t(width_) * height_ * 3 / 2) return; + const bool hd = h >= 720; + const int rv = hd ? 459 : 409, gu = hd ? 55 : 100, gv = hd ? 136 : 208, bu = hd ? 541 : 516; + rgba_.resize(size_t(w) * h * 4); + const uint8_t* luma = frame.bytes.data(); + const uint8_t* chroma = luma + size_t(width_) * height_; + for (LONG y = 0; y < h; ++y) { + const uint8_t* yl = luma + size_t(y + crop_.top) * width_ + crop_.left; + const uint8_t* cl = chroma + size_t((y + crop_.top) / 2) * width_ + (crop_.left & ~1); + uint8_t* out = &rgba_[size_t(y) * w * 4]; + for (LONG x = 0; x < w; ++x) { + const int c = (yl[x] - 16) * 298, u = cl[(x & ~1)] - 128, v = cl[(x & ~1) + 1] - 128; + out[x * 4 + 0] = static_cast(std::clamp((c + rv * v + 128) >> 8, 0, 255)); + out[x * 4 + 1] = static_cast(std::clamp((c - gu * u - gv * v + 128) >> 8, 0, 255)); + out[x * 4 + 2] = static_cast(std::clamp((c + bu * u + 128) >> 8, 0, 255)); + out[x * 4 + 3] = 255; + } + } + media_publish_frame(surface_, static_cast(w), static_cast(h), rgba_.data()); + } + + const Mime* mime_; + CodecConfig config_{}; + int64_t surface_ = 0; + Com transform_; + std::vector> csd_; + std::vector extra_, rgba_; + std::deque frames_; + std::map held_; + bool prefix_pending_ = true, draining_ = false, eos_sent_ = false, format_pending_ = false, format_ready_ = false; + uint64_t sequence_ = 0; + int64_t last_input_us_ = 0, last_output_us_ = 0; + UINT32 width_ = 0, height_ = 0, rate_ = 0, channels_ = 0; + RECT crop_{}; +}; + +std::mutex g_lock; +std::map g_windows; // ANativeWindow of a host surface -> media handle +std::map> g_extractors; +std::map> g_codecs; +int64_t g_next = 1; + +template +std::shared_ptr find(std::map>& map, int64_t id) { + std::lock_guard guard(g_lock); + auto it = map.find(id); + return it == map.end() ? nullptr : it->second; +} + +int64_t read_fd(uint64_t fd, int64_t offset, int64_t length, std::vector& bytes) { + constexpr uint64_t kPread64 = 67, kFstat = 80; + linux_abi::kstat st{}; + if (offset < 0 || sys(kFstat, fd, reinterpret_cast(&st), 0, 0) != 0 || offset > st.st_size) return kErrorMalformed; + if (length <= 0 || length > st.st_size - offset) length = st.st_size - offset; + if (length <= 0) return kErrorMalformed; + if (length > (int64_t{1} << 31)) { + LOGW("media", "AMediaExtractor: %lld MB source is too large", static_cast(length >> 20)); + return kErrorUnsupported; + } + bytes.resize(static_cast(length)); + int64_t done = 0; + while (done < length) { + const int64_t n = sys(kPread64, fd, reinterpret_cast(bytes.data() + done), static_cast(length - done), + static_cast(offset + done)); + if (n <= 0) return kErrorMalformed; + done += n; + } + return 0; +} + +int64_t extractor_call(HostCall& call, uint64_t op) { + const int64_t id = static_cast(call.x(1)); + if (op == kExtractorNew) { + std::lock_guard guard(g_lock); + const int64_t created = g_next++; + g_extractors[created] = std::make_shared(); + return created; + } + if (op == kExtractorDelete) { + std::shared_ptr gone; + std::lock_guard guard(g_lock); + auto it = g_extractors.find(id); + if (it != g_extractors.end()) { gone = std::move(it->second); g_extractors.erase(it); } + return 0; + } + auto x = find(g_extractors, id); + if (!x) return kErrorInvalidOperation; + std::lock_guard serial(x->calls); + switch (op) { + case kExtractorSourceFd: { + std::vector bytes; + const int64_t status = read_fd(call.x(2), static_cast(call.x(3)), static_cast(call.x(4)), bytes); + return status ? status : x->open(std::move(bytes)); + } + case kExtractorTrackCount: return static_cast(x->track_count()); + case kExtractorTrack: { + const auto* t = x->track(call.x(2)); + if (!t || !guest_writable(call.x(3), sizeof(TrackInfo))) return kErrorInvalidOperation; + std::memcpy(reinterpret_cast(call.x(3)), &t->info, sizeof(TrackInfo)); + return 0; + } + case kExtractorCsd: { + const auto* t = x->track(call.x(2)); + if (!t || call.x(3) >= t->csd.size()) return -1; + const auto& csd = t->csd[call.x(3)]; + if (call.x(5) >= csd.size() && guest_writable(call.x(4), csd.size())) + std::memcpy(reinterpret_cast(call.x(4)), csd.data(), csd.size()); + return static_cast(csd.size()); + } + case kExtractorSelect: return x->select(call.x(2), call.x(3) != 0); + case kExtractorSeek: return x->seek(static_cast(call.x(2))); + case kExtractorSample: { + if (!x->current()) return -1; + if (!guest_writable(call.x(2), sizeof(SampleInfo))) return kErrorInvalidOperation; + SampleInfo info{x->time_us(), x->sample_track(), x->sample_flags(), static_cast(x->sample().size()), 0}; + std::memcpy(reinterpret_cast(call.x(2)), &info, sizeof info); + return 0; + } + case kExtractorRead: { + if (!x->current()) return -1; + const auto& s = x->sample(); + if (call.x(3) < s.size()) return kErrorInvalidOperation; + if (!s.empty() && !guest_writable(call.x(2), s.size())) return kErrorInvalidOperation; + if (!s.empty()) std::memcpy(reinterpret_cast(call.x(2)), s.data(), s.size()); + return static_cast(s.size()); + } + case kExtractorAdvance: return x->advance() ? 1 : 0; + case kExtractorFileFormat: { + const std::string format = x->file_format(); + if (call.x(3) <= format.size() || !guest_writable(call.x(2), format.size() + 1)) return kErrorInvalidOperation; + std::memcpy(reinterpret_cast(call.x(2)), format.c_str(), format.size() + 1); + return 0; + } + default: return kErrorUnsupported; + } +} + +int64_t codec_call(HostCall& call, uint64_t op) { + const int64_t id = static_cast(call.x(1)); + if (op == kWindowRegister || op == kWindowHandle) { + std::lock_guard guard(g_lock); + if (op == kWindowRegister) { + g_windows[call.x(2)] = static_cast(call.x(3)); + return 0; + } + auto it = g_windows.find(call.x(2)); + return it == g_windows.end() ? 0 : it->second; + } + if (op == kCodecNew) { + char name[64]{}; + for (size_t i = 0; i + 1 < sizeof name; ++i) { + if (!guest_readable(call.x(2) + i, 1)) return 0; + name[i] = *reinterpret_cast(call.x(2) + i); + if (!name[i]) break; + } + const Mime* mime = mime_named(name); + if (!mime || *mime->subtype == MFAudioFormat_PCM) { + LOGW("media", "AMediaCodec: no decoder for %s", name); + return 0; + } + std::lock_guard guard(g_lock); + const int64_t created = g_next++; + g_codecs[created] = std::make_shared(mime); + return created; + } + if (op == kCodecDelete) { + std::shared_ptr gone; + std::lock_guard guard(g_lock); + auto it = g_codecs.find(id); + if (it != g_codecs.end()) { gone = std::move(it->second); g_codecs.erase(it); } + return 0; + } + auto c = find(g_codecs, id); + if (!c) return kErrorInvalidOperation; + std::unique_lock serial(c->calls); + switch (op) { + case kCodecConfigure: { + if (!guest_readable(call.x(2), sizeof(CodecConfig))) return kErrorInvalidOperation; + CodecConfig config; + std::memcpy(&config, reinterpret_cast(call.x(2)), sizeof config); + for (int i = 0; i < 2; ++i) + if (config.csd[i] && (config.csd_size[i] <= 0 || !guest_readable(config.csd[i], config.csd_size[i]))) config.csd[i] = 0; + return c->configure(config, static_cast(call.x(3))); + } + case kCodecStart: return c->start(); + case kCodecStop: c->stop(); return 0; + case kCodecFlush: c->flush(); return 0; + case kCodecSurface: c->set_surface(static_cast(call.x(2))); return 0; + case kCodecInput: { + const uint64_t size = call.x(3); + if (size && !guest_readable(call.x(2), size)) return kErrorInvalidOperation; + return c->input(reinterpret_cast(call.x(2)), size, static_cast(call.x(4)), static_cast(call.x(5))); + } + case kCodecOutput: { + if (!guest_writable(call.x(2), sizeof(OutputInfo))) return kErrorInvalidOperation; + const uint64_t dst = call.x(3), capacity = call.x(4); + const int64_t timeout_us = static_cast(call.x(5)); + if (dst && capacity && !guest_writable(dst, capacity)) return kErrorInvalidOperation; + OutputInfo info{}; + int64_t result = c->output(info, reinterpret_cast(dst), capacity); + // Nothing decodable is queued: honour a short part of the timeout + // so polling loops do not spin, as a device codec would block. + if (result == -1 && timeout_us > 0) serial.unlock(), std::this_thread::sleep_for(std::chrono::microseconds(std::min(timeout_us, 2000))); + std::memcpy(reinterpret_cast(call.x(2)), &info, sizeof info); + return result; + } + case kCodecRelease: c->release(call.x(2), call.x(3) != 0); return 0; + case kCodecFormat: { + if (!guest_writable(call.x(2), sizeof(OutputFormat))) return kErrorInvalidOperation; + OutputFormat format; + c->format(format); + std::memcpy(reinterpret_cast(call.x(2)), &format, sizeof format); + return 0; + } + default: return kErrorUnsupported; + } +} + +} // namespace + +void art_media_codec_init() { + register_named_function("art.mediandk", [](HostCall& call) { + // Media Foundation objects are used from whichever guest thread calls. + thread_local const bool com = SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)); + (void)com; + const uint64_t op = call.x(0); + const int64_t result = op < kCodecNew ? extractor_call(call, op) : codec_call(call, op); + call.ret(static_cast(result)); + }); +} + +} // namespace axrb diff --git a/direct/src/memory.cpp b/direct/src/memory.cpp new file mode 100644 index 0000000..666c6b2 --- /dev/null +++ b/direct/src/memory.cpp @@ -0,0 +1,709 @@ +#include "memory.h" + +#include "files.h" +#include "linux.h" +#include "log.h" + +#include + +#include + +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +namespace { +// Android arm64 user space historically had 39 address bits, and some engine +// code still packs pointers assuming that; keep every guest mapping below it. +constexpr uint64_t kPreferredBase = 0x0000001000000000ull; // 64 GiB +constexpr uint64_t kPreferredEnd = 0x0000007f00000000ull; // just under 512 GiB +constexpr uint64_t kMinimumArena = 0x0000004000000000ull; // 256 GiB +constexpr uint64_t kEagerCommitLimit = 2ull << 20; +constexpr uint64_t kEagerFileLimit = 64ull << 20; +constexpr uint64_t kChunk = 64 * 1024; +constexpr uint64_t kFileChunk = 1ull << 20; // file content read per lock release + +bool committed(uint64_t address) { + MEMORY_BASIC_INFORMATION info{}; + if (!VirtualQuery(reinterpret_cast(address), &info, sizeof info)) return false; + return info.State == MEM_COMMIT; +} + +// Visits maximal runs of pages in [start, end) by commit state. +template +void for_each_run(uint64_t start, uint64_t end, F&& visit) { + uint64_t at = start; + while (at < end) { + MEMORY_BASIC_INFORMATION info{}; + if (!VirtualQuery(reinterpret_cast(at), &info, sizeof info)) break; + uint64_t run_end = std::min(end, reinterpret_cast(info.BaseAddress) + info.RegionSize); + visit(at, run_end, info.State == MEM_COMMIT, info.Protect); + at = run_end; + } +} + +} // namespace + +uint32_t host_protection(uint32_t prot) { + // Guest code is never executed natively, so PROT_EXEC only means readable. + if (prot & PROT_WRITE_) return PAGE_READWRITE; + if (prot & (PROT_READ_ | PROT_EXEC_)) return PAGE_READONLY; + return PAGE_NOACCESS; +} + +AddressSpace& address_space() { + static AddressSpace space; + return space; +} + +bool AddressSpace::init(bool low_memory) { + void* got = VirtualAlloc(reinterpret_cast(kPreferredBase), kPreferredEnd - kPreferredBase, MEM_RESERVE, + PAGE_NOACCESS); + if (got) { + base_ = kPreferredBase; + end_ = kPreferredEnd; + } else { + // Something already lives there: take the largest free span below 2^39, + // then anywhere, as long as it is big enough to be useful. + uint64_t best = 0, best_size = 0; + for (uint64_t at = 0x10000; at < 0x00007ff000000000ull;) { + MEMORY_BASIC_INFORMATION info{}; + if (!VirtualQuery(reinterpret_cast(at), &info, sizeof info)) break; + uint64_t region = reinterpret_cast(info.BaseAddress), size = info.RegionSize; + if (info.State == MEM_FREE && size > best_size && (region + size <= kPreferredEnd || best_size < kMinimumArena)) { + best = (region + 0xffffffull) & ~0xffffffull; + best_size = region + size - best; + } + at = region + size; + } + if (best_size < kMinimumArena / 4) { + LOGE("memory", "no free address range for the guest arena"); + return false; + } + best_size = std::min(best_size, kPreferredEnd - kPreferredBase); + got = VirtualAlloc(reinterpret_cast(best), best_size, MEM_RESERVE, PAGE_NOACCESS); + if (!got) return false; + base_ = best; + end_ = best + best_size; + } + mmap_top_ = end_; + // ART uses compressed 32-bit heap references. Reserve free low address + // ranges separately, without ever replacing Windows-owned mappings. + if (low_memory) { + for (uint64_t at = 0x10000; at < 0x100000000ull;) { + MEMORY_BASIC_INFORMATION info{}; + if (!VirtualQuery(reinterpret_cast(at), &info, sizeof info)) break; + uint64_t stop = std::min(reinterpret_cast(info.BaseAddress) + info.RegionSize, 0x100000000ull); + uint64_t start = (at + kChunk - 1) & ~(kChunk - 1); + if (info.State == MEM_FREE && stop > start && VirtualAlloc(reinterpret_cast(start), stop - start, MEM_RESERVE, PAGE_NOACCESS)) { + low_reservations_.emplace_back(start, stop); + LOGI("memory", "guest low arena %llx-%llx", (unsigned long long)start, (unsigned long long)stop); + } + if (stop <= at) break; + at = stop; + } + } + LOGI("memory", "guest arena %llx-%llx (%llu GiB)", (unsigned long long)base_, (unsigned long long)end_, + (unsigned long long)((end_ - base_) >> 30)); + return true; +} + +bool AddressSpace::owns_range(uint64_t start, uint64_t end) const { + if (end <= start) return false; + if (start >= base_ && end <= end_) return true; + for (auto [lo, hi] : low_reservations_) if (start >= lo && end <= hi) return true; + return false; +} + +bool AddressSpace::overlaps_guest(uint64_t addr, uint64_t len) const { + addr = untag(addr); + if (!len) return false; + const uint64_t end = addr + len; + if (end <= addr) return true; // invalid buffers must not take the host path + if (addr < end_ && end > base_) return true; + for (auto [lo, hi] : low_reservations_) if (addr < hi && end > lo) return true; + return false; +} + +bool AddressSpace::range_free_locked(uint64_t start, uint64_t end) const { + if (!owns_range(start, end)) return false; + auto it = vmas_.upper_bound(start); + if (it != vmas_.begin()) { + auto prev = std::prev(it); + if (prev->second.end > start) return false; + } + return it == vmas_.end() || it->first >= end; +} + +uint64_t AddressSpace::find_free_locked(uint64_t length, uint64_t hint) const { + if (hint && range_free_locked(page_down(hint), page_down(hint) + length)) return page_down(hint); + // A hint below 4 GiB asks for low memory (ART's arm64 allocator probes + // for it page by page, unmapping every high result): take the first gap + // at or above the hint there. + if (hint && hint < (1ull << 32)) { + for (auto [lo, hi] : low_reservations_) { + uint64_t at = std::max(lo, page_down(hint)); + if (at >= hi) continue; + auto it = vmas_.upper_bound(at); + if (it != vmas_.begin() && std::prev(it)->second.end > at) at = std::prev(it)->second.end; + while (at + length <= hi) { + if (it == vmas_.end() || it->first >= at + length) return at; + at = std::max(at, it->second.end); + ++it; + } + } + } + // Top-down first fit, like Linux's mmap_base allocator, starting below + // the previous allocation (Linux's old free_area_cache) rather than + // walking every mapping from the top each time; then from the top. + for (uint64_t from : {mmap_top_, end_}) { + uint64_t top = from; + for (auto it = std::make_reverse_iterator(vmas_.lower_bound(from)); it != vmas_.rend(); ++it) { + if (it->second.start < base_) break; + if (it->second.end <= top && top - it->second.end >= length) return top - length; + top = std::min(top, it->second.start); + } + if (top >= base_ && top - base_ >= length) return top - length; + if (from == end_) break; + } + return 0; +} + +void AddressSpace::split_locked(uint64_t at) { + auto it = vmas_.upper_bound(at); + if (it == vmas_.begin()) return; + --it; + Vma& vma = it->second; + if (at <= vma.start || at >= vma.end) return; + Vma tail = vma; + tail.start = at; + if (tail.file) tail.offset = vma.offset + (at - vma.start); + vma.end = at; + vmas_.emplace(at, std::move(tail)); +} + +void AddressSpace::unmap_locked(uint64_t start, uint64_t end) { + // Freed space above the allocation cursor is found again from there. + if (end > mmap_top_ && start < end_) mmap_top_ = std::min(end, end_); + split_locked(start); + split_locked(end); + auto it = vmas_.lower_bound(start); + while (it != vmas_.end() && it->first < end) { + Vma& vma = it->second; + if (vma.file && (vma.flags & MAP_SHARED_) && (vma.prot & PROT_WRITE_)) { + // Write back what was touched; the file is the shared object. + for_each_run(vma.start, vma.end, [&](uint64_t a, uint64_t b, bool is_committed, DWORD) { + if (is_committed) vma.file->pwrite_host(reinterpret_cast(a), b - a, vma.offset + (a - vma.start)); + }); + } + VirtualFree(reinterpret_cast(vma.start), vma.end - vma.start, MEM_DECOMMIT); + it = vmas_.erase(it); + } +} + +bool AddressSpace::fill_locked(const Vma& vma, uint64_t start, uint64_t end, const uint8_t* content) { + // Lock-free readers (read_complete) retry rather than see a committed + // page before its content. + fill_sequence_.fetch_add(1, std::memory_order_acq_rel); + struct Done { + std::atomic& sequence; + ~Done() { sequence.fetch_add(1, std::memory_order_release); } + } done{fill_sequence_}; + // Commit writable first so file content can be placed, then apply the + // mapping's protection. + if (!VirtualAlloc(reinterpret_cast(start), end - start, MEM_COMMIT, PAGE_READWRITE)) { + LOGE("memory", "commit %llx-%llx failed: %lu", (unsigned long long)start, (unsigned long long)end, + GetLastError()); + return false; + } + if (content) { + std::memcpy(reinterpret_cast(start), content, end - start); + } else if (vma.file) { + uint64_t file_offset = vma.offset + (start - vma.start); + int64_t got = vma.file->pread_host(reinterpret_cast(start), end - start, file_offset); + if (got < 0) got = 0; + // Bytes past the end of the file read as zero, as on Linux; a fresh + // commit is already zero. + } + DWORD old = 0; + uint32_t protection = host_protection(vma.prot); + if (protection != PAGE_READWRITE) VirtualProtect(reinterpret_cast(start), end - start, protection, &old); + return true; +} + +int AddressSpace::fill_file(std::unique_lock& held, uint64_t start, uint64_t end) { + // [start, end) lies in one file mapping. Its content is read with the + // lock released, so a disk read does not stall every other thread's + // faults and mappings; the pages are committed after taking it again. + auto it = std::prev(vmas_.upper_bound(start)); + const std::shared_ptr file = it->second.file; + const uint64_t offset = it->second.offset + (start - it->second.start); + thread_local std::vector content; + content.resize(end - start); + held.unlock(); + int64_t got = file->pread_host(content.data(), end - start, offset); + got = std::max(got, 0); + // Bytes past the end of the file read as zero, as on Linux. + std::memset(content.data() + got, 0, end - start - got); + held.lock(); + // 0 if the mapping changed meanwhile and the caller should look again. + it = vmas_.upper_bound(start); + if (it == vmas_.begin()) return 0; + const Vma& vma = std::prev(it)->second; + if (vma.file != file || vma.end < end || vma.offset + (start - vma.start) != offset || vma.prot == PROT_NONE_) + return 0; + bool ok = true; + for_each_run(start, end, [&](uint64_t a, uint64_t b, bool is_committed, DWORD) { + if (!is_committed) ok = fill_locked(vma, a, b, content.data() + (a - start)) && ok; + }); + return ok ? 1 : -1; +} + +int64_t AddressSpace::map_locked(uint64_t addr, uint64_t length, uint32_t prot, uint32_t flags, + std::shared_ptr file, uint64_t offset, const std::string& name) { + length = page_up(length); + if (!length) return -EINVAL_; + uint64_t start; + if (flags & (MAP_FIXED_ | MAP_FIXED_NOREPLACE_)) { + if (addr & (kPageSize - 1)) return -EINVAL_; + start = addr; + if (!owns_range(start, start + length)) { + LOGW("memory", "fixed mapping %llx+%llx outside the guest arena", (unsigned long long)addr, + (unsigned long long)length); + return -ENOMEM_; + } + if ((flags & MAP_FIXED_NOREPLACE_) && !(flags & MAP_FIXED_) && !range_free_locked(start, start + length)) + return -EEXIST_; + unmap_locked(start, start + length); + } else { + start = find_free_locked(length, addr); + if (!start) return -ENOMEM_; + } + Vma vma; + vma.start = start; + vma.end = start + length; + vma.prot = prot; + vma.flags = flags & ~(MAP_FIXED_ | MAP_FIXED_NOREPLACE_); + vma.file = std::move(file); + vma.offset = offset; + vma.name = name; + if (vma.file && vma.name.empty()) vma.name = vma.file->guest_path(); + if (!(flags & (MAP_FIXED_ | MAP_FIXED_NOREPLACE_)) && start >= base_) mmap_top_ = start; + auto inserted = vmas_.emplace(start, std::move(vma)).first; + const Vma& placed = inserted->second; + // Small file mappings are read eagerly by mmap(), outside the lock. + if (prot != PROT_NONE_ && !placed.file && ((flags & MAP_POPULATE_) || length <= kEagerCommitLimit)) + fill_locked(placed, placed.start, placed.end); + return static_cast(start); +} + +int64_t AddressSpace::mmap(uint64_t addr, uint64_t length, uint32_t prot, uint32_t flags, + std::shared_ptr file, uint64_t offset, const char* name) { + if (offset & (kPageSize - 1)) return -EINVAL_; + if (!length || length > (1ull << 46)) return -EINVAL_; + const bool eager_file = file && prot != PROT_NONE_ && + ((flags & MAP_POPULATE_) || (page_up(length) <= kEagerFileLimit && !(flags & MAP_SHARED_))); + std::unique_lock held(lock_); + const int64_t result = map_locked(addr, length, prot, flags, std::move(file), offset, name ? name : ""); + held.unlock(); + if (result >= 0 && eager_file) populate(static_cast(result), length); + return result; +} + +int64_t AddressSpace::munmap(uint64_t addr, uint64_t length) { + if ((addr & (kPageSize - 1)) || !length) return -EINVAL_; + std::unique_lock held(lock_); + uint64_t end = addr + page_up(length); + if (!owns_range(addr, end)) { + // Not ours: only a mapping the host made could be there, and the guest + // has no business unmapping it. + return 0; + } + unmap_locked(addr, end); + return 0; +} + +int64_t AddressSpace::mprotect(uint64_t addr, uint64_t length, uint32_t prot) { + if (addr & (kPageSize - 1)) return -EINVAL_; + uint64_t end = addr + page_up(length); + if (end == addr) return 0; + std::unique_lock held(lock_); + // The whole range must be mapped. + for (uint64_t at = addr; at < end;) { + auto it = vmas_.upper_bound(at); + if (it == vmas_.begin()) return -ENOMEM_; + --it; + if (it->second.end <= at) return -ENOMEM_; + at = it->second.end; + } + split_locked(addr); + split_locked(end); + for (auto it = vmas_.lower_bound(addr); it != vmas_.end() && it->first < end; ++it) { + Vma& vma = it->second; + vma.prot = prot; + DWORD protection = host_protection(prot); + for_each_run(vma.start, vma.end, [&](uint64_t a, uint64_t b, bool is_committed, DWORD) { + DWORD old = 0; + if (is_committed) VirtualProtect(reinterpret_cast(a), b - a, protection, &old); + }); + } + return 0; +} + +int64_t AddressSpace::mremap(uint64_t old_addr, uint64_t old_size, uint64_t new_size, uint32_t flags, + uint64_t new_addr) { + if (old_addr & (kPageSize - 1)) return -EINVAL_; + old_size = page_up(old_size); + new_size = page_up(new_size); + if (!new_size) return -EINVAL_; + std::unique_lock held(lock_); + auto it = vmas_.upper_bound(old_addr); + if (it == vmas_.begin()) return -EFAULT_; + --it; + if (it->second.end <= old_addr) return -EFAULT_; + Vma original = it->second; + if (old_addr + old_size > original.end) old_size = original.end - old_addr; + if (new_size <= old_size && !(flags & MREMAP_FIXED_)) { + unmap_locked(old_addr + new_size, old_addr + old_size); + return static_cast(old_addr); + } + if (!(flags & MREMAP_FIXED_) && old_addr + old_size == original.end && + range_free_locked(old_addr + old_size, old_addr + new_size)) { + it->second.end = old_addr + new_size; + return static_cast(old_addr); + } + if (!(flags & MREMAP_MAYMOVE_)) return -ENOMEM_; + uint64_t target = (flags & MREMAP_FIXED_) ? new_addr : find_free_locked(new_size, 0); + if (!target) return -ENOMEM_; + if (flags & MREMAP_FIXED_) unmap_locked(target, target + new_size); + Vma moved = original; + moved.start = target; + moved.end = target + new_size; + if (moved.file) moved.offset = original.offset + (old_addr - original.start); + vmas_.emplace(target, moved); + // Carry over what the old pages hold. + for_each_run(old_addr, old_addr + std::min(old_size, new_size), [&](uint64_t a, uint64_t b, bool is_committed, DWORD) { + if (!is_committed) return; + uint64_t to = target + (a - old_addr); + VirtualAlloc(reinterpret_cast(to), b - a, MEM_COMMIT, PAGE_READWRITE); + std::memcpy(reinterpret_cast(to), reinterpret_cast(a), b - a); + DWORD old = 0; + VirtualProtect(reinterpret_cast(to), b - a, host_protection(moved.prot), &old); + }); + unmap_locked(old_addr, old_addr + old_size); + return static_cast(target); +} + +int64_t AddressSpace::madvise(uint64_t addr, uint64_t length, int advice) { + if (addr & (kPageSize - 1)) return -EINVAL_; + if (advice != MADV_DONTNEED_ && advice != MADV_FREE_ && advice != MADV_REMOVE_) return 0; + uint64_t end = addr + page_up(length); + std::unique_lock held(lock_); + // DONTNEED on a private mapping: the next touch sees zeros (or the file). + // Decommitting gives exactly that through the normal first-touch path. + for (uint64_t at = addr; at < end;) { + auto it = vmas_.upper_bound(at); + if (it == vmas_.begin()) break; + --it; + if (it->second.end <= at) { + auto next = vmas_.upper_bound(at); + if (next == vmas_.end()) break; + at = next->first; + continue; + } + uint64_t b = std::min(end, it->second.end); + if (!(it->second.flags & MAP_SHARED_)) VirtualFree(reinterpret_cast(at), b - at, MEM_DECOMMIT); + at = b; + } + return 0; +} + +int64_t AddressSpace::msync(uint64_t addr, uint64_t length, int) { + std::unique_lock held(lock_); + uint64_t end = addr + page_up(length); + // Unmapped guest pages are ENOMEM, as on Linux: ART probes with msync for + // free low memory. Addresses outside the arena belong to the host. + if (owns_range(page_down(addr), end)) { + for (uint64_t at = page_down(addr); at < end;) { + auto it = vmas_.upper_bound(at); + if (it == vmas_.begin() || std::prev(it)->second.end <= at) return -ENOMEM_; + at = std::prev(it)->second.end; + } + } + for (auto it = vmas_.lower_bound(page_down(addr)); it != vmas_.end() && it->first < end; ++it) { + Vma& vma = it->second; + if (!vma.file || !(vma.flags & MAP_SHARED_) || !(vma.prot & PROT_WRITE_)) continue; + for_each_run(std::max(vma.start, addr), std::min(vma.end, end), [&](uint64_t a, uint64_t b, bool is_committed, DWORD) { + if (is_committed) vma.file->pwrite_host(reinterpret_cast(a), b - a, vma.offset + (a - vma.start)); + }); + } + return 0; +} + +int64_t AddressSpace::mincore(uint64_t addr, uint64_t length, uint8_t* out) { + if (addr & (kPageSize - 1)) return -EINVAL_; + uint64_t pages = page_up(length) / kPageSize; + for (uint64_t i = 0; i < pages; ++i) out[i] = committed(addr + i * kPageSize) ? 1 : 0; + return 0; +} + +void AddressSpace::set_brk_base(uint64_t base) { + std::unique_lock held(lock_); + brk_base_ = brk_ = page_up(base); +} + +int64_t AddressSpace::brk(uint64_t addr) { + std::unique_lock held(lock_); + if (!addr || addr < brk_base_ || addr > brk_base_ + (512ull << 20)) return static_cast(brk_); + uint64_t want = page_up(addr), have = page_up(brk_); + if (want > have) { + if (!range_free_locked(have, want)) return static_cast(brk_); + map_locked(have, want - have, PROT_READ_ | PROT_WRITE_, MAP_PRIVATE_ | MAP_ANONYMOUS_ | MAP_FIXED_, nullptr, 0, + "[heap]"); + } else if (want < have) { + unmap_locked(want, have); + } + brk_ = addr; + return static_cast(brk_); +} + +void AddressSpace::name_range(uint64_t addr, uint64_t length, const std::string& name) { + std::unique_lock held(lock_); + uint64_t end = addr + page_up(length); + split_locked(addr); + split_locked(end); + for (auto it = vmas_.lower_bound(addr); it != vmas_.end() && it->first < end; ++it) it->second.name = name; +} + +// Diagnostic counters reported by the profiler next to the syscalls. +extern std::atomic g_syscall_counts[512]; + +bool AddressSpace::handle_fault(uint64_t address, bool write, bool execute) { + (void)execute; + if (!owns_range(address, address + 1)) return false; + g_syscall_counts[500].fetch_add(1, std::memory_order_relaxed); + std::unique_lock held(lock_); + for (;;) { + auto it = vmas_.upper_bound(address); + if (it == vmas_.begin()) return false; + --it; + const Vma& vma = it->second; + if (address >= vma.end) return false; + if (vma.prot == PROT_NONE_) return false; + if (write && !(vma.prot & PROT_WRITE_)) return false; + uint64_t page = page_down(address); + if (committed(page)) { + g_syscall_counts[502].fetch_add(1, std::memory_order_relaxed); + // Already there (another thread got here first). Retry only if the + // page now permits the access; otherwise it is a genuine fault. + MEMORY_BASIC_INFORMATION info{}; + VirtualQuery(reinterpret_cast(page), &info, sizeof info); + if (write) return info.Protect == PAGE_READWRITE; + return info.Protect == PAGE_READWRITE || info.Protect == PAGE_READONLY; + } + uint64_t start = std::max(vma.start, address & ~(kChunk - 1)); + uint64_t end = std::min(vma.end, (address & ~(kChunk - 1)) + kChunk); + if (vma.file) { + if (const int filled = fill_file(held, start, end)) return filled > 0; + continue; // the mapping changed while its file was read + } + bool ok = true; + for_each_run(start, end, [&](uint64_t a, uint64_t b, bool is_committed, DWORD) { + if (!is_committed) ok = fill_locked(vma, a, b) && ok; + }); + return ok; + } +} + +bool AddressSpace::check(uint64_t addr, uint64_t len, uint32_t prot) const { + addr = untag(addr); + if (len && addr + len <= addr) return false; + std::shared_lock held(lock_); + uint64_t end = addr + len; + for (uint64_t at = addr; at < end;) { + auto it = vmas_.upper_bound(at); + if (it == vmas_.begin()) return false; + --it; + if (it->second.end <= at) return false; + if ((it->second.prot & prot) != prot) return false; + at = it->second.end; + } + return true; +} + +bool AddressSpace::any_exec(uint64_t addr, uint64_t len) const { + std::shared_lock held(lock_); + uint64_t end = addr + len; + auto it = vmas_.upper_bound(addr); + if (it != vmas_.begin()) --it; + for (; it != vmas_.end() && it->first < end; ++it) + if (it->second.end > addr && (it->second.prot & PROT_EXEC_)) return true; + return false; +} + +// Reads one byte of every page; a first touch commits it through the fault handler. +static bool touch_pages(uint64_t start, uint64_t end) { + __try { + for (uint64_t page = start; page < end; page += kPageSize) (void)*reinterpret_cast(page); + return true; + } __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + return false; + } +} + +bool AddressSpace::populate(uint64_t addr, uint64_t len) { + if (!len) return true; + addr = untag(addr); + if (addr + len <= addr || addr + len > UINT64_MAX - (kPageSize - 1)) return false; + uint64_t start = page_down(addr), end = page_up(addr + len); + if (!owns_range(start, end)) return !overlaps_guest(addr, len); + g_syscall_counts[501].fetch_add(1, std::memory_order_relaxed); + if (end - start <= 256 * kPageSize) { + // Small buffers (socket and file I/O on every frame) are nearly always + // there already. Reading them finds that out without the mapping lock + // or a Windows memory query, which waits whenever another thread is + // committing or releasing memory. As in read_complete, a fill that ran + // meanwhile sends this to the locked path. + const uint64_t before = fill_sequence_.load(std::memory_order_acquire); + if (!(before & 1) && touch_pages(start, end)) { + std::atomic_thread_fence(std::memory_order_acquire); + if (fill_sequence_.load(std::memory_order_relaxed) == before) return true; + } + } + { + // Pages are filled under the exclusive lock, so a committed page seen + // under the shared lock is complete. Most calls find everything there. + std::shared_lock shared(lock_); + bool complete = true; + for (uint64_t at = start; at < end && complete;) { + auto it = vmas_.upper_bound(at); + if (it == vmas_.begin()) return false; + --it; + if (it->second.end <= at) return false; + uint64_t b = std::min(end, it->second.end); + if (it->second.prot != PROT_NONE_) + for_each_run(at, b, [&](uint64_t, uint64_t, bool is_committed, DWORD) { complete = complete && is_committed; }); + at = b; + } + if (complete) return true; + } + std::unique_lock held(lock_); + for (uint64_t at = start; at < end;) { + auto it = vmas_.upper_bound(at); + if (it == vmas_.begin()) return false; + --it; + const Vma& vma = it->second; + if (vma.end <= at) return false; + uint64_t b = std::min(end, vma.end); + if (vma.prot != PROT_NONE_ && vma.file) { + // File content a chunk at a time, read without the lock. + uint64_t hole = b; + for_each_run(at, b, [&](uint64_t x, uint64_t, bool is_committed, DWORD) { + if (!is_committed && hole == b) hole = x; + }); + if (hole != b) { + if (fill_file(held, hole, std::min(b, hole + kFileChunk)) < 0) return false; + at = hole; // look again: filled now, or the mapping changed + continue; + } + } else if (vma.prot != PROT_NONE_) { + for_each_run(at, b, [&](uint64_t x, uint64_t y, bool is_committed, DWORD) { + if (!is_committed) fill_locked(vma, x, y); + }); + } + at = b; + } + return true; +} + +bool AddressSpace::read_complete(uint64_t addr, void* out, size_t len) { + // A fill that ran meanwhile may have exposed a page before its content. + const uint64_t before = fill_sequence_.load(std::memory_order_acquire); + if (!(before & 1) && guest_read(addr, out, len)) { + std::atomic_thread_fence(std::memory_order_acquire); + if (fill_sequence_.load(std::memory_order_relaxed) == before) return true; + } + return populate(addr, len) && guest_read(addr, out, len); +} + +std::string AddressSpace::maps_text() const { + std::shared_lock held(lock_); + std::string text; + char line[512]; + for (const auto& [start, vma] : vmas_) { + std::snprintf(line, sizeof line, "%llx-%llx %c%c%c%c %08llx %s %llu", (unsigned long long)vma.start, + (unsigned long long)vma.end, (vma.prot & PROT_READ_) ? 'r' : '-', (vma.prot & PROT_WRITE_) ? 'w' : '-', + (vma.prot & PROT_EXEC_) ? 'x' : '-', (vma.flags & MAP_SHARED_) ? 's' : 'p', + (unsigned long long)vma.offset, vma.file ? "fd:01" : "00:00", + vma.file ? (unsigned long long)vma.file->inode() : 0ull); + text += line; + if (!vma.name.empty()) { + text.append(73 - std::min(72, std::strlen(line)), ' '); + text += vma.name; + } + text += '\n'; + } + return text; +} + +uint64_t AddressSpace::mapped_bytes() const { + std::shared_lock held(lock_); + uint64_t total = 0; + for (const auto& [start, vma] : vmas_) total += vma.end - vma.start; + return total; +} + +void AddressSpace::for_each(const std::function& visit) const { + std::shared_lock held(lock_); + for (const auto& [start, vma] : vmas_) visit(vma); +} + +bool guest_read(uint64_t addr, void* out, size_t len) { + if (!len) return true; + addr = untag(addr); + if (addr < 0x10000) return false; + __try { + std::memcpy(out, reinterpret_cast(addr), len); + return true; + } __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + return false; + } +} + +bool guest_write(uint64_t addr, const void* data, size_t len) { + if (!len) return true; + addr = untag(addr); + if (addr < 0x10000) return false; + __try { + std::memcpy(reinterpret_cast(addr), data, len); + return true; + } __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + return false; + } +} + +static bool guest_strlen(uint64_t addr, size_t max, size_t* out) { + __try { + const char* p = reinterpret_cast(addr); + size_t n = 0; + while (n < max && p[n]) ++n; + *out = n; + return true; + } __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + return false; + } +} + +bool guest_string(uint64_t addr, std::string& out, size_t max) { + addr = untag(addr); + if (addr < 0x10000) return false; + size_t n = 0; + if (!guest_strlen(addr, max, &n)) return false; + out.assign(reinterpret_cast(addr), n); + return true; +} + +} // namespace axrb diff --git a/direct/src/memory.h b/direct/src/memory.h new file mode 100644 index 0000000..eeac8c0 --- /dev/null +++ b/direct/src/memory.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { + +constexpr uint64_t kPageSize = 4096; +constexpr uint64_t page_down(uint64_t v) { return v & ~(kPageSize - 1); } +constexpr uint64_t page_up(uint64_t v) { return (v + kPageSize - 1) & ~(kPageSize - 1); } +// arm64 Linux ignores the top byte of user data addresses (TBI) and Android's +// allocator tags heap pointers there; host code must strip it before use. +constexpr uint64_t untag(uint64_t address) { return address & 0x00ff'ffff'ffff'ffffull; } + +class OpenFile; // files.h + +// Guest memory is identity mapped: a guest address is the host address of the +// same byte, so structures with pointers inside (Vulkan, OpenXR, JNI) cross +// the boundary unchanged. Linux mappings live inside one large reservation +// (the arena) and are committed lazily, page run by page run, on first touch: +// the vectored exception handler commits (and, for file mappings, reads) +// pages of a mapping the guest is entitled to access. +struct Vma { + uint64_t start = 0; + uint64_t end = 0; + uint32_t prot = 0; // PROT_* + uint32_t flags = 0; // MAP_* + std::shared_ptr file; // file-backed mapping (content read on fault) + uint64_t offset = 0; // file offset of `start` + std::string name; // for /proc/self/maps +}; + +class AddressSpace { +public: + bool init(bool low_memory = false); + uint64_t arena_base() const { return base_; } + uint64_t arena_end() const { return end_; } + // Includes ART's optional low reservations. Host buffers outside all guest + // reservations are also used by the existing native bridge. + bool overlaps_guest(uint64_t addr, uint64_t len) const; + + // Linux semantics; results are addresses or -errno. + int64_t mmap(uint64_t addr, uint64_t length, uint32_t prot, uint32_t flags, std::shared_ptr file, + uint64_t offset, const char* name = nullptr); + int64_t munmap(uint64_t addr, uint64_t length); + int64_t mprotect(uint64_t addr, uint64_t length, uint32_t prot); + int64_t mremap(uint64_t old_addr, uint64_t old_size, uint64_t new_size, uint32_t flags, uint64_t new_addr); + int64_t madvise(uint64_t addr, uint64_t length, int advice); + int64_t brk(uint64_t addr); + int64_t msync(uint64_t addr, uint64_t length, int flags); + int64_t mincore(uint64_t addr, uint64_t length, uint8_t* out); + void set_brk_base(uint64_t base); + void name_range(uint64_t addr, uint64_t length, const std::string& name); + + // Called by the vectored exception handler. True if the fault was a + // first touch of a mapping and the access may be retried. + bool handle_fault(uint64_t address, bool write, bool execute); + + // Whether [addr, addr+len) is mapped with at least `prot`. + bool check(uint64_t addr, uint64_t len, uint32_t prot) const; + // Whether any part of [addr, addr+len) is mapped executable. + bool any_exec(uint64_t addr, uint64_t len) const; + // Makes sure the range is committed (host code about to touch it in bulk). + bool populate(uint64_t addr, uint64_t len); + // populate() and guest_read(), without the lock when the pages are + // already complete: the translator fetches guest code this way. + bool read_complete(uint64_t addr, void* out, size_t len); + + std::string maps_text() const; + uint64_t mapped_bytes() const; + void for_each(const std::function& visit) const; + +private: + int64_t map_locked(uint64_t addr, uint64_t length, uint32_t prot, uint32_t flags, std::shared_ptr file, + uint64_t offset, const std::string& name); + void unmap_locked(uint64_t start, uint64_t end); + uint64_t find_free_locked(uint64_t length, uint64_t hint) const; + bool range_free_locked(uint64_t start, uint64_t end) const; + void split_locked(uint64_t at); + bool fill_locked(const Vma& vma, uint64_t start, uint64_t end, const uint8_t* content = nullptr); + int fill_file(std::unique_lock& held, uint64_t start, uint64_t end); + bool owns_range(uint64_t start, uint64_t end) const; + + uint64_t base_ = 0, end_ = 0; + std::vector> low_reservations_; + uint64_t mmap_top_ = 0; // next top-down allocation candidate + uint64_t brk_base_ = 0, brk_ = 0; + mutable std::shared_mutex lock_; + // Odd while a page is committed but its content not yet copied in. + std::atomic fill_sequence_{0}; + std::map vmas_; // keyed by start +}; + +AddressSpace& address_space(); + +// Host access to guest memory with fault containment; false on a bad pointer. +bool guest_read(uint64_t addr, void* out, size_t len); +bool guest_write(uint64_t addr, const void* data, size_t len); +// Reads a NUL-terminated string (bounded); false on a bad pointer. +bool guest_string(uint64_t addr, std::string& out, size_t max = 65536); +template +inline T* gptr(uint64_t addr) { return reinterpret_cast(untag(addr)); } + +uint32_t host_protection(uint32_t prot); + +} // namespace axrb diff --git a/direct/src/natives.cpp b/direct/src/natives.cpp new file mode 100644 index 0000000..50e21fe --- /dev/null +++ b/direct/src/natives.cpp @@ -0,0 +1,326 @@ +// Host natives of boot-class-path classes whose state lives in the guest: +// direct byte buffers (guest memory), file descriptors (guest fds), uncompressed +// APK assets opened as descriptors, and memory figures. + +#include "natives.h" + +#include "files.h" +#include "kernel.h" +#include "linux.h" +#include "log.h" +#include "media.h" +#include "memory.h" +#include "vfs.h" + +#include "arun/vm/heap.h" +#include "arun/vm/runtime.h" +#include "arun/vm/thread.h" +#include "arun/zip_reader.h" + +#include + +#include +#include +#include +#include + +namespace axrb { +namespace vm = arun::vm; +using namespace linux_abi; + +namespace { + +std::shared_ptr g_apk; +std::string g_guest_apk; + +GuestThread& guest_self() { + GuestThread* t = current_thread(); + if (!t) fatal("a guest-backed Java native ran on a thread that is not a guest thread"); + return *t; +} + +int64_t sys(uint64_t nr, uint64_t a0 = 0, uint64_t a1 = 0, uint64_t a2 = 0, uint64_t a3 = 0, uint64_t a4 = 0, + uint64_t a5 = 0) { + return do_syscall(guest_self(), nr, a0, a1, a2, a3, a4, a5); +} + +constexpr uint64_t kStatfs = 43, kOpenat = 56, kClose = 57, kFstat = 80, kMmap = 222; +const uint64_t kCwd = static_cast(static_cast(AT_FDCWD_)); + +void throw_new(vm::Thread* self, const char* descriptor, const std::string& message) { + self->runtime()->ThrowNew(self, descriptor, message); +} + +// --- java.nio.DirectByteBuffer -------------------------------------------------------- + +void DirectByteBuffer_nativeAlloc(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + const int32_t capacity = vm::ArgInt(a, 0); + r->j = 0; + if (capacity <= 0) return; + const uint64_t size = (static_cast(capacity) + 0xfff) & ~uint64_t{0xfff}; + vm::ScopedBlocking blocking(self); + int64_t addr = sys(kMmap, 0, size, PROT_READ_ | PROT_WRITE_, MAP_PRIVATE_ | MAP_ANONYMOUS_, + static_cast(-1), 0); + if (addr < 0 && addr > -4096) return; + r->j = addr; +} + +void DirectByteBuffer_peek(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + uint8_t b = 0; + if (!guest_read(static_cast(vm::ArgLong(a, 0)), &b, 1)) { + return throw_new(self, "Ljava/lang/IllegalStateException;", "direct buffer memory is not readable"); + } + r->i = static_cast(b); +} + +void DirectByteBuffer_poke(vm::Thread* self, vm::Frame* a, vm::JValue*) { + const uint8_t b = static_cast(vm::ArgInt(a, 2)); + if (!guest_write(static_cast(vm::ArgLong(a, 0)), &b, 1)) { + throw_new(self, "Ljava/lang/IllegalStateException;", "direct buffer memory is not writable"); + } +} + +bool array_range(vm::Thread* self, vm::Array* arr, int32_t off, int32_t len) { + if (arr == nullptr) { + throw_new(self, "Ljava/lang/NullPointerException;", "array == null"); + return false; + } + if (off < 0 || len < 0 || off > arr->length - len) { + throw_new(self, "Ljava/lang/ArrayIndexOutOfBoundsException;", "bad range"); + return false; + } + return true; +} + +void DirectByteBuffer_peekBytes(vm::Thread* self, vm::Frame* a, vm::JValue*) { + auto* arr = static_cast(vm::ArgRef(a, 2)); + const int32_t off = vm::ArgInt(a, 3), len = vm::ArgInt(a, 4); + if (!array_range(self, arr, off, len)) return; + if (!guest_read(static_cast(vm::ArgLong(a, 0)), arr->data() + off, static_cast(len))) { + throw_new(self, "Ljava/lang/IllegalStateException;", "direct buffer memory is not readable"); + } +} + +void DirectByteBuffer_pokeBytes(vm::Thread* self, vm::Frame* a, vm::JValue*) { + auto* arr = static_cast(vm::ArgRef(a, 2)); + const int32_t off = vm::ArgInt(a, 3), len = vm::ArgInt(a, 4); + if (!array_range(self, arr, off, len)) return; + if (!guest_write(static_cast(vm::ArgLong(a, 0)), arr->data() + off, static_cast(len))) { + throw_new(self, "Ljava/lang/IllegalStateException;", "direct buffer memory is not writable"); + } +} + +// --- android.content.res.AssetManager.openFd --------------------------------------------- + +void AssetManager_nativeOpenAssetFd(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + vm::Runtime* rt = self->runtime(); + r->l = nullptr; + std::string name = rt->GetStringUtf8(vm::ArgRef(a, 0)); + while (!name.empty() && name.front() == '/') name.erase(name.begin()); + if (!g_apk || name.empty() || name.find("..") != std::string::npos) return; + const arun::pkg::ZipEntry* e = g_apk->Find("assets/" + name); + if (e == nullptr || e->is_directory) return; + int64_t values[3] = {-1, 0, 0}; + if (e->method == arun::pkg::CompressionMethod::kStore) { + int64_t fd; + { + vm::ScopedBlocking blocking(self); + fd = sys(kOpenat, kCwd, reinterpret_cast(g_guest_apk.c_str()), O_RDONLY_ | O_CLOEXEC_, 0); + } + if (fd < 0) return; + values[0] = fd; + values[1] = static_cast(e->data_offset); + values[2] = static_cast(e->uncompressed_size); + } + vm::RtClass* long_array = rt->class_linker().FindClass(self, "[J", nullptr); + if (long_array == nullptr) return; + vm::Array* out = rt->heap().AllocArray(self, long_array, 3); + if (out == nullptr) return; + std::memcpy(out->data(), values, sizeof values); + r->l = out; +} + +// --- android.os.ParcelFileDescriptor ---------------------------------------------------- + +void ParcelFileDescriptor_nativeOpen(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + const std::string path = self->runtime()->GetStringUtf8(vm::ArgRef(a, 0)); + const uint32_t mode = static_cast(vm::ArgInt(a, 1)); + uint32_t flags = O_CLOEXEC_; + switch (mode & 0x30000000u) { + case 0x20000000u: flags |= O_WRONLY_; break; + case 0x30000000u: flags |= O_RDWR_; break; + default: flags |= O_RDONLY_; break; + } + if (mode & 0x08000000u) flags |= O_CREAT_; + if (mode & 0x04000000u) flags |= O_TRUNC_; + if (mode & 0x02000000u) flags |= O_APPEND_; + vm::ScopedBlocking blocking(self); + r->i = static_cast(sys(kOpenat, kCwd, reinterpret_cast(path.c_str()), flags, 0660)); +} + +void ParcelFileDescriptor_nativeStatSize(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + kstat st{}; + vm::ScopedBlocking blocking(self); + r->j = sys(kFstat, static_cast(vm::ArgInt(a, 0)), reinterpret_cast(&st)) == 0 ? st.st_size : -1; +} + +void ParcelFileDescriptor_nativeClose(vm::Thread* self, vm::Frame* a, vm::JValue*) { + vm::ScopedBlocking blocking(self); + sys(kClose, static_cast(vm::ArgInt(a, 0))); +} + +// --- android.app.ActivityManager --------------------------------------------------------- + +void ActivityManager_nativeTotalMemory(vm::Thread*, vm::Frame*, vm::JValue* r) { + MEMORYSTATUSEX m{sizeof m}; + GlobalMemoryStatusEx(&m); + r->j = static_cast(m.ullTotalPhys); +} + +void ActivityManager_nativeAvailableMemory(vm::Thread*, vm::Frame*, vm::JValue* r) { + MEMORYSTATUSEX m{sizeof m}; + GlobalMemoryStatusEx(&m); + r->j = static_cast(m.ullAvailPhys); +} + +// --- android.os.StatFs ------------------------------------------------------------------ + +void StatFs_nativeStatFs(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + vm::Runtime* rt = self->runtime(); + r->l = nullptr; + const std::string path = rt->GetStringUtf8(vm::ArgRef(a, 0)); + kstatfs st{}; + { + vm::ScopedBlocking blocking(self); + if (sys(kStatfs, reinterpret_cast(path.c_str()), reinterpret_cast(&st)) != 0) return; + } + const int64_t values[4] = {static_cast(st.f_bsize), static_cast(st.f_blocks), + static_cast(st.f_bfree), static_cast(st.f_bavail)}; + vm::RtClass* long_array = rt->class_linker().FindClass(self, "[J", nullptr); + if (long_array == nullptr) return; + vm::Array* out = rt->heap().AllocArray(self, long_array, 4); + if (out == nullptr) return; + std::memcpy(out->data(), values, sizeof values); + r->l = out; +} + +// --- java.util.zip.ZipFile -------------------------------------------------------------- +// +// Archives in the guest file system, parsed on the host (read-only access). + +std::mutex g_zip_lock; +std::map> g_zips; +int64_t g_next_zip = 1; + +std::shared_ptr zip_handle(int64_t handle) { + std::lock_guard lock(g_zip_lock); + auto it = g_zips.find(handle); + return it == g_zips.end() ? nullptr : it->second; +} + +void ZipFile_nativeOpen(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + r->j = 0; + const std::string path = self->runtime()->GetStringUtf8(vm::ArgRef(a, 0)); + Resolved where; + if (vfs().resolve(Vfs::normalize("/", path), true, where) != 0 || where.kind != Resolved::Host) return; + std::shared_ptr zip; + { + vm::ScopedBlocking blocking(self); + auto opened = arun::pkg::ZipReader::Open(where.host); + if (!opened) return; + zip = std::make_shared(std::move(*opened)); + } + std::lock_guard lock(g_zip_lock); + r->j = g_next_zip; + g_zips[g_next_zip++] = std::move(zip); +} + +void ZipFile_nativeNames(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + vm::Runtime* rt = self->runtime(); + r->l = nullptr; + auto zip = zip_handle(vm::ArgLong(a, 0)); + if (!zip) return; + const auto& entries = zip->entries(); + vm::RootScope roots(self); + auto arr = roots.Add(rt->heap().AllocArray(self, rt->wk.string_array, static_cast(entries.size()))); + if (arr.Get() == nullptr) return; + for (size_t i = 0; i < entries.size(); ++i) { + vm::Object* s = rt->NewStringUtf8(self, entries[i].name); + if (s == nullptr) return; + static_cast(arr.Get())->elements()[i] = s; + } + r->l = arr.Get(); +} + +void ZipFile_nativeEntry(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + vm::Runtime* rt = self->runtime(); + r->l = nullptr; + auto zip = zip_handle(vm::ArgLong(a, 0)); + if (!zip) return; + const arun::pkg::ZipEntry* e = zip->Find(rt->GetStringUtf8(vm::ArgRef(a, 2))); + if (e == nullptr) return; + const int64_t values[6] = {static_cast(e->uncompressed_size), static_cast(e->compressed_size), + static_cast(e->method), static_cast(e->crc32), -1, + static_cast(e->data_offset)}; + vm::RtClass* long_array = rt->class_linker().FindClass(self, "[J", nullptr); + if (long_array == nullptr) return; + vm::Array* out = rt->heap().AllocArray(self, long_array, 6); + if (out == nullptr) return; + std::memcpy(out->data(), values, sizeof values); + r->l = out; +} + +void ZipFile_nativeRead(vm::Thread* self, vm::Frame* a, vm::JValue* r) { + vm::Runtime* rt = self->runtime(); + r->l = nullptr; + auto zip = zip_handle(vm::ArgLong(a, 0)); + if (!zip) return; + const arun::pkg::ZipEntry* e = zip->Find(rt->GetStringUtf8(vm::ArgRef(a, 2))); + if (e == nullptr || e->is_directory) return; + std::vector bytes; + { + vm::ScopedBlocking blocking(self); + auto data = zip->Extract(*e); + if (!data) return; + bytes = std::move(*data); + } + vm::RtClass* byte_array = rt->class_linker().FindClass(self, "[B", nullptr); + if (byte_array == nullptr) return; + vm::Array* out = rt->heap().AllocArray(self, byte_array, static_cast(bytes.size())); + if (out == nullptr) return; + if (!bytes.empty()) std::memcpy(out->data(), bytes.data(), bytes.size()); + r->l = out; +} + +void ZipFile_nativeClose(vm::Thread*, vm::Frame* a, vm::JValue*) { + std::lock_guard lock(g_zip_lock); + g_zips.erase(vm::ArgLong(a, 0)); +} + +} // namespace + +void register_direct_natives(vm::Runtime* rt, std::shared_ptr apk, std::string guest_apk) { + g_apk = std::move(apk); + g_guest_apk = std::move(guest_apk); + auto reg = [rt](const char* key, vm::NativeFn fn) { rt->RegisterNative(key, fn); }; + reg("Ljava/nio/DirectByteBuffer;.nativeAlloc(I)J", DirectByteBuffer_nativeAlloc); + reg("Ljava/nio/DirectByteBuffer;.peek(J)B", DirectByteBuffer_peek); + reg("Ljava/nio/DirectByteBuffer;.poke(JB)V", DirectByteBuffer_poke); + reg("Ljava/nio/DirectByteBuffer;.peekBytes(J[BII)V", DirectByteBuffer_peekBytes); + reg("Ljava/nio/DirectByteBuffer;.pokeBytes(J[BII)V", DirectByteBuffer_pokeBytes); + reg("Landroid/content/res/AssetManager;.nativeOpenAssetFd(Ljava/lang/String;)[J", AssetManager_nativeOpenAssetFd); + reg("Landroid/os/ParcelFileDescriptor;.nativeOpen(Ljava/lang/String;I)I", ParcelFileDescriptor_nativeOpen); + reg("Landroid/os/ParcelFileDescriptor;.nativeStatSize(I)J", ParcelFileDescriptor_nativeStatSize); + reg("Landroid/os/ParcelFileDescriptor;.nativeClose(I)V", ParcelFileDescriptor_nativeClose); + reg("Landroid/app/ActivityManager;.nativeTotalMemory()J", ActivityManager_nativeTotalMemory); + reg("Landroid/app/ActivityManager;.nativeAvailableMemory()J", ActivityManager_nativeAvailableMemory); + reg("Landroid/os/StatFs;.nativeStatFs(Ljava/lang/String;)[J", StatFs_nativeStatFs); + reg("Ljava/util/zip/ZipFile;.nativeOpen(Ljava/lang/String;)J", ZipFile_nativeOpen); + reg("Ljava/util/zip/ZipFile;.nativeNames(J)[Ljava/lang/String;", ZipFile_nativeNames); + reg("Ljava/util/zip/ZipFile;.nativeEntry(JLjava/lang/String;)[J", ZipFile_nativeEntry); + reg("Ljava/util/zip/ZipFile;.nativeRead(JLjava/lang/String;)[B", ZipFile_nativeRead); + reg("Ljava/util/zip/ZipFile;.nativeClose(J)V", ZipFile_nativeClose); + register_media_natives(rt); +} + +} // namespace axrb diff --git a/direct/src/natives.h b/direct/src/natives.h new file mode 100644 index 0000000..f9397d5 --- /dev/null +++ b/direct/src/natives.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace arun::vm { +class Runtime; +} +namespace arun::pkg { +class ZipReader; +} + +namespace axrb { + +// Registers the host natives of boot classes backed by guest state (direct +// buffers, file descriptors, APK assets opened as descriptors). +void register_direct_natives(arun::vm::Runtime* rt, std::shared_ptr apk, std::string guest_apk); + +} // namespace axrb diff --git a/direct/src/ndk.h b/direct/src/ndk.h new file mode 100644 index 0000000..79f2d2e --- /dev/null +++ b/direct/src/ndk.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +namespace arun::pkg { +class ZipReader; +} + +namespace axrb { + +// Host side of the NDK proxy libraries under direct/guest/proxy. +struct NdkConfig { + std::shared_ptr apk; + std::string guest_apk; + int width = 1920, height = 1080, density_dpi = 480; + int sdk = 36; +}; +void ndk_init(const NdkConfig& config); +// Register real ART/AOSP ANativeWindow objects with the Windows graphics bridge. +void art_windows_init(); + +// An ANativeWindow* handed to guest code: a host object, opaque to the guest. +struct NativeWindow { + static constexpr uint32_t kMagic = 0x4e57414e; // "NAWN" + uint32_t magic = kMagic; + std::atomic refs{1}; + uint64_t surface = 0; // JNI global reference to the android.view.Surface + uint64_t handle = 0; // Surface.mNativeObject + int32_t format = 1; // AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM + int32_t width = 0, height = 0; // setBuffersGeometry overrides (0 = the surface's size) +}; +// nullptr unless `address` is a live ANativeWindow* from ANativeWindow_fromSurface. +NativeWindow* native_window(uint64_t address); +// The window's current size (the surface's, or the buffers-geometry override). +void native_window_size(NativeWindow* window, int32_t* width, int32_t* height); + +} // namespace axrb diff --git a/direct/src/ndk_android.cpp b/direct/src/ndk_android.cpp new file mode 100644 index 0000000..8722b35 --- /dev/null +++ b/direct/src/ndk_android.cpp @@ -0,0 +1,352 @@ +// Host half of libandroid.so (direct/guest/proxy/libandroid.c): native windows, +// assets and the configuration. Objects handed to the guest (ANativeWindow*, +// AAsset*, AAssetDir*, AConfiguration*) are host objects the guest only passes +// back; buffers it reads (AAsset_getBuffer, file names) are host memory, which +// guest code can address directly (the address spaces are the same). + +#include "ndk.h" + +#include "cpu.h" +#include "jni.h" +#include "kernel.h" +#include "linux.h" +#include "log.h" +#include "memory.h" + +#include "arun/zip_reader.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +namespace { + +NdkConfig g_config; + +// --- native windows ---------------------------------------------------------------------- + +std::mutex g_windows_lock; +std::unordered_set g_windows; +std::unordered_map> g_art_windows; + +void window_release(NativeWindow* w) { + if (w->refs.fetch_sub(1, std::memory_order_acq_rel) != 1) return; + { + std::lock_guard held(g_windows_lock); + g_windows.erase(w); + } + jni::release(w->surface); + delete w; +} + +// --- assets -------------------------------------------------------------------------------- + +struct Asset { + static constexpr uint32_t kMagic = 0x54535341; // "ASST" + uint32_t magic = kMagic; + const arun::pkg::ZipEntry* entry = nullptr; + std::vector inflated; // compressed entries, extracted at open + const uint8_t* data = nullptr; + uint64_t size = 0; + uint64_t pos = 0; +}; + +struct AssetDir { + std::vector names; + size_t next = 0; +}; + +std::string asset_name(uint64_t guest_name) { + std::string name; + if (!guest_name || !guest_string(guest_name, name, 4096)) return {}; + while (!name.empty() && name.front() == '/') name.erase(name.begin()); + while (name.size() > 2 && name.compare(0, 2, "./") == 0) name.erase(0, 2); + return name; +} + +Asset* asset_of(uint64_t address) { + auto* a = reinterpret_cast(untag(address)); + return a && a->magic == Asset::kMagic ? a : nullptr; +} + +int64_t asset_seek(Asset* a, int64_t offset, int whence) { + int64_t base = whence == 0 ? 0 : whence == 1 ? static_cast(a->pos) : static_cast(a->size); + int64_t to = base + offset; + if (to < 0 || to > static_cast(a->size)) return -1; + a->pos = static_cast(to); + return to; +} + +int open_apk_fd(GuestThread& thread) { + int64_t fd = do_syscall(thread, 56, static_cast(static_cast(AT_FDCWD_)), + reinterpret_cast(g_config.guest_apk.c_str()), O_RDONLY_ | O_CLOEXEC_, 0, 0, 0); + return fd < 0 ? -1 : static_cast(fd); +} + +struct Configuration { + int density = 480; +}; + +void def(const char* name, HostFunction fn) { register_named_function(std::string("android.") + name, std::move(fn)); } + +} // namespace + +NativeWindow* native_window(uint64_t address) { + auto* w = reinterpret_cast(untag(address)); + std::lock_guard held(g_windows_lock); + auto art = g_art_windows.find(untag(address)); + if (art != g_art_windows.end()) return art->second.get(); + return g_windows.count(w) ? w : nullptr; +} + +void native_window_size(NativeWindow* w, int32_t* width, int32_t* height) { + if (!w->surface && w->width > 0 && w->height > 0) { + *width = w->width; + *height = w->height; + return; + } + uint64_t raw = 0; + int32_t sw = 0, sh = 0; + if (jni::object_field(w->surface, "mWidth", "I", &raw)) sw = static_cast(raw); + if (jni::object_field(w->surface, "mHeight", "I", &raw)) sh = static_cast(raw); + if (sw <= 0 || sh <= 0) { + sw = g_config.width; + sh = g_config.height; + } + *width = w->width > 0 ? w->width : sw; + *height = w->height > 0 ? w->height : sh; +} + +void art_windows_init() { + register_named_function("art.window_register", [](HostCall& c) { + if (!c.x(0) || c.x(1) == 0 || c.x(2) == 0 || c.x(1) > 16384 || c.x(2) > 16384) return c.ret(-EINVAL_); + std::lock_guard held(g_windows_lock); + auto& window = g_art_windows[untag(c.x(0))]; + if (!window) window = std::make_unique(); + window->handle = untag(c.x(0)); + window->width = static_cast(c.x(1)); + window->height = static_cast(c.x(2)); + LOGI("art", "AOSP native window %llx (%dx%d)", window->handle, window->width, window->height); + c.ret(0); + }); +} + +void ndk_init(const NdkConfig& config) { + g_config = config; + + // ANativeWindow + def("ANativeWindow_fromSurface", [](HostCall& c) { + const uint64_t surface = c.x(1); + uint64_t handle = 0; + if (!surface || !jni::object_field(surface, "mNativeObject", "J", &handle) || handle == 0) return c.ret(0); + { + std::lock_guard held(g_windows_lock); + for (NativeWindow* w : g_windows) { + if (w->handle == handle) { + w->refs.fetch_add(1); + return c.ret(reinterpret_cast(w)); + } + } + } + auto* w = new NativeWindow; + w->surface = jni::retain(surface); + w->handle = handle; + { + std::lock_guard held(g_windows_lock); + g_windows.insert(w); + } + int32_t width, height; + native_window_size(w, &width, &height); + LOGI("ndk", "ANativeWindow %p for surface %llu (%dx%d)", static_cast(w), + static_cast(handle), width, height); + c.ret(reinterpret_cast(w)); + }); + def("ANativeWindow_toSurface", [](HostCall& c) { c.ret(0); }); + def("ANativeWindow_acquire", [](HostCall& c) { + if (NativeWindow* w = native_window(c.x(0))) w->refs.fetch_add(1); + }); + def("ANativeWindow_release", [](HostCall& c) { + if (NativeWindow* w = native_window(c.x(0))) window_release(w); + }); + def("ANativeWindow_getWidth", [](HostCall& c) { + NativeWindow* w = native_window(c.x(0)); + if (!w) return c.ret(static_cast(-EINVAL_)); + int32_t width, height; + native_window_size(w, &width, &height); + c.ret(static_cast(width)); + }); + def("ANativeWindow_getHeight", [](HostCall& c) { + NativeWindow* w = native_window(c.x(0)); + if (!w) return c.ret(static_cast(-EINVAL_)); + int32_t width, height; + native_window_size(w, &width, &height); + c.ret(static_cast(height)); + }); + def("ANativeWindow_getFormat", [](HostCall& c) { + NativeWindow* w = native_window(c.x(0)); + c.ret(w ? static_cast(w->format) : static_cast(-EINVAL_)); + }); + def("ANativeWindow_setBuffersGeometry", [](HostCall& c) { + NativeWindow* w = native_window(c.x(0)); + if (!w) return c.ret(static_cast(-EINVAL_)); + w->width = static_cast(c.x(1)); + w->height = static_cast(c.x(2)); + if (static_cast(c.x(3)) != 0) w->format = static_cast(c.x(3)); + c.ret(0); + }); + for (const char* name : {"ANativeWindow_setBuffersTransform", "ANativeWindow_setBuffersDataSpace", + "ANativeWindow_setFrameRate", "ANativeWindow_setFrameRateWithChangeStrategy", + "ANativeWindow_tryAllocateBuffers"}) { + def(name, [](HostCall& c) { c.ret(0); }); + } + def("ANativeWindow_getBuffersDataSpace", [](HostCall& c) { c.ret(0); }); + // Software rendering into the window is not supported (no CPU-visible surface). + def("ANativeWindow_lock", [](HostCall& c) { c.ret(static_cast(-EINVAL_)); }); + def("ANativeWindow_unlockAndPost", [](HostCall& c) { c.ret(static_cast(-EINVAL_)); }); + + // AAssetManager + static int asset_manager_token = 0; + def("AAssetManager_fromJava", [](HostCall& c) { c.ret(reinterpret_cast(&asset_manager_token)); }); + def("AAssetManager_open", [](HostCall& c) { + std::string name = asset_name(c.x(1)); + if (name.empty() || !g_config.apk) return c.ret(0); + const arun::pkg::ZipEntry* e = g_config.apk->Find("assets/" + name); + if (!e || e->is_directory) return c.ret(0); + auto a = std::make_unique(); + a->entry = e; + a->size = e->uncompressed_size; + if (e->method == arun::pkg::CompressionMethod::kStore) { + auto raw = g_config.apk->RawData(*e); + if (!raw.ok()) return c.ret(0); + a->data = raw->data(); + } else { + auto bytes = g_config.apk->Extract(*e); + if (!bytes.ok()) { + LOGW("ndk", "asset %s: %s", name.c_str(), bytes.status().ToString().c_str()); + return c.ret(0); + } + a->inflated = std::move(*bytes); + a->data = a->inflated.data(); + } + c.ret(reinterpret_cast(a.release())); + }); + def("AAssetManager_openDir", [](HostCall& c) { + std::string dir = asset_name(c.x(1)); + while (!dir.empty() && dir.back() == '/') dir.pop_back(); + std::string prefix = "assets/" + (dir.empty() ? std::string() : dir + "/"); + auto d = std::make_unique(); + std::set names; + if (g_config.apk) { + for (const auto& e : g_config.apk->entries()) { + if (e.is_directory || e.name.size() <= prefix.size() || e.name.compare(0, prefix.size(), prefix) != 0) + continue; + std::string rest = e.name.substr(prefix.size()); + if (rest.find('/') == std::string::npos) names.insert(rest); + } + } + d->names.assign(names.begin(), names.end()); + c.ret(reinterpret_cast(d.release())); + }); + def("AAssetDir_getNextFileName", [](HostCall& c) { + auto* d = reinterpret_cast(untag(c.x(0))); + if (!d || d->next >= d->names.size()) return c.ret(0); + c.ret(reinterpret_cast(d->names[d->next++].c_str())); + }); + def("AAssetDir_rewind", [](HostCall& c) { + if (auto* d = reinterpret_cast(untag(c.x(0)))) d->next = 0; + }); + def("AAssetDir_close", [](HostCall& c) { delete reinterpret_cast(untag(c.x(0))); }); + + // AAsset + def("AAsset_read", [](HostCall& c) { + Asset* a = asset_of(c.x(0)); + if (!a) return c.ret(static_cast(-1)); + uint64_t n = std::min(c.x(2), a->size - a->pos); + if (n && !guest_write(c.x(1), a->data + a->pos, n)) return c.ret(static_cast(-1)); + a->pos += n; + c.ret(n); + }); + auto seek = [](HostCall& c) { + Asset* a = asset_of(c.x(0)); + c.ret(a ? static_cast(asset_seek(a, static_cast(c.x(1)), static_cast(c.x(2)))) + : static_cast(-1)); + }; + def("AAsset_seek", seek); + def("AAsset_seek64", seek); + def("AAsset_close", [](HostCall& c) { + if (Asset* a = asset_of(c.x(0))) { + a->magic = 0; + delete a; + } + }); + def("AAsset_getBuffer", [](HostCall& c) { + Asset* a = asset_of(c.x(0)); + c.ret(a ? reinterpret_cast(a->data) : 0); + }); + auto length = [](HostCall& c) { + Asset* a = asset_of(c.x(0)); + c.ret(a ? a->size : static_cast(-1)); + }; + def("AAsset_getLength", length); + def("AAsset_getLength64", length); + auto remaining = [](HostCall& c) { + Asset* a = asset_of(c.x(0)); + c.ret(a ? a->size - a->pos : static_cast(-1)); + }; + def("AAsset_getRemainingLength", remaining); + def("AAsset_getRemainingLength64", remaining); + auto open_fd = [](HostCall& c) { + Asset* a = asset_of(c.x(0)); + if (!a || !a->inflated.empty() || a->entry->method != arun::pkg::CompressionMethod::kStore) + return c.ret(static_cast(-1)); + int fd = open_apk_fd(c.thread()); + if (fd < 0) return c.ret(static_cast(-1)); + int64_t start = static_cast(a->entry->data_offset), len = static_cast(a->size); + guest_write(c.x(1), &start, 8); + guest_write(c.x(2), &len, 8); + c.ret(static_cast(fd)); + }; + def("AAsset_openFileDescriptor", open_fd); + def("AAsset_openFileDescriptor64", open_fd); + def("AAsset_isAllocated", [](HostCall& c) { + Asset* a = asset_of(c.x(0)); + c.ret(a && !a->inflated.empty() ? 1 : 0); + }); + + // AConfiguration + def("AConfiguration_new", [](HostCall& c) { c.ret(reinterpret_cast(new Configuration)); }); + def("AConfiguration_delete", [](HostCall& c) { delete reinterpret_cast(untag(c.x(0))); }); + def("AConfiguration_fromAssetManager", [](HostCall&) {}); + def("AConfiguration_getDensity", [](HostCall& c) { c.ret(static_cast(g_config.density_dpi)); }); + def("AConfiguration_getOrientation", [](HostCall& c) { + c.ret(g_config.width >= g_config.height ? 2 : 1); // ACONFIGURATION_ORIENTATION_LAND / PORT + }); + def("AConfiguration_getScreenWidthDp", [](HostCall& c) { + c.ret(static_cast(g_config.width * 160 / g_config.density_dpi)); + }); + def("AConfiguration_getScreenHeightDp", [](HostCall& c) { + c.ret(static_cast(g_config.height * 160 / g_config.density_dpi)); + }); + def("AConfiguration_getSmallestScreenWidthDp", [](HostCall& c) { + c.ret(static_cast(std::min(g_config.width, g_config.height) * 160 / g_config.density_dpi)); + }); + def("AConfiguration_getScreenSize", [](HostCall& c) { c.ret(3); }); // LARGE + def("AConfiguration_getSdkVersion", [](HostCall& c) { c.ret(static_cast(g_config.sdk)); }); + def("AConfiguration_getLanguage", [](HostCall& c) { guest_write(c.x(1), "en", 2); }); + def("AConfiguration_getCountry", [](HostCall& c) { guest_write(c.x(1), "US", 2); }); + def("AConfiguration_getTouchscreen", [](HostCall& c) { c.ret(3); }); // FINGER + def("AConfiguration_getKeyboard", [](HostCall& c) { c.ret(1); }); // NOKEYS + def("AConfiguration_getNavigation", [](HostCall& c) { c.ret(1); }); // NONAV + def("AConfiguration_getUiModeType", [](HostCall& c) { c.ret(7); }); // VR_HEADSET + def("AConfiguration_getUiModeNight", [](HostCall& c) { c.ret(1); }); // NO + def("AConfiguration_getLayoutDirection", [](HostCall& c) { c.ret(0); }); +} + +} // namespace axrb diff --git a/direct/src/net.cpp b/direct/src/net.cpp new file mode 100644 index 0000000..67df2d3 --- /dev/null +++ b/direct/src/net.cpp @@ -0,0 +1,1326 @@ +#include +#include +#include + +#include "net.h" + +#include "cpu.h" +#include "log.h" +#include "memory.h" + +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +namespace { + +constexpr uint16_t kLinuxAfInet6 = 10; + +int64_t wsa_errno(int error) { + switch (error) { + case WSAEWOULDBLOCK: return -EAGAIN_; + case WSAEINPROGRESS: return -EINPROGRESS_; + case WSAEALREADY: return -EALREADY_; + case WSAENOTSOCK: return -ENOTSOCK_; + case WSAEDESTADDRREQ: return -EDESTADDRREQ_; + case WSAEMSGSIZE: return -EMSGSIZE_; + case WSAEPROTOTYPE: return -EPROTOTYPE_; + case WSAENOPROTOOPT: return -ENOPROTOOPT_; + case WSAEPROTONOSUPPORT: return -EPROTONOSUPPORT_; + case WSAEOPNOTSUPP: return -EOPNOTSUPP_; + case WSAEAFNOSUPPORT: return -EAFNOSUPPORT_; + case WSAEADDRINUSE: return -EADDRINUSE_; + case WSAEADDRNOTAVAIL: return -EADDRNOTAVAIL_; + case WSAENETDOWN: return -ENETDOWN_; + case WSAENETUNREACH: return -ENETUNREACH_; + case WSAECONNABORTED: return -ECONNABORTED_; + case WSAECONNRESET: return -ECONNRESET_; + case WSAENOBUFS: return -ENOBUFS_; + case WSAEISCONN: return -EISCONN_; + case WSAENOTCONN: return -ENOTCONN_; + case WSAESHUTDOWN: return -EPIPE_; + case WSAETIMEDOUT: return -ETIMEDOUT_; + case WSAECONNREFUSED: return -ECONNREFUSED_; + case WSAEHOSTDOWN: + case WSAEHOSTUNREACH: return -EHOSTUNREACH_; + case WSAEINVAL: return -EINVAL_; + case WSAEFAULT: return -EFAULT_; + case WSAEINTR: return -EINTR_; + case WSAEACCES: return -EACCES_; + case WSAEMFILE: return -EMFILE_; + default: return -EIO_; + } +} + +// Linux sockaddr (guest bytes) -> Windows sockaddr. Families differ only in +// the AF_INET6 value; the structures are laid out alike. +int64_t to_host_address(const uint8_t* bytes, size_t len, sockaddr_storage& out, int& out_len) { + if (len < 2) return -EINVAL_; + uint16_t family; + std::memcpy(&family, bytes, 2); + std::memset(&out, 0, sizeof out); + if (family == AF_INET_) { + if (len < sizeof(sockaddr_in)) return -EINVAL_; + auto* in = reinterpret_cast(&out); + std::memcpy(in, bytes, sizeof(sockaddr_in)); + in->sin_family = AF_INET; + // The emulator's alias for the host machine. + uint8_t* a = reinterpret_cast(&in->sin_addr); + if (a[0] == 10 && a[1] == 0 && a[2] == 2 && (a[3] == 2 || a[3] == 15)) { + a[0] = 127; + a[1] = 0; + a[2] = 0; + a[3] = 1; + } + out_len = sizeof(sockaddr_in); + return 0; + } + if (family == kLinuxAfInet6) { + if (len < 24) return -EINVAL_; + auto* in6 = reinterpret_cast(&out); + std::memcpy(in6, bytes, std::min(len, sizeof(sockaddr_in6))); + in6->sin6_family = AF_INET6; + out_len = sizeof(sockaddr_in6); + return 0; + } + return -EAFNOSUPPORT_; +} + +std::string to_guest_address(const sockaddr* address, int len) { + std::string out; + if (!address || len <= 0) return out; + out.assign(reinterpret_cast(address), static_cast(len)); + uint16_t family = address->sa_family == AF_INET6 ? kLinuxAfInet6 : static_cast(address->sa_family); + std::memcpy(out.data(), &family, 2); + return out; +} + +// Writes a sockaddr to (addr, addrlen*) the way the kernel does: truncated to +// the buffer, the full length reported. +int64_t put_address(uint64_t addr, uint64_t len_addr, const std::string& bytes) { + if (!addr || !len_addr) return 0; + uint32_t room = 0; + if (!guest_read(len_addr, &room, 4)) return -EFAULT_; + if (!guest_write(addr, bytes.data(), std::min(room, bytes.size()))) return -EFAULT_; + uint32_t actual = static_cast(bytes.size()); + return guest_write(len_addr, &actual, 4) ? 0 : -EFAULT_; +} + +int host_msg_flags(int flags) { + int out = 0; + if (flags & MSG_OOB_) out |= MSG_OOB; + if (flags & MSG_PEEK_) out |= MSG_PEEK; + if (flags & MSG_DONTROUTE_) out |= MSG_DONTROUTE; + return out; +} + +// Network interfaces as SIOCGIF* reports them: loopback plus a "wlan0" +// carrying the host's primary IPv4 address. Online SDKs consult this before +// they try to connect at all. +struct NetInterface { + const char* name; + int index; + uint32_t address; // network byte order + uint32_t netmask; + uint16_t flags; + uint16_t hw_type; + uint8_t mac[6]; +}; + +uint32_t primary_ipv4() { + static const uint32_t address = [] { + uint32_t found = htonl(0x0A00020F); // 10.0.2.15, as in the emulator + SOCKET probe = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (probe == INVALID_SOCKET) return found; + sockaddr_in target{}; + target.sin_family = AF_INET; + target.sin_port = htons(53); + target.sin_addr.s_addr = htonl(0x08080808); + sockaddr_in local{}; + int len = sizeof local; + // A connected datagram socket sends nothing; it only selects a route. + if (::connect(probe, reinterpret_cast(&target), sizeof target) == 0 && + getsockname(probe, reinterpret_cast(&local), &len) == 0 && local.sin_addr.s_addr) + found = local.sin_addr.s_addr; + closesocket(probe); + return found; + }(); + return address; +} + +const std::vector& net_interfaces() { + constexpr uint16_t kUp = 0x1, kBroadcast = 0x2, kLoopback = 0x8, kRunning = 0x40, kMulticast = 0x1000; + static const std::vector list = { + {"lo", 1, htonl(0x7F000001), htonl(0xFF000000), kUp | kLoopback | kRunning, 772, {}}, + {"wlan0", 2, primary_ipv4(), htonl(0xFFFFFF00), kUp | kBroadcast | kRunning | kMulticast, 1, + {0x02, 0x15, 0xB2, 0x00, 0x00, 0x00}}, + }; + return list; +} + +// struct ifreq on arm64: 16-byte name, then a 24-byte union. +struct LinuxIfreq { + char name[16]; + uint8_t data[24]; +}; +static_assert(sizeof(LinuxIfreq) == 40); + +void put_ifreq_ipv4(LinuxIfreq& req, uint32_t address) { + std::memset(req.data, 0, sizeof req.data); + uint16_t family = AF_INET_; + std::memcpy(req.data, &family, 2); + std::memcpy(req.data + 4, &address, 4); +} + +int64_t interface_ioctl(uint64_t request, uint64_t arg) { + constexpr uint64_t kGifName = 0x8910, kGifConf = 0x8912, kGifFlags = 0x8913, kGifAddr = 0x8915, + kGifBrdAddr = 0x8919, kGifNetmask = 0x891b, kGifMtu = 0x8921, kGifHwAddr = 0x8927, + kGifIndex = 0x8933; + const auto& list = net_interfaces(); + if (request == kGifConf) { + struct { int32_t len; int32_t pad; uint64_t buf; } conf{}; + if (!guest_read(arg, &conf, sizeof conf)) return -EFAULT_; + const int32_t needed = static_cast(list.size() * sizeof(LinuxIfreq)); + if (conf.buf == 0) { + conf.len = needed; + } else { + int32_t written = 0; + for (const auto& item : list) { + if (written + static_cast(sizeof(LinuxIfreq)) > conf.len) break; + LinuxIfreq req{}; + std::strncpy(req.name, item.name, sizeof req.name - 1); + put_ifreq_ipv4(req, item.address); + if (!guest_write(conf.buf + written, &req, sizeof req)) return -EFAULT_; + written += sizeof(LinuxIfreq); + } + conf.len = written; + } + return guest_write(arg, &conf, sizeof conf) ? 0 : -EFAULT_; + } + if (request != kGifName && request != kGifFlags && request != kGifAddr && request != kGifBrdAddr && + request != kGifNetmask && request != kGifMtu && request != kGifHwAddr && request != kGifIndex) + return -ENOTTY_; + LinuxIfreq req{}; + if (!guest_read(arg, &req, sizeof req)) return -EFAULT_; + const NetInterface* found = nullptr; + if (request == kGifName) { + int32_t index; + std::memcpy(&index, req.data, 4); + for (const auto& item : list) + if (item.index == index) found = &item; + if (!found) return -ENODEV_; + std::memset(req.name, 0, sizeof req.name); + std::strncpy(req.name, found->name, sizeof req.name - 1); + return guest_write(arg, &req, sizeof req) ? 0 : -EFAULT_; + } + req.name[sizeof req.name - 1] = 0; + for (const auto& item : list) + if (std::strcmp(item.name, req.name) == 0) found = &item; + if (!found) return -ENODEV_; + std::memset(req.data, 0, sizeof req.data); + switch (request) { + case kGifFlags: std::memcpy(req.data, &found->flags, 2); break; + case kGifAddr: put_ifreq_ipv4(req, found->address); break; + case kGifNetmask: put_ifreq_ipv4(req, found->netmask); break; + case kGifBrdAddr: put_ifreq_ipv4(req, found->address | ~found->netmask); break; + case kGifMtu: { + int32_t mtu = found->hw_type == 772 ? 65536 : 1500; + std::memcpy(req.data, &mtu, 4); + break; + } + case kGifHwAddr: + std::memcpy(req.data, &found->hw_type, 2); + std::memcpy(req.data + 2, found->mac, 6); + break; + case kGifIndex: std::memcpy(req.data, &found->index, 4); break; + } + return guest_write(arg, &req, sizeof req) ? 0 : -EFAULT_; +} + +void raise_sigpipe(GuestThread& t) { + ksiginfo info{}; + info.si_signo = SIGPIPE_; + info.si_code = SI_USER_; + send_signal(t, SIGPIPE_, info); +} + +} // namespace + +// --------------------------------------------------------------------------- +// Internet sockets on Winsock. The host socket is always non-blocking (event +// select makes it so); blocking calls wait the guest way, interruptibly. + +class SocketFile : public OpenFile { +public: + SocketFile(SOCKET socket, int family, int type, int protocol) + : socket_(socket), family_(family), type_(type), protocol_(protocol) { + event_ = WSACreateEvent(); + WSAEventSelect(socket_, event_, FD_READ | FD_WRITE | FD_ACCEPT | FD_CONNECT | FD_CLOSE | FD_OOB); + RegisterWaitForSingleObject(&wait_, event_, &SocketFile::on_event, this, INFINITE, WT_EXECUTEINWAITTHREAD); + assign_inode("socket:" + std::to_string(static_cast(socket_))); + } + ~SocketFile() override { + if (wait_) UnregisterWaitEx(wait_, INVALID_HANDLE_VALUE); + closesocket(socket_); + if (event_) WSACloseEvent(event_); + } + + SOCKET socket() const { return socket_; } + int family() const { return family_; } + int type() const { return type_; } + int protocol() const { return protocol_; } + + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override { + kiovec iov{buf, len}; + return receive(t, &iov, 1, 0, 0, 0); + } + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override { + kiovec iov{buf, len}; + return send(t, &iov, 1, 0, nullptr, 0); + } + int64_t writev(GuestThread& t, const std::vector& vecs) override { + return send(t, vecs.data(), vecs.size(), 0, nullptr, 0); + } + int64_t stat(kstat& st) override { + std::memset(&st, 0, sizeof st); + st.st_mode = S_IFSOCK_ | 0777; + st.st_nlink = 1; + st.st_ino = inode(); + st.st_blksize = 4096; + return 0; + } + int64_t ioctl(GuestThread& t, uint64_t request, uint64_t arg) override { + if (request == FIONREAD_) { + u_long available = 0; + if (ioctlsocket(socket_, FIONREAD, &available) != 0) return wsa_errno(WSAGetLastError()); + int32_t value = static_cast(available); + return guest_write(arg, &value, 4) ? 0 : -EFAULT_; + } + if ((request & 0xFF00) == 0x8900) { + int64_t r = interface_ioctl(request, arg); + TRACE("net", "ioctl(%llx) = %lld", static_cast(request), static_cast(r)); + return r; + } + return OpenFile::ioctl(t, request, arg); + } + uint32_t poll_mask() override { + if (int error = connect_error_.load()) { + (void)error; + return POLLERR_ | POLLHUP_ | POLLOUT_ | POLLIN_; + } + if (type_ == SOCK_STREAM_ && state_ == State::Fresh) return POLLOUT_ | POLLHUP_; + WSAPOLLFD p{socket_, POLLRDNORM | POLLWRNORM, 0}; + if (WSAPoll(&p, 1, 0) < 0) return POLLERR_; + uint32_t mask = 0; + if (p.revents & (POLLRDNORM | POLLRDBAND)) mask |= POLLIN_; + if (p.revents & POLLWRNORM) { + mask |= POLLOUT_; + if (state_ == State::Connecting) state_ = State::Connected; + } + if (p.revents & POLLHUP) mask |= POLLHUP_ | POLLIN_ | POLLRDHUP_; + if (p.revents & POLLERR) mask |= POLLERR_; + return mask; + } + WaitQueue* wait_queue() override { return &waiters_; } + + int64_t receive(GuestThread& t, const kiovec* iov, size_t count, int flags, uint64_t from, uint64_t from_len, + int* out_flags = nullptr) { + std::vector buffers; + for (size_t i = 0; i < count; ++i) { + if (!iov[i].len) continue; + if (!address_space().populate(iov[i].base, iov[i].len)) return -EFAULT_; + buffers.push_back({static_cast(std::min(iov[i].len, 0x7fffffff)), + reinterpret_cast(iov[i].base)}); + } + if (buffers.empty()) buffers.push_back({0, nullptr}); + sockaddr_storage address{}; + int64_t r = block(t, (flags & MSG_DONTWAIT_) != 0, rcvtimeo_ns_, POLLIN_, [&]() -> int64_t { + DWORD got = 0, host_flags = static_cast(host_msg_flags(flags)); + int address_len = sizeof address; + int rc = WSARecvFrom(socket_, buffers.data(), static_cast(buffers.size()), &got, &host_flags, + from ? reinterpret_cast(&address) : nullptr, from ? &address_len : nullptr, + nullptr, nullptr); + if (rc == 0) { + if (from) { + if (int64_t e = put_address(from, from_len, to_guest_address(reinterpret_cast(&address), address_len))) + return e; + } + return static_cast(got); + } + int error = WSAGetLastError(); + if (error == WSAEMSGSIZE) { + if (out_flags) *out_flags |= MSG_TRUNC_; + return static_cast(got); + } + if (error == WSAESHUTDOWN || error == WSAECONNABORTED) return 0; + return wsa_errno(error); + }); + return r; + } + + int64_t send(GuestThread& t, const kiovec* iov, size_t count, int flags, const sockaddr* to, int to_len) { + std::vector buffers; + for (size_t i = 0; i < count; ++i) { + if (!iov[i].len) continue; + if (!address_space().populate(iov[i].base, iov[i].len)) return -EFAULT_; + buffers.push_back({static_cast(std::min(iov[i].len, 0x7fffffff)), + reinterpret_cast(iov[i].base)}); + } + if (buffers.empty()) buffers.push_back({0, nullptr}); + int64_t r = block(t, (flags & MSG_DONTWAIT_) != 0, sndtimeo_ns_, POLLOUT_, [&]() -> int64_t { + DWORD sent = 0; + int rc = WSASendTo(socket_, buffers.data(), static_cast(buffers.size()), &sent, + static_cast(host_msg_flags(flags)), to, to_len, nullptr, nullptr); + if (rc == 0) return static_cast(sent); + return wsa_errno(WSAGetLastError()); + }); + if (r == -EPIPE_ && type_ == SOCK_STREAM_ && !(flags & MSG_NOSIGNAL_)) raise_sigpipe(t); + return r; + } + + int64_t connect(GuestThread& t, const sockaddr* address, int len) { + if (address->sa_family == AF_INET && + reinterpret_cast(address)->sin_addr.s_addr == htonl(INADDR_LOOPBACK)) { + // Fail fast on a closed local port, as Linux does, instead of + // Windows' two seconds of SYN retries. + TCP_INITIAL_RTO_PARAMETERS rto{TCP_INITIAL_RTO_UNSPECIFIED_RTT, TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS}; + DWORD unused = 0; + WSAIoctl(socket_, SIO_TCP_INITIAL_RTO, &rto, sizeof rto, nullptr, 0, &unused, nullptr, nullptr); + } + connect_error_ = 0; + if (::connect(socket_, address, len) == 0) { + state_ = State::Connected; + return 0; + } + int error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK) { + if (error == WSAEALREADY) return -EALREADY_; + return wsa_errno(error); + } + state_ = State::Connecting; + if (nonblocking() || type_ != SOCK_STREAM_) return type_ == SOCK_STREAM_ ? -EINPROGRESS_ : 0; + waiters_.add(&t); + int64_t w = block_until(t, [&] { return (poll_mask() & (POLLOUT_ | POLLERR_)) != 0; }, 0, "connect"); + waiters_.remove(&t); + if (w == -EINTR_) return -EINTR_; + return take_error(); + } + + // SO_ERROR: the pending asynchronous error, cleared by reading it. + int64_t take_error() { + int error = connect_error_.exchange(0); + if (error) { + state_ = State::Fresh; + return wsa_errno(error); + } + int value = 0, len = sizeof value; + getsockopt(socket_, SOL_SOCKET, SO_ERROR, reinterpret_cast(&value), &len); + if (value) return wsa_errno(value); + if (state_ == State::Connecting) state_ = State::Connected; + return 0; + } + + void mark_connected() { state_ = State::Connected; } + void mark_listening() { state_ = State::Listening; } + bool listening() const { return state_ == State::Listening; } + void mark_bound() { + if (state_ == State::Fresh && type_ != SOCK_STREAM_) state_ = State::Connected; + } + + int64_t rcvtimeo_ns_ = 0; + int64_t sndtimeo_ns_ = 0; + +private: + enum class State { Fresh, Connecting, Connected, Listening }; + + template + int64_t block(GuestThread& t, bool dont_wait, int64_t timeout_ns, uint32_t want, F&& attempt) { + int64_t deadline = timeout_ns ? ticks_now() + timeout_ns * ticks_per_second() / 1000000000 : 0; + for (;;) { + int64_t r = attempt(); + if (r != -EAGAIN_) return r; + if (nonblocking() || dont_wait) return -EAGAIN_; + waiters_.add(&t); + int64_t w = block_until(t, [&] { return (poll_mask() & (want | POLLERR_ | POLLHUP_)) != 0; }, deadline, "socket"); + waiters_.remove(&t); + if (w == -EINTR_) return -EINTR_; + if (w == -ETIMEDOUT_) return -EAGAIN_; + } + } + + static void CALLBACK on_event(void* param, BOOLEAN) { + auto* self = static_cast(param); + WSANETWORKEVENTS events{}; + if (WSAEnumNetworkEvents(self->socket_, self->event_, &events) == 0) { + if ((events.lNetworkEvents & FD_CONNECT) && events.iErrorCode[FD_CONNECT_BIT]) + self->connect_error_ = events.iErrorCode[FD_CONNECT_BIT]; + } + self->waiters_.wake_all(); + } + + SOCKET socket_; + int family_, type_, protocol_; + HANDLE event_ = nullptr; + HANDLE wait_ = nullptr; + WaitQueue waiters_; + std::atomic connect_error_{0}; + std::atomic state_{State::Fresh}; +}; + +// --------------------------------------------------------------------------- +// Host services behind AF_UNIX paths. + +int64_t ServiceSocket::read(GuestThread& t, uint64_t buf, uint64_t len) { + auto ready = [&] { + std::lock_guard held(lock_); + return !pending_.empty() || finished_; + }; + if (!ready()) { + if (nonblocking()) return -EAGAIN_; + waiters_.add(&t); + int64_t w = block_until(t, ready, 0, "service"); + waiters_.remove(&t); + if (w) return w; + } + std::lock_guard held(lock_); + size_t n = std::min(pending_.size(), static_cast(len)); + if (n && !guest_write(buf, pending_.data(), n)) return -EFAULT_; + pending_.erase(0, n); + return static_cast(n); +} + +int64_t ServiceSocket::write(GuestThread&, uint64_t buf, uint64_t len) { + std::string message(static_cast(len), '\0'); + if (len && !guest_read(buf, message.data(), message.size())) return -EFAULT_; + return send_message(message); +} + +int64_t ServiceSocket::writev(GuestThread&, const std::vector& vecs) { + std::string message; + for (auto& v : vecs) { + size_t at = message.size(); + message.resize(at + static_cast(v.len)); + if (v.len && !guest_read(v.base, message.data() + at, static_cast(v.len))) return -EFAULT_; + } + return send_message(message); +} + +int64_t ServiceSocket::send_message(std::string_view message) { + { + std::lock_guard held(lock_); + if (finished_) return -EPIPE_; + } + if (handler_) handler_(*this, message); + return static_cast(message.size()); +} + +void ServiceSocket::reply(const void* data, size_t len) { + { + std::lock_guard held(lock_); + pending_.append(static_cast(data), len); + } + waiters_.wake_all(); +} + +void ServiceSocket::finish() { + { + std::lock_guard held(lock_); + finished_ = true; + } + waiters_.wake_all(); +} + +int64_t ServiceSocket::stat(kstat& st) { + std::memset(&st, 0, sizeof st); + st.st_mode = S_IFSOCK_ | 0777; + st.st_nlink = 1; + st.st_blksize = 4096; + return 0; +} + +uint32_t ServiceSocket::poll_mask() { + std::lock_guard held(lock_); + uint32_t mask = finished_ ? 0 : POLLOUT_; + if (!pending_.empty()) mask |= POLLIN_; + if (finished_) mask |= POLLIN_ | POLLHUP_; + return mask; +} + +// --------------------------------------------------------------------------- +// AF_UNIX sockets between guest threads (and to host services). + +class UnixSocketFile; + +namespace { +std::mutex g_unix_lock; +std::map> g_listeners; +std::map> g_services; +} // namespace + +class UnixSocketFile : public OpenFile { +public: + explicit UnixSocketFile(int type) : type_(type) { assign_inode("unix:" + std::to_string(reinterpret_cast(this))); } + ~UnixSocketFile() override { + if (!bound_.empty()) { + std::lock_guard held(g_unix_lock); + auto it = g_listeners.find(bound_); + if (it != g_listeners.end() && it->second.expired()) g_listeners.erase(it); + } + } + + int type() const { return type_; } + std::shared_ptr peer() { + std::lock_guard held(lock_); + return peer_; + } + void set_peer(std::shared_ptr peer, std::string peer_path) { + std::lock_guard held(lock_); + peer_ = std::move(peer); + peer_path_ = std::move(peer_path); + } + const std::string& bound() const { return bound_; } + std::string peer_path() { + std::lock_guard held(lock_); + return peer_path_; + } + + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override { + auto p = peer(); + if (!p) return -ENOTCONN_; + p->status_flags = (p->status_flags & ~O_NONBLOCK_) | (status_flags & O_NONBLOCK_); + return p->read(t, buf, len); + } + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override { + auto p = peer(); + if (!p) return type_ == SOCK_STREAM_ ? -ENOTCONN_ : -EDESTADDRREQ_; + p->status_flags = (p->status_flags & ~O_NONBLOCK_) | (status_flags & O_NONBLOCK_); + return p->write(t, buf, len); + } + int64_t writev(GuestThread& t, const std::vector& vecs) override { + auto p = peer(); + if (!p) return type_ == SOCK_STREAM_ ? -ENOTCONN_ : -EDESTADDRREQ_; + return p->writev(t, vecs); + } + int64_t stat(kstat& st) override { + std::memset(&st, 0, sizeof st); + st.st_mode = S_IFSOCK_ | 0777; + st.st_nlink = 1; + st.st_ino = inode(); + st.st_blksize = 4096; + return 0; + } + int64_t ioctl(GuestThread& t, uint64_t request, uint64_t arg) override { + if (auto p = peer()) return p->ioctl(t, request, arg); + return OpenFile::ioctl(t, request, arg); + } + uint32_t poll_mask() override { + if (auto p = peer()) return p->poll_mask(); + std::lock_guard held(lock_); + if (listening_) return backlog_.empty() ? 0 : POLLIN_; + return POLLOUT_ | POLLHUP_; + } + WaitQueue* wait_queue() override { + if (auto p = peer()) return p->wait_queue(); + return &waiters_; + } + + int64_t bind(const std::string& path, const std::shared_ptr& self) { + std::lock_guard held(g_unix_lock); + auto it = g_listeners.find(path); + if (it != g_listeners.end() && !it->second.expired()) return -EADDRINUSE_; + g_listeners[path] = self; + bound_ = path; + return 0; + } + int64_t listen() { + if (bound_.empty()) return -EINVAL_; + std::lock_guard held(lock_); + listening_ = true; + return 0; + } + void enqueue(std::shared_ptr connection) { + { + std::lock_guard held(lock_); + backlog_.push_back(std::move(connection)); + } + waiters_.wake_all(); + } + bool is_listening() { + std::lock_guard held(lock_); + return listening_; + } + int64_t accept(GuestThread& t, std::shared_ptr& out) { + auto take = [&] { + std::lock_guard held(lock_); + if (backlog_.empty()) return false; + out = backlog_.front(); + backlog_.pop_front(); + return true; + }; + if (!is_listening()) return -EINVAL_; + if (take()) return 0; + if (nonblocking()) return -EAGAIN_; + waiters_.add(&t); + int64_t w = block_until(t, take, 0, "accept"); + waiters_.remove(&t); + return w; + } + +private: + int type_; + std::mutex lock_; + std::shared_ptr peer_; + std::string peer_path_; + std::string bound_; + bool listening_ = false; + std::deque> backlog_; + WaitQueue waiters_; +}; + +void register_unix_service(const std::string& path, std::function make) { + std::lock_guard held(g_unix_lock); + g_services[path] = std::move(make); +} + +namespace { + +// sockaddr_un -> a registry key: a path, or '@' + an abstract name. +int64_t unix_path(uint64_t addr, uint32_t len, std::string& out) { + if (len < 2 || len > 110) return -EINVAL_; + char bytes[110] = {}; + if (!guest_read(addr, bytes, len)) return -EFAULT_; + uint16_t family; + std::memcpy(&family, bytes, 2); + if (family != AF_UNIX_) return -EAFNOSUPPORT_; + if (len == 2) return -EINVAL_; + if (bytes[2] == 0) { + out = "@" + std::string(bytes + 3, len - 3); + while (!out.empty() && out.back() == 0) out.pop_back(); + return 0; + } + out.assign(bytes + 2, strnlen(bytes + 2, len - 2)); + out = Vfs::normalize(process().cwd, out); + return 0; +} + +std::string unix_address(const std::string& path) { + std::string out(2, '\0'); + uint16_t family = AF_UNIX_; + std::memcpy(out.data(), &family, 2); + if (path.empty()) return out; + if (path[0] == '@') { + out.push_back('\0'); + out += path.substr(1); + } else { + out += path; + out.push_back('\0'); + } + return out; +} + +int64_t unix_connect(GuestThread&, const std::shared_ptr& self, const std::string& path) { + if (self->peer()) return -EISCONN_; + std::function service; + std::shared_ptr listener; + { + std::lock_guard held(g_unix_lock); + if (auto it = g_services.find(path); it != g_services.end()) service = it->second; + else if (auto at = g_listeners.find(path); at != g_listeners.end()) listener = at->second.lock(); + } + if (service) { + self->set_peer(std::make_shared(service()), path); + TRACE("net", "unix connect %s -> host service", path.c_str()); + return 0; + } + if (!listener || !listener->is_listening()) { + TRACE("net", "unix connect %s refused", path.c_str()); + return path[0] == '@' ? -ECONNREFUSED_ : -ENOENT_; + } + if (listener->type() != self->type()) return -EPROTOTYPE_; + auto a = std::make_shared(); + auto b = std::make_shared(); + auto client_end = std::make_shared(b, a, true); + auto server_end = std::make_shared(a, b, true); + auto accepted = std::make_shared(self->type()); + accepted->set_peer(server_end, self->bound()); + self->set_peer(client_end, path); + listener->enqueue(accepted); + TRACE("net", "unix connect %s", path.c_str()); + return 0; +} + +// --------------------------------------------------------------------------- +// Built-in services. + +// /dev/socket/logdw: liblog's datagrams become log lines. +ServiceSocket::Handler logd_writer() { + return [](ServiceSocket&, std::string_view message) { + // android_log_header_t: id u8, tid u16, realtime {sec u32, nsec u32} + if (message.size() < 11) return; + uint8_t id = static_cast(message[0]); + uint16_t tid; + std::memcpy(&tid, message.data() + 1, 2); + std::string_view body = message.substr(11); + if (id == 2 || id == 5 || id == 6 || body.size() < 2) return; // binary event/stats/security buffers + static const char kLevels[] = "??VDIWEFS"; + uint8_t priority = static_cast(body[0]); + std::string_view rest = body.substr(1); + size_t tag_end = rest.find('\0'); + if (tag_end == std::string_view::npos) return; + std::string_view tag = rest.substr(0, tag_end); + std::string_view text = rest.substr(tag_end + 1); + while (!text.empty() && (text.back() == '\0' || text.back() == '\n')) text.remove_suffix(1); + char prefix[96]; + int n = std::snprintf(prefix, sizeof prefix, "%c/%.*s(%5u): ", priority < 9 ? kLevels[priority] : '?', + static_cast(std::min(tag.size(), 48)), tag.data(), tid); + // One log line per text line, like logcat. + size_t at = 0; + do { + size_t nl = text.find('\n', at); + std::string_view line = text.substr(at, nl == std::string_view::npos ? std::string_view::npos : nl - at); + std::string out(prefix, static_cast(n)); + out.append(line); + out.push_back('\n'); + log_raw(out.data(), out.size()); + at = nl == std::string_view::npos ? text.size() + 1 : nl + 1; + } while (at <= text.size()); + }; +} + +void put_be32(std::string& out, uint32_t value) { + uint32_t be = htonl(value); + out.append(reinterpret_cast(&be), 4); +} + +// /dev/socket/dnsproxyd: netd's resolver protocol, answered by the host +// resolver. Only getaddrinfo is served; other requests fail cleanly. +ServiceSocket::Handler dns_proxy() { + auto buffer = std::make_shared(); + return [buffer](ServiceSocket& socket, std::string_view message) { + buffer->append(message); + size_t end = buffer->find('\0'); + if (end == std::string::npos) return; + std::string command = buffer->substr(0, end); + buffer->erase(0, end + 1); + char host[1025] = {}, service[256] = {}; + int flags = 0, family = 0, socktype = 0, protocol = 0; + unsigned netid = 0; + std::string response; + if (std::sscanf(command.c_str(), "getaddrinfo %1024s %255s %d %d %d %d %u", host, service, &flags, &family, + &socktype, &protocol, &netid) >= 6) { + addrinfo hints{}; + hints.ai_flags = flags == -1 ? 0 : (flags & (AI_PASSIVE | AI_CANONNAME | AI_NUMERICHOST)); + hints.ai_family = family == -1 ? AF_UNSPEC : (family == kLinuxAfInet6 ? AF_INET6 : family); + hints.ai_socktype = socktype == -1 ? 0 : socktype; + hints.ai_protocol = protocol == -1 ? 0 : protocol; + addrinfo* result = nullptr; + const char* node = std::strcmp(host, "^") == 0 ? nullptr : host; + const char* serv = std::strcmp(service, "^") == 0 ? nullptr : service; + int rc = getaddrinfo(node, serv, &hints, &result); + LOGD("net", "dns %s -> %d", node ? node : "(null)", rc); + if (rc != 0) { + response.assign("401", 4); + put_be32(response, 4); + put_be32(response, 7); // EAI_NODATA + } else { + response.assign("222", 4); + for (addrinfo* ai = result; ai; ai = ai->ai_next) { + std::string address = to_guest_address(ai->ai_addr, static_cast(ai->ai_addrlen)); + put_be32(response, 1); + put_be32(response, static_cast(ai->ai_flags)); + put_be32(response, ai->ai_family == AF_INET6 ? kLinuxAfInet6 : static_cast(ai->ai_family)); + put_be32(response, static_cast(ai->ai_socktype)); + put_be32(response, static_cast(ai->ai_protocol)); + put_be32(response, static_cast(address.size())); + response += address; + if (ai->ai_canonname) { + put_be32(response, static_cast(std::strlen(ai->ai_canonname) + 1)); + response.append(ai->ai_canonname, std::strlen(ai->ai_canonname) + 1); + } else { + put_be32(response, 0); + } + } + put_be32(response, 0); + freeaddrinfo(result); + } + } else { + LOGD("net", "dnsproxyd: unsupported request '%s'", command.c_str()); + response.assign("500", 4); + put_be32(response, 4); + put_be32(response, 7); + } + socket.reply(response.data(), response.size()); + socket.finish(); + }; +} + +// --------------------------------------------------------------------------- +// Socket options. + +int64_t set_option(SocketFile& s, int level, int name, uint64_t value_addr, uint32_t len) { + int32_t value = 0; + if (len >= 4) guest_read(value_addr, &value, 4); + auto host_set = [&](int host_level, int host_name, const void* data, int size) -> int64_t { + if (setsockopt(s.socket(), host_level, host_name, static_cast(data), size) != 0) + return wsa_errno(WSAGetLastError()); + return 0; + }; + if (level == SOL_SOCKET_) { + switch (name) { + case SO_KEEPALIVE_: return host_set(SOL_SOCKET, SO_KEEPALIVE, &value, 4); + case SO_BROADCAST_: return host_set(SOL_SOCKET, SO_BROADCAST, &value, 4); + case SO_SNDBUF_: return host_set(SOL_SOCKET, SO_SNDBUF, &value, 4); + case SO_RCVBUF_: return host_set(SOL_SOCKET, SO_RCVBUF, &value, 4); + case SO_OOBINLINE_: return host_set(SOL_SOCKET, SO_OOBINLINE, &value, 4); + case SO_LINGER_: { + klinger l{}; + if (!guest_read(value_addr, &l, sizeof l)) return -EFAULT_; + linger host{static_cast(l.l_onoff), static_cast(l.l_linger)}; + return host_set(SOL_SOCKET, SO_LINGER, &host, sizeof host); + } + case SO_RCVTIMEO_: + case SO_SNDTIMEO_: { + ktimeval tv{}; + if (!guest_read(value_addr, &tv, sizeof tv)) return -EFAULT_; + int64_t ns = tv.tv_sec * 1000000000 + tv.tv_usec * 1000; + (name == SO_RCVTIMEO_ ? s.rcvtimeo_ns_ : s.sndtimeo_ns_) = ns; + return 0; + } + default: + // SO_REUSEADDR (Windows' version would allow port stealing), + // SO_REUSEPORT, SO_PASSCRED, priorities: accepted. + return 0; + } + } + if (level == IPPROTO_TCP_) { + switch (name) { + case TCP_NODELAY_: return host_set(IPPROTO_TCP, TCP_NODELAY, &value, 4); + case TCP_KEEPIDLE_: return host_set(IPPROTO_TCP, TCP_KEEPIDLE, &value, 4); + case TCP_KEEPINTVL_: return host_set(IPPROTO_TCP, TCP_KEEPINTVL, &value, 4); + case TCP_KEEPCNT_: return host_set(IPPROTO_TCP, TCP_KEEPCNT, &value, 4); + default: return 0; + } + } + if (level == IPPROTO_IPV6_ && name == 26) return host_set(IPPROTO_IPV6, IPV6_V6ONLY, &value, 4); + return 0; +} + +int64_t get_option(SocketFile& s, int level, int name, uint64_t value_addr, uint64_t len_addr) { + uint32_t room = 0; + if (!guest_read(len_addr, &room, 4)) return -EFAULT_; + auto put = [&](const void* data, uint32_t size) -> int64_t { + if (!guest_write(value_addr, data, std::min(room, size))) return -EFAULT_; + uint32_t actual = std::min(room, size); + return guest_write(len_addr, &actual, 4) ? 0 : -EFAULT_; + }; + auto host_int = [&](int host_level, int host_name) -> int64_t { + int value = 0, size = sizeof value; + if (getsockopt(s.socket(), host_level, host_name, reinterpret_cast(&value), &size) != 0) + return wsa_errno(WSAGetLastError()); + int32_t v = value; + return put(&v, 4); + }; + if (level == SOL_SOCKET_) { + switch (name) { + case SO_ERROR_: { + int32_t error = static_cast(-s.take_error()); + return put(&error, 4); + } + case SO_TYPE_: { + int32_t v = s.type(); + return put(&v, 4); + } + case SO_DOMAIN_: { + int32_t v = s.family(); + return put(&v, 4); + } + case SO_PROTOCOL_: { + int32_t v = s.protocol() ? s.protocol() : (s.type() == SOCK_STREAM_ ? IPPROTO_TCP_ : IPPROTO_UDP_); + return put(&v, 4); + } + case SO_ACCEPTCONN_: { + int32_t v = s.listening() ? 1 : 0; + return put(&v, 4); + } + case SO_SNDBUF_: return host_int(SOL_SOCKET, SO_SNDBUF); + case SO_RCVBUF_: return host_int(SOL_SOCKET, SO_RCVBUF); + case SO_KEEPALIVE_: return host_int(SOL_SOCKET, SO_KEEPALIVE); + case SO_RCVTIMEO_: + case SO_SNDTIMEO_: { + int64_t ns = name == SO_RCVTIMEO_ ? s.rcvtimeo_ns_ : s.sndtimeo_ns_; + ktimeval tv{ns / 1000000000, (ns % 1000000000) / 1000}; + return put(&tv, sizeof tv); + } + default: { + int32_t zero = 0; + return put(&zero, 4); + } + } + } + if (level == IPPROTO_TCP_ && name == TCP_NODELAY_) return host_int(IPPROTO_TCP, TCP_NODELAY); + int32_t zero = 0; + return put(&zero, 4); +} + +int64_t read_iovecs(uint64_t addr, uint64_t count, std::vector& out) { + if (count > 1024) return -EINVAL_; + out.resize(static_cast(count)); + if (count && !guest_read(addr, out.data(), out.size() * sizeof(kiovec))) return -EFAULT_; + return 0; +} + +int64_t send_message(GuestThread& t, int fd, const kmsghdr& msg, const std::vector& iov, int flags) { + auto file = fds().get(fd); + if (!file) return -EBADF_; + if (auto* s = dynamic_cast(file.get())) { + sockaddr_storage to{}; + int to_len = 0; + if (msg.msg_name && msg.msg_namelen) { + std::vector bytes(msg.msg_namelen); + if (!guest_read(msg.msg_name, bytes.data(), bytes.size())) return -EFAULT_; + if (int64_t e = to_host_address(bytes.data(), bytes.size(), to, to_len)) return e; + } + return s->send(t, iov.data(), iov.size(), flags, to_len ? reinterpret_cast(&to) : nullptr, to_len); + } + std::shared_ptr target = file; + if (auto* u = dynamic_cast(file.get())) { + target = u->peer(); + if (!target && msg.msg_name && msg.msg_namelen) { + // An unconnected datagram socket sending to a path. + std::string path; + if (int64_t e = unix_path(msg.msg_name, msg.msg_namelen, path)) return e; + auto temp = std::make_shared(u->type()); + if (int64_t e = unix_connect(t, temp, path)) return e; + target = temp->peer(); + } + if (!target) return -ENOTCONN_; + } + return target->writev(t, iov); +} + +// Receives into `iov`; the source address goes to (name, name_len_addr) when +// both are given (zero length for AF_UNIX peers). +int64_t receive_message(GuestThread& t, int fd, std::vector& iov, int flags, uint64_t name, + uint64_t name_len_addr, int& out_flags) { + auto file = fds().get(fd); + if (!file) return -EBADF_; + int64_t r; + if (auto* s = dynamic_cast(file.get())) { + r = s->receive(t, iov.data(), iov.size(), flags, name, name ? name_len_addr : 0, &out_flags); + } else { + std::shared_ptr source = file; + if (auto* u = dynamic_cast(file.get())) { + source = u->peer(); + if (!source) return -ENOTCONN_; + } + uint32_t saved = source->status_flags; + if (flags & MSG_DONTWAIT_) source->status_flags |= O_NONBLOCK_; + r = 0; + for (size_t i = 0; i < iov.size(); ++i) { + if (!iov[i].len) continue; + if (r > 0 && !(source->poll_mask() & POLLIN_)) break; + int64_t got = source->read(t, iov[i].base, iov[i].len); + if (got < 0) { + if (!r) r = got; + break; + } + r += got; + if (static_cast(got) < iov[i].len) break; + } + source->status_flags = saved; + uint32_t zero = 0; + if (name && name_len_addr) guest_write(name_len_addr, &zero, 4); + } + return r; +} + +int64_t do_sendmsg(GuestThread& t, int fd, uint64_t msg_addr, int flags) { + kmsghdr msg{}; + if (!guest_read(msg_addr, &msg, sizeof msg)) return -EFAULT_; + std::vector iov; + if (int64_t e = read_iovecs(msg.msg_iov, msg.msg_iovlen, iov)) return e; + return send_message(t, fd, msg, iov, flags); +} + +int64_t do_recvmsg(GuestThread& t, int fd, uint64_t msg_addr, int flags) { + kmsghdr msg{}; + if (!guest_read(msg_addr, &msg, sizeof msg)) return -EFAULT_; + std::vector iov; + if (int64_t e = read_iovecs(msg.msg_iov, msg.msg_iovlen, iov)) return e; + int out_flags = 0; + int64_t r = receive_message(t, fd, iov, flags, msg.msg_name, msg_addr + offsetof(kmsghdr, msg_namelen), out_flags); + if (r < 0) return r; + uint64_t no_control = 0; + guest_write(msg_addr + offsetof(kmsghdr, msg_controllen), &no_control, 8); + guest_write(msg_addr + offsetof(kmsghdr, msg_flags), &out_flags, 4); + return r; +} + +} // namespace + +void net_init() { + WSADATA data; + if (WSAStartup(MAKEWORD(2, 2), &data) != 0) LOGE("net", "WSAStartup failed"); + register_unix_service("/dev/socket/logdw", logd_writer); + register_unix_service("/dev/socket/dnsproxyd", dns_proxy); +} + +int64_t sys_net(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled) { + handled = true; + int fd = static_cast(static_cast(a[0])); + switch (nr) { + case 198: { // socket(domain, type, protocol) + int domain = static_cast(a[0]); + int type = static_cast(a[1]) & 0xf; + bool nonblock = (a[1] & SOCK_NONBLOCK_) != 0, cloexec = (a[1] & SOCK_CLOEXEC_) != 0; + int protocol = static_cast(a[2]); + std::shared_ptr file; + if (domain == AF_UNIX_) { + if (type != SOCK_STREAM_ && type != SOCK_DGRAM_ && type != SOCK_SEQPACKET_) return -EINVAL_; + file = std::make_shared(type); + } else if (domain == AF_INET_ || domain == AF_INET6_) { + if (type != SOCK_STREAM_ && type != SOCK_DGRAM_) return -EPROTONOSUPPORT_; + SOCKET s = ::socket(domain == AF_INET6_ ? AF_INET6 : AF_INET, type == SOCK_STREAM_ ? SOCK_STREAM : SOCK_DGRAM, + protocol); + if (s == INVALID_SOCKET) return wsa_errno(WSAGetLastError()); + file = std::make_shared(s, domain, type, protocol); + } else { + TRACE("net", "socket(%d, %d) unsupported", domain, type); + return -EAFNOSUPPORT_; + } + file->status_flags = O_RDWR_ | (nonblock ? O_NONBLOCK_ : 0); + int r = fds().install(file, cloexec); + TRACE("net", "socket(%d, %d, %d) = %d", domain, type, protocol, r); + return r; + } + case 199: { // socketpair + if (a[0] != AF_UNIX_) return -EAFNOSUPPORT_; + auto x = std::make_shared(); + auto y = std::make_shared(); + auto one = std::make_shared(x, y, true); + auto two = std::make_shared(y, x, true); + uint32_t flags = (a[1] & SOCK_NONBLOCK_) ? O_NONBLOCK_ : 0; + one->status_flags = two->status_flags = O_RDWR_ | flags; + bool cloexec = (a[1] & SOCK_CLOEXEC_) != 0; + int32_t pair[2] = {fds().install(one, cloexec), fds().install(two, cloexec)}; + if (!guest_write(a[3], pair, sizeof pair)) return -EFAULT_; + return 0; + } + case 200: // bind + case 203: { // connect + auto file = fds().get(fd); + if (!file) return -EBADF_; + if (auto* s = dynamic_cast(file.get())) { + uint32_t len = static_cast(std::min(a[2], 128)); + std::vector bytes(len); + if (!guest_read(a[1], bytes.data(), len)) return -EFAULT_; + sockaddr_storage address{}; + int address_len = 0; + if (int64_t e = to_host_address(bytes.data(), len, address, address_len)) return e; + if (nr == 200) { + if (::bind(s->socket(), reinterpret_cast(&address), address_len) != 0) + return wsa_errno(WSAGetLastError()); + s->mark_bound(); + return 0; + } + int64_t r = s->connect(t, reinterpret_cast(&address), address_len); + TRACE("net", "connect(%d) = %lld", fd, (long long)r); + return r; + } + auto unix_file = std::dynamic_pointer_cast(file); + if (!unix_file) return -ENOTSOCK_; + std::string path; + if (int64_t e = unix_path(a[1], static_cast(a[2]), path)) return e; + if (nr == 200) return unix_file->bind(path, unix_file); + return unix_connect(t, unix_file, path); + } + case 201: { // listen + auto file = fds().get(fd); + if (!file) return -EBADF_; + if (auto* s = dynamic_cast(file.get())) { + if (::listen(s->socket(), static_cast(a[1]) > 0 ? static_cast(a[1]) : SOMAXCONN) != 0) + return wsa_errno(WSAGetLastError()); + s->mark_listening(); + return 0; + } + if (auto* u = dynamic_cast(file.get())) return u->listen(); + return -ENOTSOCK_; + } + case 202: // accept + case 242: { // accept4 + auto file = fds().get(fd); + if (!file) return -EBADF_; + int flags = nr == 242 ? static_cast(a[3]) : 0; + std::shared_ptr accepted; + std::string peer_address; + if (auto* s = dynamic_cast(file.get())) { + if (!s->listening()) return -EINVAL_; + for (;;) { + sockaddr_storage address{}; + int len = sizeof address; + SOCKET c = ::accept(s->socket(), reinterpret_cast(&address), &len); + if (c != INVALID_SOCKET) { + auto connection = std::make_shared(c, s->family(), s->type(), s->protocol()); + connection->mark_connected(); + accepted = connection; + peer_address = to_guest_address(reinterpret_cast(&address), len); + break; + } + int error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK) return wsa_errno(error); + if (s->nonblocking()) return -EAGAIN_; + s->wait_queue()->add(&t); + int64_t w = block_until(t, [&] { return (s->poll_mask() & POLLIN_) != 0; }, 0, "accept"); + s->wait_queue()->remove(&t); + if (w) return w; + } + } else if (auto* u = dynamic_cast(file.get())) { + std::shared_ptr connection; + if (int64_t e = u->accept(t, connection)) return e; + peer_address = unix_address(connection->peer_path()); + accepted = connection; + } else { + return -ENOTSOCK_; + } + accepted->status_flags = O_RDWR_ | ((flags & SOCK_NONBLOCK_) ? O_NONBLOCK_ : 0); + if (a[1]) put_address(a[1], a[2], peer_address); + return fds().install(accepted, (flags & SOCK_CLOEXEC_) != 0); + } + case 204: // getsockname + case 205: { // getpeername + auto file = fds().get(fd); + if (!file) return -EBADF_; + if (auto* s = dynamic_cast(file.get())) { + sockaddr_storage address{}; + int len = sizeof address; + int rc = nr == 204 ? getsockname(s->socket(), reinterpret_cast(&address), &len) + : getpeername(s->socket(), reinterpret_cast(&address), &len); + if (rc != 0) return wsa_errno(WSAGetLastError()); + return put_address(a[1], a[2], to_guest_address(reinterpret_cast(&address), len)); + } + if (auto* u = dynamic_cast(file.get())) { + if (nr == 205 && !u->peer()) return -ENOTCONN_; + return put_address(a[1], a[2], unix_address(nr == 204 ? u->bound() : u->peer_path())); + } + if (auto* p = dynamic_cast(file.get()); p && p->is_socket()) return put_address(a[1], a[2], unix_address("")); + return -ENOTSOCK_; + } + case 206: { // sendto(fd, buf, len, flags, addr, addrlen) + auto file = fds().get(fd); + if (!file) return -EBADF_; + kiovec iov{a[1], a[2]}; + int flags = static_cast(a[3]); + if (auto* s = dynamic_cast(file.get())) { + sockaddr_storage to{}; + int to_len = 0; + if (a[4] && a[5]) { + std::vector bytes(static_cast(std::min(a[5], 128))); + if (!guest_read(a[4], bytes.data(), bytes.size())) return -EFAULT_; + if (int64_t e = to_host_address(bytes.data(), bytes.size(), to, to_len)) return e; + } + return s->send(t, &iov, 1, flags, to_len ? reinterpret_cast(&to) : nullptr, to_len); + } + // Everything else through the sendmsg path. + kmsghdr msg{}; + msg.msg_name = a[4]; + msg.msg_namelen = static_cast(a[5]); + return send_message(t, fd, msg, {iov}, flags); + } + case 207: { // recvfrom(fd, buf, len, flags, addr, addrlen) + auto file = fds().get(fd); + if (!file) return -EBADF_; + kiovec iov{a[1], a[2]}; + int flags = static_cast(a[3]); + if (auto* s = dynamic_cast(file.get())) return s->receive(t, &iov, 1, flags, a[4], a[5]); + std::vector vec{iov}; + int out_flags = 0; + return receive_message(t, fd, vec, flags, a[4], a[5], out_flags); + } + case 211: return do_sendmsg(t, fd, a[1], static_cast(a[2])); // sendmsg + case 212: return do_recvmsg(t, fd, a[1], static_cast(a[2])); // recvmsg + case 269: { // sendmmsg(fd, msgvec, vlen, flags): struct mmsghdr { msghdr; u32 len; pad } + uint64_t count = std::min(a[2], 1024); + for (uint64_t i = 0; i < count; ++i) { + uint64_t entry = a[1] + i * 64; + int64_t r = do_sendmsg(t, fd, entry, static_cast(a[3])); + if (r < 0) return i ? static_cast(i) : r; + uint32_t len = static_cast(r); + guest_write(entry + 56, &len, 4); + } + return static_cast(count); + } + case 243: { // recvmmsg + uint64_t count = std::min(a[2], 1024); + for (uint64_t i = 0; i < count; ++i) { + uint64_t entry = a[1] + i * 64; + int64_t r = do_recvmsg(t, fd, entry, static_cast(a[3]) | (i ? MSG_DONTWAIT_ : 0)); + if (r < 0) return i ? static_cast(i) : r; + uint32_t len = static_cast(r); + guest_write(entry + 56, &len, 4); + } + return static_cast(count); + } + case 208: { // setsockopt(fd, level, name, value, len) + auto file = fds().get(fd); + if (!file) return -EBADF_; + if (auto* s = dynamic_cast(file.get())) + return set_option(*s, static_cast(a[1]), static_cast(a[2]), a[3], static_cast(a[4])); + return 0; + } + case 209: { // getsockopt(fd, level, name, value, len*) + auto file = fds().get(fd); + if (!file) return -EBADF_; + if (auto* s = dynamic_cast(file.get())) + return get_option(*s, static_cast(a[1]), static_cast(a[2]), a[3], a[4]); + uint32_t room = 0; + if (!guest_read(a[4], &room, 4)) return -EFAULT_; + if (a[1] == SOL_SOCKET_ && a[2] == SO_PEERCRED_) { + int32_t cred[3] = {process().pid, static_cast(process().uid), static_cast(process().gid)}; + uint32_t n = std::min(room, sizeof cred); + guest_write(a[3], cred, n); + return guest_write(a[4], &n, 4) ? 0 : -EFAULT_; + } + int32_t value = 0; + if (a[1] == SOL_SOCKET_ && a[2] == SO_TYPE_) { + if (auto* u = dynamic_cast(file.get())) value = u->type(); + else value = SOCK_STREAM_; + } else if (a[1] == SOL_SOCKET_ && (a[2] == SO_SNDBUF_ || a[2] == SO_RCVBUF_)) { + value = 65536; + } else if (a[1] == SOL_SOCKET_ && a[2] == SO_DOMAIN_) { + value = AF_UNIX_; + } + uint32_t n = std::min(room, 4); + guest_write(a[3], &value, n); + return guest_write(a[4], &n, 4) ? 0 : -EFAULT_; + } + case 210: { // shutdown + auto file = fds().get(fd); + if (!file) return -EBADF_; + int how = static_cast(a[1]); + if (auto* s = dynamic_cast(file.get())) { + int host_how = how == SHUT_RD_ ? SD_RECEIVE : how == SHUT_WR_ ? SD_SEND : SD_BOTH; + if (::shutdown(s->socket(), host_how) != 0) return wsa_errno(WSAGetLastError()); + return 0; + } + std::shared_ptr target = file; + if (auto* u = dynamic_cast(file.get())) target = u->peer(); + if (auto* p = dynamic_cast(target.get())) { + p->shutdown(how); + return 0; + } + if (auto* service = dynamic_cast(target.get())) { + (void)service; + return 0; + } + return -ENOTCONN_; + } + default: + handled = false; + return 0; + } +} + +} // namespace axrb diff --git a/direct/src/net.h b/direct/src/net.h new file mode 100644 index 0000000..d543bcf --- /dev/null +++ b/direct/src/net.h @@ -0,0 +1,46 @@ +#pragma once + +#include "files.h" + +#include +#include +#include + +namespace axrb { + +// A connection to a service the host implements behind an AF_UNIX path +// (/dev/socket/logdw, property_service, dnsproxyd). Every write (or sendmsg) +// is handed to the handler as one message; replies queue up for read. +class ServiceSocket : public OpenFile { +public: + using Handler = std::function; + explicit ServiceSocket(Handler handler) : handler_(std::move(handler)) {} + int64_t read(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t write(GuestThread& t, uint64_t buf, uint64_t len) override; + int64_t writev(GuestThread& t, const std::vector& vecs) override; + int64_t stat(linux_abi::kstat& st) override; + uint32_t poll_mask() override; + WaitQueue* wait_queue() override { return &waiters_; } + // Message-level entry used by sendmsg after gathering. + int64_t send_message(std::string_view message); + // For handlers. + void reply(const void* data, size_t len); + void finish(); + +private: + Handler handler_; + std::mutex lock_; + std::string pending_; + bool finished_ = false; + WaitQueue waiters_; +}; + +// Registers a host service at an AF_UNIX path (abstract names start with '@'). +// `make` is called once per connection and returns that connection's handler. +void register_unix_service(const std::string& path, std::function make); + +// Guest -> host address rewriting: the emulator's host alias 10.0.2.2 is the +// local machine here. +void net_init(); + +} // namespace axrb diff --git a/direct/src/profiler.cpp b/direct/src/profiler.cpp new file mode 100644 index 0000000..f6992b1 --- /dev/null +++ b/direct/src/profiler.cpp @@ -0,0 +1,679 @@ +// AXRB_DIRECT_PROFILE=: a sampling profiler over every thread of the +// process. Each millisecond, threads that consumed CPU since the previous +// sample are suspended briefly and their instruction pointer attributed to a +// module and symbol (translated guest code shows as [jit]). A sample weighs +// the CPU time its thread used since that thread's previous sample, so the +// figures are milliseconds of CPU. A guest thread found blocked in a system +// call ran only briefly in between: its time goes to "[burst before ]". +// AXRB_DIRECT_PROFILE_JIT=1 (Digitalis) also writes .jit, every sample +// in translated code with its region, and .code, those regions' host +// code, for an instruction-level look with a disassembler. +// The report is +// written after AXRB_DIRECT_PROFILE_SECONDS (default 60) seconds of sampling, +// starting AXRB_DIRECT_PROFILE_START seconds (default 0) after launch. Host +// samples are also attributed to call stacks. AXRB_DIRECT_PROFILE_TIMELINE +// (thread name prefixes, comma separated) writes every sample of those threads +// in order, running or waiting, to .timeline for finding stalls. + +#include "profiler.h" + +#include "cpu.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { +namespace { + +struct ThreadState { + HANDLE handle = nullptr; + ULONG64 cycles = 0; + std::string label; +}; + +// Thread cycle counts are processor timestamp cycles. +double cycles_per_microsecond() { + LARGE_INTEGER frequency, a, b; + QueryPerformanceFrequency(&frequency); + QueryPerformanceCounter(&a); + const uint64_t first = __rdtsc(); + Sleep(50); + QueryPerformanceCounter(&b); + const uint64_t second = __rdtsc(); + return static_cast(second - first) * frequency.QuadPart / (static_cast(b.QuadPart - a.QuadPart) * 1e6); +} + +std::string module_of(uint64_t address, uint64_t* base) { + HMODULE module = nullptr; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(address), &module)) + return {}; + wchar_t path[MAX_PATH]{}; + GetModuleFileNameW(module, path, MAX_PATH); + std::wstring w = path; + w = w.substr(w.find_last_of(L'\\') + 1); + *base = reinterpret_cast(module); + return std::string(w.begin(), w.end()); +} + +std::string symbol_of(uint64_t address) { + alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + 512]{}; + auto* info = reinterpret_cast(buffer); + info->SizeOfStruct = sizeof(SYMBOL_INFO); + info->MaxNameLen = 511; + DWORD64 displacement = 0; + if (SymFromAddr(GetCurrentProcess(), address, &displacement, info)) return info->Name; + return {}; +} + +std::string label_for(DWORD tid) { + std::lock_guard held(process().threads_lock); + for (const auto& [guest_tid, thread] : process().threads) + if (thread->host_tid == tid) return thread->name + "#" + std::to_string(guest_tid); + return "host#" + std::to_string(tid); +} + +// The top of a suspended thread's stack. Unwinding runs on this copy after +// the thread resumes: the function table lookup may need a loader lock the +// suspended thread holds. Nothing here allocates, as the suspended thread may +// hold the heap lock too. +struct StackCopy { + static constexpr uint64_t kBytes = 32 << 10; + uint64_t at = 0; // the thread's stack pointer + uint64_t copied = 0; + std::vector bytes = std::vector(kBytes + 4096); // zero slack past the copied top +}; +bool copy_stack(const CONTEXT& context, StackCopy& copy) { + MEMORY_BASIC_INFORMATION info{}; + if (!VirtualQuery(reinterpret_cast(context.Rsp), &info, sizeof info) || info.State != MEM_COMMIT) return false; + const uint64_t end = std::min(reinterpret_cast(info.BaseAddress) + info.RegionSize, + context.Rsp + StackCopy::kBytes); + copy.at = context.Rsp; + copy.copied = end - context.Rsp; + std::memcpy(copy.bytes.data(), reinterpret_cast(context.Rsp), copy.copied); + std::memset(copy.bytes.data() + copy.copied, 0, copy.bytes.size() - copy.copied); + return true; +} + +// Host call stack through modules with unwind data. Stops at generated code, +// which has none, and at the end of the copied stack. +using HostStack = std::array; +HostStack unwind_host(CONTEXT context, const StackCopy& copy, const std::vector>& modules) { + auto in_module = [&](uint64_t ip) { + for (auto [lo, hi] : modules) if (ip >= lo && ip < hi) return true; + return false; + }; + const uint64_t copied = copy.copied; + const uint64_t base = reinterpret_cast(copy.bytes.data()); + // Registers that point into the stack (frame pointers) point into the copy. + auto rebase = [&] { + for (DWORD64* r = &context.Rax; r <= &context.R15; ++r) + if (*r >= copy.at && *r < copy.at + copied) *r = *r - copy.at + base; + }; + HostStack stack{}; + for (size_t depth = 0; depth < stack.size() && in_module(context.Rip); ++depth) { + stack[depth] = context.Rip; + rebase(); + if (context.Rsp < base || context.Rsp + 512 > base + copied) break; + DWORD64 image = 0; + if (auto* entry = RtlLookupFunctionEntry(context.Rip, &image, nullptr)) { + void* handler = nullptr; + DWORD64 frame = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, image, context.Rip, entry, &context, &handler, &frame, nullptr); + } else { + // A leaf function: the return address is on top of the stack. + context.Rip = *reinterpret_cast(context.Rsp); + context.Rsp += 8; + } + } + return stack; +} + +void run(std::string path, double start, double seconds) { + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); + std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(start * 1000))); + SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); + SymInitialize(GetCurrentProcess(), nullptr, TRUE); + const double per_microsecond = cycles_per_microsecond(); + timeBeginPeriod(1); + const DWORD self = GetCurrentThreadId(); + const DWORD pid = GetCurrentProcessId(); + std::unordered_map threads; + std::unordered_map addresses; // ip -> samples + std::map> thread_addresses; // label -> ip -> samples + std::map> by_thread; // label -> bucket -> samples + std::map thread_totals; + uint64_t total = 0; + std::map bursts; + uint64_t syscalls_before[512]; + for (int i = 0; i < 512; ++i) syscalls_before[i] = g_syscall_counts[i].load(); + g_syscall_stacks_enabled = true; + const auto begin = std::chrono::steady_clock::now(); + // AXRB_DIRECT_PROFILE_TIMELINE=RenderThread,GameThread: every sample of + // threads with these name prefixes, in order, for finding stalls. + std::vector timeline_names; + if (const char* names = std::getenv("AXRB_DIRECT_PROFILE_TIMELINE")) { + for (std::string rest = names; !rest.empty();) { + const size_t comma = rest.find(','); + if (comma) timeline_names.push_back(rest.substr(0, comma)); + rest = comma == std::string::npos ? "" : rest.substr(comma + 1); + } + } + struct Moment { + uint32_t ms; + std::string label; + HostStack stack; + uint64_t guest_pc; + const char* why; + }; + std::vector timeline; + StackCopy copy; + auto on_timeline = [&](const std::string& label) { + for (const auto& name : timeline_names) if (label.rfind(name, 0) == 0) return true; + return false; + }; + auto now_ms = [&] { + return static_cast(start * 1000 + + std::chrono::duration(std::chrono::steady_clock::now() - begin).count()); + }; + auto refreshed = begin - std::chrono::seconds(10); + std::unordered_map> module_cache; // page -> (module, base) + struct Sample { + std::string label; + uint64_t ip, weight; // microseconds of CPU + const char* burst; // the system call the thread was found blocked in + int tier; // translated code: 1 first tier, 2 optimized + }; + std::vector pending; + std::vector> modules; // host images, for unwinding + std::map host_stacks; + std::unordered_map> jit_cache; // host ip -> guest block + std::unordered_map guest_pcs; // guest block -> samples + std::unordered_map> tier_cache; // host ip -> tier + std::map> thread_guest_pcs; // label -> guest block -> samples + const bool record_jit = std::getenv("AXRB_DIRECT_PROFILE_JIT") != nullptr; + struct JitSample { std::string label; uint64_t ip, region, guest_pc, weight; int tier; }; + std::vector jit_samples; + std::map jit_regions; // host start -> bytes + // Wall time of the main guest thread: what it waits for when it does not run. + DWORD main_tid = 0; + uint64_t main_running = 0; + std::map>, uint64_t> main_blocked; // (why, guest stack) + std::unordered_map main_host_waits; // host ip -> samples + while (std::chrono::steady_clock::now() - begin < std::chrono::duration(seconds)) { + const auto now = std::chrono::steady_clock::now(); + if (now - refreshed > std::chrono::seconds(1)) { + refreshed = now; + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + THREADENTRY32 entry{sizeof entry}; + std::unordered_map alive; + for (BOOL ok = Thread32First(snap, &entry); ok; ok = Thread32Next(snap, &entry)) { + if (entry.th32OwnerProcessID != pid || entry.th32ThreadID == self) continue; + alive[entry.th32ThreadID] = true; + auto& state = threads[entry.th32ThreadID]; + if (!state.handle) { + state.handle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, + entry.th32ThreadID); + if (state.handle) QueryThreadCycleTime(state.handle, &state.cycles); + } + state.label = label_for(entry.th32ThreadID); + } + CloseHandle(snap); + HMODULE loaded[1024]; + DWORD needed = 0; + if (EnumProcessModules(GetCurrentProcess(), loaded, sizeof loaded, &needed)) { + modules.clear(); + for (DWORD i = 0; i < std::min(needed / sizeof(HMODULE), 1024); ++i) { + MODULEINFO info{}; + if (GetModuleInformation(GetCurrentProcess(), loaded[i], &info, sizeof info)) + modules.emplace_back(reinterpret_cast(info.lpBaseOfDll), + reinterpret_cast(info.lpBaseOfDll) + info.SizeOfImage); + } + } + { + std::lock_guard held(process().threads_lock); + // The busiest guest thread so far (the engine's main loop). + uint64_t most = 0; + for (const auto& [guest_tid, thread] : process().threads) { + auto n = thread_totals.find(thread->name + "#" + std::to_string(guest_tid)); + if (n != thread_totals.end() && n->second > most) { + most = n->second; + main_tid = thread->host_tid; + } + } + } + for (auto it = threads.begin(); it != threads.end();) { + if (!alive.count(it->first)) { + if (it->second.handle) CloseHandle(it->second.handle); + it = threads.erase(it); + } else { + ++it; + } + } + } + for (auto& [tid, state] : threads) { + if (!state.handle) continue; + ULONG64 cycles = 0; + QueryThreadCycleTime(state.handle, &cycles); + const bool ran = cycles - state.cycles > 200000; // ~0.05 ms of CPU + const uint64_t weight = static_cast((cycles - state.cycles) / per_microsecond); + if (ran) state.cycles = cycles; // shorter runs count toward the next sample + if (!ran && on_timeline(state.label)) { + const char* why = nullptr; + uint64_t lr = 0; + { + std::lock_guard held(process().threads_lock); + for (const auto& [guest_tid, thread] : process().threads) + if (thread->host_tid == tid && (why = thread->blocked_in.load())) lr = get_reg(*thread, 30); + } + HostStack stack{}; + if (!why && SuspendThread(state.handle) != static_cast(-1)) { + CONTEXT context{}; + context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; + const bool copied = GetThreadContext(state.handle, &context) && copy_stack(context, copy); + ResumeThread(state.handle); + if (copied) stack = unwind_host(context, copy, modules); + } + timeline.push_back({now_ms(), state.label, stack, lr, why ? why : "host wait"}); + } + if (!ran) { + if (tid != main_tid) continue; + std::lock_guard held(process().threads_lock); + GuestThread* main = nullptr; + for (const auto& [guest_tid, thread] : process().threads) + if (thread->host_tid == tid) main = thread; + if (!main) continue; + if (const char* why = main->blocked_in.load()) { + std::vector frames{get_reg(*main, 30)}; + uint64_t fp = get_reg(*main, 29); + for (int depth = 0; depth < 6 && fp && (fp & 7) == 0; ++depth) { + uint64_t record[2]; + if (!guest_read(fp, record, sizeof record)) break; + frames.push_back(record[1]); + if (record[0] <= fp) break; + fp = record[0]; + } + ++main_blocked[{why, frames}]; + } else if (SuspendThread(state.handle) != static_cast(-1)) { + CONTEXT context{}; + context.ContextFlags = CONTEXT_CONTROL; + const bool got = GetThreadContext(state.handle, &context); + ResumeThread(state.handle); + if (got) ++main_host_waits[context.Rip]; + } + continue; + } + if (tid == main_tid) ++main_running; + // The registry lock is held across the suspension so the guest + // thread (and its JIT) cannot go away; a thread waiting for the + // lock is not running and holds nothing the lookup needs. + std::lock_guard held(process().threads_lock); + GuestThread* guest = nullptr; + for (const auto& [guest_tid, thread] : process().threads) + if (thread->host_tid == tid) guest = thread; + const char* burst = guest ? guest->blocked_in.load() : nullptr; + if (SuspendThread(state.handle) == static_cast(-1)) continue; + CONTEXT context{}; + context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; + const bool got = GetThreadContext(state.handle, &context); + const bool copied = got && copy_stack(context, copy); + uint64_t guest_pc = ~uint64_t{0}; + bool looked_up = false; + if (got && guest) { + bool cached = false; + if (auto cache = jit_cache.find(guest); cache != jit_cache.end()) { + if (auto hit = cache->second.find(context.Rip); hit != cache->second.end()) { + guest_pc = hit->second; + cached = true; + } + } + if (!cached) { + guest_pc = guest_pc_for_host_code(*guest, context.Rip); + looked_up = true; + } + } + ResumeThread(state.handle); + if (!got) continue; + int tier = 0; + if (looked_up && guest_pc == ~uint64_t{0}) { + bool optimized = false; + guest_pc = guest_pc_for_host_code_resumed(*guest, context.Rip, &optimized); + if (guest_pc != ~uint64_t{0}) tier = optimized ? 2 : 1; + if (tier && tier_cache[guest].size() < 200000) tier_cache[guest].emplace(context.Rip, tier); + } else if (guest) { + if (auto cache = tier_cache.find(guest); cache != tier_cache.end()) + if (auto hit = cache->second.find(context.Rip); hit != cache->second.end()) tier = hit->second; + } + // Cached only now: inserting allocates, and the thread may have held the heap lock. + if (looked_up && jit_cache[guest].size() < 200000) jit_cache[guest].emplace(context.Rip, guest_pc); + HostStack stack{}; + if (copied) stack = unwind_host(context, copy, modules); + pending.push_back({state.label, context.Rip, weight, burst, tier}); + if (stack[0] && !burst) host_stacks[stack] += weight; + if (on_timeline(state.label)) timeline.push_back({now_ms(), state.label, stack, guest_pc, nullptr}); + if (record_jit && guest && guest_pc != ~uint64_t{0} && !burst) { + bool optimized = false; + uint64_t region = 0; + uint32_t bytes = 0; + if (guest_pc_for_host_code_resumed(*guest, context.Rip, &optimized, ®ion, &bytes) != ~uint64_t{0}) { + jit_samples.push_back({state.label, context.Rip, region, guest_pc, weight, optimized ? 2 : 1}); + jit_regions[region] = bytes; + } + } + if (guest_pc != ~uint64_t{0} && !burst) { + guest_pcs[guest_pc] += weight; + thread_guest_pcs[state.label][guest_pc] += weight; + } + } + // Attribution happens outside the suspension window. + for (const auto& [label, ip, weight, burst, tier] : pending) { + const uint64_t page = ip >> 12; + auto cached = module_cache.find(page); + if (cached == module_cache.end()) { + uint64_t base = 0; + std::string module = module_of(ip, &base); + cached = module_cache.emplace(page, std::make_pair(module, base)).first; + } + std::string bucket = !cached->second.first.empty() ? cached->second.first : + tier == 2 ? "[jit optimized]" : tier == 1 ? "[jit first tier]" : "[jit]"; + if (burst) { + bucket = std::string("[burst before ") + burst + "]"; + bursts[bucket] += weight; + } else if (!cached->second.first.empty()) { + addresses[ip] += weight; + thread_addresses[label][ip] += weight; + } + // Symbols are resolved once per address at report time; buckets here are modules. + by_thread[label][bucket] += weight; + thread_totals[label] += weight; + total += weight; + } + pending.clear(); + Sleep(1); + } + timeEndPeriod(1); + g_syscall_stacks_enabled = false; + std::vector> syscalls; + for (int i = 0; i < 512; ++i) { + const uint64_t n = g_syscall_counts[i].load() - syscalls_before[i]; + if (n) syscalls.emplace_back(n, i); + } + std::sort(syscalls.rbegin(), syscalls.rend()); + // Per-address symbols, folded into module!function. + std::map functions; + for (const auto& [ip, n] : addresses) { + uint64_t base = 0; + std::string module = module_of(ip, &base); + std::string symbol = symbol_of(ip); + functions[module + "!" + (symbol.empty() ? "?" : symbol)] += n; + } + // Guest blocks folded into library+offset (the library's file offset, as + // llvm-symbolizer takes it for a .so mapped from offset 0). + for (auto& [pc, n] : guest_pcs) n /= 1000; + std::vector code; + address_space().for_each([&](const Vma& vma) { + if ((vma.prot & 4) && !vma.name.empty()) code.push_back(vma); // PROT_EXEC + }); + std::map guest_libraries; + std::map guest_blocks; + uint64_t guest_total = 0; + for (const auto& [pc, n] : guest_pcs) { + std::string where = "?"; + for (const auto& vma : code) { + if (pc >= vma.start && pc < vma.end) { + char text[64]; + std::snprintf(text, sizeof text, "+0x%llx", static_cast(pc - vma.start + vma.offset)); + std::string name = vma.name.substr(vma.name.find_last_of('/') + 1); + guest_libraries[name] += n; + where = name + text; + break; + } + } + if (where == "?") guest_libraries["?"] += n; + guest_blocks[where] += n; + guest_total += n; + } + if (record_jit) { + // Regions stay in place until their thread's cache is reset; a failed read skips one. + std::map offsets; + if (FILE* code_file = std::fopen((path + ".code").c_str(), "wb")) { + std::vector bytes; + uint64_t offset = 0; + for (const auto& [start, size] : jit_regions) { + bytes.resize(size); + SIZE_T got = 0; + if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(start), bytes.data(), size, &got) || got != size) continue; + std::fwrite(bytes.data(), 1, size, code_file); + offsets[start] = offset; + offset += size; + } + std::fclose(code_file); + } + if (FILE* jit = std::fopen((path + ".jit").c_str(), "w")) { + for (const auto& [start, offset] : offsets) + std::fprintf(jit, "R %llx %u %llu\n", static_cast(start), jit_regions[start], + static_cast(offset)); + for (const auto& vma : code) + std::fprintf(jit, "M %llx %llx %llx %s\n", static_cast(vma.start), + static_cast(vma.end), static_cast(vma.offset), + vma.name.substr(vma.name.find_last_of('/') + 1).c_str()); + // The last field is the guest instruction before the region: a call there makes it a return's landing. + for (const auto& s : jit_samples) { + uint32_t before = 0; + SIZE_T got = 0; + ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(s.guest_pc - 4), &before, sizeof before, &got); + std::fprintf(jit, "S %s|%llx|%llx|%llx|%d|%llu|%x\n", s.label.c_str(), static_cast(s.ip), + static_cast(s.region), static_cast(s.guest_pc), s.tier, + static_cast(s.weight), before); + } + std::fclose(jit); + } + } + FILE* f = std::fopen(path.c_str(), "w"); + if (!f) return; + std::fprintf(f, "%.0f ms of CPU over %.0f s: %.2f cores (the figures below are ms of CPU)\n\n", + total / 1e3, seconds, total / 1e6 / seconds); + // Weights were collected in microseconds. + for (auto& [bucket, n] : bursts) + std::fprintf(f, "%9.0f %5.1f%% %s\n", n / 1e3, 100.0 * n / std::max(total, 1), bucket.c_str()); + total /= 1000; + for (auto& [ip, n] : addresses) n /= 1000; + for (auto& [label, ips] : thread_addresses) for (auto& [ip, n] : ips) n /= 1000; + for (auto& [label, buckets] : by_thread) for (auto& [bucket, n] : buckets) n /= 1000; + for (auto& [label, n] : thread_totals) n /= 1000; + for (auto& [stack, n] : host_stacks) n /= 1000; + for (auto& [label, pcs] : thread_guest_pcs) for (auto& [pc, n] : pcs) n /= 1000; + std::fprintf(f, "\n"); + std::vector> ordered(thread_totals.begin(), thread_totals.end()); + std::sort(ordered.begin(), ordered.end(), [](auto& a, auto& b) { return a.second > b.second; }); + std::fprintf(f, "== syscalls (per second)\n"); + for (size_t i = 0; i < syscalls.size() && i < 25; ++i) + std::fprintf(f, "%10.0f %s (%d)\n", syscalls[i].first / seconds, syscall_name(syscalls[i].second), syscalls[i].second); + auto where_is = [&](uint64_t pc) { + for (const auto& vma : code) { + if (pc >= vma.start && pc < vma.end) { + char text[64]; + std::snprintf(text, sizeof text, "+0x%llx", static_cast(pc - vma.start + vma.offset)); + return vma.name.substr(vma.name.find_last_of('/') + 1) + text; + } + } + char text[32]; + std::snprintf(text, sizeof text, "0x%llx", static_cast(pc)); + return std::string(text); + }; + { + std::vector>>> stacks; + { + std::lock_guard held(g_syscall_stacks_lock); + for (const auto& [key, n] : g_syscall_stacks) stacks.emplace_back(n, key); + } + std::sort(stacks.begin(), stacks.end(), [](auto& a, auto& b) { return a.first > b.first; }); + std::fprintf(f, "\n== syscall stacks (1 sample per 4096 calls)\n"); + for (size_t i = 0; i < stacks.size() && i < 15; ++i) { + std::fprintf(f, "%7llu %s:", static_cast(stacks[i].first), syscall_name(stacks[i].second.first)); + for (uint64_t pc : stacks[i].second.second) std::fprintf(f, " %s", where_is(pc).c_str()); + std::fprintf(f, "\n"); + } + } + std::fprintf(f, "\n== threads\n"); + for (const auto& [label, n] : ordered) { + std::fprintf(f, "%7llu %5.1f%% %s:", static_cast(n), 100.0 * n / std::max(total, 1), + label.c_str()); + std::vector> buckets(by_thread[label].begin(), by_thread[label].end()); + std::sort(buckets.begin(), buckets.end(), [](auto& a, auto& b) { return a.second > b.second; }); + for (size_t i = 0; i < buckets.size() && i < 5; ++i) + std::fprintf(f, " %s %.0f%%", buckets[i].first.c_str(), 100.0 * buckets[i].second / n); + std::fprintf(f, "\n"); + } + std::fprintf(f, "\n== host functions of the busiest threads\n"); + for (size_t t = 0; t < ordered.size() && t < 4; ++t) { + std::map mine; + for (const auto& [ip, n] : thread_addresses[ordered[t].first]) { + uint64_t base = 0; + const std::string symbol = symbol_of(ip); + mine[module_of(ip, &base) + "!" + (symbol.empty() ? "?" : symbol)] += n; + } + std::vector> v(mine.begin(), mine.end()); + std::sort(v.begin(), v.end(), [](auto& a, auto& b) { return a.second > b.second; }); + std::fprintf(f, " %s\n", ordered[t].first.c_str()); + for (size_t i = 0; i < v.size() && i < 15; ++i) + std::fprintf(f, "%9llu %5.1f%% %s\n", static_cast(v[i].second), + 100.0 * v[i].second / std::max(ordered[t].second, 1), v[i].first.c_str()); + } + std::fprintf(f, "\n== guest blocks of the busiest threads\n"); + for (size_t t = 0; t < ordered.size() && t < 8; ++t) { + std::vector> v; + for (const auto& [pc, n] : thread_guest_pcs[ordered[t].first]) v.emplace_back(n, pc); + std::sort(v.rbegin(), v.rend()); + std::fprintf(f, " %s\n", ordered[t].first.c_str()); + for (size_t i = 0; i < v.size() && i < 12; ++i) + std::fprintf(f, "%9llu %5.1f%% %s\n", static_cast(v[i].first), + 100.0 * v[i].first / std::max(ordered[t].second, 1), where_is(v[i].second).c_str()); + } + { + uint64_t blocked = 0, waiting = 0; + for (const auto& [key, n] : main_blocked) blocked += n; + for (const auto& [ip, n] : main_host_waits) waiting += n; + std::fprintf(f, "\n== busiest thread wall time: running %llu, blocked in syscalls %llu, waiting in host code %llu\n", + static_cast(main_running), static_cast(blocked), + static_cast(waiting)); + std::vector>>> v; + for (const auto& [key, n] : main_blocked) v.emplace_back(n, key); + std::sort(v.begin(), v.end(), [](auto& a, auto& b) { return a.first > b.first; }); + for (size_t i = 0; i < v.size() && i < 25; ++i) { + std::fprintf(f, "%7llu %s:", static_cast(v[i].first), v[i].second.first.c_str()); + for (uint64_t pc : v[i].second.second) std::fprintf(f, " %s", where_is(pc).c_str()); + std::fprintf(f, "\n"); + } + std::map waits; + for (const auto& [ip, n] : main_host_waits) { + uint64_t base = 0; + const std::string symbol = symbol_of(ip); + waits[module_of(ip, &base) + "!" + (symbol.empty() ? "?" : symbol)] += n; + } + std::vector> w(waits.begin(), waits.end()); + std::sort(w.begin(), w.end(), [](auto& a, auto& b) { return a.second > b.second; }); + for (size_t i = 0; i < w.size() && i < 15; ++i) + std::fprintf(f, "%7llu host wait %s\n", static_cast(w[i].second), w[i].first.c_str()); + } + { + // Folded by symbol, so different offsets within a function count together. + std::map folded; + std::unordered_map names; + for (const auto& [stack, n] : host_stacks) { + std::string text; + for (uint64_t ip : stack) { + if (!ip) break; + auto it = names.find(ip); + if (it == names.end()) { + uint64_t base = 0; + const std::string symbol = symbol_of(ip); + it = names.emplace(ip, module_of(ip, &base) + "!" + (symbol.empty() ? "?" : symbol)).first; + } + text += (text.empty() ? "" : " < ") + it->second; + } + folded[text] += n; + } + std::vector> v(folded.begin(), folded.end()); + std::sort(v.begin(), v.end(), [](auto& a, auto& b) { return a.second > b.second; }); + std::fprintf(f, "\n== host stacks (non-JIT samples, callers to the right)\n"); + for (size_t i = 0; i < v.size() && i < 60; ++i) + std::fprintf(f, "%7llu %s\n", static_cast(v[i].second), v[i].first.c_str()); + } + std::vector> top(functions.begin(), functions.end()); + std::sort(top.begin(), top.end(), [](auto& a, auto& b) { return a.second > b.second; }); + std::fprintf(f, "\n== host functions (non-JIT samples)\n"); + for (size_t i = 0; i < top.size() && i < 80; ++i) + std::fprintf(f, "%7llu %5.1f%% %s\n", static_cast(top[i].second), + 100.0 * top[i].second / std::max(total, 1), top[i].first.c_str()); + auto sorted = [](const std::map& m) { + std::vector> v(m.begin(), m.end()); + std::sort(v.begin(), v.end(), [](auto& a, auto& b) { return a.second > b.second; }); + return v; + }; + std::fprintf(f, "\n== guest libraries (JIT samples: %llu)\n", static_cast(guest_total)); + for (const auto& [name, n] : sorted(guest_libraries)) + std::fprintf(f, "%7llu %5.1f%% %s\n", static_cast(n), 100.0 * n / std::max(guest_total, 1), + name.c_str()); + std::fprintf(f, "\n== guest blocks\n"); + auto blocks = sorted(guest_blocks); + for (size_t i = 0; i < blocks.size() && i < 400; ++i) + std::fprintf(f, "%7llu %5.1f%% %s\n", static_cast(blocks[i].second), + 100.0 * blocks[i].second / std::max(guest_total, 1), blocks[i].first.c_str()); + std::fclose(f); + if (!timeline.empty()) { + std::unordered_map names; + auto host_name = [&](uint64_t ip) -> const std::string& { + auto it = names.find(ip); + if (it == names.end()) { + uint64_t base = 0; + const std::string symbol = symbol_of(ip); + it = names.emplace(ip, module_of(ip, &base) + "!" + (symbol.empty() ? "?" : symbol)).first; + } + return it->second; + }; + if (FILE* t = std::fopen((path + ".timeline").c_str(), "w")) { + for (const auto& m : timeline) { + std::fprintf(t, "%7u %-22s %s", m.ms, m.label.c_str(), m.why ? m.why : "run"); + if (m.guest_pc != ~uint64_t{0} && m.guest_pc) std::fprintf(t, " guest=%s", where_is(m.guest_pc).c_str()); + for (uint64_t ip : m.stack) { + if (!ip) break; + std::fprintf(t, " < %s", host_name(ip).c_str()); + } + std::fprintf(t, "\n"); + } + std::fclose(t); + } + } + LOGI("profile", "report written to %s", path.c_str()); +} + +} // namespace + +void profiler_start() { + const char* path = std::getenv("AXRB_DIRECT_PROFILE"); + if (!path || !*path) return; + const char* start = std::getenv("AXRB_DIRECT_PROFILE_START"); + const char* seconds = std::getenv("AXRB_DIRECT_PROFILE_SECONDS"); + std::thread(run, std::string(path), start ? std::atof(start) : 0.0, seconds ? std::atof(seconds) : 60.0).detach(); +} + +} // namespace axrb diff --git a/direct/src/profiler.h b/direct/src/profiler.h new file mode 100644 index 0000000..10c3219 --- /dev/null +++ b/direct/src/profiler.h @@ -0,0 +1,8 @@ +#pragma once + +namespace axrb { + +// Starts the sampling profiler when AXRB_DIRECT_PROFILE names a report file. +void profiler_start(); + +} // namespace axrb diff --git a/direct/src/props.cpp b/direct/src/props.cpp new file mode 100644 index 0000000..d5da6c0 --- /dev/null +++ b/direct/src/props.cpp @@ -0,0 +1,334 @@ +#include "props.h" + +#include "files.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" +#include "net.h" +#include "vfs.h" + +#include + +#include +#include +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +namespace { + +// bionic/libc/system_properties: prop_area, prop_bt, prop_info. +constexpr uint32_t kAreaMagic = 0x504f5250; +constexpr uint32_t kAreaVersion = 0xfc6ed0ab; +constexpr uint32_t kHeaderSize = 128; // bytes_used, serial, magic, version, reserved[28] +constexpr uint32_t kBtSize = 20; // namelen, prop, left, right, children +constexpr uint32_t kValueMax = 92; // PROP_VALUE_MAX +constexpr uint32_t kInfoSize = 4 + kValueMax; +constexpr uint32_t kLongFlag = 1u << 16; +constexpr char kLongLegacyError[] = "Must use __system_property_read_callback() to read"; +constexpr uint64_t kAreaSize = 1ull << 20; + +std::mutex g_lock; +uint8_t* g_area = nullptr; + +uint8_t* data() { return g_area + kHeaderSize; } +uint32_t& bytes_used() { return *reinterpret_cast(g_area); } +std::atomic& area_serial() { return *reinterpret_cast*>(g_area + 4); } +std::atomic& field(uint32_t offset) { return *reinterpret_cast*>(data() + offset); } + +uint32_t allocate(uint32_t size) { + uint32_t aligned = (size + 3) & ~3u; + if (bytes_used() + aligned > kAreaSize - kHeaderSize) return 0; + uint32_t offset = bytes_used(); + bytes_used() += aligned; + return offset; +} + +uint32_t new_bt(const char* name, uint32_t len) { + uint32_t offset = allocate(kBtSize + len + 1); + if (!offset) return 0; + uint8_t* bt = data() + offset; + std::memcpy(bt, &len, 4); + std::memcpy(bt + kBtSize, name, len); + bt[kBtSize + len] = 0; + return offset; +} + +uint32_t new_info(const std::string& name, const std::string& value) { + uint32_t offset = allocate(kInfoSize + static_cast(name.size()) + 1); + if (!offset) return 0; + uint8_t* info = data() + offset; + std::memcpy(info + kInfoSize, name.c_str(), name.size() + 1); + if (value.size() >= kValueMax) { + uint32_t long_offset = allocate(static_cast(value.size()) + 1); + if (!long_offset) return 0; + std::memcpy(data() + long_offset, value.c_str(), value.size() + 1); + std::memcpy(info + 4, kLongLegacyError, sizeof kLongLegacyError); + uint32_t relative = long_offset - offset; + std::memcpy(info + 4 + 56, &relative, 4); + field(offset).store(kLongFlag | static_cast(sizeof kLongLegacyError - 1) << 24, std::memory_order_release); + } else { + std::memcpy(info + 4, value.c_str(), value.size() + 1); + field(offset).store(static_cast(value.size()) << 24, std::memory_order_release); + } + return offset; +} + +int compare(const char* one, uint32_t one_len, const uint8_t* bt) { + uint32_t two_len; + std::memcpy(&two_len, bt, 4); + if (one_len < two_len) return -1; + if (one_len > two_len) return 1; + return std::strncmp(one, reinterpret_cast(bt + kBtSize), one_len); +} + +// One level of the trie: a binary tree ordered by (length, bytes). +uint32_t find_bt(uint32_t root, const char* name, uint32_t len, bool create) { + uint32_t current = root; + for (;;) { + int r = compare(name, len, data() + current); + if (r == 0) return current; + uint32_t link = current + (r < 0 ? 8 : 12); + uint32_t next = field(link).load(std::memory_order_acquire); + if (!next) { + if (!create) return 0; + next = new_bt(name, len); + if (!next) return 0; + field(link).store(next, std::memory_order_release); + return next; + } + current = next; + } +} + +// Offset of the prop_info for `name` (creating the path and the info). +uint32_t find_info(const std::string& name, const std::string* create_value) { + uint32_t current = 0; // the root node + const char* remaining = name.c_str(); + for (;;) { + const char* dot = std::strchr(remaining, '.'); + uint32_t len = dot ? static_cast(dot - remaining) : static_cast(std::strlen(remaining)); + uint32_t children = field(current + 16).load(std::memory_order_acquire); + if (!children) { + if (!create_value) return 0; + children = new_bt(remaining, len); + if (!children) return 0; + field(current + 16).store(children, std::memory_order_release); + current = children; + } else { + current = find_bt(children, remaining, len, create_value != nullptr); + if (!current) return 0; + } + if (!dot) break; + remaining = dot + 1; + } + uint32_t info = field(current + 4).load(std::memory_order_acquire); + if (info || !create_value) return info; + info = new_info(name, *create_value); + if (info) field(current + 4).store(info, std::memory_order_release); + return info; +} + +std::string read_value(uint32_t info) { + uint32_t serial = field(info).load(std::memory_order_acquire); + const char* base = reinterpret_cast(data() + info); + if (serial & kLongFlag) { + uint32_t relative; + std::memcpy(&relative, base + 4 + 56, 4); + return std::string(base + relative); + } + return std::string(base + 4, serial >> 24); +} + +bool set_locked(const std::string& name, const std::string& value) { + if (name.empty() || name.size() > 256) return false; + uint32_t info = find_info(name, nullptr); + if (!info) { + if (!find_info(name, &value)) { + LOGE("props", "property area full adding %s", name.c_str()); + return false; + } + } else { + uint32_t serial = field(info).load(std::memory_order_relaxed); + if (serial & kLongFlag) return false; // read-only long values + if (name.rfind("ro.", 0) == 0) return false; + std::string stored = value.substr(0, kValueMax - 1); + // Readers that see the dirty bit read the old value from the backup + // area right after the root node. + char* backup = reinterpret_cast(data() + kBtSize); + std::memcpy(backup, data() + info + 4, kValueMax); + field(info).store(serial | 1, std::memory_order_release); + std::memcpy(data() + info + 4, stored.c_str(), stored.size() + 1); + field(info).store((static_cast(stored.size()) << 24) | ((serial + 1) & 0xffffff), std::memory_order_release); + wake_futex(reinterpret_cast(data() + info), 1 << 30); + } + area_serial().fetch_add(1, std::memory_order_release); + wake_futex(reinterpret_cast(g_area + 4), 1 << 30); + return true; +} + +// /dev/__properties__ as bionic opens it (single-file "pre-split" layout). +class PropertyAreaFile : public OpenFile { +public: + int64_t read(GuestThread&, uint64_t, uint64_t) override { return 0; } + int64_t stat(kstat& st) override { + std::memset(&st, 0, sizeof st); + st.st_mode = S_IFREG_ | 0444; // root-owned, not group/other writable: bionic checks + st.st_nlink = 1; + st.st_size = static_cast(kAreaSize); + st.st_blksize = 4096; + st.st_ino = 0x50524f50; + return 0; + } + uint64_t fixed_mapping() const override { return reinterpret_cast(g_area); } +}; + +void load_prop_file(const std::wstring& path, std::map& out) { + std::ifstream in(path); + std::string line; + while (std::getline(in, line)) { + while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back(); + if (line.empty() || line[0] == '#' || line.rfind("import ", 0) == 0) continue; + size_t eq = line.find('='); + if (eq == std::string::npos) continue; + out[line.substr(0, eq)] = line.substr(eq + 1); + } +} + +std::string host_timezone() { + DYNAMIC_TIME_ZONE_INFORMATION info{}; + if (GetDynamicTimeZoneInformation(&info) == TIME_ZONE_ID_INVALID) return "UTC"; + // Windows 10 ships ICU; it maps Windows zone names to IANA ones. + using Fn = int32_t(__cdecl*)(const wchar_t*, int32_t, const char*, wchar_t*, int32_t, int*); + static HMODULE icu = LoadLibraryW(L"icu.dll"); + if (!icu) return "UTC"; + auto fn = reinterpret_cast(GetProcAddress(icu, "ucal_getTimeZoneIDForWindowsID")); + if (!fn) return "UTC"; + wchar_t id[128] = {}; + int status = 0; + int32_t n = fn(info.TimeZoneKeyName, -1, nullptr, id, 127, &status); + if (status > 0 || n <= 0) return "UTC"; + return narrow(std::wstring(id, static_cast(n))); +} + +ServiceSocket::Handler property_service() { + auto buffer = std::make_shared(); + return [buffer](ServiceSocket& socket, std::string_view message) { + buffer->append(message); + if (buffer->size() < 4) return; + uint32_t command; + std::memcpy(&command, buffer->data(), 4); + if (command == 0x00020001) { // PROP_MSG_SETPROP2: len-prefixed name and value + auto take = [&](size_t& at, std::string& out) { + if (buffer->size() < at + 4) return false; + uint32_t len; + std::memcpy(&len, buffer->data() + at, 4); + if (buffer->size() < at + 4 + len) return false; + out.assign(buffer->data() + at + 4, len); + at += 4 + len; + return true; + }; + size_t at = 4; + std::string name, value; + if (!take(at, name) || !take(at, value)) return; + buffer->erase(0, at); + bool ok = property_set(name, value); + LOGD("props", "setprop %s=%s%s", name.c_str(), value.c_str(), ok ? "" : " (refused)"); + uint32_t result = ok ? 0 : 0x18; // PROP_ERROR_SET_FAILED + socket.reply(&result, 4); + } else if (command == 1) { // PROP_MSG_SETPROP: fixed 128-byte record + if (buffer->size() < 128) return; + std::string name(buffer->data() + 4, strnlen(buffer->data() + 4, 32)); + std::string value(buffer->data() + 36, strnlen(buffer->data() + 36, 92)); + buffer->erase(0, 128); + property_set(name, value); + socket.finish(); + } else { + buffer->clear(); + socket.finish(); + } + }; +} + +} // namespace + +std::string property_get(const std::string& name, const std::string& fallback) { + std::lock_guard held(g_lock); + if (!g_area) return fallback; + uint32_t info = find_info(name, nullptr); + return info ? read_value(info) : fallback; +} + +bool property_set(const std::string& name, const std::string& value) { + std::lock_guard held(g_lock); + if (!g_area) return false; + if (name.rfind("ctl.", 0) == 0) return true; // service control: nothing to start or stop + return set_locked(name, value); +} + +void props_init(const std::wstring& sysroot_host, const std::map& overrides) { + int64_t at = address_space().mmap(0, kAreaSize, PROT_READ_ | PROT_WRITE_, MAP_SHARED_ | MAP_ANONYMOUS_, nullptr, 0, + "/dev/__properties__"); + if (at < 0) fatal("cannot map the property area"); + address_space().populate(static_cast(at), kAreaSize); + g_area = reinterpret_cast(at); + std::memset(g_area, 0, kAreaSize); + std::memcpy(g_area + 8, &kAreaMagic, 4); + std::memcpy(g_area + 12, &kAreaVersion, 4); + bytes_used() = kBtSize + kValueMax; // the root node and the dirty backup area + + std::map props; + load_prop_file(sysroot_host + L"\\system\\build.prop", props); + load_prop_file(sysroot_host + L"\\vendor-build.prop", props); + + // What init derives at boot on the emulator image the games are known to + // run on (ro.hardware=ranchu also selects AXRB's loopback transport). + for (const char* key : {"brand", "device", "manufacturer", "model", "name"}) { + std::string vendor = props["ro.product.vendor." + std::string(key)]; + if (!vendor.empty()) props["ro.product." + std::string(key)] = vendor; + } + props["ro.product.model"] = "sdk_gphone64_arm64"; + props["ro.product.manufacturer"] = "Google"; + props["ro.product.brand"] = "google"; + props["ro.product.name"] = "sdk_gphone64_arm64"; + props["ro.product.cpu.abi"] = "arm64-v8a"; + props["ro.product.cpu.abilist"] = "arm64-v8a"; + props["ro.product.cpu.abilist64"] = "arm64-v8a"; + props["ro.product.cpu.abilist32"] = ""; + props["ro.hardware"] = "ranchu"; + props["ro.boot.hardware"] = "ranchu"; + props["ro.boot.qemu"] = "1"; + props["ro.arch"] = "arm64"; + props["ro.build.fingerprint"] = props.count("ro.system.build.fingerprint") ? props["ro.system.build.fingerprint"] : ""; + props["ro.serialno"] = "AXRBDIRECT0001"; + props["ro.boot.serialno"] = "AXRBDIRECT0001"; + props["ro.debuggable"] = "0"; + props["ro.secure"] = "1"; + props["ro.property_service.version"] = "2"; + props["ro.opengles.version"] = "196610"; + props["ro.sf.lcd_density"] = "440"; + props["sys.boot_completed"] = "1"; + props["dev.bootcomplete"] = "1"; + props["persist.sys.locale"] = "en-US"; + props["persist.sys.timezone"] = host_timezone(); + props["persist.sys.dalvik.vm.lib.2"] = "libart.so"; + props["ro.dalvik.vm.native.bridge"] = "0"; + for (auto& [name, value] : overrides) props[name] = value; + + { + std::lock_guard held(g_lock); + for (auto& [name, value] : props) { + if (!find_info(name, &value)) LOGW("props", "could not add %s", name.c_str()); + } + } + LOGI("props", "%zu properties, %u bytes of %llu", props.size(), bytes_used(), (unsigned long long)kAreaSize); + + register_device("properties", [] { return std::make_shared(); }); + vfs().add_device("/dev/__properties__", "properties"); + register_unix_service("/dev/socket/property_service", property_service); +} + +} // namespace axrb diff --git a/direct/src/props.h b/direct/src/props.h new file mode 100644 index 0000000..a415efe --- /dev/null +++ b/direct/src/props.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace axrb { + +// System properties. The property area bionic maps from /dev/__properties__ +// lives in guest memory and is maintained by the host exactly as init's +// property service would (the trie, serials and futex wakes), so +// __system_property_get/_wait and setprop through /dev/socket/property_service +// all work unmodified. +void props_init(const std::wstring& sysroot_host, const std::map& overrides); +std::string property_get(const std::string& name, const std::string& fallback = ""); +bool property_set(const std::string& name, const std::string& value); + +} // namespace axrb diff --git a/direct/src/signals.cpp b/direct/src/signals.cpp new file mode 100644 index 0000000..f7e3a2c --- /dev/null +++ b/direct/src/signals.cpp @@ -0,0 +1,434 @@ +#include "cpu.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" + +#include + +#include + +namespace axrb { +using namespace linux_abi; + +GuestThread* find_thread(int tid); + +namespace { + +constexpr uint64_t bit(int signal) { return 1ull << (signal - 1); } +constexpr uint64_t kUnblockable = bit(SIGKILL_) | bit(SIGSTOP_); + +// arm64 signal frame (arch/arm64/kernel/signal.c, uapi/asm/sigcontext.h). +constexpr uint64_t kUcontextOffset = 128; // after siginfo +constexpr uint64_t kUcStack = 16; // within ucontext +constexpr uint64_t kUcSigmask = 40; +constexpr uint64_t kUcMcontext = 176; // sigcontext, 16-aligned +constexpr uint64_t kScFaultAddress = 0; +constexpr uint64_t kScRegs = 8; +constexpr uint64_t kScSp = 256; +constexpr uint64_t kScPc = 264; +constexpr uint64_t kScPstate = 272; +constexpr uint64_t kScReserved = 288; +constexpr uint32_t kFpsimdMagic = 0x46508001; +constexpr uint32_t kFpsimdSize = 528; +constexpr uint64_t kFrameSize = kUcontextOffset + kUcMcontext + kScReserved + 4096; // 4688 +constexpr uint64_t kFrameRecord = (kFrameSize + 15) & ~15ull; +constexpr uint64_t kFrameTotal = kFrameRecord + 16; + +bool default_ignored(int signal) { + return signal == SIGCHLD_ || signal == SIGURG_ || signal == SIGWINCH_ || signal == SIGCONT_; +} + +uint64_t sigreturn_trampoline() { + static uint64_t address = [] { + // mov x8, #139 ; svc #0 -- for handlers installed without SA_RESTORER + int64_t at = address_space().mmap(0, kPageSize, PROT_READ_ | PROT_WRITE_, MAP_PRIVATE_ | MAP_ANONYMOUS_, nullptr, 0, + "[sigreturn]"); + address_space().populate(static_cast(at), kPageSize); + uint32_t code[2] = {0xd2801168u, 0xd4000001u}; + std::memcpy(reinterpret_cast(at), code, sizeof code); + address_space().mprotect(static_cast(at), kPageSize, PROT_READ_ | PROT_EXEC_); + return static_cast(at); + }(); + return address; +} + +[[noreturn]] void die_of(GuestThread& thread, int signal, const ksiginfo& info) { + static const char* names[] = {"?", "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", "SIGABRT", "SIGBUS", + "SIGFPE", "SIGKILL", "SIGUSR1", "SIGSEGV", "SIGUSR2", "SIGPIPE", "SIGALRM", "SIGTERM"}; + char why[160]; + std::snprintf(why, sizeof why, "fatal signal %d (%s), code %d, fault addr %llx", signal, + signal < 16 ? names[signal] : "?", info.si_code, (unsigned long long)info.fields.fault.addr); + dump_thread_state(thread, why); + log_flush(); + process().exiting = true; + TerminateProcess(GetCurrentProcess(), 128 + signal); + for (;;) { + } +} + +// Pushes a frame for `signal` onto the thread's stack and points it at the +// handler. The cpu context is the state to resume when the handler returns. +void push_frame(GuestThread& thread, int signal, const ksiginfo& info, const ksigaction& action) { + CpuContext c; + save_context(thread, c); + if (thread.restart_pc) { + if (action.flags & SA_RESTART_) { + c.pc = thread.restart_pc; + c.x[0] = thread.restart_x0; + } + thread.restart_pc = 0; + } + uint64_t sp = c.sp; + bool on_altstack = thread.altstack.ss_flags != SS_DISABLE_ && sp > thread.altstack.ss_sp && + sp <= thread.altstack.ss_sp + thread.altstack.ss_size; + if ((action.flags & SA_ONSTACK_) && thread.altstack.ss_flags != SS_DISABLE_ && !on_altstack) + sp = thread.altstack.ss_sp + thread.altstack.ss_size; + uint64_t frame = (sp - kFrameTotal) & ~15ull; + address_space().populate(frame, kFrameTotal); + std::vector bytes(kFrameTotal, 0); + std::memcpy(bytes.data(), &info, sizeof info); + uint8_t* uc = bytes.data() + kUcontextOffset; + kstack_t stack = thread.altstack; + if (on_altstack) stack.ss_flags |= SS_ONSTACK_; + std::memcpy(uc + kUcStack, &stack, sizeof stack); + uint64_t saved_mask = thread.restore_sigmask ? thread.saved_sigmask : thread.sigmask; + thread.restore_sigmask = false; + std::memcpy(uc + kUcSigmask, &saved_mask, 8); + uint8_t* mc = uc + kUcMcontext; + uint64_t fault = (signal == SIGSEGV_ || signal == SIGBUS_ || signal == SIGILL_ || signal == SIGTRAP_) ? info.fields.fault.addr : 0; + std::memcpy(mc + kScFaultAddress, &fault, 8); + std::memcpy(mc + kScRegs, c.x, sizeof c.x); + std::memcpy(mc + kScSp, &c.sp, 8); + std::memcpy(mc + kScPc, &c.pc, 8); + uint64_t pstate = c.pstate; + std::memcpy(mc + kScPstate, &pstate, 8); + uint8_t* fp = mc + kScReserved; + std::memcpy(fp, &kFpsimdMagic, 4); + std::memcpy(fp + 4, &kFpsimdSize, 4); + std::memcpy(fp + 8, &c.fpsr, 4); + std::memcpy(fp + 12, &c.fpcr, 4); + std::memcpy(fp + 16, c.v, 512); + // A terminating null record follows (already zero). + uint64_t record[2] = {c.x[29], c.pc}; + std::memcpy(bytes.data() + kFrameRecord, record, 16); + if (!guest_write(frame, bytes.data(), bytes.size())) { + LOGE("signal", "cannot write a signal frame at %llx (sp %llx)", (unsigned long long)frame, (unsigned long long)sp); + die_of(thread, SIGSEGV_, info); + } + set_reg(thread, 0, static_cast(signal)); + set_reg(thread, 1, frame); + set_reg(thread, 2, frame + kUcontextOffset); + set_reg(thread, 29, frame + kFrameRecord); + set_reg(thread, 30, (action.flags & SA_RESTORER_) && action.restorer ? action.restorer : sigreturn_trampoline()); + set_sp(thread, frame); + set_pc(thread, action.handler); + uint64_t mask = thread.sigmask | action.mask; + if (!(action.flags & SA_NODEFER_)) mask |= bit(signal); + thread.sigmask = mask & ~kUnblockable; + TRACE("signal", "thread %d: signal %d -> handler %s", thread.tid, signal, describe_address(action.handler).c_str()); +} + +ksigaction action_for(int signal) { + std::lock_guard held(process().sigaction_lock); + ksigaction action = process().actions[signal]; + if ((action.flags & SA_RESETHAND_) && action.handler > SIG_IGN_) { + process().actions[signal].handler = SIG_DFL_; + process().actions[signal].flags &= ~SA_SIGINFO_; + } + return action; +} + +} // namespace + +bool deliver_signals(GuestThread& thread) { + for (;;) { + int signal = 0; + ksiginfo info{}; + { + std::lock_guard held(thread.signal_lock); + uint64_t ready = thread.pending.load() & ~thread.sigmask; + if (!ready) return false; + unsigned long index; + _BitScanForward64(&index, ready); + signal = static_cast(index) + 1; + bool more = false; + bool found = false; + for (auto it = thread.pending_info.begin(); it != thread.pending_info.end();) { + if (it->si_signo == signal) { + if (!found) { + info = *it; + it = thread.pending_info.erase(it); + found = true; + continue; + } + more = true; + } + ++it; + } + if (!found) { + info.si_signo = signal; + info.si_code = SI_KERNEL_; + } + if (!more) thread.pending.fetch_and(~bit(signal)); + } + ksigaction action = action_for(signal); + if (action.handler == SIG_IGN_) continue; + if (action.handler == SIG_DFL_) { + if (default_ignored(signal)) continue; + die_of(thread, signal, info); + } + push_frame(thread, signal, info, action); + return true; + } +} + +int64_t send_signal(GuestThread& target, int signal, const ksiginfo& info) { + if (signal <= 0 || signal > NSIG_) return -EINVAL_; + if (signal == SIGKILL_) { + LOGI("signal", "SIGKILL: terminating"); + log_flush(); + TerminateProcess(GetCurrentProcess(), 128 + SIGKILL_); + } + { + std::lock_guard held(target.signal_lock); + bool realtime = signal >= SIGRTMIN_; + if (realtime || !(target.pending.load() & bit(signal))) target.pending_info.push_back(info); + target.pending.fetch_or(bit(signal)); + } + kick_thread(target); + return 0; +} + +void raise_fault(GuestThread& thread, int signal, int code, uint64_t address) { + ksiginfo info{}; + info.si_signo = signal; + info.si_code = code; + info.fields.fault.addr = address; + ksigaction action = action_for(signal); + bool blocked = (thread.sigmask & bit(signal)) != 0; + if (action.handler <= SIG_IGN_ || blocked) { + LOGE("signal", "unhandled synchronous signal %d at %s", signal, describe_address(get_pc(thread)).c_str()); + die_of(thread, signal, info); + } + LOGW("signal", "thread %d: signal %d (code %d, addr %llx) at %s -> guest handler", thread.tid, signal, code, + (unsigned long long)address, describe_address(get_pc(thread)).c_str()); +#ifdef AXRB_ARM_SERVICE_EMBEDDED + LOGW("signal", "ARM bridge fault caller %s",describe_address(get_reg(thread,30)).c_str()); + MEMORY_BASIC_INFORMATION mbi{};VirtualQuery(reinterpret_cast(address),&mbi,sizeof mbi); + LOGW("signal", "ARM bridge fault memory base=%p size=%llx state=%lx protection=%lx sp=%llx",mbi.BaseAddress,(unsigned long long)mbi.RegionSize,mbi.State,mbi.Protect,(unsigned long long)get_sp(thread)); +#endif + push_frame(thread, signal, info, action); +} + +static int64_t sigreturn(GuestThread& thread) { + uint64_t frame = get_sp(thread); + std::vector bytes(kFrameSize); + if (!guest_read(frame, bytes.data(), bytes.size())) { + ksiginfo info{}; + info.si_signo = SIGSEGV_; + die_of(thread, SIGSEGV_, info); + } + const uint8_t* uc = bytes.data() + kUcontextOffset; + const uint8_t* mc = uc + kUcMcontext; + CpuContext c; + std::memcpy(c.x, mc + kScRegs, sizeof c.x); + std::memcpy(&c.sp, mc + kScSp, 8); + std::memcpy(&c.pc, mc + kScPc, 8); + uint64_t pstate; + std::memcpy(&pstate, mc + kScPstate, 8); + c.pstate = static_cast(pstate) & 0xf0000000u; + const uint8_t* fp = mc + kScReserved; + uint32_t magic; + std::memcpy(&magic, fp, 4); + if (magic == kFpsimdMagic) { + std::memcpy(&c.fpsr, fp + 8, 4); + std::memcpy(&c.fpcr, fp + 12, 4); + std::memcpy(c.v, fp + 16, 512); + } else { + CpuContext now; + save_context(thread, now); + c.fpsr = now.fpsr; + c.fpcr = now.fpcr; + std::memcpy(c.v, now.v, sizeof c.v); + } + uint64_t mask; + std::memcpy(&mask, uc + kUcSigmask, 8); + thread.sigmask = mask & ~kUnblockable; + kstack_t stack; + std::memcpy(&stack, uc + kUcStack, sizeof stack); + load_context(thread, c); + return 0; +} + +int64_t sys_signals(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled) { + handled = true; + switch (nr) { + case 134: { // rt_sigaction(sig, act, oact, size) + int signal = static_cast(a[0]); + if (signal <= 0 || signal > NSIG_) return -EINVAL_; + ksigaction old{}, next{}; + if (a[1] && !guest_read(a[1], &next, sizeof next)) return -EFAULT_; + { + std::lock_guard held(process().sigaction_lock); + old = process().actions[signal]; + if (a[1]) { + if (signal == SIGKILL_ || signal == SIGSTOP_) return -EINVAL_; + next.mask &= ~kUnblockable; + process().actions[signal] = next; + } + } + if (a[1]) TRACE("signal", "sigaction(%d) handler %s flags %llx", signal, describe_address(next.handler).c_str(), + (unsigned long long)next.flags); + if (a[2] && !guest_write(a[2], &old, sizeof old)) return -EFAULT_; + return 0; + } + case 135: { // rt_sigprocmask(how, set, oset, size) + uint64_t old = t.sigmask; + if (a[1]) { + uint64_t set; + if (!guest_read(a[1], &set, 8)) return -EFAULT_; + switch (static_cast(a[0])) { + case SIG_BLOCK_: t.sigmask |= set; break; + case SIG_UNBLOCK_: t.sigmask &= ~set; break; + case SIG_SETMASK_: t.sigmask = set; break; + default: return -EINVAL_; + } + t.sigmask &= ~kUnblockable; + } + if (a[2] && !guest_write(a[2], &old, 8)) return -EFAULT_; + return 0; + } + case 136: { // rt_sigpending + uint64_t pending = t.pending.load() & t.sigmask; + return guest_write(a[0], &pending, 8) ? 0 : -EFAULT_; + } + case 133: { // rt_sigsuspend(mask) + uint64_t mask; + if (!guest_read(a[0], &mask, 8)) return -EFAULT_; + t.saved_sigmask = t.sigmask; + t.restore_sigmask = true; + t.sigmask = mask & ~kUnblockable; + block_until(t, [] { return false; }, 0, "sigsuspend"); + return -EINTR_; + } + case 137: { // rt_sigtimedwait(set, info, timeout, size) + uint64_t set; + if (!guest_read(a[0], &set, 8)) return -EFAULT_; + int64_t deadline = 0; + if (a[2]) { + ktimespec ts; + if (!guest_read(a[2], &ts, sizeof ts)) return -EFAULT_; + deadline = (ts.tv_sec == 0 && ts.tv_nsec == 0) ? -1 : ticks_now() + ticks_from_timespec_relative(ts); + } + int got = 0; + ksiginfo info{}; + auto take = [&] { + std::lock_guard held(t.signal_lock); + uint64_t ready = t.pending.load() & set; + if (!ready) return false; + unsigned long index; + _BitScanForward64(&index, ready); + got = static_cast(index) + 1; + bool more = false, found = false; + for (auto it = t.pending_info.begin(); it != t.pending_info.end();) { + if (it->si_signo == got) { + if (!found) { + info = *it; + it = t.pending_info.erase(it); + found = true; + continue; + } + more = true; + } + ++it; + } + if (!found) info.si_signo = got; + if (!more) t.pending.fetch_and(~bit(got)); + return true; + }; + // Signals in `set` are blocked by the caller; only others interrupt. + uint64_t saved = t.sigmask; + t.sigmask |= set; + int64_t r = block_until(t, take, deadline, "sigtimedwait"); + t.sigmask = saved; + if (r == -ETIMEDOUT_) return -EAGAIN_; + if (r) return r; + if (a[1]) guest_write(a[1], &info, sizeof info); + return got; + } + case 139: // rt_sigreturn + return sigreturn(t); + case 132: { // sigaltstack(ss, oss) + kstack_t old = t.altstack; + uint64_t sp = get_sp(t); + if (old.ss_flags != SS_DISABLE_ && sp > old.ss_sp && sp <= old.ss_sp + old.ss_size) old.ss_flags |= SS_ONSTACK_; + if (a[0]) { + kstack_t next; + if (!guest_read(a[0], &next, sizeof next)) return -EFAULT_; + if (old.ss_flags & SS_ONSTACK_) return -EPERM_; + if (next.ss_flags & SS_DISABLE_) t.altstack = {0, SS_DISABLE_, 0, 0}; + else { + if (next.ss_size < 2048) return -ENOMEM_; + t.altstack = next; + t.altstack.ss_flags = 0; + } + } + if (a[1] && !guest_write(a[1], &old, sizeof old)) return -EFAULT_; + return 0; + } + case 129: { // kill(pid, sig) + int pid = static_cast(a[0]), signal = static_cast(a[1]); + if (pid != process().pid && pid != 0 && pid != -1) return -ESRCH_; + if (!signal) return 0; + ksiginfo info{}; + info.si_signo = signal; + info.si_code = SI_USER_; + info.fields.kill.pid = process().pid; + info.fields.kill.uid = process().uid; + // Process-directed: the first thread not blocking it. + GuestThread* target = nullptr; + { + std::lock_guard held(process().threads_lock); + for (auto& [tid, thread] : process().threads) + if (!(thread->sigmask & bit(signal))) { + target = thread; + break; + } + if (!target && !process().threads.empty()) target = process().threads.begin()->second; + } + if (!target) return -ESRCH_; + return send_signal(*target, signal, info); + } + case 130: // tkill(tid, sig) + case 131: { // tgkill(tgid, tid, sig) + int tid = static_cast(nr == 130 ? a[0] : a[1]); + int signal = static_cast(nr == 130 ? a[1] : a[2]); + if (nr == 131 && static_cast(a[0]) != process().pid) return -ESRCH_; + GuestThread* target = tid == t.tid ? &t : find_thread(tid); + if (!target) return -ESRCH_; + if (!signal) return 0; + ksiginfo info{}; + info.si_signo = signal; + info.si_code = SI_TKILL_; + info.fields.kill.pid = process().pid; + info.fields.kill.uid = process().uid; + return send_signal(*target, signal, info); + } + case 138: // rt_sigqueueinfo(pid, sig, info) + case 240: { // rt_tgsigqueueinfo(tgid, tid, sig, info) + int signal = static_cast(nr == 138 ? a[1] : a[2]); + ksiginfo info{}; + if (!guest_read(nr == 138 ? a[2] : a[3], &info, sizeof info)) return -EFAULT_; + info.si_signo = signal; + GuestThread* target = nr == 240 ? find_thread(static_cast(a[1])) : &t; + if (!target) return -ESRCH_; + return send_signal(*target, signal, info); + } + case 102: case 103: return -ENOSYS_; // getitimer, setitimer + default: + handled = false; + return 0; + } +} + +} // namespace axrb diff --git a/direct/src/sysroot.cpp b/direct/src/sysroot.cpp new file mode 100644 index 0000000..41948ce --- /dev/null +++ b/direct/src/sysroot.cpp @@ -0,0 +1,511 @@ +#include "sysroot.h" +#include "binder.h" + +#include "files.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" +#include "vfs.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +int guest_cpu_count(); + +namespace { + +std::wstring exe_directory() { + wchar_t path[MAX_PATH * 4]; + DWORD n = GetModuleFileNameW(nullptr, path, static_cast(std::size(path))); + std::wstring dir(path, n); + size_t slash = dir.find_last_of(L"\\/"); + return slash == std::wstring::npos ? L"." : dir.substr(0, slash); +} + +bool is_directory(const std::wstring& path) { + DWORD a = GetFileAttributesW(path.c_str()); + return a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY); +} + +// The few JSON files mkroot.py writes are flat {"a": "b", ...} maps. +std::vector> read_flat_json(const std::wstring& path) { + std::ifstream in(path, std::ios::binary); + std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + std::vector> out; + std::vector strings; + for (size_t i = 0; i < text.size(); ++i) { + if (text[i] != '"') continue; + std::string s; + for (++i; i < text.size() && text[i] != '"'; ++i) { + if (text[i] == '\\' && i + 1 < text.size()) ++i; + s.push_back(text[i]); + } + strings.push_back(s); + } + for (size_t i = 0; i + 1 < strings.size(); i += 2) out.push_back({strings[i], strings[i + 1]}); + return out; +} + +std::string format(const char* fmt, ...) { + char buffer[4096]; + va_list args; + va_start(args, fmt); + int n = std::vsnprintf(buffer, sizeof buffer, fmt, args); + va_end(args); + return std::string(buffer, static_cast(std::max(0, std::min(n, sizeof buffer - 1)))); +} + +uint64_t total_ram() { + MEMORYSTATUSEX status{sizeof status}; + GlobalMemoryStatusEx(&status); + return std::min(status.ullTotalPhys, 8ull << 30); +} + +uint64_t available_ram() { + MEMORYSTATUSEX status{sizeof status}; + GlobalMemoryStatusEx(&status); + return std::min(status.ullAvailPhys, total_ram() * 3 / 4); +} + +std::string cpuinfo() { + std::string text; + for (int i = 0; i < guest_cpu_count(); ++i) { + text += format( + "processor\t: %d\nBogoMIPS\t: 38.40\nFeatures\t: fp asimd evtstrm aes pmull sha1 sha2 crc32\n" + "CPU implementer\t: 0x41\nCPU architecture: 8\nCPU variant\t: 0x0\nCPU part\t: 0xd4b\nCPU revision\t: 0\n\n", + i); + } + text += "Hardware\t: ranchu\n"; + return text; +} + +std::string meminfo() { + uint64_t total = total_ram() / 1024, available = available_ram() / 1024; + return format( + "MemTotal: %8llu kB\nMemFree: %8llu kB\nMemAvailable: %8llu kB\nBuffers: 0 kB\n" + "Cached: %8llu kB\nSwapCached: 0 kB\nActive: %8llu kB\nInactive: %8llu kB\n" + "SwapTotal: 0 kB\nSwapFree: 0 kB\nDirty: 0 kB\n" + "AnonPages: %8llu kB\nMapped: %8llu kB\nShmem: 0 kB\n" + "CommitLimit: %8llu kB\nCommitted_AS: %8llu kB\nVmallocTotal: 263061440 kB\n", + (unsigned long long)total, (unsigned long long)(available / 2), (unsigned long long)available, + (unsigned long long)(available / 2), (unsigned long long)(total - available) / 2, + (unsigned long long)(total - available) / 2, (unsigned long long)(total - available) / 2, + (unsigned long long)(total - available) / 4, (unsigned long long)total, + (unsigned long long)(address_space().mapped_bytes() / 1024 / 8)); +} + +std::string proc_stat() { + // Jiffies from host idle/kernel/user times so CPU-load heuristics see + // plausible numbers. + FILETIME idle, kernel, user; + GetSystemTimes(&idle, &kernel, &user); + auto ticks = [](const FILETIME& f) { + return ((static_cast(f.dwHighDateTime) << 32) | f.dwLowDateTime) / 100000; // 100 Hz + }; + uint64_t i = ticks(idle), k = ticks(kernel) - i, u = ticks(user); + int cpus = guest_cpu_count(); + std::string text = format("cpu %llu 0 %llu %llu 0 0 0 0 0 0\n", (unsigned long long)u, (unsigned long long)k, + (unsigned long long)i); + for (int c = 0; c < cpus; ++c) + text += format("cpu%d %llu 0 %llu %llu 0 0 0 0 0 0\n", c, (unsigned long long)(u / cpus), + (unsigned long long)(k / cpus), (unsigned long long)(i / cpus)); + text += format("intr 0\nctxt 0\nbtime %lld\nprocesses 1\nprocs_running 1\nprocs_blocked 0\n", + (long long)((realtime_ns() - monotonic_ns()) / 1000000000)); + return text; +} + +std::string thread_stat(int tid, const std::string& name) { + PROCESS_MEMORY_COUNTERS counters{}; + GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof counters); + uint64_t vsize = address_space().mapped_bytes(); + uint64_t rss_pages = counters.WorkingSetSize / 4096; + FILETIME c, e, k, u; + GetProcessTimes(GetCurrentProcess(), &c, &e, &k, &u); + auto ticks = [](const FILETIME& f) { + return ((static_cast(f.dwHighDateTime) << 32) | f.dwLowDateTime) / 100000; + }; + return format( + "%d (%s) R 1 %d %d 0 -1 4210944 0 0 0 0 %llu %llu 0 0 20 0 %zu 0 0 %llu %llu 18446744073709551615 0 0 %llu 0 0 " + "4612 1073775864 0 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0 0\n", + tid, name.c_str(), process().pid, process().pid, (unsigned long long)ticks(u), (unsigned long long)ticks(k), + process().threads.size(), (unsigned long long)vsize, (unsigned long long)rss_pages, (unsigned long long)process().start_stack); +} + +std::string process_status(int tid, const std::string& name) { + PROCESS_MEMORY_COUNTERS counters{}; + GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof counters); + uint64_t vm = address_space().mapped_bytes() / 1024; + return format( + "Name:\t%s\nUmask:\t0077\nState:\tR (running)\nTgid:\t%d\nNgid:\t0\nPid:\t%d\nPPid:\t1\nTracerPid:\t0\n" + "Uid:\t%u\t%u\t%u\t%u\nGid:\t%u\t%u\t%u\t%u\nFDSize:\t256\nGroups:\t%u\nVmPeak:\t%8llu kB\nVmSize:\t%8llu kB\n" + "VmLck:\t 0 kB\nVmPin:\t 0 kB\nVmHWM:\t%8llu kB\nVmRSS:\t%8llu kB\nRssAnon:\t%8llu kB\n" + "RssFile:\t 0 kB\nRssShmem:\t 0 kB\nVmData:\t%8llu kB\nVmStk:\t 8192 kB\nVmExe:\t 0 kB\n" + "VmLib:\t 0 kB\nVmSwap:\t 0 kB\nThreads:\t%zu\nSigQ:\t0/31529\nSigPnd:\t0000000000000000\n" + "ShdPnd:\t0000000000000000\nSigBlk:\t0000000000000000\nSigIgn:\t0000000000001000\nSigCgt:\t0000000000000000\n" + "CapInh:\t0000000000000000\nCapPrm:\t0000000000000000\nCapEff:\t0000000000000000\nCapBnd:\t0000000000000000\n" + "NoNewPrivs:\t0\nSeccomp:\t0\nCpus_allowed_list:\t0-%d\nvoluntary_ctxt_switches:\t0\n" + "nonvoluntary_ctxt_switches:\t0\n", + name.c_str(), process().pid, tid, process().uid, process().uid, process().uid, process().uid, process().gid, + process().gid, process().gid, process().gid, process().gid, (unsigned long long)vm, (unsigned long long)vm, + (unsigned long long)(counters.PeakWorkingSetSize / 1024), (unsigned long long)(counters.WorkingSetSize / 1024), + (unsigned long long)(counters.WorkingSetSize / 1024), (unsigned long long)vm, process().threads.size(), + guest_cpu_count() - 1); +} + +std::string random_uuid() { + uint8_t b[16]; + BCryptGenRandom(nullptr, b, sizeof b, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + b[6] = (b[6] & 0x0f) | 0x40; + b[8] = (b[8] & 0x3f) | 0x80; + return format("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x\n", b[0], b[1], b[2], b[3], b[4], + b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]); +} + +std::string thread_name(int tid) { + std::lock_guard held(process().threads_lock); + auto it = process().threads.find(tid); + return it == process().threads.end() ? std::string() : it->second->name; +} + +bool thread_exists(int tid) { + std::lock_guard held(process().threads_lock); + return process().threads.count(tid) != 0; +} + +std::vector thread_ids() { + std::lock_guard held(process().threads_lock); + std::vector out; + for (auto& [tid, thread] : process().threads) out.push_back(std::to_string(tid)); + return out; +} + +void make_file(Resolved& out, std::function reader) { + out.kind = Resolved::Synthetic; + out.reader = std::move(reader); + out.read_only = true; +} + +void make_dir(Resolved& out, std::vector children) { + out.kind = Resolved::SyntheticDir; + out.children = std::move(children); + out.read_only = true; +} + +void make_link(Resolved& out, std::string target) { + out.kind = Resolved::None; + out.guest = std::move(target); +} + +std::string fd_target(int fd) { + auto file = fds().get(fd); + if (!file) return {}; + if (!file->guest_path().empty()) return file->guest_path(); + kstat st{}; + file->stat(st); + const char* kind = (st.st_mode & S_IFMT_) == S_IFSOCK_ ? "socket" : (st.st_mode & S_IFMT_) == S_IFIFO_ ? "pipe" : "anon_inode"; + if (std::strcmp(kind, "anon_inode") == 0) return "anon_inode:[eventfd]"; + return format("%s:[%llu]", kind, (unsigned long long)file->inode()); +} + +// Everything below /proc/ (and /proc/self/task/). +bool resolve_task(const std::string& rest, int tid, bool process_level, Resolved& out) { + std::string name = thread_name(tid); + if (name.empty()) name = "main"; + if (rest.empty()) { + std::vector children = {"stat", "status", "comm", "cmdline", "maps", "smaps", "statm", "exe", + "fd", "cwd", "auxv", "environ", "mounts", "mountinfo", "limits", "cgroup", + "oom_score_adj", "oom_adj", "timerslack_ns", "sched", "schedstat", "io"}; + if (process_level) children.push_back("task"); + make_dir(out, children); + return true; + } + if (rest == "/stat") return make_file(out, [=] { return thread_stat(tid, name); }), true; + if (rest == "/status") return make_file(out, [=] { return process_status(tid, name); }), true; + if (rest == "/comm") return make_file(out, [=] { return name + "\n"; }), true; + if (rest == "/cmdline") return make_file(out, [] { return process().cmdline; }), true; + if (rest == "/maps" || rest == "/smaps") return make_file(out, [] { return address_space().maps_text(); }), true; + if (rest == "/statm") { + return make_file(out, [] { + uint64_t pages = address_space().mapped_bytes() / 4096; + return format("%llu %llu 0 0 0 %llu 0\n", (unsigned long long)pages, (unsigned long long)(pages / 4), + (unsigned long long)(pages / 2)); + }), true; + } + if (rest == "/exe") return make_link(out, process().exe_path), true; + if (rest == "/cwd") return make_link(out, process().cwd), true; + if (rest == "/auxv") { + return make_file(out, [] { + const auto& v = process().auxv; + return std::string(reinterpret_cast(v.data()), v.size() * 8); + }), true; + } + if (rest == "/environ") return make_file(out, [] { return std::string(); }), true; + if (rest == "/mounts" || rest == "/mountinfo") { + return make_file(out, [] { + return std::string( + "/dev/root / ext4 ro,seclabel,relatime 0 0\n" + "tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,mode=755 0 0\n" + "proc /proc proc rw,relatime,gid=3009,hidepid=invisible 0 0\n" + "sysfs /sys sysfs rw,seclabel,relatime 0 0\n" + "/dev/block/dm-5 /data f2fs rw,lazytime,seclabel,nosuid,nodev,noatime 0 0\n" + "/dev/fuse /storage/emulated fuse rw,lazytime,nosuid,nodev,noexec,noatime 0 0\n"); + }), true; + } + if (rest == "/limits") { + return make_file(out, [] { + return std::string( + "Limit Soft Limit Hard Limit Units \n" + "Max stack size 8388608 unlimited bytes \n" + "Max open files 32768 32768 files \n"); + }), true; + } + if (rest == "/cgroup") return make_file(out, [] { return std::string("0::/uid_10123/pid_1\n"); }), true; + if (rest == "/oom_score_adj") return make_file(out, [] { return std::string("0\n"); }), true; + if (rest == "/oom_adj") return make_file(out, [] { return std::string("0\n"); }), true; + if (rest == "/timerslack_ns") return make_file(out, [] { return std::string("50000\n"); }), true; + if (rest == "/sched" || rest == "/schedstat" || rest == "/io") return make_file(out, [] { return std::string(); }), true; + if (rest == "/fd" || rest == "/fdinfo") { + std::vector children; + for (int fd : fds().list()) children.push_back(std::to_string(fd)); + make_dir(out, children); + return true; + } + if (rest.rfind("/fd/", 0) == 0) { + int fd = std::atoi(rest.c_str() + 4); + std::string target = fd_target(fd); + if (target.empty()) return false; + make_link(out, target); + return true; + } + if (rest.rfind("/fdinfo/", 0) == 0) { + int fd = std::atoi(rest.c_str() + 8); + auto file = fds().get(fd); + if (!file) return false; + uint32_t flags = file->status_flags; + return make_file(out, [=] { return format("pos:\t0\nflags:\t0%o\nmnt_id:\t1\n", flags); }), true; + } + if (process_level && rest == "/task") return make_dir(out, thread_ids()), true; + if (process_level && rest.rfind("/task/", 0) == 0) { + std::string tail = rest.substr(6); + size_t slash = tail.find('/'); + int child = std::atoi(tail.substr(0, slash).c_str()); + if (!thread_exists(child)) return false; + return resolve_task(slash == std::string::npos ? "" : tail.substr(slash), child, false, out); + } + return false; +} + +bool resolve_proc(const std::string& rest, Resolved& out) { + const std::string pid = std::to_string(process().pid); + if (rest.empty()) { + make_dir(out, {"self", "thread-self", pid, "cpuinfo", "meminfo", "stat", "uptime", "version", "loadavg", + "filesystems", "sys"}); + return true; + } + if (rest == "/self") return make_link(out, "/proc/" + pid), true; + if (rest == "/thread-self") { + GuestThread* t = current_thread(); + return make_link(out, "/proc/" + pid + "/task/" + std::to_string(t ? t->tid : process().pid)), true; + } + if (rest == "/" + pid || rest.rfind("/" + pid + "/", 0) == 0) + return resolve_task(rest.substr(pid.size() + 1), process().pid, true, out); + if (rest == "/cpuinfo") return make_file(out, cpuinfo), true; + if (rest == "/meminfo") return make_file(out, meminfo), true; + if (rest == "/stat") return make_file(out, proc_stat), true; + if (rest == "/uptime") { + return make_file(out, [] { + double up = monotonic_ns() / 1e9; + return format("%.2f %.2f\n", up, up * 0.5); + }), true; + } + if (rest == "/loadavg") return make_file(out, [] { return std::string("0.50 0.40 0.30 1/400 1\n"); }), true; + if (rest == "/version") + return make_file(out, [] { return std::string("Linux version 5.10.226-android12-9-axrb (axrb@direct) #1 SMP PREEMPT\n"); }), true; + if (rest == "/filesystems") return make_file(out, [] { return std::string("nodev\tproc\nnodev\ttmpfs\n\text4\n\tf2fs\nnodev\tfuse\n"); }), true; + if (rest == "/sys/kernel/random/boot_id") { + static std::string boot_id = random_uuid(); + return make_file(out, [] { return boot_id; }), true; + } + if (rest == "/sys/kernel/random/uuid") return make_file(out, random_uuid), true; + if (rest == "/sys/kernel/random/entropy_avail") return make_file(out, [] { return std::string("256\n"); }), true; + if (rest == "/sys/kernel/pid_max") return make_file(out, [] { return std::string("32768\n"); }), true; + if (rest == "/sys/kernel/osrelease") return make_file(out, [] { return std::string("5.10.226-android12-9-axrb\n"); }), true; + if (rest == "/sys/kernel/perf_event_paranoid") return make_file(out, [] { return std::string("3\n"); }), true; + if (rest == "/sys/vm/overcommit_memory") return make_file(out, [] { return std::string("1\n"); }), true; + if (rest == "/sys/vm/max_map_count") return make_file(out, [] { return std::string("65530\n"); }), true; + if (rest == "/sys/fs/inotify/max_user_watches") return make_file(out, [] { return std::string("8192\n"); }), true; + if (rest == "/sys" || rest == "/sys/kernel" || rest == "/sys/vm" || rest == "/sys/kernel/random") + return make_dir(out, {}), true; + return false; +} + +// /sys/devices/system/cpu: what engines read to find core counts and the +// "big" cores. A uniform cluster at Quest-3-like clocks. +bool resolve_cpu(const std::string& rest, Resolved& out) { + int cpus = guest_cpu_count(); + std::string range = cpus > 1 ? format("0-%d\n", cpus - 1) : std::string("0\n"); + if (rest.empty()) { + std::vector children = {"possible", "present", "online", "offline", "kernel_max"}; + for (int i = 0; i < cpus; ++i) children.push_back("cpu" + std::to_string(i)); + make_dir(out, children); + return true; + } + if (rest == "/possible" || rest == "/present" || rest == "/online") return make_file(out, [=] { return range; }), true; + if (rest == "/offline") return make_file(out, [] { return std::string("\n"); }), true; + if (rest == "/kernel_max") return make_file(out, [] { return std::string("31\n"); }), true; + if (rest.rfind("/cpu", 0) != 0) return false; + size_t end = 4; + while (end < rest.size() && std::isdigit(static_cast(rest[end]))) ++end; + if (end == 4) return false; + int index = std::atoi(rest.substr(4, end - 4).c_str()); + if (index >= cpus) return false; + std::string tail = rest.substr(end); + if (tail.empty()) return make_dir(out, {"online", "cpufreq", "topology", "cpu_capacity"}), true; + if (tail == "/online") return make_file(out, [] { return std::string("1\n"); }), true; + if (tail == "/cpu_capacity") return make_file(out, [] { return std::string("1024\n"); }), true; + if (tail == "/cpufreq") + return make_dir(out, {"cpuinfo_max_freq", "cpuinfo_min_freq", "scaling_max_freq", "scaling_min_freq", + "scaling_cur_freq", "cpuinfo_cur_freq", "related_cpus", "affected_cpus", + "scaling_governor", "scaling_available_frequencies"}), + true; + if (tail == "/cpufreq/cpuinfo_max_freq" || tail == "/cpufreq/scaling_max_freq" || + tail == "/cpufreq/scaling_cur_freq" || tail == "/cpufreq/cpuinfo_cur_freq") + return make_file(out, [] { return std::string("2841600\n"); }), true; + if (tail == "/cpufreq/cpuinfo_min_freq" || tail == "/cpufreq/scaling_min_freq") + return make_file(out, [] { return std::string("300000\n"); }), true; + if (tail == "/cpufreq/scaling_available_frequencies") + return make_file(out, [] { return std::string("300000 1497600 2841600\n"); }), true; + if (tail == "/cpufreq/related_cpus" || tail == "/cpufreq/affected_cpus") { + std::string list; + for (int i = 0; i < cpus; ++i) list += (i ? " " : "") + std::to_string(i); + return make_file(out, [=] { return list + "\n"; }), true; + } + if (tail == "/cpufreq/scaling_governor") return make_file(out, [] { return std::string("schedutil\n"); }), true; + if (tail == "/topology") return make_dir(out, {"core_id", "physical_package_id", "cluster_id", "core_siblings_list"}), true; + if (tail == "/topology/core_id") return make_file(out, [=] { return std::to_string(index) + "\n"; }), true; + if (tail == "/topology/physical_package_id" || tail == "/topology/cluster_id") + return make_file(out, [] { return std::string("0\n"); }), true; + if (tail == "/topology/core_siblings_list") return make_file(out, [=] { return range; }), true; + return false; +} + +} // namespace + +std::wstring default_sysroot() { + wchar_t env[MAX_PATH * 4]; + if (DWORD n = GetEnvironmentVariableW(L"AXRB_DIRECT_SYSROOT", env, static_cast(std::size(env)))) + return std::wstring(env, n); + std::wstring dir = exe_directory(); + if (is_directory(dir + L"\\sysroot\\system")) return dir + L"\\sysroot"; + // Development layout: walk up to the repository's out\sysimg\root64. + for (std::wstring at = dir; !at.empty();) { + if (is_directory(at + L"\\out\\sysimg\\root64\\system")) return at + L"\\out\\sysimg\\root64"; + size_t slash = at.find_last_of(L"\\/"); + if (slash == std::wstring::npos || slash < 3) break; + at = at.substr(0, slash); + } + return dir + L"\\sysroot"; +} + +std::wstring data_root() { + wchar_t env[MAX_PATH * 4]; + if (DWORD n = GetEnvironmentVariableW(L"AXRB_DIRECT_DATA", env, static_cast(std::size(env)))) + return std::wstring(env, n); + PWSTR local = nullptr; + std::wstring root; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &local))) root = local; + CoTaskMemFree(local); + if (root.empty()) root = exe_directory(); + return root + L"\\AXRB Runtime\\direct"; +} + +bool sysroot_init(const std::wstring& root) { + if (!is_directory(root + L"\\system\\lib64")) { + LOGE("sysroot", "no Android system root at %s (run out/sysimg/mkroot.py)", narrow(root).c_str()); + return false; + } + vfs().add_mount("/", root, true); + int links = 0; + for (auto& [from, to] : read_flat_json(root + L"\\.symlinks.json")) { + vfs().add_symlink(from, to); + ++links; + } + // Directories bionic, the linker and libraries expect. + vfs().add_symlink("/bin", "/system/bin"); + vfs().add_symlink("/etc", "/system/etc"); + vfs().add_symlink("/sdcard", "/storage/self/primary"); + vfs().add_symlink("/storage/self/primary", "/storage/emulated/0"); + // Android normally generates this at boot. Direct mode has no init or + // linkerconfig service, so supply the search paths for our extracted + // system libraries. In particular harfbuzz (loaded by Unity) needs ICU + // from the i18n APEX. LD_LIBRARY_PATH still takes precedence for the + // app's libraries and our NDK proxies. + vfs().add_synthetic_dir("/linkerconfig", {"ld.config.txt"}); + const bool has_art = is_directory(root + L"\\apex\\com.android.art"); + std::vector> apex_namespaces; + if (has_art) { + for (const auto& item : std::filesystem::directory_iterator(root + L"\\apex")) { + if (!item.is_directory()) continue; + std::string name = item.path().filename().string(), ns = name; + std::replace(ns.begin(), ns.end(), '.', '_'); + apex_namespaces.emplace_back(name, ns); + } + std::sort(apex_namespaces.begin(), apex_namespaces.end()); + } + vfs().add_synthetic("/linkerconfig/ld.config.txt", [has_art, apex_namespaces] { + std::string config( + "dir.direct = /apex\n" + "dir.direct = /data\n" + "dir.direct = /system\n" + "[direct]\n" + "namespace.default.isolated = false\n" + "namespace.default.visible = true\n" + "namespace.default.search.paths = /system/${LIB}:/apex/com.android.runtime/${LIB}/bionic:/apex/com.android.i18n/${LIB}\n"); + if (has_art) { + config += "namespace.default.search.paths += "; + for (size_t i = 0; i < apex_namespaces.size(); ++i) + config += (i ? ":" : "") + std::string("/apex/") + apex_namespaces[i].first + "/${LIB}"; + config += "\nadditional.namespaces = "; + for (size_t i = 0; i < apex_namespaces.size(); ++i) + config += (i ? "," : "") + apex_namespaces[i].second; + config += "\n"; + // ART resolves boot-JAR JNI libraries using exported APEX names. + // Delegate to the common namespace so libc/libart are not loaded twice. + for (const auto& [apex, name] : apex_namespaces) { + std::string prefix = std::string("namespace.") + name; + config += prefix + ".isolated = false\n" + prefix + ".visible = true\n"; + config += prefix + ".links = default\n" + prefix + ".link.default.allow_all_shared_libs = true\n"; + } + } + return config; + }); + LOGI("sysroot", "system root %s (%d symlinks)", narrow(root).c_str(), links); + return true; +} + +void proc_init() { + vfs().set_dynamic_dir("/proc", resolve_proc); + vfs().set_dynamic_dir("/sys/devices/system/cpu", resolve_cpu); + vfs().add_synthetic("/sys/kernel/mm/transparent_hugepage/enabled", [] { return std::string("always [madvise] never\n"); }); + vfs().add_synthetic_dir("/sys/fs/cgroup", {}); + vfs().add_device("/dev/null", "null"); + vfs().add_device("/dev/zero", "zero"); + vfs().add_device("/dev/full", "zero"); + vfs().add_device("/dev/random", "random"); + vfs().add_device("/dev/urandom", "urandom"); + vfs().add_device("/dev/tty", "null"); + init_binder(); + vfs().add_synthetic_dir("/dev/socket", {"logdw", "property_service", "dnsproxyd"}); +} + +} // namespace axrb diff --git a/direct/src/sysroot.h b/direct/src/sysroot.h new file mode 100644 index 0000000..6bc4f74 --- /dev/null +++ b/direct/src/sysroot.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace axrb { + +// The Android system root extracted from the SDK system image (see +// out/sysimg/mkroot.py): mounted read-only at /, with its symlinks. +bool sysroot_init(const std::wstring& root); +// Synthesized /proc, /sys and /dev. +void proc_init(); +// Default location of the extracted system root and of AXRB Direct's +// persistent data (%LOCALAPPDATA%\AXRB Runtime\direct). +std::wstring default_sysroot(); +std::wstring data_root(); + +} // namespace axrb diff --git a/direct/src/threads.cpp b/direct/src/threads.cpp new file mode 100644 index 0000000..510441d --- /dev/null +++ b/direct/src/threads.cpp @@ -0,0 +1,627 @@ +#include "cpu.h" +#include "kernel.h" +#include "log.h" +#include "memory.h" +#include "time_math.h" + +#include + +#include +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +// --------------------------------------------------------------------------- +// Time + +int64_t ticks_per_second() { + static int64_t frequency = [] { + LARGE_INTEGER f; + QueryPerformanceFrequency(&f); + return static_cast(f.QuadPart); + }(); + return frequency; +} + +int64_t ticks_now() { + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + return static_cast(now.QuadPart); +} + +int64_t ticks_from_timespec_relative(const ktimespec& ts) { + int64_t f = ticks_per_second(); + if (ts.tv_sec < 0) return 0; + return add_time(scale_time(ts.tv_sec, f, 1), scale_time(ts.tv_nsec, f, 1000000000)); +} + +int64_t monotonic_ns() { + int64_t t = ticks_now(), f = ticks_per_second(); + return (t / f) * 1000000000 + (t % f) * 1000000000 / f; +} + +int64_t realtime_ns() { + FILETIME ft; + GetSystemTimePreciseAsFileTime(&ft); + uint64_t t = (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + return static_cast((t - 116444736000000000ull) * 100); +} + +// --------------------------------------------------------------------------- +// Threads and blocking + +GuestThread::GuestThread() { + event = CreateEventW(nullptr, FALSE, FALSE, nullptr); +} + +GuestThread::~GuestThread() { + if (event) CloseHandle(event); +} + +static thread_local HANDLE t_timer = nullptr; + +static bool signal_ready(GuestThread& thread) { + uint64_t pending = thread.pending.load(std::memory_order_acquire); + return (pending & ~thread.sigmask) != 0; +} + +int64_t block_until(GuestThread& thread, const std::function& ready, int64_t deadline, const char* why) { + for (;;) { + if (ready()) return 0; + if (signal_ready(thread)) return -EINTR_; + if (deadline == -1) return -ETIMEDOUT_; + HANDLE handles[2] = {thread.event, nullptr}; + DWORD count = 1; + DWORD wait = INFINITE; + if (deadline > 0) { + int64_t remaining = deadline - ticks_now(); + if (remaining <= 0) return ready() ? 0 : -ETIMEDOUT_; + int64_t hundred_ns = scale_time(remaining, 10000000, ticks_per_second()); + if (!t_timer) + t_timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + if (t_timer) { + LARGE_INTEGER due; + due.QuadPart = -std::max(1, hundred_ns); + SetWaitableTimer(t_timer, &due, 0, nullptr, nullptr, FALSE); + handles[count++] = t_timer; + } else { + // INFINITE is reserved by Win32; a finite long wait must not + // truncate to zero or accidentally become that sentinel. + wait = static_cast(std::clamp(hundred_ns / 10000, 1, MAXDWORD - 1)); + } + } + thread.blocked_in.store(why, std::memory_order_relaxed); + WaitForMultipleObjects(count, handles, FALSE, wait); + thread.blocked_in.store(nullptr, std::memory_order_relaxed); + if (count == 2) CancelWaitableTimer(t_timer); + } +} + +void WaitQueue::add(GuestThread* thread) { + std::lock_guard held(lock_); + waiters_.push_back(thread); +} + +void WaitQueue::remove(GuestThread* thread) { + std::lock_guard held(lock_); + auto it = std::find(waiters_.begin(), waiters_.end(), thread); + if (it != waiters_.end()) waiters_.erase(it); +} + +void WaitQueue::wake_all() { + std::lock_guard held(lock_); + for (GuestThread* thread : waiters_) SetEvent(thread->event); +} + +// --------------------------------------------------------------------------- +// Futexes + +namespace { + +struct FutexWaiter { + uint64_t address; + uint32_t bitset; + GuestThread* thread; + std::atomic woken{false}; +}; + +struct Bucket { + std::mutex lock; + std::list waiters; +}; + +Bucket g_buckets[1024]; +Bucket& bucket_for(uint64_t address) { return g_buckets[(address >> 2) % 1024]; } + +int wake_locked(Bucket& b, uint64_t address, int count, uint32_t bitset) { + int woken = 0; + for (auto it = b.waiters.begin(); it != b.waiters.end() && woken < count;) { + FutexWaiter* w = *it; + if (w->address == address && (w->bitset & bitset)) { + it = b.waiters.erase(it); + w->woken.store(true, std::memory_order_release); + SetEvent(w->thread->event); + ++woken; + } else { + ++it; + } + } + return woken; +} + +int64_t futex_wait(GuestThread& t, uint64_t address, uint32_t expected, int64_t deadline, uint32_t bitset) { + if (!bitset) return -EINVAL_; + if (address & 3) return -EINVAL_; + FutexWaiter waiter{address, bitset, &t}; + Bucket& b = bucket_for(address); + { + std::lock_guard held(b.lock); + uint32_t current = 0; + if (!guest_read(address, ¤t, 4)) return -EFAULT_; + if (current != expected) return -EAGAIN_; + b.waiters.push_back(&waiter); + } + int64_t r = block_until(t, [&] { return waiter.woken.load(std::memory_order_acquire); }, deadline, "futex"); + std::lock_guard held(b.lock); + if (waiter.woken.load(std::memory_order_acquire)) return 0; + b.waiters.remove(&waiter); + return r == 0 ? 0 : r; +} + +int64_t futex_wake(uint64_t address, int count, uint32_t bitset) { + if (!bitset) return -EINVAL_; + Bucket& b = bucket_for(address); + std::lock_guard held(b.lock); + return wake_locked(b, address, count, bitset); +} + +int64_t futex_requeue(uint64_t from, uint64_t to, int wake, int requeue, bool compare, uint32_t expected) { + Bucket& a = bucket_for(from); + Bucket& b = bucket_for(to); + std::unique_lock first(&a < &b ? a.lock : b.lock, std::defer_lock); + std::unique_lock second(&a < &b ? b.lock : a.lock, std::defer_lock); + first.lock(); + if (&a != &b) second.lock(); + if (compare) { + uint32_t current = 0; + if (!guest_read(from, ¤t, 4)) return -EFAULT_; + if (current != expected) return -EAGAIN_; + } + int woken = wake_locked(a, from, wake, ~0u); + int moved = 0; + for (auto it = a.waiters.begin(); it != a.waiters.end() && moved < requeue;) { + FutexWaiter* w = *it; + if (w->address == from) { + it = a.waiters.erase(it); + w->address = to; + b.waiters.push_back(w); + ++moved; + } else { + ++it; + } + } + return woken + moved; +} + +bool futex_atomic_op(uint64_t address, uint32_t encoded_op, uint32_t oparg, uint32_t& old) { + auto* word = reinterpret_cast(address); + __try { + long prev = *word, next; + for (;;) { + uint32_t v = static_cast(prev); + switch (encoded_op) { + case 0: next = static_cast(oparg); break; + case 1: next = static_cast(v + oparg); break; + case 2: next = static_cast(v | oparg); break; + case 3: next = static_cast(v & ~oparg); break; + default: next = static_cast(v ^ oparg); break; + } + long seen = _InterlockedCompareExchange(word, next, prev); + if (seen == prev) break; + prev = seen; + } + old = static_cast(prev); + return true; + } __except (EXCEPTION_EXECUTE_HANDLER) { + return false; + } +} + +int64_t futex_wake_op(uint64_t uaddr, uint64_t uaddr2, int count, int count2, uint32_t op) { + Bucket& a = bucket_for(uaddr); + Bucket& b = bucket_for(uaddr2); + std::unique_lock first(&a < &b ? a.lock : b.lock, std::defer_lock); + std::unique_lock second(&a < &b ? b.lock : a.lock, std::defer_lock); + first.lock(); + if (&a != &b) second.lock(); + uint32_t encoded_op = (op >> 28) & 7, cmp = (op >> 24) & 15; + uint32_t oparg = (op >> 12) & 0xfff, cmparg = op & 0xfff; + if ((op >> 28) & 8) oparg = 1u << (oparg & 31); + uint32_t old = 0; + if (!futex_atomic_op(uaddr2, encoded_op, oparg, old)) return -EFAULT_; + int woken = wake_locked(a, uaddr, count, ~0u); + bool hit; + switch (cmp) { + case 0: hit = old == cmparg; break; + case 1: hit = old != cmparg; break; + case 2: hit = static_cast(old) < static_cast(cmparg); break; + case 3: hit = static_cast(old) <= static_cast(cmparg); break; + case 4: hit = static_cast(old) > static_cast(cmparg); break; + default: hit = static_cast(old) >= static_cast(cmparg); break; + } + if (hit) woken += wake_locked(b, uaddr2, count2, ~0u); + return woken; +} + +// Priority inheritance is not modelled; ownership and hand-off follow the +// kernel's protocol on the futex word. +int64_t futex_lock_pi(GuestThread& t, uint64_t address, int64_t deadline, bool try_only) { + auto* word = reinterpret_cast(address); + for (;;) { + long value; + if (!guest_read(address, &value, 4)) return -EFAULT_; + uint32_t v = static_cast(value); + if ((v & FUTEX_TID_MASK_) == static_cast(t.tid)) return -EDEADLK_; + if ((v & FUTEX_TID_MASK_) == 0) { + long desired = static_cast(static_cast(t.tid) | (v & FUTEX_WAITERS_)); + if (_InterlockedCompareExchange(word, desired, value) == value) return 0; + continue; + } + if (try_only) return -EAGAIN_; + if (!(v & FUTEX_WAITERS_)) { + if (_InterlockedCompareExchange(word, static_cast(v | FUTEX_WAITERS_), value) != value) continue; + v |= FUTEX_WAITERS_; + } + int64_t r = futex_wait(t, address, v, deadline, ~0u); + if (r == -ETIMEDOUT_ || r == -EINTR_) return r; + } +} + +int64_t futex_unlock_pi(GuestThread& t, uint64_t address) { + auto* word = reinterpret_cast(address); + long value; + if (!guest_read(address, &value, 4)) return -EFAULT_; + if ((static_cast(value) & FUTEX_TID_MASK_) != static_cast(t.tid)) return -EPERM_; + _InterlockedExchange(word, 0); + futex_wake(address, 1, ~0u); + return 0; +} + +int64_t timeout_deadline(uint64_t timeout_addr, bool absolute, bool realtime, int64_t& deadline) { + deadline = 0; + if (!timeout_addr) return 0; + ktimespec ts; + if (!guest_read(timeout_addr, &ts, sizeof ts)) return -EFAULT_; + if (ts.tv_sec < 0 || ts.tv_nsec < 0 || ts.tv_nsec >= 1000000000) return -EINVAL_; + if (absolute) { + int64_t target = add_time(scale_time(ts.tv_sec, 1000000000, 1), ts.tv_nsec); + int64_t now = realtime ? realtime_ns() : monotonic_ns(); + int64_t left = target - now; + deadline = left <= 0 ? -1 : add_time(ticks_now(), scale_time(left, ticks_per_second(), 1000000000)); + } else { + int64_t ticks = ticks_from_timespec_relative(ts); + deadline = ticks == 0 ? -1 : add_time(ticks_now(), ticks); + } + return 0; +} + +} // namespace + +int64_t do_futex(GuestThread& t, uint64_t uaddr, int op, uint32_t val, uint64_t timeout, uint64_t uaddr2, uint32_t val3) { + int cmd = op & 127; + bool realtime = (op & FUTEX_CLOCK_REALTIME_) != 0; + switch (cmd) { + case FUTEX_WAIT_: { + int64_t deadline; + if (int64_t e = timeout_deadline(timeout, false, false, deadline)) return e; + return futex_wait(t, uaddr, val, deadline, ~0u); + } + case FUTEX_WAIT_BITSET_: { + int64_t deadline; + if (int64_t e = timeout_deadline(timeout, true, realtime, deadline)) return e; + const int64_t result = futex_wait(t, uaddr, val, deadline, val3); + if (trace_enabled("wait")) { + static thread_local unsigned reports = 0; + if (reports++ < 16) { + ktimespec ts{}; + if (timeout) guest_read(timeout, &ts, sizeof ts); + LOGI("wait", "tid=%d name=%s op=%x timeout=%lld.%09lld realtime=%d now=%lld deadline=%lld result=%lld", + t.tid, t.name.c_str(), op, (long long)ts.tv_sec, (long long)ts.tv_nsec, + realtime, (long long)(realtime ? realtime_ns() : monotonic_ns()), + (long long)deadline, (long long)result); + } + } + return result; + } + case FUTEX_WAKE_: return futex_wake(uaddr, static_cast(val), ~0u); + case FUTEX_WAKE_BITSET_: return futex_wake(uaddr, static_cast(val), val3); + case FUTEX_REQUEUE_: + return futex_requeue(uaddr, uaddr2, static_cast(val), static_cast(timeout), false, 0); + case FUTEX_CMP_REQUEUE_: + return futex_requeue(uaddr, uaddr2, static_cast(val), static_cast(timeout), true, val3); + case FUTEX_WAKE_OP_: + return futex_wake_op(uaddr, uaddr2, static_cast(val), static_cast(timeout), val3); + case FUTEX_LOCK_PI_: + case FUTEX_LOCK_PI2_: { + int64_t deadline; + if (int64_t e = timeout_deadline(timeout, true, cmd == FUTEX_LOCK_PI_ || realtime, deadline)) return e; + return futex_lock_pi(t, uaddr, deadline, false); + } + case FUTEX_TRYLOCK_PI_: return futex_lock_pi(t, uaddr, 0, true); + case FUTEX_UNLOCK_PI_: return futex_unlock_pi(t, uaddr); + default: + LOGW("futex", "unsupported futex op %d", op); + return -ENOSYS_; + } +} + +// --------------------------------------------------------------------------- +// Thread lifecycle + +static void register_thread(GuestThread* thread) { + std::lock_guard held(process().threads_lock); + process().threads[thread->tid] = thread; +} + +static void unregister_thread(GuestThread* thread) { + std::lock_guard held(process().threads_lock); + process().threads.erase(thread->tid); +} + +int64_t wake_futex(uint64_t address, int count) { return futex_wake(address, count, ~0u); } +int64_t wake_futex_bitset(uint64_t address, int count, uint32_t bitset) { return futex_wake(address, count, bitset); } + +GuestThread* find_thread(int tid) { + std::lock_guard held(process().threads_lock); + auto it = process().threads.find(tid); + return it == process().threads.end() ? nullptr : it->second; +} + +void register_main_thread(GuestThread* thread) { register_thread(thread); } + +// The end of a thread, as the kernel does it: CLONE_CHILD_CLEARTID's word is +// zeroed and woken (pthread_join waits on it), robust futexes are released. +void finish_thread(GuestThread& thread) { + if (thread.robust_list) { + // struct robust_list_head { next; futex_offset; list_op_pending; } + uint64_t head[3] = {}; + if (guest_read(thread.robust_list, head, sizeof head)) { + auto release = [&](uint64_t entry) { + uint64_t word = entry + head[1]; + uint32_t value = 0; + if (!guest_read(word, &value, 4)) return; + if ((value & FUTEX_TID_MASK_) != static_cast(thread.tid)) return; + uint32_t updated = (value & FUTEX_WAITERS_) | FUTEX_OWNER_DIED_; + guest_write(word, &updated, 4); + if (value & FUTEX_WAITERS_) futex_wake(word, 1, ~0u); + }; + uint64_t entry = head[0]; + for (int guard = 0; entry && entry != thread.robust_list && guard < 2048; ++guard) { + release(entry); + if (!guest_read(entry, &entry, 8)) break; + } + if (head[2]) release(head[2]); + } + } + if (thread.clear_child_tid) { + uint32_t zero = 0; + guest_write(thread.clear_child_tid, &zero, 4); + futex_wake(thread.clear_child_tid, 1 << 30, ~0u); + } + unregister_thread(&thread); +} + +static DWORD WINAPI thread_entry(void* param) { + auto* thread = static_cast(param); + set_current_thread(thread); + attach_cpu(*thread); + CpuContext* start = static_cast(thread->java_thread); // the child's first register state + thread->java_thread = nullptr; + load_context(*thread, *start); + delete start; + try { + run_guest(*thread, false); + } catch (const ThreadExit& exit) { + thread->exit_code = exit.code; + } + finish_thread(*thread); + detach_cpu(*thread); + if (thread->host_thread) CloseHandle(thread->host_thread); + delete thread; + return 0; +} + +// Host-side stacks are large because the Java interpreter and nested guest +// calls recurse on them; only touched pages are committed. +constexpr SIZE_T kHostStack = 64ull << 20; + +static int64_t do_clone(GuestThread& parent, uint64_t flags, uint64_t newsp, uint64_t ptid, uint64_t tls, uint64_t ctid) { + if (!(flags & CLONE_VM_) || (flags & CLONE_VFORK_)) { + // fork/vfork: a second address space is not something one process can + // provide. Callers (crash handlers, Runtime.exec) see the failure. + LOGW("thread", "fork-style clone (flags %llx) refused", (unsigned long long)flags); + return -ENOSYS_; + } + auto* child = new GuestThread(); + child->tid = process().next_tid.fetch_add(1); + child->tpidr = (flags & CLONE_SETTLS_) ? tls : parent.tpidr; + child->sigmask = parent.sigmask; + child->name = parent.name; + if (flags & CLONE_CHILD_CLEARTID_) child->clear_child_tid = ctid; + auto* start = new CpuContext(); + save_context(parent, *start); + start->x[0] = 0; + if (newsp) start->sp = newsp; + child->java_thread = start; + if (flags & CLONE_PARENT_SETTID_) guest_write(ptid, &child->tid, 4); + if (flags & CLONE_CHILD_SETTID_) guest_write(ctid, &child->tid, 4); + register_thread(child); + HANDLE h = CreateThread(nullptr, kHostStack, thread_entry, child, CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION, + &child->host_tid); + if (!h) { + unregister_thread(child); + delete start; + delete child; + return -EAGAIN_; + } + child->host_thread = h; + int tid = child->tid; + TRACE("thread", "clone flags %llx -> tid %d", (unsigned long long)flags, tid); + ResumeThread(h); + return tid; +} + +static int nice_to_priority(int nice) { + if (nice <= -16) return THREAD_PRIORITY_HIGHEST; + if (nice <= -8) return THREAD_PRIORITY_ABOVE_NORMAL; + if (nice < 5) return THREAD_PRIORITY_NORMAL; + if (nice < 15) return THREAD_PRIORITY_BELOW_NORMAL; + return THREAD_PRIORITY_LOWEST; +} + +int guest_cpu_count() { + static int count = [] { + if (const char* text = std::getenv("AXRB_DIRECT_CPUS")) return std::clamp(std::atoi(text), 1, 64); + // The processors this process may run on: fewer than the machine's + // when the player (or a core-parking tool) restricts it. + int processors = 0; + DWORD_PTR allowed = 0, system = 0; + if (GetProcessAffinityMask(GetCurrentProcess(), &allowed, &system)) processors = static_cast(__popcnt64(allowed)); + if (!processors) { + SYSTEM_INFO info; + GetSystemInfo(&info); + processors = static_cast(info.dwNumberOfProcessors); + } + return std::clamp(processors, 2, 8); + }(); + return count; +} + +int64_t sys_threads(GuestThread& t, uint64_t nr, const uint64_t* a, bool& handled) { + handled = true; + switch (nr) { + case 98: // futex + return do_futex(t, a[0], static_cast(a[1]), static_cast(a[2]), a[3], a[4], static_cast(a[5])); + case 220: // clone(flags, newsp, ptid, tls, ctid) + return do_clone(t, a[0], a[1], a[2], a[3], a[4]); + case 435: { // clone3 + uint64_t args[11] = {}; + if (!guest_read(a[0], args, std::min(a[1], sizeof args))) return -EFAULT_; + // flags, pidfd, child_tid, parent_tid, exit_signal, stack, stack_size, tls + uint64_t sp = args[5] ? args[5] + args[6] : 0; + return do_clone(t, args[0] | (args[4] & 0xff), sp, args[3], args[7], args[2]); + } + case 93: // exit + throw ThreadExit{static_cast(a[0])}; + case 94: { // exit_group + LOGI("process", "exit_group(%d)", static_cast(a[0])); + if (trace_enabled("exit")) { + dump_thread_state(t, "exit_group caller (diagnostic trace)"); + CpuContext context; + save_context(t, context); + auto describe_mapping = [](uint64_t address) { + address_space().for_each([&](const Vma& vma) { + if (address >= vma.start && address < vma.end) + LOGI("process", "exit address %llx: %s file-offset=%llx mapping=%llx-%llx", + (unsigned long long)address, vma.name.c_str(), + (unsigned long long)(vma.offset + address - vma.start), + (unsigned long long)vma.start, (unsigned long long)vma.end); + }); + }; + describe_mapping(context.pc); + describe_mapping(context.x[30]); + uint64_t fp = context.x[29]; + for (int depth = 0; depth < 16 && fp; ++depth) { + uint64_t frame[2]; + if ((fp & 7) || !guest_read(fp, frame, sizeof frame)) break; + describe_mapping(frame[1]); + if (frame[0] <= fp) break; + fp = frame[0]; + } + } + process().exiting = true; + log_flush(); + TerminateProcess(GetCurrentProcess(), static_cast(a[0] & 0xff)); + return 0; + } + case 96: // set_tid_address + t.clear_child_tid = a[0]; + return t.tid; + case 99: // set_robust_list + t.robust_list = a[0]; + t.robust_list_len = a[1]; + return 0; + case 100: { // get_robust_list + GuestThread* target = a[0] ? find_thread(static_cast(a[0])) : &t; + if (!target) return -ESRCH_; + guest_write(a[1], &target->robust_list, 8); + guest_write(a[2], &target->robust_list_len, 8); + return 0; + } + case 178: return t.tid; // gettid + case 172: return process().pid; // getpid + case 173: return 1; // getppid + case 124: SwitchToThread(); return 0; // sched_yield + case 122: return 0; // sched_setaffinity + case 123: { // sched_getaffinity + uint64_t size = a[1]; + if (size < 8) return -EINVAL_; + uint64_t mask = (guest_cpu_count() >= 64) ? ~0ull : ((1ull << guest_cpu_count()) - 1); + std::vector bytes(static_cast(std::min(size, 128)), 0); + std::memcpy(bytes.data(), &mask, 8); + if (!guest_write(a[2], bytes.data(), bytes.size())) return -EFAULT_; + return static_cast(bytes.size()); + } + case 118: case 119: return 0; // sched_setparam, sched_setscheduler + case 120: return 0; // sched_getscheduler: SCHED_OTHER + case 121: { // sched_getparam + int32_t zero = 0; + return guest_write(a[1], &zero, 4) ? 0 : -EFAULT_; + } + case 125: return (a[0] == 1 || a[0] == 2) ? 99 : 0; // sched_get_priority_max + case 126: return (a[0] == 1 || a[0] == 2) ? 1 : 0; // sched_get_priority_min + case 274: return 0; // sched_setattr + case 275: return -ENOSYS_; // sched_getattr + case 140: { // setpriority(which, who, nice) + if (a[0] == 0) { // PRIO_PROCESS: a thread id on Linux + GuestThread* target = a[1] ? find_thread(static_cast(a[1])) : &t; + if (target && target->host_thread) SetThreadPriority(target->host_thread, nice_to_priority(static_cast(a[2]))); + else if (target == &t) SetThreadPriority(GetCurrentThread(), nice_to_priority(static_cast(a[2]))); + } + return 0; + } + case 141: return 20; // getpriority: nice 0 + case 168: { // getcpu + uint32_t cpu = GetCurrentProcessorNumber() % static_cast(guest_cpu_count()), node = 0; + if (a[0]) guest_write(a[0], &cpu, 4); + if (a[1]) guest_write(a[1], &node, 4); + return 0; + } + case 101: // nanosleep + case 115: { // clock_nanosleep + uint64_t req = nr == 101 ? a[0] : a[2], rem = nr == 101 ? a[1] : a[3]; + bool absolute = nr == 115 && (a[1] & TIMER_ABSTIME_); + int64_t deadline; + if (int64_t e = timeout_deadline(req, absolute, nr == 115 && a[0] == CLOCK_REALTIME_, deadline)) return e; + if (deadline == -1) return 0; + int64_t r = block_until(t, [] { return false; }, deadline, "sleep"); + if (r == -EINTR_) { + if (rem && !absolute) { + int64_t left = scale_time(std::max(0, deadline - ticks_now()), 1000000000, ticks_per_second()); + ktimespec out{left / 1000000000, left % 1000000000}; + guest_write(rem, &out, sizeof out); + } + return -EINTR_; + } + return 0; + } + default: + handled = false; + return 0; + } +} + +} // namespace axrb diff --git a/direct/src/time_math.h b/direct/src/time_math.h new file mode 100644 index 0000000..2f55d91 --- /dev/null +++ b/direct/src/time_math.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace axrb { +// Timeouts can be decades in the future (including effectively infinite +// library waits). A multiply before a divide must not wrap into a past date. +inline int64_t scale_time(int64_t value, int64_t numerator, int64_t denominator) { + if (value <= 0) return 0; + constexpr uint64_t limit = static_cast(std::numeric_limits::max()); + uint64_t high, remainder; + const uint64_t low = _umul128(static_cast(value), static_cast(numerator), &high); + if (high >= static_cast(denominator)) return static_cast(limit); + const uint64_t result = _udiv128(high, low, static_cast(denominator), &remainder); + return static_cast(result > limit ? limit : result); +} + +inline int64_t add_time(int64_t a, int64_t b) { + constexpr int64_t limit = std::numeric_limits::max(); + return b > limit - a ? limit : a + b; +} +} // namespace axrb diff --git a/direct/src/vfs.cpp b/direct/src/vfs.cpp new file mode 100644 index 0000000..457f2bf --- /dev/null +++ b/direct/src/vfs.cpp @@ -0,0 +1,308 @@ +#include "vfs.h" + +#include "linux.h" +#include "log.h" + +#include + +#include +#include +#include + +namespace axrb { +using namespace linux_abi; + +Vfs& vfs() { + static Vfs instance; + return instance; +} + +std::wstring widen(const std::string& utf8) { + if (utf8.empty()) return {}; + int n = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); + std::wstring out(static_cast(n), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), out.data(), n); + return out; +} + +std::string narrow(const std::wstring& wide) { + if (wide.empty()) return {}; + int n = WideCharToMultiByte(CP_UTF8, 0, wide.data(), static_cast(wide.size()), nullptr, 0, nullptr, nullptr); + std::string out(static_cast(n), '\0'); + WideCharToMultiByte(CP_UTF8, 0, wide.data(), static_cast(wide.size()), out.data(), n, nullptr, nullptr); + return out; +} + +// Characters Windows refuses in a name go to U+F000 + c, the convention +// Cygwin and WSL use, so they show up recognisably in Explorer. +std::wstring escape_component(const std::string& name) { + std::wstring wide = widen(name); + std::wstring out; + out.reserve(wide.size()); + for (size_t i = 0; i < wide.size(); ++i) { + wchar_t c = wide[i]; + bool trailing = (i + 1 == wide.size()) && (c == L'.' || c == L' ') && name != "." && name != ".."; + if (c < 0x20 || c == L':' || c == L'*' || c == L'?' || c == L'"' || c == L'<' || c == L'>' || c == L'|' || + c == L'\\' || trailing) + out.push_back(static_cast(0xf000 + c)); + else + out.push_back(c); + } + return out; +} + +std::string unescape_component(const std::wstring& name) { + std::wstring out = name; + for (auto& c : out) + if (c >= 0xf000 && c < 0xf080) c = static_cast(c - 0xf000); + return narrow(out); +} + +static std::vector split_path(const std::string& path) { + std::vector parts; + size_t at = 0; + while (at < path.size()) { + size_t slash = path.find('/', at); + if (slash == std::string::npos) slash = path.size(); + if (slash > at) parts.push_back(path.substr(at, slash - at)); + at = slash + 1; + } + return parts; +} + +std::string Vfs::normalize(const std::string& base, const std::string& path) { + std::string joined = (!path.empty() && path[0] == '/') ? path : (base + "/" + path); + std::vector out; + for (auto& part : split_path(joined)) { + if (part == ".") continue; + if (part == "..") { + if (!out.empty()) out.pop_back(); + continue; + } + out.push_back(part); + } + std::string result; + for (auto& part : out) result += "/" + part; + return result.empty() ? "/" : result; +} + +void Vfs::add_mount(const std::string& guest, const std::wstring& host, bool read_only, bool is_file) { + // Mount ancestors in writable storage must also exist on the host. A + // purely virtual Android/obb ancestor otherwise makes File.mkdirs stop + // early, and creating its Android/data sibling fails with ENOENT. + const std::string normalized = normalize("/", guest); + const size_t slash = normalized.rfind('/'); + if (slash != std::string::npos && slash > 0) { + Resolved parent; + if (!resolve(normalized.substr(0, slash), true, parent) && parent.mount && + !parent.mount->read_only && !parent.host.empty()) { + std::error_code error; + std::filesystem::create_directories(parent.host, error); + if (error) LOGW("vfs", "cannot create mount parent for %s: %s", guest.c_str(), error.message().c_str()); + } + } + std::unique_lock held(lock_); + std::string prefix = guest == "/" ? "" : normalize("/", guest); + mounts_.erase(std::remove_if(mounts_.begin(), mounts_.end(), [&](const Mount& m) { return m.guest == prefix; }), + mounts_.end()); + mounts_.push_back({prefix, host, read_only, is_file}); + std::sort(mounts_.begin(), mounts_.end(), [](const Mount& a, const Mount& b) { return a.guest.size() > b.guest.size(); }); +} + +void Vfs::add_symlink(const std::string& guest, const std::string& target) { + std::unique_lock held(lock_); + symlinks_[normalize("/", guest)] = target; +} + +void Vfs::add_synthetic(const std::string& guest, SyntheticReader reader) { + std::unique_lock held(lock_); + synthetic_[normalize("/", guest)] = std::move(reader); +} + +void Vfs::add_synthetic_dir(const std::string& guest, std::vector children) { + std::unique_lock held(lock_); + synthetic_dirs_[normalize("/", guest)] = std::move(children); +} + +void Vfs::add_device(const std::string& guest, const std::string& device) { + std::unique_lock held(lock_); + devices_[normalize("/", guest)] = device; +} + +void Vfs::set_dynamic_dir(const std::string& prefix, std::function resolver) { + std::unique_lock held(lock_); + dynamic_.push_back({normalize("/", prefix), std::move(resolver)}); +} + +std::wstring Vfs::host_path_for(const Mount& mount, const std::string& guest) const { + std::wstring host = mount.host; + if (mount.is_file) return host; + std::string rest = guest.substr(mount.guest.size()); + for (auto& part : split_path(rest)) { + if (!host.empty() && host.back() != L'\\') host.push_back(L'\\'); + host += escape_component(part); + } + return host; +} + +static bool host_exists(const std::wstring& path) { + return GetFileAttributesW(path.c_str()) != INVALID_FILE_ATTRIBUTES; +} + +int Vfs::resolve_once(const std::string& path, Resolved& out) const { + out = Resolved{}; + out.guest = path; + if (auto it = devices_.find(path); it != devices_.end()) { + out.kind = Resolved::Device; + out.device = it->second; + return 0; + } + if (auto it = synthetic_.find(path); it != synthetic_.end()) { + out.kind = Resolved::Synthetic; + out.reader = it->second; + out.read_only = true; + return 0; + } + if (auto it = synthetic_dirs_.find(path); it != synthetic_dirs_.end()) { + out.kind = Resolved::SyntheticDir; + out.children = it->second; + out.read_only = true; + return 0; + } + for (const auto& [prefix, resolver] : dynamic_) { + if (path == prefix || (path.size() > prefix.size() && path.compare(0, prefix.size(), prefix) == 0 && + path[prefix.size()] == '/')) { + std::string rest = path.substr(prefix.size()); + if (resolver(rest, out)) { + out.guest = path; + return out.kind == Resolved::None ? -ENOENT_ : 0; + } + } + } + for (const Mount& mount : mounts_) { + bool match = mount.guest.empty() || path == mount.guest || + (path.size() > mount.guest.size() && path.compare(0, mount.guest.size(), mount.guest) == 0 && + path[mount.guest.size()] == '/'); + if (!match) continue; + if (mount.is_file && path != mount.guest) return -ENOTDIR_; + out.kind = Resolved::Host; + out.mount = &mount; + out.read_only = mount.read_only; + out.host = host_path_for(mount, path); + if (!host_exists(out.host)) { + // A directory that exists only because something is mounted or + // synthesized beneath it (/data, /proc, /sys/devices...). + std::string below = path == "/" ? "/" : path + "/"; + bool parent_of_something = false; + for (const Mount& other : mounts_) + if (!other.guest.empty() && other.guest.compare(0, below.size(), below) == 0) parent_of_something = true; + for (const auto& [name, reader] : synthetic_) + if (name.compare(0, below.size(), below) == 0) parent_of_something = true; + for (const auto& [name, children] : synthetic_dirs_) + if (name.compare(0, below.size(), below) == 0) parent_of_something = true; + for (const auto& [name, target] : symlinks_) + if (name.compare(0, below.size(), below) == 0) parent_of_something = true; + for (const auto& [name, device] : devices_) + if (name.compare(0, below.size(), below) == 0) parent_of_something = true; + for (const auto& [name, resolver] : dynamic_) + if (name.compare(0, below.size(), below) == 0) parent_of_something = true; + if (parent_of_something) { + out.kind = Resolved::SyntheticDir; + out.read_only = true; + } + } + return 0; + } + return -ENOENT_; +} + +bool Vfs::dynamic_link(const std::string& path, std::string& target) const { + for (const auto& [prefix, resolver] : dynamic_) { + if (path.size() > prefix.size() && path.compare(0, prefix.size(), prefix) == 0 && path[prefix.size()] == '/') { + Resolved r; + if (resolver(path.substr(prefix.size()), r) && r.kind == Resolved::None && !r.guest.empty()) { + target = r.guest; // dynamic symlink: resolver reports the target in `guest` + return true; + } + } + } + return false; +} + +int Vfs::resolve(const std::string& absolute, bool follow, Resolved& out) const { + std::shared_lock held(lock_); + std::string path = normalize("/", absolute); + for (int hops = 0; hops < 40; ++hops) { + auto parts = split_path(path); + std::string current; + bool restarted = false; + for (size_t i = 0; i < parts.size(); ++i) { + std::string candidate = current + "/" + parts[i]; + bool last = i + 1 == parts.size(); + std::string target; + bool is_link = false; + if (!last || follow) { + auto link = symlinks_.find(candidate); + if (link != symlinks_.end()) { + target = link->second; + is_link = true; + } else { + is_link = dynamic_link(candidate, target); + } + } + if (is_link) { + std::string base = current.empty() ? "/" : current; + std::string next = normalize(base, target); + for (size_t k = i + 1; k < parts.size(); ++k) next += "/" + parts[k]; + path = normalize("/", next); + restarted = true; + break; + } + current = candidate; + } + if (!restarted) return resolve_once(current.empty() ? "/" : current, out); + } + return -ELOOP_; +} + +bool Vfs::readlink(const std::string& absolute, std::string& target) const { + // Links in the parent directories are followed (/proc/self/fd/3). + std::string path = normalize("/", absolute); + size_t slash = path.rfind('/'); + if (slash != 0 && slash != std::string::npos) { + Resolved parent; + if (resolve(path.substr(0, slash), true, parent) == 0) path = (parent.guest == "/" ? "" : parent.guest) + path.substr(slash); + } + std::shared_lock held(lock_); + auto it = symlinks_.find(path); + if (it != symlinks_.end()) { + target = it->second; + return true; + } + return dynamic_link(path, target); +} + +std::vector> Vfs::virtual_children(const std::string& dir) const { + std::shared_lock held(lock_); + std::string below = dir == "/" ? "/" : dir + "/"; + std::map names; + auto consider = [&](const std::string& full, uint8_t type) { + if (full.size() <= below.size() || full.compare(0, below.size(), below) != 0) return; + std::string rest = full.substr(below.size()); + size_t slash = rest.find('/'); + if (slash == std::string::npos) names.emplace(rest, type); + else names.emplace(rest.substr(0, slash), DT_DIR_); + }; + for (const Mount& m : mounts_) + if (!m.guest.empty()) consider(m.guest, m.is_file ? DT_REG_ : DT_DIR_); + for (const auto& [name, reader] : synthetic_) consider(name, DT_REG_); + for (const auto& [name, children] : synthetic_dirs_) consider(name, DT_DIR_); + for (const auto& [name, target] : symlinks_) consider(name, DT_LNK_); + for (const auto& [name, device] : devices_) consider(name, DT_CHR_); + for (const auto& [name, resolver] : dynamic_) consider(name, DT_DIR_); + if (auto it = synthetic_dirs_.find(dir == "/" ? "/" : dir); it != synthetic_dirs_.end()) + for (auto& child : it->second) names.emplace(child, DT_UNKNOWN_); + return {names.begin(), names.end()}; +} + +} // namespace axrb diff --git a/direct/src/vfs.h b/direct/src/vfs.h new file mode 100644 index 0000000..e15928f --- /dev/null +++ b/direct/src/vfs.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { + +// The guest file system: a Linux path namespace assembled from mounts. +// +// / the Android system root (read-only, from the SDK image) +// /data/app/ the APK (in place) and its extracted native libraries +// /data/data/ private storage (saves), persistent across sessions +// /sdcard shared storage; Android/obb/ is the game's download folder +// /proc, /sys synthesized +// +// Host paths never leak into the guest, and guest names that Windows cannot +// store (':' '*' '?' '"' '<' '>' '|' '\\', trailing dots/spaces) are escaped +// into the private-use area so any Linux name round-trips. +struct Mount { + std::string guest; // normalized guest prefix, no trailing slash ("" for /) + std::wstring host; // host directory or file + bool read_only = false; + bool is_file = false; // a single file mounted at `guest` +}; + +using SyntheticReader = std::function; + +struct Resolved { + enum Kind { None, Host, Synthetic, SyntheticDir, Device } kind = None; + std::string guest; // canonical guest path (symlinks resolved) + std::wstring host; // for Host + const Mount* mount = nullptr; + bool read_only = false; + SyntheticReader reader; // for Synthetic + std::vector children; // for SyntheticDir + std::string device; // for Device (null, zero, urandom, ...) +}; + +class Vfs { +public: + void add_mount(const std::string& guest, const std::wstring& host, bool read_only, bool is_file = false); + void add_symlink(const std::string& guest, const std::string& target); + void add_synthetic(const std::string& guest, SyntheticReader reader); + void add_synthetic_dir(const std::string& guest, std::vector children); + void add_device(const std::string& guest, const std::string& device); + void set_dynamic_dir(const std::string& prefix, + std::function resolver); + + // Normalizes `path` against `base` (an absolute directory) without + // resolving symlinks. + static std::string normalize(const std::string& base, const std::string& path); + // Resolves symlinks in every component (the last one too, if follow). + // Returns 0 or -errno. + int resolve(const std::string& absolute, bool follow, Resolved& out) const; + // The symlink target of `absolute` itself, if it is one we know. + bool readlink(const std::string& absolute, std::string& target) const; + // Lists names a directory has beyond what the host directory holds + // (mount points, synthetic entries, symlinks). + std::vector> virtual_children(const std::string& dir) const; + + std::wstring host_path_for(const Mount& mount, const std::string& guest) const; + +private: + int resolve_once(const std::string& path, Resolved& out) const; + bool dynamic_link(const std::string& path, std::string& target) const; + + mutable std::shared_mutex lock_; + std::vector mounts_; // sorted longest guest prefix first + std::map symlinks_; + std::map synthetic_; + std::map> synthetic_dirs_; + std::map devices_; + std::vector>> dynamic_; +}; + +Vfs& vfs(); + +std::wstring widen(const std::string& utf8); +std::string narrow(const std::wstring& wide); +// Linux file name component <-> Windows file name component. +std::wstring escape_component(const std::string& name); +std::string unescape_component(const std::wstring& name); + +} // namespace axrb diff --git a/direct/src/vk_entries.inc b/direct/src/vk_entries.inc new file mode 100644 index 0000000..2d77f13 --- /dev/null +++ b/direct/src/vk_entries.inc @@ -0,0 +1,628 @@ +// Generated by direct/tools/gen_vulkan.py from the NDK's Vulkan headers. Do not edit. +// 624 commands +static const VkCommand kCommands[] = { + {"vkCreateInstance", kGlobal, &Invoker::call}, + {"vkDestroyInstance", kInstance, &Invoker::call}, + {"vkEnumeratePhysicalDevices", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceFeatures", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceFormatProperties", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceImageFormatProperties", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceProperties", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceQueueFamilyProperties", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceMemoryProperties", kInstance, &Invoker::call}, + {"vkCreateDevice", kInstance, &Invoker::call}, + {"vkDestroyDevice", kDevice, &Invoker::call}, + {"vkEnumerateInstanceExtensionProperties", kGlobal, &Invoker::call}, + {"vkEnumerateDeviceExtensionProperties", kInstance, &Invoker::call}, + {"vkEnumerateInstanceLayerProperties", kGlobal, &Invoker::call}, + {"vkEnumerateDeviceLayerProperties", kInstance, &Invoker::call}, + {"vkGetDeviceQueue", kDevice, &Invoker::call}, + {"vkQueueSubmit", kDevice, &Invoker::call}, + {"vkQueueWaitIdle", kDevice, &Invoker::call}, + {"vkDeviceWaitIdle", kDevice, &Invoker::call}, + {"vkAllocateMemory", kDevice, &Invoker::call}, + {"vkFreeMemory", kDevice, &Invoker::call}, + {"vkMapMemory", kDevice, &Invoker::call}, + {"vkUnmapMemory", kDevice, &Invoker::call}, + {"vkFlushMappedMemoryRanges", kDevice, &Invoker::call}, + {"vkInvalidateMappedMemoryRanges", kDevice, &Invoker::call}, + {"vkGetDeviceMemoryCommitment", kDevice, &Invoker::call}, + {"vkBindBufferMemory", kDevice, &Invoker::call}, + {"vkBindImageMemory", kDevice, &Invoker::call}, + {"vkGetBufferMemoryRequirements", kDevice, &Invoker::call}, + {"vkGetImageMemoryRequirements", kDevice, &Invoker::call}, + {"vkGetImageSparseMemoryRequirements", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceSparseImageFormatProperties", kInstance, &Invoker::call}, + {"vkQueueBindSparse", kDevice, &Invoker::call}, + {"vkCreateFence", kDevice, &Invoker::call}, + {"vkDestroyFence", kDevice, &Invoker::call}, + {"vkResetFences", kDevice, &Invoker::call}, + {"vkGetFenceStatus", kDevice, &Invoker::call}, + {"vkWaitForFences", kDevice, &Invoker::call}, + {"vkCreateSemaphore", kDevice, &Invoker::call}, + {"vkDestroySemaphore", kDevice, &Invoker::call}, + {"vkCreateEvent", kDevice, &Invoker::call}, + {"vkDestroyEvent", kDevice, &Invoker::call}, + {"vkGetEventStatus", kDevice, &Invoker::call}, + {"vkSetEvent", kDevice, &Invoker::call}, + {"vkResetEvent", kDevice, &Invoker::call}, + {"vkCreateQueryPool", kDevice, &Invoker::call}, + {"vkDestroyQueryPool", kDevice, &Invoker::call}, + {"vkGetQueryPoolResults", kDevice, &Invoker::call}, + {"vkCreateBuffer", kDevice, &Invoker::call}, + {"vkDestroyBuffer", kDevice, &Invoker::call}, + {"vkCreateBufferView", kDevice, &Invoker::call}, + {"vkDestroyBufferView", kDevice, &Invoker::call}, + {"vkCreateImage", kDevice, &Invoker::call}, + {"vkDestroyImage", kDevice, &Invoker::call}, + {"vkGetImageSubresourceLayout", kDevice, &Invoker::call}, + {"vkCreateImageView", kDevice, &Invoker::call}, + {"vkDestroyImageView", kDevice, &Invoker::call}, + {"vkCreateShaderModule", kDevice, &Invoker::call}, + {"vkDestroyShaderModule", kDevice, &Invoker::call}, + {"vkCreatePipelineCache", kDevice, &Invoker::call}, + {"vkDestroyPipelineCache", kDevice, &Invoker::call}, + {"vkGetPipelineCacheData", kDevice, &Invoker::call}, + {"vkMergePipelineCaches", kDevice, &Invoker::call}, + {"vkCreateGraphicsPipelines", kDevice, &Invoker::call}, + {"vkCreateComputePipelines", kDevice, &Invoker::call}, + {"vkDestroyPipeline", kDevice, &Invoker::call}, + {"vkCreatePipelineLayout", kDevice, &Invoker::call}, + {"vkDestroyPipelineLayout", kDevice, &Invoker::call}, + {"vkCreateSampler", kDevice, &Invoker::call}, + {"vkDestroySampler", kDevice, &Invoker::call}, + {"vkCreateDescriptorSetLayout", kDevice, &Invoker::call}, + {"vkDestroyDescriptorSetLayout", kDevice, &Invoker::call}, + {"vkCreateDescriptorPool", kDevice, &Invoker::call}, + {"vkDestroyDescriptorPool", kDevice, &Invoker::call}, + {"vkResetDescriptorPool", kDevice, &Invoker::call}, + {"vkAllocateDescriptorSets", kDevice, &Invoker::call}, + {"vkFreeDescriptorSets", kDevice, &Invoker::call}, + {"vkUpdateDescriptorSets", kDevice, &Invoker::call}, + {"vkCreateFramebuffer", kDevice, &Invoker::call}, + {"vkDestroyFramebuffer", kDevice, &Invoker::call}, + {"vkCreateRenderPass", kDevice, &Invoker::call}, + {"vkDestroyRenderPass", kDevice, &Invoker::call}, + {"vkGetRenderAreaGranularity", kDevice, &Invoker::call}, + {"vkCreateCommandPool", kDevice, &Invoker::call}, + {"vkDestroyCommandPool", kDevice, &Invoker::call}, + {"vkResetCommandPool", kDevice, &Invoker::call}, + {"vkAllocateCommandBuffers", kDevice, &Invoker::call}, + {"vkFreeCommandBuffers", kDevice, &Invoker::call}, + {"vkBeginCommandBuffer", kDevice, &Invoker::call}, + {"vkEndCommandBuffer", kDevice, &Invoker::call}, + {"vkResetCommandBuffer", kDevice, &Invoker::call}, + {"vkCmdBindPipeline", kDevice, &Invoker::call}, + {"vkCmdSetViewport", kDevice, &Invoker::call}, + {"vkCmdSetScissor", kDevice, &Invoker::call}, + {"vkCmdSetLineWidth", kDevice, &Invoker::call}, + {"vkCmdSetDepthBias", kDevice, &Invoker::call}, + {"vkCmdSetBlendConstants", kDevice, &Invoker::call}, + {"vkCmdSetDepthBounds", kDevice, &Invoker::call}, + {"vkCmdSetStencilCompareMask", kDevice, &Invoker::call}, + {"vkCmdSetStencilWriteMask", kDevice, &Invoker::call}, + {"vkCmdSetStencilReference", kDevice, &Invoker::call}, + {"vkCmdBindDescriptorSets", kDevice, &Invoker::call}, + {"vkCmdBindIndexBuffer", kDevice, &Invoker::call}, + {"vkCmdBindVertexBuffers", kDevice, &Invoker::call}, + {"vkCmdDraw", kDevice, &Invoker::call}, + {"vkCmdDrawIndexed", kDevice, &Invoker::call}, + {"vkCmdDrawIndirect", kDevice, &Invoker::call}, + {"vkCmdDrawIndexedIndirect", kDevice, &Invoker::call}, + {"vkCmdDispatch", kDevice, &Invoker::call}, + {"vkCmdDispatchIndirect", kDevice, &Invoker::call}, + {"vkCmdCopyBuffer", kDevice, &Invoker::call}, + {"vkCmdCopyImage", kDevice, &Invoker::call}, + {"vkCmdBlitImage", kDevice, &Invoker::call}, + {"vkCmdCopyBufferToImage", kDevice, &Invoker::call}, + {"vkCmdCopyImageToBuffer", kDevice, &Invoker::call}, + {"vkCmdUpdateBuffer", kDevice, &Invoker::call}, + {"vkCmdFillBuffer", kDevice, &Invoker::call}, + {"vkCmdClearColorImage", kDevice, &Invoker::call}, + {"vkCmdClearDepthStencilImage", kDevice, &Invoker::call}, + {"vkCmdClearAttachments", kDevice, &Invoker::call}, + {"vkCmdResolveImage", kDevice, &Invoker::call}, + {"vkCmdSetEvent", kDevice, &Invoker::call}, + {"vkCmdResetEvent", kDevice, &Invoker::call}, + {"vkCmdWaitEvents", kDevice, &Invoker::call}, + {"vkCmdPipelineBarrier", kDevice, &Invoker::call}, + {"vkCmdBeginQuery", kDevice, &Invoker::call}, + {"vkCmdEndQuery", kDevice, &Invoker::call}, + {"vkCmdResetQueryPool", kDevice, &Invoker::call}, + {"vkCmdWriteTimestamp", kDevice, &Invoker::call}, + {"vkCmdCopyQueryPoolResults", kDevice, &Invoker::call}, + {"vkCmdPushConstants", kDevice, &Invoker::call}, + {"vkCmdBeginRenderPass", kDevice, &Invoker::call}, + {"vkCmdNextSubpass", kDevice, &Invoker::call}, + {"vkCmdEndRenderPass", kDevice, &Invoker::call}, + {"vkCmdExecuteCommands", kDevice, &Invoker::call}, + {"vkEnumerateInstanceVersion", kGlobal, &Invoker::call}, + {"vkBindBufferMemory2", kDevice, &Invoker::call}, + {"vkBindImageMemory2", kDevice, &Invoker::call}, + {"vkGetDeviceGroupPeerMemoryFeatures", kDevice, &Invoker::call}, + {"vkCmdSetDeviceMask", kDevice, &Invoker::call}, + {"vkCmdDispatchBase", kDevice, &Invoker::call}, + {"vkEnumeratePhysicalDeviceGroups", kInstance, &Invoker::call}, + {"vkGetImageMemoryRequirements2", kDevice, &Invoker::call}, + {"vkGetBufferMemoryRequirements2", kDevice, &Invoker::call}, + {"vkGetImageSparseMemoryRequirements2", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceFeatures2", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceProperties2", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceFormatProperties2", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceImageFormatProperties2", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceQueueFamilyProperties2", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceMemoryProperties2", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSparseImageFormatProperties2", kInstance, &Invoker::call}, + {"vkTrimCommandPool", kDevice, &Invoker::call}, + {"vkGetDeviceQueue2", kDevice, &Invoker::call}, + {"vkCreateSamplerYcbcrConversion", kDevice, &Invoker::call}, + {"vkDestroySamplerYcbcrConversion", kDevice, &Invoker::call}, + {"vkCreateDescriptorUpdateTemplate", kDevice, &Invoker::call}, + {"vkDestroyDescriptorUpdateTemplate", kDevice, &Invoker::call}, + {"vkUpdateDescriptorSetWithTemplate", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceExternalBufferProperties", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceExternalFenceProperties", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceExternalSemaphoreProperties", kInstance, &Invoker::call}, + {"vkGetDescriptorSetLayoutSupport", kDevice, &Invoker::call}, + {"vkCmdDrawIndirectCount", kDevice, &Invoker::call}, + {"vkCmdDrawIndexedIndirectCount", kDevice, &Invoker::call}, + {"vkCreateRenderPass2", kDevice, &Invoker::call}, + {"vkCmdBeginRenderPass2", kDevice, &Invoker::call}, + {"vkCmdNextSubpass2", kDevice, &Invoker::call}, + {"vkCmdEndRenderPass2", kDevice, &Invoker::call}, + {"vkResetQueryPool", kDevice, &Invoker::call}, + {"vkGetSemaphoreCounterValue", kDevice, &Invoker::call}, + {"vkWaitSemaphores", kDevice, &Invoker::call}, + {"vkSignalSemaphore", kDevice, &Invoker::call}, + {"vkGetBufferDeviceAddress", kDevice, &Invoker::call}, + {"vkGetBufferOpaqueCaptureAddress", kDevice, &Invoker::call}, + {"vkGetDeviceMemoryOpaqueCaptureAddress", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceToolProperties", kInstance, &Invoker::call}, + {"vkCreatePrivateDataSlot", kDevice, &Invoker::call}, + {"vkDestroyPrivateDataSlot", kDevice, &Invoker::call}, + {"vkSetPrivateData", kDevice, &Invoker::call}, + {"vkGetPrivateData", kDevice, &Invoker::call}, + {"vkCmdSetEvent2", kDevice, &Invoker::call}, + {"vkCmdResetEvent2", kDevice, &Invoker::call}, + {"vkCmdWaitEvents2", kDevice, &Invoker::call}, + {"vkCmdPipelineBarrier2", kDevice, &Invoker::call}, + {"vkCmdWriteTimestamp2", kDevice, &Invoker::call}, + {"vkQueueSubmit2", kDevice, &Invoker::call}, + {"vkCmdCopyBuffer2", kDevice, &Invoker::call}, + {"vkCmdCopyImage2", kDevice, &Invoker::call}, + {"vkCmdCopyBufferToImage2", kDevice, &Invoker::call}, + {"vkCmdCopyImageToBuffer2", kDevice, &Invoker::call}, + {"vkCmdBlitImage2", kDevice, &Invoker::call}, + {"vkCmdResolveImage2", kDevice, &Invoker::call}, + {"vkCmdBeginRendering", kDevice, &Invoker::call}, + {"vkCmdEndRendering", kDevice, &Invoker::call}, + {"vkCmdSetCullMode", kDevice, &Invoker::call}, + {"vkCmdSetFrontFace", kDevice, &Invoker::call}, + {"vkCmdSetPrimitiveTopology", kDevice, &Invoker::call}, + {"vkCmdSetViewportWithCount", kDevice, &Invoker::call}, + {"vkCmdSetScissorWithCount", kDevice, &Invoker::call}, + {"vkCmdBindVertexBuffers2", kDevice, &Invoker::call}, + {"vkCmdSetDepthTestEnable", kDevice, &Invoker::call}, + {"vkCmdSetDepthWriteEnable", kDevice, &Invoker::call}, + {"vkCmdSetDepthCompareOp", kDevice, &Invoker::call}, + {"vkCmdSetDepthBoundsTestEnable", kDevice, &Invoker::call}, + {"vkCmdSetStencilTestEnable", kDevice, &Invoker::call}, + {"vkCmdSetStencilOp", kDevice, &Invoker::call}, + {"vkCmdSetRasterizerDiscardEnable", kDevice, &Invoker::call}, + {"vkCmdSetDepthBiasEnable", kDevice, &Invoker::call}, + {"vkCmdSetPrimitiveRestartEnable", kDevice, &Invoker::call}, + {"vkGetDeviceBufferMemoryRequirements", kDevice, &Invoker::call}, + {"vkGetDeviceImageMemoryRequirements", kDevice, &Invoker::call}, + {"vkGetDeviceImageSparseMemoryRequirements", kDevice, &Invoker::call}, + {"vkDestroySurfaceKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSurfaceSupportKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSurfaceCapabilitiesKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSurfaceFormatsKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSurfacePresentModesKHR", kInstance, &Invoker::call}, + {"vkCreateSwapchainKHR", kDevice, &Invoker::call}, + {"vkDestroySwapchainKHR", kDevice, &Invoker::call}, + {"vkGetSwapchainImagesKHR", kDevice, &Invoker::call}, + {"vkAcquireNextImageKHR", kDevice, &Invoker::call}, + {"vkQueuePresentKHR", kDevice, &Invoker::call}, + {"vkGetDeviceGroupPresentCapabilitiesKHR", kDevice, &Invoker::call}, + {"vkGetDeviceGroupSurfacePresentModesKHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDevicePresentRectanglesKHR", kInstance, &Invoker::call}, + {"vkAcquireNextImage2KHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceDisplayPropertiesKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceDisplayPlanePropertiesKHR", kInstance, &Invoker::call}, + {"vkGetDisplayPlaneSupportedDisplaysKHR", kInstance, &Invoker::call}, + {"vkGetDisplayModePropertiesKHR", kInstance, &Invoker::call}, + {"vkCreateDisplayModeKHR", kInstance, &Invoker::call}, + {"vkGetDisplayPlaneCapabilitiesKHR", kInstance, &Invoker::call}, + {"vkCreateDisplayPlaneSurfaceKHR", kInstance, &Invoker::call}, + {"vkCreateSharedSwapchainsKHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceVideoCapabilitiesKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceVideoFormatPropertiesKHR", kInstance, &Invoker::call}, + {"vkCreateVideoSessionKHR", kDevice, &Invoker::call}, + {"vkDestroyVideoSessionKHR", kDevice, &Invoker::call}, + {"vkGetVideoSessionMemoryRequirementsKHR", kDevice, &Invoker::call}, + {"vkBindVideoSessionMemoryKHR", kDevice, &Invoker::call}, + {"vkCreateVideoSessionParametersKHR", kDevice, &Invoker::call}, + {"vkUpdateVideoSessionParametersKHR", kDevice, &Invoker::call}, + {"vkDestroyVideoSessionParametersKHR", kDevice, &Invoker::call}, + {"vkCmdBeginVideoCodingKHR", kDevice, &Invoker::call}, + {"vkCmdEndVideoCodingKHR", kDevice, &Invoker::call}, + {"vkCmdControlVideoCodingKHR", kDevice, &Invoker::call}, + {"vkCmdDecodeVideoKHR", kDevice, &Invoker::call}, + {"vkCmdBeginRenderingKHR", kDevice, &Invoker::call}, + {"vkCmdEndRenderingKHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceFeatures2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceProperties2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceFormatProperties2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceImageFormatProperties2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceQueueFamilyProperties2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceMemoryProperties2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSparseImageFormatProperties2KHR", kInstance, &Invoker::call}, + {"vkGetDeviceGroupPeerMemoryFeaturesKHR", kDevice, &Invoker::call}, + {"vkCmdSetDeviceMaskKHR", kDevice, &Invoker::call}, + {"vkCmdDispatchBaseKHR", kDevice, &Invoker::call}, + {"vkTrimCommandPoolKHR", kDevice, &Invoker::call}, + {"vkEnumeratePhysicalDeviceGroupsKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceExternalBufferPropertiesKHR", kInstance, &Invoker::call}, + {"vkGetMemoryFdKHR", kDevice, &Invoker::call}, + {"vkGetMemoryFdPropertiesKHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceExternalSemaphorePropertiesKHR", kInstance, &Invoker::call}, + {"vkImportSemaphoreFdKHR", kDevice, &Invoker::call}, + {"vkGetSemaphoreFdKHR", kDevice, &Invoker::call}, + {"vkCmdPushDescriptorSetKHR", kDevice, &Invoker::call}, + {"vkCmdPushDescriptorSetWithTemplateKHR", kDevice, &Invoker::call}, + {"vkCreateDescriptorUpdateTemplateKHR", kDevice, &Invoker::call}, + {"vkDestroyDescriptorUpdateTemplateKHR", kDevice, &Invoker::call}, + {"vkUpdateDescriptorSetWithTemplateKHR", kDevice, &Invoker::call}, + {"vkCreateRenderPass2KHR", kDevice, &Invoker::call}, + {"vkCmdBeginRenderPass2KHR", kDevice, &Invoker::call}, + {"vkCmdNextSubpass2KHR", kDevice, &Invoker::call}, + {"vkCmdEndRenderPass2KHR", kDevice, &Invoker::call}, + {"vkGetSwapchainStatusKHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceExternalFencePropertiesKHR", kInstance, &Invoker::call}, + {"vkImportFenceFdKHR", kDevice, &Invoker::call}, + {"vkGetFenceFdKHR", kDevice, &Invoker::call}, + {"vkEnumeratePhysicalDeviceQueueFamilyPerformanceQueryCountersKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceQueueFamilyPerformanceQueryPassesKHR", kInstance, &Invoker::call}, + {"vkAcquireProfilingLockKHR", kDevice, &Invoker::call}, + {"vkReleaseProfilingLockKHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceSurfaceCapabilities2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSurfaceFormats2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceDisplayProperties2KHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceDisplayPlaneProperties2KHR", kInstance, &Invoker::call}, + {"vkGetDisplayModeProperties2KHR", kInstance, &Invoker::call}, + {"vkGetDisplayPlaneCapabilities2KHR", kInstance, &Invoker::call}, + {"vkGetImageMemoryRequirements2KHR", kDevice, &Invoker::call}, + {"vkGetBufferMemoryRequirements2KHR", kDevice, &Invoker::call}, + {"vkGetImageSparseMemoryRequirements2KHR", kDevice, &Invoker::call}, + {"vkCreateSamplerYcbcrConversionKHR", kDevice, &Invoker::call}, + {"vkDestroySamplerYcbcrConversionKHR", kDevice, &Invoker::call}, + {"vkBindBufferMemory2KHR", kDevice, &Invoker::call}, + {"vkBindImageMemory2KHR", kDevice, &Invoker::call}, + {"vkGetDescriptorSetLayoutSupportKHR", kDevice, &Invoker::call}, + {"vkCmdDrawIndirectCountKHR", kDevice, &Invoker::call}, + {"vkCmdDrawIndexedIndirectCountKHR", kDevice, &Invoker::call}, + {"vkGetSemaphoreCounterValueKHR", kDevice, &Invoker::call}, + {"vkWaitSemaphoresKHR", kDevice, &Invoker::call}, + {"vkSignalSemaphoreKHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceFragmentShadingRatesKHR", kInstance, &Invoker::call}, + {"vkCmdSetFragmentShadingRateKHR", kDevice, &Invoker::call}, + {"vkWaitForPresentKHR", kDevice, &Invoker::call}, + {"vkGetBufferDeviceAddressKHR", kDevice, &Invoker::call}, + {"vkGetBufferOpaqueCaptureAddressKHR", kDevice, &Invoker::call}, + {"vkGetDeviceMemoryOpaqueCaptureAddressKHR", kDevice, &Invoker::call}, + {"vkCreateDeferredOperationKHR", kDevice, &Invoker::call}, + {"vkDestroyDeferredOperationKHR", kDevice, &Invoker::call}, + {"vkGetDeferredOperationMaxConcurrencyKHR", kDevice, &Invoker::call}, + {"vkGetDeferredOperationResultKHR", kDevice, &Invoker::call}, + {"vkDeferredOperationJoinKHR", kDevice, &Invoker::call}, + {"vkGetPipelineExecutablePropertiesKHR", kDevice, &Invoker::call}, + {"vkGetPipelineExecutableStatisticsKHR", kDevice, &Invoker::call}, + {"vkGetPipelineExecutableInternalRepresentationsKHR", kDevice, &Invoker::call}, + {"vkMapMemory2KHR", kDevice, &Invoker::call}, + {"vkUnmapMemory2KHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceVideoEncodeQualityLevelPropertiesKHR", kInstance, &Invoker::call}, + {"vkGetEncodedVideoSessionParametersKHR", kDevice, &Invoker::call}, + {"vkCmdEncodeVideoKHR", kDevice, &Invoker::call}, + {"vkCmdSetEvent2KHR", kDevice, &Invoker::call}, + {"vkCmdResetEvent2KHR", kDevice, &Invoker::call}, + {"vkCmdWaitEvents2KHR", kDevice, &Invoker::call}, + {"vkCmdPipelineBarrier2KHR", kDevice, &Invoker::call}, + {"vkCmdWriteTimestamp2KHR", kDevice, &Invoker::call}, + {"vkQueueSubmit2KHR", kDevice, &Invoker::call}, + {"vkCmdWriteBufferMarker2AMD", kDevice, &Invoker::call}, + {"vkGetQueueCheckpointData2NV", kDevice, &Invoker::call}, + {"vkCmdCopyBuffer2KHR", kDevice, &Invoker::call}, + {"vkCmdCopyImage2KHR", kDevice, &Invoker::call}, + {"vkCmdCopyBufferToImage2KHR", kDevice, &Invoker::call}, + {"vkCmdCopyImageToBuffer2KHR", kDevice, &Invoker::call}, + {"vkCmdBlitImage2KHR", kDevice, &Invoker::call}, + {"vkCmdResolveImage2KHR", kDevice, &Invoker::call}, + {"vkCmdTraceRaysIndirect2KHR", kDevice, &Invoker::call}, + {"vkGetDeviceBufferMemoryRequirementsKHR", kDevice, &Invoker::call}, + {"vkGetDeviceImageMemoryRequirementsKHR", kDevice, &Invoker::call}, + {"vkGetDeviceImageSparseMemoryRequirementsKHR", kDevice, &Invoker::call}, + {"vkCmdBindIndexBuffer2KHR", kDevice, &Invoker::call}, + {"vkGetRenderingAreaGranularityKHR", kDevice, &Invoker::call}, + {"vkGetDeviceImageSubresourceLayoutKHR", kDevice, &Invoker::call}, + {"vkGetImageSubresourceLayout2KHR", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceCalibrateableTimeDomainsKHR", kInstance, &Invoker::call}, + {"vkGetCalibratedTimestampsKHR", kDevice, &Invoker::call}, + {"vkCmdBindDescriptorSets2KHR", kDevice, &Invoker::call}, + {"vkCmdPushConstants2KHR", kDevice, &Invoker::call}, + {"vkCmdPushDescriptorSet2KHR", kDevice, &Invoker::call}, + {"vkCmdPushDescriptorSetWithTemplate2KHR", kDevice, &Invoker::call}, + {"vkCmdSetDescriptorBufferOffsets2EXT", kDevice, &Invoker::call}, + {"vkCmdBindDescriptorBufferEmbeddedSamplers2EXT", kDevice, &Invoker::call}, + {"vkCreateDebugReportCallbackEXT", kInstance, &Invoker::call}, + {"vkDestroyDebugReportCallbackEXT", kInstance, &Invoker::call}, + {"vkDebugReportMessageEXT", kInstance, &Invoker::call}, + {"vkDebugMarkerSetObjectTagEXT", kDevice, &Invoker::call}, + {"vkDebugMarkerSetObjectNameEXT", kDevice, &Invoker::call}, + {"vkCmdDebugMarkerBeginEXT", kDevice, &Invoker::call}, + {"vkCmdDebugMarkerEndEXT", kDevice, &Invoker::call}, + {"vkCmdDebugMarkerInsertEXT", kDevice, &Invoker::call}, + {"vkCmdBindTransformFeedbackBuffersEXT", kDevice, &Invoker::call}, + {"vkCmdBeginTransformFeedbackEXT", kDevice, &Invoker::call}, + {"vkCmdEndTransformFeedbackEXT", kDevice, &Invoker::call}, + {"vkCmdBeginQueryIndexedEXT", kDevice, &Invoker::call}, + {"vkCmdEndQueryIndexedEXT", kDevice, &Invoker::call}, + {"vkCmdDrawIndirectByteCountEXT", kDevice, &Invoker::call}, + {"vkCreateCuModuleNVX", kDevice, &Invoker::call}, + {"vkCreateCuFunctionNVX", kDevice, &Invoker::call}, + {"vkDestroyCuModuleNVX", kDevice, &Invoker::call}, + {"vkDestroyCuFunctionNVX", kDevice, &Invoker::call}, + {"vkCmdCuLaunchKernelNVX", kDevice, &Invoker::call}, + {"vkGetImageViewHandleNVX", kDevice, &Invoker::call}, + {"vkGetImageViewAddressNVX", kDevice, &Invoker::call}, + {"vkCmdDrawIndirectCountAMD", kDevice, &Invoker::call}, + {"vkCmdDrawIndexedIndirectCountAMD", kDevice, &Invoker::call}, + {"vkGetShaderInfoAMD", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceExternalImageFormatPropertiesNV", kInstance, &Invoker::call}, + {"vkCmdBeginConditionalRenderingEXT", kDevice, &Invoker::call}, + {"vkCmdEndConditionalRenderingEXT", kDevice, &Invoker::call}, + {"vkCmdSetViewportWScalingNV", kDevice, &Invoker::call}, + {"vkReleaseDisplayEXT", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSurfaceCapabilities2EXT", kInstance, &Invoker::call}, + {"vkDisplayPowerControlEXT", kDevice, &Invoker::call}, + {"vkRegisterDeviceEventEXT", kDevice, &Invoker::call}, + {"vkRegisterDisplayEventEXT", kDevice, &Invoker::call}, + {"vkGetSwapchainCounterEXT", kDevice, &Invoker::call}, + {"vkGetRefreshCycleDurationGOOGLE", kDevice, &Invoker::call}, + {"vkGetPastPresentationTimingGOOGLE", kDevice, &Invoker::call}, + {"vkCmdSetDiscardRectangleEXT", kDevice, &Invoker::call}, + {"vkCmdSetDiscardRectangleEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetDiscardRectangleModeEXT", kDevice, &Invoker::call}, + {"vkSetHdrMetadataEXT", kDevice, &Invoker::call}, + {"vkSetDebugUtilsObjectNameEXT", kDevice, &Invoker::call}, + {"vkSetDebugUtilsObjectTagEXT", kDevice, &Invoker::call}, + {"vkQueueBeginDebugUtilsLabelEXT", kDevice, &Invoker::call}, + {"vkQueueEndDebugUtilsLabelEXT", kDevice, &Invoker::call}, + {"vkQueueInsertDebugUtilsLabelEXT", kDevice, &Invoker::call}, + {"vkCmdBeginDebugUtilsLabelEXT", kDevice, &Invoker::call}, + {"vkCmdEndDebugUtilsLabelEXT", kDevice, &Invoker::call}, + {"vkCmdInsertDebugUtilsLabelEXT", kDevice, &Invoker::call}, + {"vkCreateDebugUtilsMessengerEXT", kInstance, &Invoker::call}, + {"vkDestroyDebugUtilsMessengerEXT", kInstance, &Invoker::call}, + {"vkSubmitDebugUtilsMessageEXT", kInstance, &Invoker::call}, + {"vkCmdSetSampleLocationsEXT", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceMultisamplePropertiesEXT", kInstance, &Invoker::call}, + {"vkGetImageDrmFormatModifierPropertiesEXT", kDevice, &Invoker::call}, + {"vkCreateValidationCacheEXT", kDevice, &Invoker::call}, + {"vkDestroyValidationCacheEXT", kDevice, &Invoker::call}, + {"vkMergeValidationCachesEXT", kDevice, &Invoker::call}, + {"vkGetValidationCacheDataEXT", kDevice, &Invoker::call}, + {"vkCmdBindShadingRateImageNV", kDevice, &Invoker::call}, + {"vkCmdSetViewportShadingRatePaletteNV", kDevice, &Invoker::call}, + {"vkCmdSetCoarseSampleOrderNV", kDevice, &Invoker::call}, + {"vkCreateAccelerationStructureNV", kDevice, &Invoker::call}, + {"vkDestroyAccelerationStructureNV", kDevice, &Invoker::call}, + {"vkGetAccelerationStructureMemoryRequirementsNV", kDevice, &Invoker::call}, + {"vkBindAccelerationStructureMemoryNV", kDevice, &Invoker::call}, + {"vkCmdBuildAccelerationStructureNV", kDevice, &Invoker::call}, + {"vkCmdCopyAccelerationStructureNV", kDevice, &Invoker::call}, + {"vkCmdTraceRaysNV", kDevice, &Invoker::call}, + {"vkCreateRayTracingPipelinesNV", kDevice, &Invoker::call}, + {"vkGetRayTracingShaderGroupHandlesKHR", kDevice, &Invoker::call}, + {"vkGetRayTracingShaderGroupHandlesNV", kDevice, &Invoker::call}, + {"vkGetAccelerationStructureHandleNV", kDevice, &Invoker::call}, + {"vkCmdWriteAccelerationStructuresPropertiesNV", kDevice, &Invoker::call}, + {"vkCompileDeferredNV", kDevice, &Invoker::call}, + {"vkGetMemoryHostPointerPropertiesEXT", kDevice, &Invoker::call}, + {"vkCmdWriteBufferMarkerAMD", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceCalibrateableTimeDomainsEXT", kInstance, &Invoker::call}, + {"vkGetCalibratedTimestampsEXT", kDevice, &Invoker::call}, + {"vkCmdDrawMeshTasksNV", kDevice, &Invoker::call}, + {"vkCmdDrawMeshTasksIndirectNV", kDevice, &Invoker::call}, + {"vkCmdDrawMeshTasksIndirectCountNV", kDevice, &Invoker::call}, + {"vkCmdSetExclusiveScissorEnableNV", kDevice, &Invoker::call}, + {"vkCmdSetExclusiveScissorNV", kDevice, &Invoker::call}, + {"vkCmdSetCheckpointNV", kDevice, &Invoker::call}, + {"vkGetQueueCheckpointDataNV", kDevice, &Invoker::call}, + {"vkInitializePerformanceApiINTEL", kDevice, &Invoker::call}, + {"vkUninitializePerformanceApiINTEL", kDevice, &Invoker::call}, + {"vkCmdSetPerformanceMarkerINTEL", kDevice, &Invoker::call}, + {"vkCmdSetPerformanceStreamMarkerINTEL", kDevice, &Invoker::call}, + {"vkCmdSetPerformanceOverrideINTEL", kDevice, &Invoker::call}, + {"vkAcquirePerformanceConfigurationINTEL", kDevice, &Invoker::call}, + {"vkReleasePerformanceConfigurationINTEL", kDevice, &Invoker::call}, + {"vkQueueSetPerformanceConfigurationINTEL", kDevice, &Invoker::call}, + {"vkGetPerformanceParameterINTEL", kDevice, &Invoker::call}, + {"vkSetLocalDimmingAMD", kDevice, &Invoker::call}, + {"vkGetBufferDeviceAddressEXT", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceToolPropertiesEXT", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceCooperativeMatrixPropertiesNV", kInstance, &Invoker::call}, + {"vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV", kInstance, &Invoker::call}, + {"vkCreateHeadlessSurfaceEXT", kInstance, &Invoker::call}, + {"vkCmdSetLineStippleEXT", kDevice, &Invoker::call}, + {"vkResetQueryPoolEXT", kDevice, &Invoker::call}, + {"vkCmdSetCullModeEXT", kDevice, &Invoker::call}, + {"vkCmdSetFrontFaceEXT", kDevice, &Invoker::call}, + {"vkCmdSetPrimitiveTopologyEXT", kDevice, &Invoker::call}, + {"vkCmdSetViewportWithCountEXT", kDevice, &Invoker::call}, + {"vkCmdSetScissorWithCountEXT", kDevice, &Invoker::call}, + {"vkCmdBindVertexBuffers2EXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthTestEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthWriteEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthCompareOpEXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthBoundsTestEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetStencilTestEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetStencilOpEXT", kDevice, &Invoker::call}, + {"vkCopyMemoryToImageEXT", kDevice, &Invoker::call}, + {"vkCopyImageToMemoryEXT", kDevice, &Invoker::call}, + {"vkCopyImageToImageEXT", kDevice, &Invoker::call}, + {"vkTransitionImageLayoutEXT", kDevice, &Invoker::call}, + {"vkGetImageSubresourceLayout2EXT", kDevice, &Invoker::call}, + {"vkReleaseSwapchainImagesEXT", kDevice, &Invoker::call}, + {"vkGetGeneratedCommandsMemoryRequirementsNV", kDevice, &Invoker::call}, + {"vkCmdPreprocessGeneratedCommandsNV", kDevice, &Invoker::call}, + {"vkCmdExecuteGeneratedCommandsNV", kDevice, &Invoker::call}, + {"vkCmdBindPipelineShaderGroupNV", kDevice, &Invoker::call}, + {"vkCreateIndirectCommandsLayoutNV", kDevice, &Invoker::call}, + {"vkDestroyIndirectCommandsLayoutNV", kDevice, &Invoker::call}, + {"vkCmdSetDepthBias2EXT", kDevice, &Invoker::call}, + {"vkAcquireDrmDisplayEXT", kInstance, &Invoker::call}, + {"vkGetDrmDisplayEXT", kInstance, &Invoker::call}, + {"vkCreatePrivateDataSlotEXT", kDevice, &Invoker::call}, + {"vkDestroyPrivateDataSlotEXT", kDevice, &Invoker::call}, + {"vkSetPrivateDataEXT", kDevice, &Invoker::call}, + {"vkGetPrivateDataEXT", kDevice, &Invoker::call}, + {"vkCreateCudaModuleNV", kDevice, &Invoker::call}, + {"vkGetCudaModuleCacheNV", kDevice, &Invoker::call}, + {"vkCreateCudaFunctionNV", kDevice, &Invoker::call}, + {"vkDestroyCudaModuleNV", kDevice, &Invoker::call}, + {"vkDestroyCudaFunctionNV", kDevice, &Invoker::call}, + {"vkCmdCudaLaunchKernelNV", kDevice, &Invoker::call}, + {"vkGetDescriptorSetLayoutSizeEXT", kDevice, &Invoker::call}, + {"vkGetDescriptorSetLayoutBindingOffsetEXT", kDevice, &Invoker::call}, + {"vkGetDescriptorEXT", kDevice, &Invoker::call}, + {"vkCmdBindDescriptorBuffersEXT", kDevice, &Invoker::call}, + {"vkCmdSetDescriptorBufferOffsetsEXT", kDevice, &Invoker::call}, + {"vkCmdBindDescriptorBufferEmbeddedSamplersEXT", kDevice, &Invoker::call}, + {"vkGetBufferOpaqueCaptureDescriptorDataEXT", kDevice, &Invoker::call}, + {"vkGetImageOpaqueCaptureDescriptorDataEXT", kDevice, &Invoker::call}, + {"vkGetImageViewOpaqueCaptureDescriptorDataEXT", kDevice, &Invoker::call}, + {"vkGetSamplerOpaqueCaptureDescriptorDataEXT", kDevice, &Invoker::call}, + {"vkGetAccelerationStructureOpaqueCaptureDescriptorDataEXT", kDevice, &Invoker::call}, + {"vkCmdSetFragmentShadingRateEnumNV", kDevice, &Invoker::call}, + {"vkGetDeviceFaultInfoEXT", kDevice, &Invoker::call}, + {"vkCmdSetVertexInputEXT", kDevice, &Invoker::call}, + {"vkGetDeviceSubpassShadingMaxWorkgroupSizeHUAWEI", kDevice, &Invoker::call}, + {"vkCmdSubpassShadingHUAWEI", kDevice, &Invoker::call}, + {"vkCmdBindInvocationMaskHUAWEI", kDevice, &Invoker::call}, + {"vkGetMemoryRemoteAddressNV", kDevice, &Invoker::call}, + {"vkGetPipelinePropertiesEXT", kDevice, &Invoker::call}, + {"vkCmdSetPatchControlPointsEXT", kDevice, &Invoker::call}, + {"vkCmdSetRasterizerDiscardEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthBiasEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetLogicOpEXT", kDevice, &Invoker::call}, + {"vkCmdSetPrimitiveRestartEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetColorWriteEnableEXT", kDevice, &Invoker::call}, + {"vkCmdDrawMultiEXT", kDevice, &Invoker::call}, + {"vkCmdDrawMultiIndexedEXT", kDevice, &Invoker::call}, + {"vkCreateMicromapEXT", kDevice, &Invoker::call}, + {"vkDestroyMicromapEXT", kDevice, &Invoker::call}, + {"vkCmdBuildMicromapsEXT", kDevice, &Invoker::call}, + {"vkBuildMicromapsEXT", kDevice, &Invoker::call}, + {"vkCopyMicromapEXT", kDevice, &Invoker::call}, + {"vkCopyMicromapToMemoryEXT", kDevice, &Invoker::call}, + {"vkCopyMemoryToMicromapEXT", kDevice, &Invoker::call}, + {"vkWriteMicromapsPropertiesEXT", kDevice, &Invoker::call}, + {"vkCmdCopyMicromapEXT", kDevice, &Invoker::call}, + {"vkCmdCopyMicromapToMemoryEXT", kDevice, &Invoker::call}, + {"vkCmdCopyMemoryToMicromapEXT", kDevice, &Invoker::call}, + {"vkCmdWriteMicromapsPropertiesEXT", kDevice, &Invoker::call}, + {"vkGetDeviceMicromapCompatibilityEXT", kDevice, &Invoker::call}, + {"vkGetMicromapBuildSizesEXT", kDevice, &Invoker::call}, + {"vkCmdDrawClusterHUAWEI", kDevice, &Invoker::call}, + {"vkCmdDrawClusterIndirectHUAWEI", kDevice, &Invoker::call}, + {"vkSetDeviceMemoryPriorityEXT", kDevice, &Invoker::call}, + {"vkGetDescriptorSetLayoutHostMappingInfoVALVE", kDevice, &Invoker::call}, + {"vkGetDescriptorSetHostMappingVALVE", kDevice, &Invoker::call}, + {"vkCmdCopyMemoryIndirectNV", kDevice, &Invoker::call}, + {"vkCmdCopyMemoryToImageIndirectNV", kDevice, &Invoker::call}, + {"vkCmdDecompressMemoryNV", kDevice, &Invoker::call}, + {"vkCmdDecompressMemoryIndirectCountNV", kDevice, &Invoker::call}, + {"vkGetPipelineIndirectMemoryRequirementsNV", kDevice, &Invoker::call}, + {"vkCmdUpdatePipelineIndirectBufferNV", kDevice, &Invoker::call}, + {"vkGetPipelineIndirectDeviceAddressNV", kDevice, &Invoker::call}, + {"vkCmdSetTessellationDomainOriginEXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthClampEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetPolygonModeEXT", kDevice, &Invoker::call}, + {"vkCmdSetRasterizationSamplesEXT", kDevice, &Invoker::call}, + {"vkCmdSetSampleMaskEXT", kDevice, &Invoker::call}, + {"vkCmdSetAlphaToCoverageEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetAlphaToOneEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetLogicOpEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetColorBlendEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetColorBlendEquationEXT", kDevice, &Invoker::call}, + {"vkCmdSetColorWriteMaskEXT", kDevice, &Invoker::call}, + {"vkCmdSetRasterizationStreamEXT", kDevice, &Invoker::call}, + {"vkCmdSetConservativeRasterizationModeEXT", kDevice, &Invoker::call}, + {"vkCmdSetExtraPrimitiveOverestimationSizeEXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthClipEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetSampleLocationsEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetColorBlendAdvancedEXT", kDevice, &Invoker::call}, + {"vkCmdSetProvokingVertexModeEXT", kDevice, &Invoker::call}, + {"vkCmdSetLineRasterizationModeEXT", kDevice, &Invoker::call}, + {"vkCmdSetLineStippleEnableEXT", kDevice, &Invoker::call}, + {"vkCmdSetDepthClipNegativeOneToOneEXT", kDevice, &Invoker::call}, + {"vkCmdSetViewportWScalingEnableNV", kDevice, &Invoker::call}, + {"vkCmdSetViewportSwizzleNV", kDevice, &Invoker::call}, + {"vkCmdSetCoverageToColorEnableNV", kDevice, &Invoker::call}, + {"vkCmdSetCoverageToColorLocationNV", kDevice, &Invoker::call}, + {"vkCmdSetCoverageModulationModeNV", kDevice, &Invoker::call}, + {"vkCmdSetCoverageModulationTableEnableNV", kDevice, &Invoker::call}, + {"vkCmdSetCoverageModulationTableNV", kDevice, &Invoker::call}, + {"vkCmdSetShadingRateImageEnableNV", kDevice, &Invoker::call}, + {"vkCmdSetRepresentativeFragmentTestEnableNV", kDevice, &Invoker::call}, + {"vkCmdSetCoverageReductionModeNV", kDevice, &Invoker::call}, + {"vkGetShaderModuleIdentifierEXT", kDevice, &Invoker::call}, + {"vkGetShaderModuleCreateInfoIdentifierEXT", kDevice, &Invoker::call}, + {"vkGetPhysicalDeviceOpticalFlowImageFormatsNV", kInstance, &Invoker::call}, + {"vkCreateOpticalFlowSessionNV", kDevice, &Invoker::call}, + {"vkDestroyOpticalFlowSessionNV", kDevice, &Invoker::call}, + {"vkBindOpticalFlowSessionImageNV", kDevice, &Invoker::call}, + {"vkCmdOpticalFlowExecuteNV", kDevice, &Invoker::call}, + {"vkCreateShadersEXT", kDevice, &Invoker::call}, + {"vkDestroyShaderEXT", kDevice, &Invoker::call}, + {"vkGetShaderBinaryDataEXT", kDevice, &Invoker::call}, + {"vkCmdBindShadersEXT", kDevice, &Invoker::call}, + {"vkGetFramebufferTilePropertiesQCOM", kDevice, &Invoker::call}, + {"vkGetDynamicRenderingTilePropertiesQCOM", kDevice, &Invoker::call}, + {"vkSetLatencySleepModeNV", kDevice, &Invoker::call}, + {"vkLatencySleepNV", kDevice, &Invoker::call}, + {"vkSetLatencyMarkerNV", kDevice, &Invoker::call}, + {"vkGetLatencyTimingsNV", kDevice, &Invoker::call}, + {"vkQueueNotifyOutOfBandNV", kDevice, &Invoker::call}, + {"vkCmdSetAttachmentFeedbackLoopEnableEXT", kDevice, &Invoker::call}, + {"vkCreateAccelerationStructureKHR", kDevice, &Invoker::call}, + {"vkDestroyAccelerationStructureKHR", kDevice, &Invoker::call}, + {"vkCmdBuildAccelerationStructuresKHR", kDevice, &Invoker::call}, + {"vkCmdBuildAccelerationStructuresIndirectKHR", kDevice, &Invoker::call}, + {"vkBuildAccelerationStructuresKHR", kDevice, &Invoker::call}, + {"vkCopyAccelerationStructureKHR", kDevice, &Invoker::call}, + {"vkCopyAccelerationStructureToMemoryKHR", kDevice, &Invoker::call}, + {"vkCopyMemoryToAccelerationStructureKHR", kDevice, &Invoker::call}, + {"vkWriteAccelerationStructuresPropertiesKHR", kDevice, &Invoker::call}, + {"vkCmdCopyAccelerationStructureKHR", kDevice, &Invoker::call}, + {"vkCmdCopyAccelerationStructureToMemoryKHR", kDevice, &Invoker::call}, + {"vkCmdCopyMemoryToAccelerationStructureKHR", kDevice, &Invoker::call}, + {"vkGetAccelerationStructureDeviceAddressKHR", kDevice, &Invoker::call}, + {"vkCmdWriteAccelerationStructuresPropertiesKHR", kDevice, &Invoker::call}, + {"vkGetDeviceAccelerationStructureCompatibilityKHR", kDevice, &Invoker::call}, + {"vkGetAccelerationStructureBuildSizesKHR", kDevice, &Invoker::call}, + {"vkCmdTraceRaysKHR", kDevice, &Invoker::call}, + {"vkCreateRayTracingPipelinesKHR", kDevice, &Invoker::call}, + {"vkGetRayTracingCaptureReplayShaderGroupHandlesKHR", kDevice, &Invoker::call}, + {"vkCmdTraceRaysIndirectKHR", kDevice, &Invoker::call}, + {"vkGetRayTracingShaderGroupStackSizeKHR", kDevice, &Invoker::call}, + {"vkCmdSetRayTracingPipelineStackSizeKHR", kDevice, &Invoker::call}, + {"vkCmdDrawMeshTasksEXT", kDevice, &Invoker::call}, + {"vkCmdDrawMeshTasksIndirectEXT", kDevice, &Invoker::call}, + {"vkCmdDrawMeshTasksIndirectCountEXT", kDevice, &Invoker::call}, + {"vkCreateAndroidSurfaceKHR", kInstance, &Invoker::call}, + {"vkGetAndroidHardwareBufferPropertiesANDROID", kDevice, &Invoker::call}, + {"vkGetMemoryAndroidHardwareBufferANDROID", kDevice, &Invoker::call}, +}; diff --git a/direct/src/vk_host.cpp b/direct/src/vk_host.cpp new file mode 100644 index 0000000..7359e68 --- /dev/null +++ b/direct/src/vk_host.cpp @@ -0,0 +1,494 @@ +// Host side of the guest's libvulkan.so (direct/guest/proxy/libvulkan.cpp): the +// guest's Vulkan commands run on the host GPU through the system Vulkan loader. +// +// Guest and host share memory and structure layouts, so a command is the host +// driver's function called with the guest's arguments. What differs is the +// platform, and that is translated here: +// - Android surfaces (ANativeWindow) become Win32 surfaces on a desktop window, +// and VK_KHR_android_surface is offered in place of VK_KHR_win32_surface; +// - Windows-only extensions are hidden, guest layers are not loaded; +// - guest allocation callbacks and debug callbacks (guest code) are dropped. + +#include "vk_host.h" + +#include "cpu.h" +#include "log.h" +#include "ndk.h" +#include "window.h" + +#define VK_NO_PROTOTYPES 1 +#define VK_USE_PLATFORM_WIN32_KHR 1 +#define VK_USE_PLATFORM_ANDROID_KHR 1 +#include + +#include "vk_texture.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace axrb { +namespace { + +enum Level : uint32_t { kGlobal, kInstance, kDevice }; + +using Invoke = uint64_t (*)(void* fn, const uint64_t* args); + +struct VkCommand { + const char* name; + Level level; + Invoke invoke; +}; + +template +T arg(uint64_t v) { + if constexpr (std::is_same_v) { + return nullptr; // guest code: the host driver allocates for itself + } else if constexpr (std::is_pointer_v) { + return reinterpret_cast(static_cast(v)); + } else if constexpr (std::is_same_v) { + float f; + const uint32_t bits = static_cast(v); + std::memcpy(&f, &bits, sizeof f); + return f; + } else if constexpr (std::is_enum_v) { + return static_cast(static_cast>(v)); + } else { + return static_cast(v); + } +} + +template +uint64_t result(R r) { + if constexpr (std::is_pointer_v) { + return reinterpret_cast(r); + } else { + return static_cast(static_cast(r)); + } +} + +template +struct Invoker; + +template +struct Invoker { + static uint64_t call(void* fn, const uint64_t* a) { return call(fn, a, std::index_sequence_for{}); } + + template + static uint64_t call(void* fn, [[maybe_unused]] const uint64_t* a, std::index_sequence) { + auto f = reinterpret_cast(fn); + if constexpr (std::is_void_v) { + f(arg(a[I])...); + return 0; + } else { + return result(f(arg(a[I])...)); + } + } +}; + +#include "vk_entries.inc" + +constexpr uint32_t kCount = sizeof kCommands / sizeof kCommands[0]; + +using Override = uint64_t (*)(const uint64_t* args); + +std::once_flag g_load_once; +PFN_vkGetInstanceProcAddr g_gipa = nullptr; +std::atomic g_fns[kCount]; +Override g_overrides[kCount]; +// Live instances, oldest first. Commands are resolved through any of them: the +// loader's trampolines dispatch on the handle, whichever instance they came from. +std::mutex g_instances_lock; +std::vector g_instances; +std::atomic g_android_surface{false}; +std::atomic g_fake_handles{0x0a0b0000}; + +bool load_loader() { + std::call_once(g_load_once, [] { + HMODULE m = LoadLibraryW(L"vulkan-1.dll"); + if (!m) { + LOGE("vulkan", "vulkan-1.dll is not installed: the guest has no GPU"); + return; + } + g_gipa = reinterpret_cast(GetProcAddress(m, "vkGetInstanceProcAddr")); + LOGI("vulkan", "host Vulkan loader ready (%u guest commands)", kCount); + }); + return g_gipa != nullptr; +} + +template +F host_proc(VkInstance instance, const char* name) { + return load_loader() ? reinterpret_cast(g_gipa(instance, name)) : nullptr; +} + +VkInstance any_instance() { + std::lock_guard lock(g_instances_lock); + return g_instances.empty() ? VK_NULL_HANDLE : g_instances.front(); +} + +void* resolve(uint32_t id) { + void* fn = g_fns[id].load(std::memory_order_acquire); + if (fn) return fn; + const VkInstance instance = kCommands[id].level == kGlobal ? VK_NULL_HANDLE : any_instance(); + if (kCommands[id].level != kGlobal && instance == VK_NULL_HANDLE) return nullptr; + fn = host_proc(instance, kCommands[id].name); + if (fn) g_fns[id].store(fn, std::memory_order_release); + return fn; +} + +template +F command(const char* name) { + for (uint32_t i = 0; i < kCount; ++i) { + if (std::strcmp(kCommands[i].name, name) == 0) return reinterpret_cast(resolve(i)); + } + return host_proc(any_instance(), name); +} + +// Unlinks structures from a pNext chain for the duration of a call (the chain is +// guest memory, so it is relinked afterwards). +class ChainFilter { +public: + ChainFilter(const void** head, std::initializer_list drop) { + const void** link = head; + while (*link) { + auto* s = static_cast(*link); + if (std::find(drop.begin(), drop.end(), s->sType) != drop.end()) { + saved_.emplace_back(link, *link); + *link = s->pNext; + } else { + link = reinterpret_cast(const_cast(&s->pNext)); + } + } + } + ~ChainFilter() { + for (auto it = saved_.rbegin(); it != saved_.rend(); ++it) *it->first = it->second; + } + +private: + std::vector> saved_; +}; + +template +VkResult fill(const std::vector& items, uint32_t* count, T* out) { + if (out == nullptr) { + *count = static_cast(items.size()); + return VK_SUCCESS; + } + const uint32_t n = std::min(*count, static_cast(items.size())); + std::memcpy(out, items.data(), n * sizeof(T)); + *count = n; + return n < items.size() ? VK_INCOMPLETE : VK_SUCCESS; +} + +bool windows_only(const char* name) { + return std::strstr(name, "win32") != nullptr || std::strstr(name, "WIN32") != nullptr || + std::strcmp(name, VK_EXT_FULL_SCREEN_EXCLUSIVE_EXTENSION_NAME) == 0 || + std::strcmp(name, "VK_NV_acquire_winrt_display") == 0 || + std::strcmp(name, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0; +} + +std::vector host_instance_extensions() { + std::vector out; + auto enumerate = host_proc(VK_NULL_HANDLE, + "vkEnumerateInstanceExtensionProperties"); + uint32_t n = 0; + if (!enumerate || enumerate(nullptr, &n, nullptr) != VK_SUCCESS) return out; + out.resize(n); + enumerate(nullptr, &n, out.data()); + out.resize(n); + return out; +} + +std::vector host_device_extensions(VkPhysicalDevice pd) { + std::vector out; + auto enumerate = command("vkEnumerateDeviceExtensionProperties"); + uint32_t n = 0; + if (!enumerate || enumerate(pd, nullptr, &n, nullptr) != VK_SUCCESS) return out; + out.resize(n); + enumerate(pd, nullptr, &n, out.data()); + out.resize(n); + return out; +} + +bool has_extension(const std::vector& list, const char* name) { + for (const auto& e : list) { + if (std::strcmp(e.extensionName, name) == 0) return true; + } + return false; +} + +// --- overrides ------------------------------------------------------------------------ + +uint64_t EnumerateInstanceExtensionProperties(const uint64_t* a) { + auto* layer = arg(a[0]); + auto* count = arg(a[1]); + auto* props = arg(a[2]); + if (layer != nullptr) return result(VK_ERROR_LAYER_NOT_PRESENT); + std::vector visible; + for (const auto& e : host_instance_extensions()) { + if (std::strcmp(e.extensionName, VK_KHR_WIN32_SURFACE_EXTENSION_NAME) == 0) { + VkExtensionProperties android{}; + std::strcpy(android.extensionName, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME); + android.specVersion = VK_KHR_ANDROID_SURFACE_SPEC_VERSION; + visible.push_back(android); + } else if (!windows_only(e.extensionName)) { + visible.push_back(e); + } + } + return result(fill(visible, count, props)); +} + +uint64_t EnumerateLayerProperties(const uint64_t* a, int count_index) { + *arg(a[count_index]) = 0; + return result(VK_SUCCESS); +} + +uint64_t EnumerateInstanceLayerProperties(const uint64_t* a) { return EnumerateLayerProperties(a, 0); } +uint64_t EnumerateDeviceLayerProperties(const uint64_t* a) { return EnumerateLayerProperties(a, 1); } + +uint64_t CreateInstance(const uint64_t* a) { + auto* info = arg(a[0]); + auto* out = arg(a[2]); + auto create = host_proc(VK_NULL_HANDLE, "vkCreateInstance"); + if (!create) return result(VK_ERROR_INITIALIZATION_FAILED); + const auto available = host_instance_extensions(); + VkInstanceCreateInfo ci = *info; + std::vector extensions; + bool android_surface = false; + for (uint32_t i = 0; i < info->enabledExtensionCount; ++i) { + const char* name = info->ppEnabledExtensionNames[i]; + if (std::strcmp(name, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME) == 0) { + android_surface = true; + name = VK_KHR_WIN32_SURFACE_EXTENSION_NAME; + } + if (!has_extension(available, name)) { + LOGW("vulkan", "instance extension %s is not available on the host GPU: not enabled", name); + continue; + } + extensions.push_back(name); + } + for (uint32_t i = 0; i < info->enabledLayerCount; ++i) { + LOGI("vulkan", "guest layer %s is not loaded", info->ppEnabledLayerNames[i]); + } + ci.enabledExtensionCount = static_cast(extensions.size()); + ci.ppEnabledExtensionNames = extensions.data(); + ci.enabledLayerCount = 0; + ci.ppEnabledLayerNames = nullptr; + ChainFilter filter(&ci.pNext, {VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, + VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT}); + const VkApplicationInfo* app = info->pApplicationInfo; + const VkResult r = create(&ci, nullptr, out); + LOGI("vulkan", "vkCreateInstance(%s, api %u.%u, %u extensions) = %d", app && app->pApplicationName ? app->pApplicationName : "?", + app ? VK_API_VERSION_MAJOR(app->apiVersion) : 1, app ? VK_API_VERSION_MINOR(app->apiVersion) : 0, + ci.enabledExtensionCount, r); + if (r == VK_SUCCESS) { + { + std::lock_guard lock(g_instances_lock); + g_instances.push_back(*out); + } + if (android_surface) g_android_surface.store(true); + } + return result(r); +} + +uint64_t DestroyInstance(const uint64_t* a) { + auto instance = arg(a[0]); + if (instance == VK_NULL_HANDLE) return 0; + auto destroy = command("vkDestroyInstance"); + if (destroy) destroy(instance, nullptr); + std::lock_guard lock(g_instances_lock); + g_instances.erase(std::remove(g_instances.begin(), g_instances.end(), instance), g_instances.end()); + return 0; +} + +uint64_t EnumerateDeviceExtensionProperties(const uint64_t* a) { + auto pd = arg(a[0]); + auto* layer = arg(a[1]); + auto* count = arg(a[2]); + auto* props = arg(a[3]); + if (layer != nullptr) return result(VK_ERROR_LAYER_NOT_PRESENT); + std::vector visible; + for (const auto& e : host_device_extensions(pd)) { + if (!windows_only(e.extensionName)) visible.push_back(e); + } + return result(fill(visible, count, props)); +} + +uint64_t CreateDevice(const uint64_t* a) { + auto pd = arg(a[0]); + auto* info = arg(a[1]); + auto* out = arg(a[3]); + auto create = command("vkCreateDevice"); + if (!create) return result(VK_ERROR_INITIALIZATION_FAILED); + const auto available = host_device_extensions(pd); + VkDeviceCreateInfo ci = *info; + std::vector extensions; + for (uint32_t i = 0; i < info->enabledExtensionCount; ++i) { + const char* name = info->ppEnabledExtensionNames[i]; + if (!has_extension(available, name)) { + LOGW("vulkan", "device extension %s is not available on the host GPU: not enabled", name); + continue; + } + extensions.push_back(name); + LOGI("vulkan", "device extension %s", name); + } + ci.enabledExtensionCount = static_cast(extensions.size()); + ci.ppEnabledExtensionNames = extensions.data(); + ci.enabledLayerCount = 0; + ci.ppEnabledLayerNames = nullptr; + const VkInstance instance = any_instance(); + vktex::DeviceFeatures features; + vktex::prepare_device(instance, pd, ci, features); + const VkResult r = create(pd, &ci, nullptr, out); + vktex::finish_device(instance, pd, ci, features, r, r == VK_SUCCESS ? *out : VK_NULL_HANDLE); + LOGI("vulkan", "vkCreateDevice(%u queue families, %u extensions) = %d", ci.queueCreateInfoCount, + ci.enabledExtensionCount, r); + return result(r); +} + +uint64_t CreateAndroidSurfaceKHR(const uint64_t* a) { + auto instance = arg(a[0]); + auto* info = arg(a[1]); + auto* out = arg(a[3]); + const uint64_t address = reinterpret_cast(info->window); + NativeWindow* window = native_window(address); + if (window == nullptr) { + LOGE("vulkan", "vkCreateAndroidSurfaceKHR: %llx is not an ANativeWindow", static_cast(address)); + return result(VK_ERROR_NATIVE_WINDOW_IN_USE_KHR); + } + int32_t width = 0, height = 0; + native_window_size(window, &width, &height); + HWND hwnd = host_window_for(address, width, height); + auto create = host_proc(instance, "vkCreateWin32SurfaceKHR"); + if (!hwnd || !create) return result(VK_ERROR_INITIALIZATION_FAILED); + VkWin32SurfaceCreateInfoKHR ci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; + ci.hinstance = GetModuleHandleW(nullptr); + ci.hwnd = hwnd; + const VkResult r = create(instance, &ci, nullptr, out); + LOGI("vulkan", "android surface %dx%d -> win32 surface: %d", width, height, r); + return result(r); +} + +uint64_t CreateFakeHandle(const uint64_t* a, int out_index) { + *arg(a[out_index]) = g_fake_handles.fetch_add(1); + return result(VK_SUCCESS); +} + +uint64_t CreateDebugUtilsMessengerEXT(const uint64_t* a) { return CreateFakeHandle(a, 3); } +uint64_t CreateDebugReportCallbackEXT(const uint64_t* a) { return CreateFakeHandle(a, 3); } +uint64_t Nothing(const uint64_t*) { return 0; } +uint64_t ExtensionNotPresent(const uint64_t*) { return result(VK_ERROR_EXTENSION_NOT_PRESENT); } + +void install_overrides() { + const std::pair table[] = { + {"vkEnumerateInstanceExtensionProperties", EnumerateInstanceExtensionProperties}, + {"vkEnumerateInstanceLayerProperties", EnumerateInstanceLayerProperties}, + {"vkEnumerateDeviceLayerProperties", EnumerateDeviceLayerProperties}, + {"vkCreateInstance", CreateInstance}, + {"vkDestroyInstance", DestroyInstance}, + {"vkEnumerateDeviceExtensionProperties", EnumerateDeviceExtensionProperties}, + {"vkCreateDevice", CreateDevice}, + {"vkCreateAndroidSurfaceKHR", CreateAndroidSurfaceKHR}, + {"vkCreateDebugUtilsMessengerEXT", CreateDebugUtilsMessengerEXT}, + {"vkDestroyDebugUtilsMessengerEXT", Nothing}, + {"vkSubmitDebugUtilsMessageEXT", Nothing}, + {"vkCreateDebugReportCallbackEXT", CreateDebugReportCallbackEXT}, + {"vkDestroyDebugReportCallbackEXT", Nothing}, + {"vkDebugReportMessageEXT", Nothing}, + {"vkGetAndroidHardwareBufferPropertiesANDROID", ExtensionNotPresent}, + {"vkGetMemoryAndroidHardwareBufferANDROID", ExtensionNotPresent}, + }; + auto install = [](const char* name, Override fn) { + for (uint32_t i = 0; i < kCount; ++i) { + if (std::strcmp(kCommands[i].name, name) == 0) { + g_overrides[i] = fn; + return true; + } + } + return false; + }; + for (const auto& [name, fn] : table) { + if (!install(name, fn)) fatal("vulkan: override for unknown command %s", name); + } + // Texture substitution (vk_texture.h); commands newer than the guest + // headers are simply never called. + for (const auto& [name, fn] : vktex::overrides()) install(name, fn); +} + +PFN_vkVoidFunction texture_lookup(VkInstance instance, const char* name) { + if (instance) return host_proc(instance, name); + return command(name); +} + +bool supported(uint32_t id, uint64_t handle, uint32_t level) { + const char* name = kCommands[id].name; + if (std::strcmp(name, "vkCreateAndroidSurfaceKHR") == 0) return g_android_surface.load(); + if (std::strstr(name, "AndroidHardwareBuffer") != nullptr) return false; + if (g_overrides[id] && level == kInstance) return true; + if (level == kInstance) { + return host_proc(reinterpret_cast(handle), name) != nullptr; + } + auto gdpa = host_proc(any_instance(), "vkGetDeviceProcAddr"); + return gdpa && gdpa(reinterpret_cast(handle), name) != nullptr; +} + +} // namespace + +bool vulkan_prepare_gpu_share() { + wchar_t value[MAX_PATH]{}; + if (GetEnvironmentVariableW(L"AXRB_GPU_SHARE", value, MAX_PATH) && std::wcscmp(value, L"0") == 0) return false; + std::wstring dir; + if (GetEnvironmentVariableW(L"AXRB_GPU_LAYER_DIR", value, MAX_PATH)) { + dir = value; + } else { + wchar_t exe[MAX_PATH]{}; + GetModuleFileNameW(nullptr, exe, MAX_PATH); + dir = exe; + dir = dir.substr(0, dir.find_last_of(L'\\')) + L"\\gpu"; + } + if (GetFileAttributesW((dir + L"\\axrb_gpu_layer.json").c_str()) == INVALID_FILE_ATTRIBUTES) { + LOGI("vulkan", "GPU-share layer not found in %ls: eye images use pixel transfer", dir.c_str()); + return false; + } + auto prepend = [](const wchar_t* name, const std::wstring& item, wchar_t separator) { + wchar_t old[4096]{}; + std::wstring v = item; + if (GetEnvironmentVariableW(name, old, 4096) && old[0]) v += separator + std::wstring(old); + SetEnvironmentVariableW(name, v.c_str()); + }; + prepend(L"VK_LAYER_PATH", dir, L';'); + prepend(L"VK_INSTANCE_LAYERS", L"VK_LAYER_AXRB_gpu_share", L';'); + LOGI("vulkan", "GPU-share layer from %ls", dir.c_str()); + return true; +} + +void vulkan_init() { + vktex::init(texture_lookup); + install_overrides(); + register_named_function("vk.call", [](HostCall& c) { + const uint32_t id = static_cast(c.x(0)); + if (id >= kCount) return c.ret(result(VK_ERROR_INITIALIZATION_FAILED)); + const auto* args = reinterpret_cast(static_cast(c.x(1))); + TRACE("vulkan", "%s", kCommands[id].name); + if (g_overrides[id]) return c.ret(g_overrides[id](args)); + void* fn = resolve(id); + if (!fn) { + LOGE("vulkan", "%s is not available on the host", kCommands[id].name); + return c.ret(result(VK_ERROR_EXTENSION_NOT_PRESENT)); + } + c.ret(kCommands[id].invoke(fn, args)); + }); + register_named_function("vk.query", [](HostCall& c) { + const uint32_t id = static_cast(c.x(0)); + c.ret(id < kCount && supported(id, c.x(1), static_cast(c.x(2))) ? 1 : 0); + }); +} + +} // namespace axrb diff --git a/direct/src/vk_host.h b/direct/src/vk_host.h new file mode 100644 index 0000000..420668c --- /dev/null +++ b/direct/src/vk_host.h @@ -0,0 +1,13 @@ +#pragma once + +namespace axrb { + +// Registers the host functions behind the guest's libvulkan.so ("vk.*"). +void vulkan_init(); + +// Activates AXRB's GPU-share layer (VK_LAYER_AXRB_gpu_share) for the host +// instances, from AXRB_GPU_LAYER_DIR or gpu\ beside the executable, unless +// AXRB_GPU_SHARE=0. Must run before the first Vulkan call. True if active. +bool vulkan_prepare_gpu_share(); + +} // namespace axrb diff --git a/direct/src/vk_linear.inc b/direct/src/vk_linear.inc new file mode 100644 index 0000000..317e335 --- /dev/null +++ b/direct/src/vk_linear.inc @@ -0,0 +1,293 @@ +// Linear mobile-compressed images expose a CPU staging buffer and a separate +// optimal desktop-compressed image. The guest never writes desktop tiled data. +struct LinearImage { + VkDevice device{}; + VkImage image{}; + VkBuffer buffer{}; + VkDeviceMemory backing{}; + VkMemoryRequirements requirements{}; + std::vector layouts; + std::vector regions; + uint32_t levels{}; +}; +std::mutex g_linear_lock; +std::map> g_linear; +std::shared_ptr linear_image(VkImage image) { + std::lock_guard guard(g_linear_lock); + auto it = g_linear.find(image); + return it == g_linear.end() ? nullptr : it->second; +} +VkResult create_linear(VkDevice device, const VkImageCreateInfo *info, + VkImage *out) { + auto calls = calls_for(device); + if (!calls) + return VK_ERROR_FORMAT_NOT_SUPPORTED; + if (info->imageType != VK_IMAGE_TYPE_2D || info->extent.depth != 1) + return VK_ERROR_FORMAT_NOT_SUPPORTED; + int bw, bh; + bool srgb; + texture::Codec codec; + VkFormat target; + if (!texture::compressed_format(info->format, bw, bh, srgb, codec, target)) + return VK_ERROR_FORMAT_NOT_SUPPORTED; + auto linear = std::make_shared(); + linear->device = device; + linear->levels = info->mipLevels; + VkDeviceSize size = 0; + const uint32_t blockBytes = + codec == texture::Codec::Astc ? 16 : texture::block_bytes(codec); + for (uint32_t layer = 0; layer < info->arrayLayers; ++layer) { + for (uint32_t level = 0; level < info->mipLevels; ++level) { + uint32_t w = std::max(1u, info->extent.width >> level), + h = std::max(1u, info->extent.height >> level); + VkSubresourceLayout layout{}; + layout.offset = size; + layout.rowPitch = uint64_t((w + bw - 1) / bw) * blockBytes; + layout.size = layout.rowPitch * ((h + bh - 1) / bh); + layout.depthPitch = layout.size; + linear->layouts.push_back(layout); + VkBufferImageCopy region{}; + region.bufferOffset = size; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1}; + region.imageExtent = {w, h, 1}; + linear->regions.push_back(region); + size += layout.size; + size = (size + 15) & ~VkDeviceSize(15); + } + } + for (auto &layout : linear->layouts) + layout.arrayPitch = size / info->arrayLayers; + VkBufferCreateInfo buffer{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + buffer.size = size; + buffer.usage = + VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + buffer.sharingMode = info->sharingMode; + buffer.queueFamilyIndexCount = info->queueFamilyIndexCount; + buffer.pQueueFamilyIndices = info->pQueueFamilyIndices; + VkResult result = + calls->createBuffer(device, &buffer, nullptr, &linear->buffer); + if (result != VK_SUCCESS) + return result; + calls->bufferRequirements(device, linear->buffer, &linear->requirements); + auto optimal = *info; + optimal.tiling = VK_IMAGE_TILING_OPTIMAL; + optimal.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + optimal.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + result = images().create_image(device, &optimal, nullptr, out); + if (result != VK_SUCCESS) { + calls->destroyBuffer(device, linear->buffer, nullptr); + return result; + } + linear->image = *out; + images().created_buffer(linear->buffer, size); + std::lock_guard guard(g_linear_lock); + g_linear[*out] = std::move(linear); + return VK_SUCCESS; +} +uint64_t GetImageMemoryRequirements(const uint64_t *a) { + auto linear = linear_image(a_(a[1])); + if (linear) + *a_(a[2]) = linear->requirements; + else + host("vkGetImageMemoryRequirements")( + a_(a[0]), a_(a[1]), + a_(a[2])); + return 0; +} +uint64_t image_memory_requirements2(const uint64_t *a, const char* name) { + auto *info = a_(a[1]); + auto linear = linear_image(info->image); + auto *out = a_(a[2]); + if (!linear) + host(name)( + a_(a[0]), info, out); + else { + out->memoryRequirements = linear->requirements; + for (auto *p = static_cast(out->pNext); p; + p = p->pNext) + if (p->sType == VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS) { + auto *dedicated = reinterpret_cast(p); + dedicated->prefersDedicatedAllocation = + dedicated->requiresDedicatedAllocation = VK_FALSE; + } + } + return 0; +} +uint64_t GetImageMemoryRequirements2(const uint64_t *a) { + return image_memory_requirements2(a, "vkGetImageMemoryRequirements2"); +} +uint64_t GetImageMemoryRequirements2KHR(const uint64_t *a) { + return image_memory_requirements2(a, "vkGetImageMemoryRequirements2KHR"); +} +VkResult bind_linear(VkDevice device, VkImage image, VkDeviceMemory memory, + VkDeviceSize offset) { + auto linear = linear_image(image); + auto bind = host("vkBindImageMemory"); + if (!linear) + return bind(device, image, memory, offset); + auto calls = calls_for(device); + VkMemoryRequirements req{}; + host("vkGetImageMemoryRequirements")( + device, image, &req); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = req.size; + while (allocation.memoryTypeIndex < calls->memory.memoryTypeCount && + !(req.memoryTypeBits & (1u << allocation.memoryTypeIndex))) + ++allocation.memoryTypeIndex; + VkResult result = VK_SUCCESS; + if (!linear->backing) { + result = calls->allocateMemory(device, &allocation, nullptr, &linear->backing); + if (result != VK_SUCCESS) + return result; + result = bind(device, image, linear->backing, 0); + if (result != VK_SUCCESS) { + calls->freeMemory(device, linear->backing, nullptr); + linear->backing = VK_NULL_HANDLE; + return result; + } + } + if (result == VK_SUCCESS) + result = calls->bindBufferMemory(device, linear->buffer, memory, offset); + if (result == VK_SUCCESS) + images().bound_buffer(linear->buffer, memory, offset); + return result; +} +uint64_t BindImageMemory(const uint64_t *a) { + return r_(bind_linear(a_(a[0]), a_(a[1]), + a_(a[2]), a[3])); +} +uint64_t bind_image_memory2(const uint64_t *a, const char* name) { + auto *infos = a_(a[2]); + for (uint32_t i = 0; i < a[1]; ++i) { + VkResult result; + if (linear_image(infos[i].image)) + result = bind_linear(a_(a[0]), infos[i].image, infos[i].memory, + infos[i].memoryOffset); + else + result = host(name)( + a_(a[0]), 1, infos + i); + if (result != VK_SUCCESS) + return r_(result); + } + return r_(VK_SUCCESS); +} +uint64_t BindImageMemory2(const uint64_t *a) { + return bind_image_memory2(a, "vkBindImageMemory2"); +} +uint64_t BindImageMemory2KHR(const uint64_t *a) { + return bind_image_memory2(a, "vkBindImageMemory2KHR"); +} +uint64_t GetImageSubresourceLayout(const uint64_t *a) { + auto linear = linear_image(a_(a[1])); + auto *sub = a_(a[2]); + if (linear) { + size_t index = size_t(sub->arrayLayer) * linear->levels + sub->mipLevel; + if (index < linear->layouts.size()) + *a_(a[3]) = linear->layouts[index]; + } else + host("vkGetImageSubresourceLayout")( + a_(a[0]), a_(a[1]), sub, + a_(a[3])); + return 0; +} +void destroy_linear(VkDevice device, VkImage image) { + auto linear = linear_image(image); + if (!linear) + return; + auto calls = calls_for(device); + images().destroyed_buffer(linear->buffer); + calls->destroyBuffer(device, linear->buffer, nullptr); + if (linear->backing) + calls->freeMemory(device, linear->backing, nullptr); + std::lock_guard guard(g_linear_lock); + g_linear.erase(image); +} + +bool upload_linear(VkCommandBuffer command, VkImage image, + VkImageLayout oldLayout, VkAccessFlags2 sourceAccess, + const VkImageSubresourceRange &range) { + auto linear = linear_image(image); + if (!linear || (!(sourceAccess & VK_ACCESS_2_HOST_WRITE_BIT) && + oldLayout != VK_IMAGE_LAYOUT_PREINITIALIZED)) + return false; + VkImageMemoryBarrier transition{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + transition.oldLayout = oldLayout == VK_IMAGE_LAYOUT_PREINITIALIZED + ? VK_IMAGE_LAYOUT_UNDEFINED + : oldLayout; + transition.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + transition.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + transition.srcQueueFamilyIndex = transition.dstQueueFamilyIndex = + VK_QUEUE_FAMILY_IGNORED; + transition.image = image; + transition.subresourceRange = range; + host("vkCmdPipelineBarrier")( + command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, + &transition); + std::vector selected; + for (const auto ®ion : linear->regions) { + const auto &sub = region.imageSubresource; + if (sub.mipLevel >= range.baseMipLevel && + (range.levelCount == VK_REMAINING_MIP_LEVELS || + sub.mipLevel - range.baseMipLevel < range.levelCount) && + sub.baseArrayLayer >= range.baseArrayLayer && + (range.layerCount == VK_REMAINING_ARRAY_LAYERS || + sub.baseArrayLayer - range.baseArrayLayer < range.layerCount)) + selected.push_back(region); + } + images().copy_buffer_to_image(command, linear->device, linear->buffer, image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + static_cast(selected.size()), + selected.data()); + return true; +} +uint64_t CmdPipelineBarrier(const uint64_t *a) { + auto command = a_(a[0]); + auto *input = a_(a[9]); + std::vector barriers; + if (a[8]) + barriers.assign(input, input + a[8]); + VkPipelineStageFlags source = static_cast(a[1]); + for (auto &barrier : barriers) + if (upload_linear(command, barrier.image, barrier.oldLayout, + barrier.srcAccessMask, barrier.subresourceRange)) { + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + source |= VK_PIPELINE_STAGE_TRANSFER_BIT; + } + host("vkCmdPipelineBarrier")( + command, source, static_cast(a[2]), + static_cast(a[3]), static_cast(a[4]), + a_(a[5]), static_cast(a[6]), + a_(a[7]), + static_cast(barriers.size()), barriers.data()); + return 0; +} +uint64_t pipeline_barrier2(const uint64_t *a, const char* name) { + auto command = a_(a[0]); + auto info = *a_(a[1]); + std::vector barriers; + if (info.imageMemoryBarrierCount) + barriers.assign(info.pImageMemoryBarriers, + info.pImageMemoryBarriers + info.imageMemoryBarrierCount); + for (auto &barrier : barriers) + if (upload_linear(command, barrier.image, barrier.oldLayout, + barrier.srcAccessMask, barrier.subresourceRange)) { + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + barrier.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT; + } + info.pImageMemoryBarriers = barriers.data(); + // Preserve the entry point requested by the guest. A Vulkan 1.1 device can + // enable KHR_synchronization2 without enabling the Vulkan 1.3 core command. + // An instance lookup may still return a core trampoline in that case. + auto next = host(name); + next(command, &info); + return 0; +} +uint64_t CmdPipelineBarrier2(const uint64_t *a) { + return pipeline_barrier2(a, "vkCmdPipelineBarrier2"); +} +uint64_t CmdPipelineBarrier2KHR(const uint64_t *a) { + return pipeline_barrier2(a, "vkCmdPipelineBarrier2KHR"); +} diff --git a/direct/src/vk_texture.cpp b/direct/src/vk_texture.cpp new file mode 100644 index 0000000..5fbf63f --- /dev/null +++ b/direct/src/vk_texture.cpp @@ -0,0 +1,740 @@ +// Host build of the emulator layer's texture substitution (see vk_texture.h). +// The wiring mirrors runtime/vulkan/android_vulkan_layer.cpp; "next" is the +// host loader's entry point instead of the next layer's. + +#include "vk_texture.h" + +#include "log.h" +#include "props.h" + +#include "texture/astc_substitution.h" + +#include + +#include +#include +#include +#include +#include +#include + +// --- android/log.h and sys/system_properties.h for the shared code ------------------------- + +int __android_log_print(int priority, const char* tag, const char* format, ...) { + va_list args; + va_start(args, format); + const auto level = priority >= ANDROID_LOG_ERROR ? axrb::LogLevel::Error + : priority == ANDROID_LOG_WARN ? axrb::LogLevel::Warn + : priority == ANDROID_LOG_INFO ? axrb::LogLevel::Info + : axrb::LogLevel::Debug; + axrb::log_writev(level, tag, format, args); + va_end(args); + return 0; +} + +int __system_property_get(const char* name, char* value) { + const std::string v = axrb::property_get(name); + const size_t n = std::min(v.size(), PROP_VALUE_MAX - 1); + std::memcpy(value, v.data(), n); + value[n] = 0; + return static_cast(n); +} + +namespace axrb::vktex { +namespace { + +using texture::AstcSubstitution; +using texture::DeviceCalls; + +Lookup g_lookup = nullptr; + +void* key(const void* handle) { return handle ? *reinterpret_cast(handle) : nullptr; } + +struct Device { + VkDevice handle = VK_NULL_HANDLE; + std::shared_ptr calls; +}; + +std::shared_mutex g_lock; +std::map g_devices; + +std::shared_ptr calls_for(const void* handle) { + std::shared_lock lock(g_lock); + auto it = g_devices.find(key(handle)); + return it == g_devices.end() ? nullptr : it->second.calls; +} + +// Loader entry points (trampolines, valid for every device), resolved once per +// name: used when a device has no substitution, and for physical devices. +std::shared_mutex g_resolved_lock; +std::map g_resolved; + +template +F host(const char* name) { + { + std::shared_lock lock(g_resolved_lock); + auto it = g_resolved.find(name); + if (it != g_resolved.end()) return reinterpret_cast(it->second); + } + PFN_vkVoidFunction fn = g_lookup(VK_NULL_HANDLE, name); + if (fn) { + std::unique_lock lock(g_resolved_lock); + g_resolved[name] = fn; + } + return reinterpret_cast(fn); +} + +template +F global(const char* name) { + return host(name); +} + +template +T a_(uint64_t v) { + if constexpr (std::is_pointer_v) return reinterpret_cast(static_cast(v)); + else return static_cast(v); +} + +uint64_t r_(VkResult r) { return static_cast(static_cast(r)); } + +AstcSubstitution& images() { return AstcSubstitution::instance(); } + +// Like vkSetDeviceLoaderData: dispatchable objects created inside the +// substitution share the device's loader dispatch pointer. +VKAPI_ATTR VkResult VKAPI_CALL set_loader_data(VkDevice device, void* object) { + *reinterpret_cast(object) = *reinterpret_cast(device); + return VK_SUCCESS; +} + +bool target_format(VkFormat format, VkFormat* target) { + int bw = 0, bh = 0; + bool srgb = false; + texture::Codec codec = texture::Codec::None; + return texture::compressed_format(format, bw, bh, srgb, codec, *target); +} + +// --- physical-device queries -------------------------------------------------------------- + +// Linear compressed images are CPU staging buffers backed by optimal images. +// Advertise the operations implemented by that backing image, rather than the +// desktop driver's (usually empty) native linear compressed-image capabilities. +constexpr VkFormatFeatureFlags kLinearFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | + VK_FORMAT_FEATURE_TRANSFER_DST_BIT; +constexpr VkImageUsageFlags kLinearUsage = VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + +uint64_t GetPhysicalDeviceFeatures(const uint64_t* a) { + auto pd = a_(a[0]); + auto* out = a_(a[1]); + global("vkGetPhysicalDeviceFeatures")(pd, out); + if (out->textureCompressionBC) out->textureCompressionASTC_LDR = out->textureCompressionETC2 = VK_TRUE; + return 0; +} + +uint64_t features2(const uint64_t* a, const char* name) { + auto pd = a_(a[0]); + auto* out = a_(a[1]); + auto next = global(name); + if (!next) next = global("vkGetPhysicalDeviceFeatures2"); + next(pd, out); + if (out->features.textureCompressionBC) + out->features.textureCompressionASTC_LDR = out->features.textureCompressionETC2 = VK_TRUE; + return 0; +} +uint64_t GetPhysicalDeviceFeatures2(const uint64_t* a) { return features2(a, "vkGetPhysicalDeviceFeatures2"); } +uint64_t GetPhysicalDeviceFeatures2KHR(const uint64_t* a) { return features2(a, "vkGetPhysicalDeviceFeatures2KHR"); } + +uint64_t GetPhysicalDeviceFormatProperties(const uint64_t* a) { + auto pd = a_(a[0]); + auto format = static_cast(a[1]); + VkFormat target; + const bool substituted = target_format(format, &target); + if (substituted) format = target; + global("vkGetPhysicalDeviceFormatProperties")( + pd, format, a_(a[2])); + if (substituted) { + auto* out = a_(a[2]); + out->linearTilingFeatures = out->optimalTilingFeatures & kLinearFeatures; + out->bufferFeatures = 0; + } + return 0; +} + +uint64_t format_properties2(const uint64_t* a, const char* name) { + auto pd = a_(a[0]); + auto format = static_cast(a[1]); + VkFormat target; + const bool substituted = target_format(format, &target); + if (substituted) format = target; + auto next = global(name); + if (!next) next = global("vkGetPhysicalDeviceFormatProperties2"); + next(pd, format, a_(a[2])); + if (substituted) { + auto* out = a_(a[2]); + out->formatProperties.linearTilingFeatures = out->formatProperties.optimalTilingFeatures & kLinearFeatures; + out->formatProperties.bufferFeatures = 0; + for (auto* chain = static_cast(out->pNext); chain; chain = chain->pNext) + if (chain->sType == VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3) { + auto* props = reinterpret_cast(chain); + props->linearTilingFeatures = props->optimalTilingFeatures & kLinearFeatures; + props->bufferFeatures = 0; + } + } + return 0; +} +uint64_t GetPhysicalDeviceFormatProperties2(const uint64_t* a) { + return format_properties2(a, "vkGetPhysicalDeviceFormatProperties2"); +} +uint64_t GetPhysicalDeviceFormatProperties2KHR(const uint64_t* a) { + return format_properties2(a, "vkGetPhysicalDeviceFormatProperties2KHR"); +} + +uint64_t GetPhysicalDeviceImageFormatProperties(const uint64_t* a) { + auto format = static_cast(a[1]); + auto tiling = static_cast(a[3]); + auto usage = static_cast(a[4]); + VkFormat target; + if (target_format(format, &target)) { + if (a[2] != VK_IMAGE_TYPE_2D && a[2] != VK_IMAGE_TYPE_3D) + return r_(VK_ERROR_FORMAT_NOT_SUPPORTED); + if (tiling == VK_IMAGE_TILING_LINEAR) { + if (a[2] != VK_IMAGE_TYPE_2D || (usage & ~kLinearUsage)) return r_(VK_ERROR_FORMAT_NOT_SUPPORTED); + tiling = VK_IMAGE_TILING_OPTIMAL; + usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + } else if (tiling != VK_IMAGE_TILING_OPTIMAL) return r_(VK_ERROR_FORMAT_NOT_SUPPORTED); + format = target; + } + return r_(global("vkGetPhysicalDeviceImageFormatProperties")( + a_(a[0]), format, static_cast(a[2]), tiling, + usage, static_cast(a[5]), + a_(a[6]))); +} + +uint64_t image_format_properties2(const uint64_t* a, const char* name) { + auto info = *a_(a[1]); + VkFormat target; + if (target_format(info.format, &target)) { + if (info.type != VK_IMAGE_TYPE_2D && info.type != VK_IMAGE_TYPE_3D) + return r_(VK_ERROR_FORMAT_NOT_SUPPORTED); + if (info.tiling == VK_IMAGE_TILING_LINEAR) { + if (info.type != VK_IMAGE_TYPE_2D || (info.usage & ~kLinearUsage)) return r_(VK_ERROR_FORMAT_NOT_SUPPORTED); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + } else if (info.tiling != VK_IMAGE_TILING_OPTIMAL) return r_(VK_ERROR_FORMAT_NOT_SUPPORTED); + info.format = target; + } + auto next = global(name); + if (!next) next = global("vkGetPhysicalDeviceImageFormatProperties2"); + return r_(next(a_(a[0]), &info, a_(a[2]))); +} +uint64_t GetPhysicalDeviceImageFormatProperties2(const uint64_t* a) { + return image_format_properties2(a, "vkGetPhysicalDeviceImageFormatProperties2"); +} +uint64_t GetPhysicalDeviceImageFormatProperties2KHR(const uint64_t* a) { + return image_format_properties2(a, "vkGetPhysicalDeviceImageFormatProperties2KHR"); +} + +// --- device commands ---------------------------------------------------------------------- + +#define NEXT(field, name) (c ? c->field : host(name)) + +#include "vk_linear.inc" + +uint64_t DestroyDevice(const uint64_t* a) { + auto device = a_(a[0]); + if (!device) return 0; + if (calls_for(device)) { + std::vector linearImages; + { + std::lock_guard lock(g_linear_lock); + for (const auto& [image, linear] : g_linear) + if (linear->device == device) linearImages.push_back(image); + } + for (VkImage image : linearImages) { + images().destroy_image(device, image, nullptr); + destroy_linear(device, image); + } + images().remove_device(device); + std::unique_lock lock(g_lock); + g_devices.erase(key(device)); + } + host("vkDestroyDevice")(device, nullptr); + return 0; +} + +uint64_t CreateImage(const uint64_t* a) { + auto device = a_(a[0]); + auto* info = a_(a[1]); + auto* out = a_(a[3]); + for (auto* next = static_cast(info->pNext); next; next = next->pNext) + TRACE("vkimage", "image extension sType=%u", next->sType); + TRACE("vkimage", "create image format=%u type=%u extent=%ux%ux%u mips=%u layers=%u samples=%u flags=0x%x usage=0x%x next=%p", + info->format, info->imageType, info->extent.width, info->extent.height, info->extent.depth, + info->mipLevels, info->arrayLayers, info->samples, info->flags, info->usage, info->pNext); + if (!calls_for(device)) return r_(host("vkCreateImage")(device, info, nullptr, out)); + VkFormat substituted; + if (info->tiling == VK_IMAGE_TILING_LINEAR && target_format(info->format, &substituted)) + return r_(create_linear(device, info, out)); + const VkResult result = images().create_image(device, info, nullptr, out); + TRACE("vkimage", "created image=%p result=%d usage=0x%x", result == VK_SUCCESS ? *out : VK_NULL_HANDLE, result, info->usage); + if (result != VK_SUCCESS) LOGW("vulkan", "image creation failed %d format=%u tiling=%u", result, info->format, info->tiling); + return r_(result); +} + +uint64_t DestroyImage(const uint64_t* a) { + auto device = a_(a[0]); + auto image = a_(a[1]); + if (!calls_for(device)) host("vkDestroyImage")(device, image, nullptr); + else images().destroy_image(device, image, nullptr); + destroy_linear(device, image); + return 0; +} + +uint64_t CreateImageView(const uint64_t* a) { + auto device = a_(a[0]); + auto* info = a_(a[1]); + auto* out = a_(a[3]); + if (!calls_for(device)) return r_(host("vkCreateImageView")(device, info, nullptr, out)); + return r_(images().create_image_view(device, info, nullptr, out)); +} + +uint64_t device_image_memory_requirements(const uint64_t* a, const char* name) { + const auto* info = a_(a[1]); + VkFormat target; + if (!target_format(info->pCreateInfo->format, &target)) { + host(name)( + a_(a[0]), info, a_(a[2])); + return 0; + } + // Maintenance4 queries must use the same substitution and linear staging + // requirements as normal image creation, never the unsupported ASTC ICD path. + VkImage image{}; + const uint64_t create[] = {a[0], reinterpret_cast(info->pCreateInfo), 0, reinterpret_cast(&image)}; + if (static_cast(CreateImage(create)) != VK_SUCCESS) { + a_(a[2])->memoryRequirements = {}; + return 0; + } + VkImageMemoryRequirementsInfo2 query{VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2}; + query.image = image; + const uint64_t requirements[] = {a[0], reinterpret_cast(&query), a[2]}; + GetImageMemoryRequirements2(requirements); + const uint64_t destroy[] = {a[0], reinterpret_cast(image), 0}; + DestroyImage(destroy); + return 0; +} +uint64_t GetDeviceImageMemoryRequirements(const uint64_t* a) { + return device_image_memory_requirements(a, "vkGetDeviceImageMemoryRequirements"); +} +uint64_t GetDeviceImageMemoryRequirementsKHR(const uint64_t* a) { + return device_image_memory_requirements(a, "vkGetDeviceImageMemoryRequirementsKHR"); +} + +uint64_t CreateBuffer(const uint64_t* a) { + auto device = a_(a[0]); + auto* info = a_(a[1]); + auto* out = a_(a[3]); + auto c = calls_for(device); + const VkResult r = NEXT(createBuffer, "vkCreateBuffer")(device, info, nullptr, out); + if (r == VK_SUCCESS && c) images().created_buffer(*out, info->size); + return r_(r); +} + +uint64_t DestroyBuffer(const uint64_t* a) { + auto device = a_(a[0]); + auto buffer = a_(a[1]); + auto c = calls_for(device); + if (c) images().destroyed_buffer(buffer); + NEXT(destroyBuffer, "vkDestroyBuffer")(device, buffer, nullptr); + return 0; +} + +uint64_t BindBufferMemory(const uint64_t* a) { + auto device = a_(a[0]); + auto buffer = a_(a[1]); + auto memory = a_(a[2]); + auto c = calls_for(device); + const VkResult r = NEXT(bindBufferMemory, "vkBindBufferMemory")(device, buffer, memory, a[3]); + if (r == VK_SUCCESS && c) images().bound_buffer(buffer, memory, a[3]); + return r_(r); +} + +uint64_t bind_buffer_memory2(const uint64_t* a, const char* name) { + auto device = a_(a[0]); + const auto count = static_cast(a[1]); + auto* infos = a_(a[2]); + auto c = calls_for(device); + auto next = host(name); + const VkResult r = next(device, count, infos); + if (r == VK_SUCCESS && c) + for (uint32_t i = 0; i < count; ++i) images().bound_buffer(infos[i].buffer, infos[i].memory, infos[i].memoryOffset); + return r_(r); +} +uint64_t BindBufferMemory2(const uint64_t* a) { return bind_buffer_memory2(a, "vkBindBufferMemory2"); } +uint64_t BindBufferMemory2KHR(const uint64_t* a) { return bind_buffer_memory2(a, "vkBindBufferMemory2KHR"); } + +uint64_t AllocateMemory(const uint64_t* a) { + auto device = a_(a[0]); + auto* info = a_(a[1]); + auto* out = a_(a[3]); + auto c = calls_for(device); + const VkResult r = NEXT(allocateMemory, "vkAllocateMemory")(device, info, nullptr, out); + if (r == VK_SUCCESS && c) images().allocated_memory(device, info, *out); + return r_(r); +} + +uint64_t FreeMemory(const uint64_t* a) { + auto device = a_(a[0]); + auto memory = a_(a[1]); + auto c = calls_for(device); + if (c) images().free_memory(memory); + NEXT(freeMemory, "vkFreeMemory")(device, memory, nullptr); + return 0; +} + +uint64_t MapMemory(const uint64_t* a) { + auto device = a_(a[0]); + auto memory = a_(a[1]); + auto** out = a_(a[5]); + auto c = calls_for(device); + const VkResult r = NEXT(mapMemory, "vkMapMemory")(device, memory, a[2], a[3], static_cast(a[4]), out); + if (r == VK_SUCCESS && c) images().mapped(memory, a[2], *out); + return r_(r); +} + +uint64_t UnmapMemory(const uint64_t* a) { + auto device = a_(a[0]); + auto memory = a_(a[1]); + auto c = calls_for(device); + if (c) images().unmapped(memory); + NEXT(unmapMemory, "vkUnmapMemory")(device, memory); + return 0; +} + +uint64_t CreateCommandPool(const uint64_t* a) { + auto device = a_(a[0]); + auto* info = a_(a[1]); + auto* out = a_(a[3]); + const VkResult r = host("vkCreateCommandPool")(device, info, nullptr, out); + if (r == VK_SUCCESS && calls_for(device)) images().created_pool(*out, info->queueFamilyIndex); + return r_(r); +} + +uint64_t AllocateCommandBuffers(const uint64_t* a) { + auto device = a_(a[0]); + auto* info = a_(a[1]); + auto* out = a_(a[2]); + // Through the loader's trampoline, which sets up the new handles' dispatch. + const VkResult r = host("vkAllocateCommandBuffers")(device, info, out); + if (r == VK_SUCCESS && calls_for(device)) + images().allocated_command_buffers(device, info->commandPool, info->commandBufferCount, out, + info->level == VK_COMMAND_BUFFER_LEVEL_PRIMARY); + return r_(r); +} + +uint64_t FreeCommandBuffers(const uint64_t* a) { + auto device = a_(a[0]); + auto pool = a_(a[1]); + const auto count = static_cast(a[2]); + auto* buffers = a_(a[3]); + if (calls_for(device)) images().freed_command_buffers(count, buffers); + host("vkFreeCommandBuffers")(device, pool, count, buffers); + return 0; +} + +uint64_t ResetCommandPool(const uint64_t* a) { + auto device = a_(a[0]); + auto pool = a_(a[1]); + auto c = calls_for(device); + if (c) images().reset_pool(pool, false); + return r_(NEXT(resetCommandPool, "vkResetCommandPool")(device, pool, static_cast(a[2]))); +} + +uint64_t DestroyCommandPool(const uint64_t* a) { + auto device = a_(a[0]); + auto pool = a_(a[1]); + auto c = calls_for(device); + if (c) images().reset_pool(pool, true); + NEXT(destroyCommandPool, "vkDestroyCommandPool")(device, pool, nullptr); + return 0; +} + +uint64_t BeginCommandBuffer(const uint64_t* a) { + auto command = a_(a[0]); + auto* info = a_(a[1]); + auto c = calls_for(command); + if (c) images().reset_command_buffer(command); + const VkResult r = NEXT(beginCommandBuffer, "vkBeginCommandBuffer")(command, info); + if (r == VK_SUCCESS && c) images().began(command, info->flags); + return r_(r); +} + +uint64_t ResetCommandBuffer(const uint64_t* a) { + auto command = a_(a[0]); + auto c = calls_for(command); + if (c) images().reset_command_buffer(command); + return r_(NEXT(resetCommandBuffer, "vkResetCommandBuffer")(command, static_cast(a[1]))); +} + +uint64_t CmdCopyBufferToImage(const uint64_t* a) { + auto command = a_(a[0]); + auto source = a_(a[1]); + auto target = a_(a[2]); + auto layout = static_cast(a[3]); + const auto count = static_cast(a[4]); + auto* regions = a_(a[5]); + auto c = calls_for(command); + if (c && images().copy_buffer_to_image(command, c->device, source, target, layout, count, regions)) return 0; + NEXT(copyBufferToImage, "vkCmdCopyBufferToImage")(command, source, target, layout, count, regions); + return 0; +} + +uint64_t copy_buffer_to_image2(const uint64_t* a, const char* name) { + auto command = a_(a[0]); + auto* info = a_(a[1]); + auto c = calls_for(command); + if (c) { + std::vector regions(info->regionCount); + for (uint32_t i = 0; i < info->regionCount; ++i) { + const auto& r = info->pRegions[i]; + regions[i] = {r.bufferOffset, r.bufferRowLength, r.bufferImageHeight, r.imageSubresource, r.imageOffset, r.imageExtent}; + } + if (images().copy_buffer_to_image(command, c->device, info->srcBuffer, info->dstImage, info->dstImageLayout, + info->regionCount, regions.data())) + return 0; + } + auto next = host(name); + next(command, info); + return 0; +} +uint64_t CmdCopyBufferToImage2(const uint64_t* a) { return copy_buffer_to_image2(a, "vkCmdCopyBufferToImage2"); } +uint64_t CmdCopyBufferToImage2KHR(const uint64_t* a) { return copy_buffer_to_image2(a, "vkCmdCopyBufferToImage2KHR"); } + +uint64_t CmdExecuteCommands(const uint64_t* a) { + auto command = a_(a[0]); + const auto count = static_cast(a[1]); + auto* secondaries = a_(a[2]); + auto c = calls_for(command); + if (c) images().executed(command, count, secondaries); + NEXT(executeCommands, "vkCmdExecuteCommands")(command, count, secondaries); + return 0; +} + +uint64_t QueueSubmit(const uint64_t* a) { + auto queue = a_(a[0]); + const auto count = static_cast(a[1]); + auto* submits = a_(a[2]); + auto fence = a_(a[3]); + auto c = calls_for(queue); + auto next = NEXT(queueSubmit, "vkQueueSubmit"); + if (!c) return r_(next(queue, count, submits, fence)); + for (uint32_t i = 0; i < count; ++i) images().before_submit(submits[i].commandBufferCount, submits[i].pCommandBuffers); + if (!c->computeFamilies) return r_(next(queue, count, submits, fence)); + std::vector modified; + std::vector> lists(count); + for (uint32_t i = 0; i < count; ++i) { + for (uint32_t j = 0; j < submits[i].commandBufferCount; ++j) { + images().gpu_commands(submits[i].pCommandBuffers[j], lists[i]); + lists[i].push_back(submits[i].pCommandBuffers[j]); + } + if (lists[i].size() != submits[i].commandBufferCount) { + if (modified.empty()) modified.assign(submits, submits + count); + modified[i].commandBufferCount = static_cast(lists[i].size()); + modified[i].pCommandBuffers = lists[i].data(); + } + } + return r_(next(queue, count, modified.empty() ? submits : modified.data(), fence)); +} + +uint64_t queue_submit2(const uint64_t* a, const char* name) { + auto queue = a_(a[0]); + const auto count = static_cast(a[1]); + auto* submits = a_(a[2]); + auto fence = a_(a[3]); + auto c = calls_for(queue); + auto next = host(name); + if (!c) return r_(next(queue, count, submits, fence)); + for (uint32_t i = 0; i < count; ++i) { + std::vector buffers; + for (uint32_t j = 0; j < submits[i].commandBufferInfoCount; ++j) + buffers.push_back(submits[i].pCommandBufferInfos[j].commandBuffer); + images().before_submit(static_cast(buffers.size()), buffers.data()); + } + if (!c->computeFamilies) return r_(next(queue, count, submits, fence)); + std::vector modified; + std::vector> lists(count); + for (uint32_t i = 0; i < count; ++i) { + for (uint32_t j = 0; j < submits[i].commandBufferInfoCount; ++j) { + const auto& original = submits[i].pCommandBufferInfos[j]; + std::vector extra; + images().gpu_commands(original.commandBuffer, extra); + for (auto command : extra) { + VkCommandBufferSubmitInfo item{VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO}; + item.commandBuffer = command; + item.deviceMask = original.deviceMask; + lists[i].push_back(item); + } + lists[i].push_back(original); + } + if (lists[i].size() != submits[i].commandBufferInfoCount) { + if (modified.empty()) modified.assign(submits, submits + count); + modified[i].commandBufferInfoCount = static_cast(lists[i].size()); + modified[i].pCommandBufferInfos = lists[i].data(); + } + } + return r_(next(queue, count, modified.empty() ? submits : modified.data(), fence)); +} +uint64_t QueueSubmit2(const uint64_t* a) { return queue_submit2(a, "vkQueueSubmit2"); } +uint64_t QueueSubmit2KHR(const uint64_t* a) { return queue_submit2(a, "vkQueueSubmit2KHR"); } + +#undef NEXT + +} // namespace + +void init(Lookup lookup) { g_lookup = lookup; } + +bool enabled() { return texture::astc_to_bc7_enabled(); } + +std::vector> overrides() { + if (!enabled()) return {}; + return { + {"vkGetPhysicalDeviceFeatures", GetPhysicalDeviceFeatures}, + {"vkGetPhysicalDeviceFeatures2", GetPhysicalDeviceFeatures2}, + {"vkGetPhysicalDeviceFeatures2KHR", GetPhysicalDeviceFeatures2KHR}, + {"vkGetPhysicalDeviceFormatProperties", GetPhysicalDeviceFormatProperties}, + {"vkGetPhysicalDeviceFormatProperties2", GetPhysicalDeviceFormatProperties2}, + {"vkGetPhysicalDeviceFormatProperties2KHR", GetPhysicalDeviceFormatProperties2KHR}, + {"vkGetPhysicalDeviceImageFormatProperties", GetPhysicalDeviceImageFormatProperties}, + {"vkGetPhysicalDeviceImageFormatProperties2", GetPhysicalDeviceImageFormatProperties2}, + {"vkGetPhysicalDeviceImageFormatProperties2KHR", GetPhysicalDeviceImageFormatProperties2KHR}, + {"vkDestroyDevice", DestroyDevice}, + {"vkCreateImage", CreateImage}, + {"vkDestroyImage", DestroyImage}, + {"vkCreateImageView", CreateImageView}, + {"vkGetImageMemoryRequirements", GetImageMemoryRequirements}, + {"vkGetImageMemoryRequirements2", GetImageMemoryRequirements2}, + {"vkGetImageMemoryRequirements2KHR", GetImageMemoryRequirements2KHR}, + {"vkGetDeviceImageMemoryRequirements", GetDeviceImageMemoryRequirements}, + {"vkGetDeviceImageMemoryRequirementsKHR", GetDeviceImageMemoryRequirementsKHR}, + {"vkBindImageMemory", BindImageMemory}, + {"vkBindImageMemory2", BindImageMemory2}, + {"vkBindImageMemory2KHR", BindImageMemory2KHR}, + {"vkGetImageSubresourceLayout", GetImageSubresourceLayout}, + {"vkCmdPipelineBarrier", CmdPipelineBarrier}, + {"vkCmdPipelineBarrier2", CmdPipelineBarrier2}, + {"vkCmdPipelineBarrier2KHR", CmdPipelineBarrier2KHR}, + {"vkCreateBuffer", CreateBuffer}, + {"vkDestroyBuffer", DestroyBuffer}, + {"vkBindBufferMemory", BindBufferMemory}, + {"vkBindBufferMemory2", BindBufferMemory2}, + {"vkBindBufferMemory2KHR", BindBufferMemory2KHR}, + {"vkAllocateMemory", AllocateMemory}, + {"vkFreeMemory", FreeMemory}, + {"vkMapMemory", MapMemory}, + {"vkUnmapMemory", UnmapMemory}, + {"vkCreateCommandPool", CreateCommandPool}, + {"vkAllocateCommandBuffers", AllocateCommandBuffers}, + {"vkFreeCommandBuffers", FreeCommandBuffers}, + {"vkResetCommandPool", ResetCommandPool}, + {"vkDestroyCommandPool", DestroyCommandPool}, + {"vkBeginCommandBuffer", BeginCommandBuffer}, + {"vkResetCommandBuffer", ResetCommandBuffer}, + {"vkCmdCopyBufferToImage", CmdCopyBufferToImage}, + {"vkCmdCopyBufferToImage2", CmdCopyBufferToImage2}, + {"vkCmdCopyBufferToImage2KHR", CmdCopyBufferToImage2KHR}, + {"vkCmdExecuteCommands", CmdExecuteCommands}, + {"vkQueueSubmit", QueueSubmit}, + {"vkQueueSubmit2", QueueSubmit2}, + {"vkQueueSubmit2KHR", QueueSubmit2KHR}, + }; +} + +void prepare_device(VkInstance instance, VkPhysicalDevice physical, VkDeviceCreateInfo& info, DeviceFeatures& state) { + if (!enabled()) return; + VkPhysicalDeviceFeatures supported{}; + reinterpret_cast(g_lookup(instance, "vkGetPhysicalDeviceFeatures"))(physical, &supported); + if (!supported.textureCompressionBC) return; + state.active = true; + for (auto* node = static_cast(info.pNext); node; node = node->pNext) + if (node->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2) + state.features2 = reinterpret_cast(const_cast(node)); + auto adjust = [](VkPhysicalDeviceFeatures& f) { + f.textureCompressionBC = VK_TRUE; + f.textureCompressionASTC_LDR = VK_FALSE; + f.textureCompressionETC2 = VK_FALSE; + }; + if (state.features2) { + // Guest memory: restored in finish_device. + state.saved2 = state.features2->features; + adjust(state.features2->features); + } else { + if (info.pEnabledFeatures) state.plain = *info.pEnabledFeatures; + adjust(state.plain); + info.pEnabledFeatures = &state.plain; + } +} + +void finish_device(VkInstance instance, VkPhysicalDevice physical, const VkDeviceCreateInfo&, DeviceFeatures& state, + VkResult result, VkDevice device) { + if (state.features2) state.features2->features = state.saved2; + if (!state.active || result != VK_SUCCESS) return; + auto gipa = [&](const char* name) { return g_lookup(instance, name); }; + DeviceCalls calls; + calls.device = device; + calls.gdpa = reinterpret_cast(gipa("vkGetDeviceProcAddr")); + calls.loader = set_loader_data; + if (property_get("debug.axrb.astc_gpu") != "0") { + auto query = reinterpret_cast( + gipa("vkGetPhysicalDeviceQueueFamilyProperties")); + uint32_t n = 0; + query(physical, &n, nullptr); + std::vector families(n); + query(physical, &n, families.data()); + for (uint32_t i = 0; i < n && i < 32; ++i) + if (families[i].queueFlags & VK_QUEUE_COMPUTE_BIT) calls.computeFamilies |= 1u << i; + VkPhysicalDeviceProperties props{}; + reinterpret_cast(gipa("vkGetPhysicalDeviceProperties"))(physical, &props); + calls.maxStorageRange = props.limits.maxStorageBufferRange; + } + reinterpret_cast(gipa("vkGetPhysicalDeviceMemoryProperties"))(physical, + &calls.memory); + auto fn = [&](const char* name) { return calls.gdpa(device, name); }; +#define AXRB_CALL(field, name) calls.field = reinterpret_cast(fn(#name)) + AXRB_CALL(createImage, vkCreateImage); + AXRB_CALL(destroyImage, vkDestroyImage); + AXRB_CALL(createImageView, vkCreateImageView); + AXRB_CALL(createBuffer, vkCreateBuffer); + AXRB_CALL(destroyBuffer, vkDestroyBuffer); + AXRB_CALL(bufferRequirements, vkGetBufferMemoryRequirements); + AXRB_CALL(bindBufferMemory, vkBindBufferMemory); + AXRB_CALL(bindBufferMemory2, vkBindBufferMemory2); + AXRB_CALL(allocateMemory, vkAllocateMemory); + AXRB_CALL(freeMemory, vkFreeMemory); + AXRB_CALL(mapMemory, vkMapMemory); + AXRB_CALL(unmapMemory, vkUnmapMemory); + AXRB_CALL(allocateCommandBuffers, vkAllocateCommandBuffers); + AXRB_CALL(freeCommandBuffers, vkFreeCommandBuffers); + AXRB_CALL(resetCommandPool, vkResetCommandPool); + AXRB_CALL(destroyCommandPool, vkDestroyCommandPool); + AXRB_CALL(beginCommandBuffer, vkBeginCommandBuffer); + AXRB_CALL(resetCommandBuffer, vkResetCommandBuffer); + AXRB_CALL(copyBufferToImage, vkCmdCopyBufferToImage); + AXRB_CALL(copyBufferToImage2, vkCmdCopyBufferToImage2); + AXRB_CALL(copyImageToBuffer, vkCmdCopyImageToBuffer); + AXRB_CALL(executeCommands, vkCmdExecuteCommands); + AXRB_CALL(queueSubmit, vkQueueSubmit); + AXRB_CALL(queueSubmit2, vkQueueSubmit2); +#undef AXRB_CALL + if (!calls.bindBufferMemory2) calls.bindBufferMemory2 = reinterpret_cast(fn("vkBindBufferMemory2KHR")); + if (!calls.copyBufferToImage2) + calls.copyBufferToImage2 = reinterpret_cast(fn("vkCmdCopyBufferToImage2KHR")); + if (!calls.queueSubmit2) calls.queueSubmit2 = reinterpret_cast(fn("vkQueueSubmit2KHR")); + auto shared = std::make_shared(calls); + { + std::unique_lock lock(g_lock); + g_devices[key(device)] = {device, shared}; + } + images().add_device(calls); +} + +} // namespace axrb::vktex diff --git a/direct/src/vk_texture.h b/direct/src/vk_texture.h new file mode 100644 index 0000000..7468d87 --- /dev/null +++ b/direct/src/vk_texture.h @@ -0,0 +1,39 @@ +#pragma once + +#define VK_NO_PROTOTYPES 1 +#include + +#include +#include +#include + +namespace axrb::vktex { + +// Mobile texture formats on a desktop GPU: ASTC, ETC2 and EAC images are +// stored in native BC formats and their uploads transcoded on the way +// (runtime/vulkan/texture, the same code as the emulator's guest layer). The +// guest is told these formats are supported, as they are on a Quest. + +using Override = uint64_t (*)(const uint64_t* args); +using Lookup = PFN_vkVoidFunction (*)(VkInstance instance, const char* name); + +// lookup resolves host commands (instance-level, through the system loader). +void init(Lookup lookup); +bool enabled(); + +// Overrides for the commands the substitution needs to see. +std::vector> overrides(); + +// vkCreateDevice: adjusts the enabled features (BC instead of ASTC/ETC2) for the +// duration of the call, then registers the created device. +struct DeviceFeatures { + VkPhysicalDeviceFeatures plain{}; + VkPhysicalDeviceFeatures2* features2 = nullptr; + VkPhysicalDeviceFeatures saved2{}; + bool active = false; +}; +void prepare_device(VkInstance instance, VkPhysicalDevice physical, VkDeviceCreateInfo& info, DeviceFeatures& state); +void finish_device(VkInstance instance, VkPhysicalDevice physical, const VkDeviceCreateInfo& info, DeviceFeatures& state, + VkResult result, VkDevice device); + +} // namespace axrb::vktex diff --git a/direct/src/window.cpp b/direct/src/window.cpp new file mode 100644 index 0000000..2a3286d --- /dev/null +++ b/direct/src/window.cpp @@ -0,0 +1,111 @@ +// Desktop windows for Android surfaces. They run on their own message thread so +// that guest threads (which block in Vulkan, futexes, ...) never have to pump. + +#include "window.h" + +#include "app.h" + +#include "log.h" + +#include +#include +#include +#include + +namespace axrb { +namespace { + +constexpr UINT kCreateWindow = WM_APP + 1; +constexpr wchar_t kClassName[] = L"AXRBDirectSurface"; + +struct CreateRequest { + int32_t width, height; + HWND result = nullptr; + HANDLE done = nullptr; +}; + +std::mutex g_lock; +DWORD g_thread_id = 0; +std::map g_windows; + +LRESULT CALLBACK window_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + switch (msg) { + case WM_CLOSE: + LOGI("window", "surface window closed: ending the application"); + request_session_end("window closed"); + return 0; + case WM_ERASEBKGND: + return 1; // the swapchain paints everything + } + return DefWindowProcW(hwnd, msg, wp, lp); +} + +HWND create_on_thread(int32_t width, int32_t height) { + // Fit the surface into three quarters of the work area, keeping its aspect. + RECT work{0, 0, 1920, 1080}; + SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0); + const double max_w = (work.right - work.left) * 0.75, max_h = (work.bottom - work.top) * 0.75; + double scale = 1.0; + if (width > max_w) scale = max_w / width; + if (height * scale > max_h) scale = max_h / height; + RECT r{0, 0, static_cast(width * scale), static_cast(height * scale)}; + const DWORD style = WS_OVERLAPPEDWINDOW; + AdjustWindowRect(&r, style, FALSE); + HWND hwnd = CreateWindowExW(0, kClassName, L"AXRB Direct", style, CW_USEDEFAULT, CW_USEDEFAULT, r.right - r.left, + r.bottom - r.top, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); + if (hwnd) ShowWindow(hwnd, SW_SHOWNOACTIVATE); + return hwnd; +} + +void message_thread(HANDLE ready) { + WNDCLASSEXW wc{sizeof wc}; + wc.lpfnWndProc = window_proc; + wc.hInstance = GetModuleHandleW(nullptr); + wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wc.hbrBackground = static_cast(GetStockObject(BLACK_BRUSH)); + wc.lpszClassName = kClassName; + RegisterClassExW(&wc); + MSG msg; + PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE); // creates this thread's queue + SetEvent(ready); + while (GetMessageW(&msg, nullptr, 0, 0) > 0) { + if (msg.hwnd == nullptr && msg.message == kCreateWindow) { + auto* req = reinterpret_cast(msg.lParam); + req->result = create_on_thread(req->width, req->height); + SetEvent(req->done); + continue; + } + TranslateMessage(&msg); + DispatchMessageW(&msg); + } +} + +DWORD ensure_thread() { + if (g_thread_id) return g_thread_id; + HANDLE ready = CreateEventW(nullptr, TRUE, FALSE, nullptr); + std::thread t(message_thread, ready); + g_thread_id = GetThreadId(t.native_handle()); + t.detach(); + WaitForSingleObject(ready, INFINITE); + CloseHandle(ready); + return g_thread_id; +} + +} // namespace + +HWND host_window_for(uint64_t native_window, int32_t width, int32_t height) { + std::lock_guard lock(g_lock); + auto it = g_windows.find(native_window); + if (it != g_windows.end() && IsWindow(it->second)) return it->second; + CreateRequest req{width > 0 ? width : 1920, height > 0 ? height : 1080}; + req.done = CreateEventW(nullptr, TRUE, FALSE, nullptr); + PostThreadMessageW(ensure_thread(), kCreateWindow, 0, reinterpret_cast(&req)); + WaitForSingleObject(req.done, INFINITE); + CloseHandle(req.done); + if (req.result) g_windows[native_window] = req.result; + LOGI("window", "surface window %p for ANativeWindow %llx (%dx%d)", req.result, + static_cast(native_window), req.width, req.height); + return req.result; +} + +} // namespace axrb diff --git a/direct/src/window.h b/direct/src/window.h new file mode 100644 index 0000000..a8fe1da --- /dev/null +++ b/direct/src/window.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include + +namespace axrb { + +// The desktop window showing an Android surface (the flat mirror of a VR app). +// One per ANativeWindow, created on a dedicated message thread on first use and +// sized like the surface. Closing it ends the application, like the system +// back-to-home on a device. +HWND host_window_for(uint64_t native_window, int32_t width, int32_t height); + +} // namespace axrb diff --git a/direct/tests/art/ArtSmoke.java b/direct/tests/art/ArtSmoke.java new file mode 100644 index 0000000..1ef8edb --- /dev/null +++ b/direct/tests/art/ArtSmoke.java @@ -0,0 +1,35 @@ +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +/** Runs on unmodified Android ART, not AXRB's managed interpreter. */ +public final class ArtSmoke { + private static native int nativeRoundTrip(int[] values); + public static int twice(int value) { return value * 2; } + + public static void main(String[] args) throws Exception { + System.out.println("ART_SMOKE_START " + System.getProperty("java.vm.name")); + Method method = ArtSmoke.class.getMethod("twice", int.class); + if (!Integer.valueOf(42).equals(method.invoke(null, 21))) throw new AssertionError("reflection"); + System.load("/system/lib64/libaxrb_art_smoke.so"); + int[] values = {21}; + if (nativeRoundTrip(values) != 42 || values[0] != 42) throw new AssertionError("JNI callback/array/thread"); + AtomicInteger result = new AtomicInteger(); + CountDownLatch finished = new CountDownLatch(1); + Thread worker = new Thread(() -> { result.set(42); finished.countDown(); }, "art-smoke"); + worker.start(); + finished.await(); + worker.join(); + if (result.get() != 42) throw new AssertionError("thread visibility"); + for (int i = 0; i < 10000; i++) { + byte[] bytes = new byte[4096]; + bytes[i % bytes.length] = 42; + } + System.gc(); + try { throw new IllegalStateException("expected"); } + catch (IllegalStateException expected) { + if (!"expected".equals(expected.getMessage())) throw new AssertionError("exception"); + } + System.out.println("ART_SMOKE_PASS"); + } +} diff --git a/direct/tests/art/AudioDeviceProbe.java b/direct/tests/art/AudioDeviceProbe.java new file mode 100644 index 0000000..cda211d --- /dev/null +++ b/direct/tests/art/AudioDeviceProbe.java @@ -0,0 +1,46 @@ +package com.axrb.audioprobe; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.media.*; +import java.util.Arrays; + +/** Exercise the real AOSP enumeration and initial device-callback path. */ +public final class AudioDeviceProbe extends Activity { + private boolean delivered; + private static void require(boolean ok, String text) { + if (!ok) throw new AssertionError(text); + } + @Override public void onCreate(Bundle state) { + super.onCreate(state); + AudioManager manager = (AudioManager)getSystemService(AUDIO_SERVICE); + AudioDeviceInfo[] outputs = manager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); + require(outputs.length == 1, "one default output route"); + AudioDeviceInfo output = outputs[0]; + require(output.isSink() && !output.isSource(), "output direction"); + require(output.getId() == 2, "same route ID as the AAudio backend"); + require(Arrays.stream(output.getSampleRates()).anyMatch(v -> v == 48000), "48kHz"); + require(Arrays.stream(output.getChannelCounts()).anyMatch(v -> v == 2), "stereo"); + require(Arrays.stream(output.getEncodings()).anyMatch(v -> v == AudioFormat.ENCODING_PCM_16BIT), "PCM16"); + require(Arrays.stream(output.getEncodings()).anyMatch(v -> v == AudioFormat.ENCODING_PCM_FLOAT), "float PCM"); + require(manager.getDevices(AudioManager.GET_DEVICES_INPUTS).length == 0, "no microphone advertised"); + require(manager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)[0].getId() == output.getId(), "stable route"); + require(!manager.isBluetoothA2dpOn(), "no guest Bluetooth route"); + Handler handler = new Handler(Looper.getMainLooper()); + AudioDeviceCallback callback = new AudioDeviceCallback() { + @Override public void onAudioDevicesAdded(AudioDeviceInfo[] added) { + require(added.length == 1 && added[0].getId() == output.getId(), "initial output callback"); + delivered = true; + manager.unregisterAudioDeviceCallback(this); + System.out.println("AUDIO_DEVICE_PROBE_PASS"); + System.exit(0); + } + }; + manager.registerAudioDeviceCallback(callback, handler); + handler.postDelayed(() -> { + if (!delivered) { System.err.println("AUDIO_DEVICE_CALLBACK_TIMEOUT"); System.exit(1); } + }, 10000); + } +} diff --git a/direct/tests/art/CompanionApkProbe.java b/direct/tests/art/CompanionApkProbe.java new file mode 100644 index 0000000..e000aa7 --- /dev/null +++ b/direct/tests/art/CompanionApkProbe.java @@ -0,0 +1,27 @@ +package com.axrb.companionprobe; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +public final class CompanionApkProbe extends Activity { + @Override protected void onCreate(Bundle state) { + super.onCreate(state); + try { + String name = "com.axrb.companionfixture"; + boolean visible = getPackageManager().getPackageInfo(name, 0) != null; + Context context = createPackageContext(name, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); + Object value = context.getClassLoader().loadClass(name + ".CompanionHelper").getMethod("value").invoke(null); + boolean main = false; + for (ResolveInfo info : getPackageManager().queryIntentActivities(new Intent(Intent.ACTION_MAIN), 0)) + if (name.equals(info.activityInfo.packageName)) main = true; + System.out.println("COMPANION_APK_PROBE visible=" + visible + " context=" + (context != null) + + " class=" + "companion-loaded".equals(value) + " main=" + main); + } catch (Throwable error) { + System.out.println("COMPANION_APK_PROBE_FAILED " + error); + error.printStackTrace(System.out); + } + System.exit(0); + } +} diff --git a/direct/tests/art/CompanionHelper.java b/direct/tests/art/CompanionHelper.java new file mode 100644 index 0000000..3630d85 --- /dev/null +++ b/direct/tests/art/CompanionHelper.java @@ -0,0 +1,5 @@ +package com.axrb.companionfixture; +import android.app.Activity; +public final class CompanionHelper extends Activity { + public static String value() { return "companion-loaded"; } +} diff --git a/direct/tests/art/KeyboardProbe.java b/direct/tests/art/KeyboardProbe.java new file mode 100644 index 0000000..7c548f4 --- /dev/null +++ b/direct/tests/art/KeyboardProbe.java @@ -0,0 +1,30 @@ +package com.axrb.keyboardprobe; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.widget.EditText; +import android.view.inputmethod.InputMethodManager; + +/** Asks for the soft keyboard the way Unreal's virtual keyboard does, on a focused text field. */ +public final class KeyboardProbe extends Activity { + @Override protected void onCreate(Bundle state) { + super.onCreate(state); + EditText field = new EditText(this); + setContentView(field); + new Handler(getMainLooper()).postDelayed(() -> { + try { + field.requestFocus(); + InputMethodManager input = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + boolean shown = input.showSoftInput(field, 0); + boolean hidden = input.hideSoftInputFromWindow(field.getWindowToken(), 0); + System.out.println("KEYBOARD_PROBE shown=" + shown + " hidden=" + hidden); + } catch (Throwable error) { + System.out.println("KEYBOARD_PROBE_FAILED " + error); + error.printStackTrace(System.out); + } + System.exit(0); + }, 500); + } +} diff --git a/direct/tests/art/LegacyDriverProbe.java b/direct/tests/art/LegacyDriverProbe.java new file mode 100644 index 0000000..0d0cdb1 --- /dev/null +++ b/direct/tests/art/LegacyDriverProbe.java @@ -0,0 +1,31 @@ +package com.axrb.legacyprobe; + +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import java.lang.reflect.Method; + +/** Loads the runtime the way older Oculus OpenXR loaders do: through com.oculus.systemdriver. */ +public final class LegacyDriverProbe extends Activity { + @Override protected void onCreate(Bundle state) { + super.onCreate(state); + try { + Context driver = createPackageContext("com.oculus.systemdriver", + Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); + Class loader = driver.getClassLoader().loadClass("com.oculus.systemdriver.DriverLoader"); + loader.getMethod("loadXrRuntime").invoke(null); + Method lookup = loader.getMethod("getProcAddr", String.class); + System.out.println("LEGACY_DRIVER_PROBE manufacturer=" + Build.MANUFACTURER + " brand=" + Build.BRAND + + " model=" + Build.MODEL + + " gipa=" + ((Long) lookup.invoke(null, "xrGetInstanceProcAddr") != 0) + + " negotiate=" + ((Long) lookup.invoke(null, "xrNegotiateLoaderRuntimeInterface") != 0) + + " initialize=" + ((Long) lookup.invoke(null, "xrInitializeLoaderKHR") != 0) + + " missing=" + ((Long) lookup.invoke(null, "xrAxrbNoSuchFunction") != 0)); + } catch (Throwable error) { + System.out.println("LEGACY_DRIVER_PROBE_FAILED " + error); + error.printStackTrace(System.out); + } + System.exit(0); + } +} diff --git a/direct/tests/art/MediaCodecProbe.java b/direct/tests/art/MediaCodecProbe.java new file mode 100644 index 0000000..a3c49e2 --- /dev/null +++ b/direct/tests/art/MediaCodecProbe.java @@ -0,0 +1,18 @@ +package com.axrb.mediaprobe; + +import android.app.Activity; +import android.content.res.AssetManager; +import android.os.Bundle; + +/** Decodes an MP4 asset with AMediaExtractor and AMediaCodec (media_codec_probe.c). */ +public class MediaCodecProbe extends Activity { + static native String run(AssetManager assets); + + @Override protected void onCreate(Bundle state) { + super.onCreate(state); + System.loadLibrary("mediaprobe"); + String result = run(getAssets()); + System.out.println(result); + System.exit(result.endsWith("MEDIA_CODEC_PROBE_PASS") ? 0 : 1); + } +} diff --git a/direct/tests/art/PermissionProbe.java b/direct/tests/art/PermissionProbe.java new file mode 100644 index 0000000..2be9993 --- /dev/null +++ b/direct/tests/art/PermissionProbe.java @@ -0,0 +1,43 @@ +package com.axrb.permissionprobe; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.content.pm.PackageManager; + +/** Runtime requests must complete for both Unity's Fragment and Activity APIs. */ +public final class PermissionProbe extends Activity { + private static final String[] PERMISSIONS = {"android.permission.READ_EXTERNAL_STORAGE", "android.permission.CAMERA"}; + private static void verify(int code, int expected, String[] names, int[] values) { + if (code != expected || !java.util.Arrays.equals(names, PERMISSIONS) + || !java.util.Arrays.equals(values, new int[]{PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_DENIED})) + throw new AssertionError("Wrong permission callback " + code + ": " + java.util.Arrays.toString(values)); + } + public static final class RequestFragment extends Fragment { + @Override public void onCreate(Bundle state) { + super.onCreate(state); + requestPermissions(PERMISSIONS, 101); + } + @Override public void onRequestPermissionsResult(int code, String[] names, int[] values) { + verify(code, 101, names, values); + if (shouldShowRequestPermissionRationale(PERMISSIONS[1])) + throw new AssertionError("Unsupported camera has no permission dialog"); + System.out.println("PERMISSION_FRAGMENT_PASS"); + getActivity().requestPermissions(PERMISSIONS, 102); + } + } + @Override public void onCreate(Bundle state) { + super.onCreate(state); + getFragmentManager().beginTransaction().add(new RequestFragment(), "permissions").commit(); + new Handler(Looper.getMainLooper()).postDelayed(() -> System.exit(2), 10000); + } + @Override public void onRequestPermissionsResult(int code, String[] names, int[] values) { + verify(code, 102, names, values); + if (checkSelfPermission(PERMISSIONS[1]) != PackageManager.PERMISSION_DENIED) + throw new AssertionError("Unsupported camera permission was granted"); + System.out.println("PERMISSION_PROBE_PASS"); + System.exit(0); + } +} diff --git a/direct/tests/art/StorageProbe.java b/direct/tests/art/StorageProbe.java new file mode 100644 index 0000000..1cfc2dd --- /dev/null +++ b/direct/tests/art/StorageProbe.java @@ -0,0 +1,85 @@ +package com.axrb.storageprobe; +import android.app.Activity; +import android.os.Bundle; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** Exercises real AOSP Context storage across independent launcher sessions. */ +public final class StorageProbe extends Activity { + private static final String VALUE = "AXRB save: \u00e4 \u65e5\u672c 12345"; + private static void require(boolean ok, String detail) { if (!ok) throw new AssertionError(detail); } + private static void write(File file) throws Exception { + require(file.getParentFile().isDirectory() || file.getParentFile().mkdirs(), "mkdir " + file); + try (FileOutputStream out = new FileOutputStream(file)) { + out.write(VALUE.getBytes(StandardCharsets.UTF_8)); out.getFD().sync(); + } + } + private static void verify(File file) throws Exception { + require(VALUE.equals(new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)), "read " + file); + } + @Override public void onResume() { + super.onResume(); + System.out.println("STORAGE_PROBE_READY_FOR_STOP"); + } + @Override public void onStop() { + super.onStop(); + getSharedPreferences("lifecycle", MODE_PRIVATE).edit().putString("savedOnStop", VALUE).apply(); + System.out.println("STORAGE_PROBE_ASYNC_SAVE_QUEUED"); + } + @Override public void onCreate(Bundle state) { + super.onCreate(state); + try { + android.content.pm.PackageManager pm = getPackageManager(); + android.content.pm.InstallSourceInfo source = pm.getInstallSourceInfo(getPackageName()); + require(source.getInstallingPackageName() == null && source.getInitiatingPackageName() == null, + "local install has no installer identity"); + require(source.getPackageSource() == android.content.pm.PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE, + "local package source"); + try { pm.getInstallSourceInfo("com.axrb.missing"); throw new AssertionError("unknown package accepted"); } + catch (android.content.pm.PackageManager.NameNotFoundException expected) {} + require(pm.getInstalledApplications(0).stream().anyMatch(a -> a.packageName.equals(getPackageName())), "installed app list"); + require(pm.getInstalledPackages(0).stream().anyMatch(a -> a.packageName.equals(getPackageName())), "installed package list"); + android.app.ActivityManager am = (android.app.ActivityManager)getSystemService(ACTIVITY_SERVICE); + require(am.getRunningAppProcesses().stream().anyMatch(p -> p.pid == android.os.Process.myPid() + && p.uid == android.os.Process.myUid()), "running process identity"); + android.widget.Toast.makeText(this, "AXRB package probe", android.widget.Toast.LENGTH_SHORT).show(); + System.out.println("PACKAGE_SERVICES_PROBE_PASS"); + String phase = System.getenv("AXRB_STORAGE_TEST_PHASE"); + if ("stop".equals(phase)) return; + File save = new File(getFilesDir(), "save.json"); + File external = new File(getExternalFilesDir(null), "external-save.txt"); + require(getExternalFilesDir(null) != null, "external storage available"); + SharedPreferences prefs = getSharedPreferences("progress", MODE_PRIVATE); + SQLiteDatabase db = openOrCreateDatabase("progress.db", MODE_PRIVATE, null); + db.enableWriteAheadLogging(); + if ("write".equals(phase)) { + File temporary = new File(getFilesDir(), "save.tmp"); + write(temporary); require(temporary.renameTo(save), "atomic rename"); + write(external); + require(prefs.edit().putString("name", VALUE).putInt("level", 42).commit(), "prefs commit"); + db.execSQL("CREATE TABLE progress (level INTEGER, name TEXT)"); + db.beginTransaction(); + try { db.execSQL("INSERT INTO progress VALUES (?, ?)", new Object[]{42, VALUE}); db.setTransactionSuccessful(); } + finally { db.endTransaction(); } + write(new File(getCacheDir(), "cache-test")); + } else { + require("read".equals(phase), "phase"); + verify(save); verify(external); + require(VALUE.equals(getSharedPreferences("lifecycle", MODE_PRIVATE).getString("savedOnStop", "")), "onStop apply survives shutdown"); + require(VALUE.equals(prefs.getString("name", "")) && prefs.getInt("level", 0) == 42, "prefs survive restart"); + try (Cursor c = db.rawQuery("SELECT level, name FROM progress", null)) { + require(c.moveToFirst() && c.getInt(0) == 42 && VALUE.equals(c.getString(1)) && !c.moveToNext(), "SQLite survives restart"); + } + File scratch = new File(getFilesDir(), "delete-test"); write(scratch); + require(scratch.delete() && !scratch.exists(), "delete"); + } + db.close(); + System.out.println("STORAGE_PROBE_" + phase.toUpperCase() + "_PASS"); + System.exit(0); + } catch (Throwable error) { error.printStackTrace(); System.exit(2); } + } +} diff --git a/direct/tests/art/art_linux_selftest.c b/direct/tests/art/art_linux_selftest.c new file mode 100644 index 0000000..47be4b5 --- /dev/null +++ b/direct/tests/art/art_linux_selftest.c @@ -0,0 +1,42 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + pthread_attr_t attr; + void* stack = 0; + size_t size = 0; + if (pthread_getattr_np(pthread_self(), &attr) || pthread_attr_getstack(&attr, &stack, &size)) return 1; + uintptr_t local = (uintptr_t)&attr; + if (local < (uintptr_t)stack || local >= (uintptr_t)stack + size) return 2; + pthread_attr_destroy(&attr); + if (access("/system/build.prop", R_OK)) return 3; + errno = 0; + if (access("/system/build.prop", W_OK) == 0 || (errno != EROFS && errno != EACCES)) return 4; + void* low = mmap((void*)0x02000000, 64 * 1024 * 1024, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (low == MAP_FAILED || low != (void*)0x02000000) return 5; + if (mprotect(low, 4096, PROT_READ | PROT_WRITE)) return 6; + *(volatile uint64_t*)low = 0x12345678; + if (*(volatile uint64_t*)low != 0x12345678) return 7; + int zero = open("/dev/zero", O_RDONLY); + if (zero < 0) return 10; + if (mprotect(low, 4096, PROT_READ)) return 11; + errno = 0; + if (read(zero, low, 8) != -1 || errno != EFAULT) return 12; + if (mprotect(low, 4096, PROT_READ | PROT_WRITE)) return 13; + void* tagged = (void*)((uintptr_t)low | UINT64_C(0xab00000000000000)); + if (read(zero, tagged, 8) != 8 || *(volatile uint64_t*)low != 0) return 14; + close(zero); + errno = 0; + void* collision = mmap(low, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (collision != MAP_FAILED || errno != EEXIST) return 8; + if (munmap(low, 64 * 1024 * 1024)) return 9; + puts("ART_LINUX_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/art_smoke_jni.c b/direct/tests/art/art_smoke_jni.c new file mode 100644 index 0000000..f63bc8d --- /dev/null +++ b/direct/tests/art/art_smoke_jni.c @@ -0,0 +1,40 @@ +#include +#include + +static JavaVM* vm; +struct callback { jclass cls; jint input; jint result; }; + +static void* worker(void* argument) { + struct callback* call = argument; + JNIEnv* env = 0; + if ((*vm)->AttachCurrentThread(vm, &env, 0) != JNI_OK) return 0; + jmethodID method = (*env)->GetStaticMethodID(env, call->cls, "twice", "(I)I"); + if (method) call->result = (*env)->CallStaticIntMethod(env, call->cls, method, call->input); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + call->result = -1; + } + (*vm)->DetachCurrentThread(vm); + return 0; +} + +static jint round_trip(JNIEnv* env, jclass cls, jintArray values) { + struct callback call = { (*env)->NewGlobalRef(env, cls), 0, -1 }; + (*env)->GetIntArrayRegion(env, values, 0, 1, &call.input); + pthread_t thread; + if (pthread_create(&thread, 0, worker, &call) == 0) pthread_join(thread, 0); + (*env)->DeleteGlobalRef(env, call.cls); + (*env)->SetIntArrayRegion(env, values, 0, 1, &call.result); + return call.result; +} + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* runtime, void* reserved) { + (void)reserved; + vm = runtime; + JNIEnv* env = 0; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; + jclass cls = (*env)->FindClass(env, "ArtSmoke"); + JNINativeMethod method = { "nativeRoundTrip", "([I)I", (void*)round_trip }; + if (!cls || (*env)->RegisterNatives(env, cls, &method, 1)) return JNI_ERR; + return JNI_VERSION_1_6; +} diff --git a/direct/tests/art/binder_selftest.c b/direct/tests/art/binder_selftest.c new file mode 100644 index 0000000..f785410 --- /dev/null +++ b/direct/tests/art/binder_selftest.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "Binder check failed line %d: %s (errno %d)\n", __LINE__, #x, errno); _exit(1); } } while (0) +static int server; + +static void send_command(int fd, uint32_t command, const void* payload) { + unsigned char buffer[128]; + unsigned size = _IOC_SIZE(command); + memcpy(buffer, &command, 4); + if (size) memcpy(buffer + 4, payload, size); + struct binder_write_read wr = {.write_size = 4 + size, .write_buffer = (uintptr_t)buffer}; + CHECK(ioctl(fd, BINDER_WRITE_READ, &wr) == 0); + CHECK(wr.write_consumed == wr.write_size); +} + +static struct binder_transaction_data receive_transaction(int fd, uint32_t expected) { + for (;;) { + unsigned char buffer[512]; + struct binder_write_read wr = {.read_size = sizeof buffer, .read_buffer = (uintptr_t)buffer}; + CHECK(ioctl(fd, BINDER_WRITE_READ, &wr) == 0); + for (size_t at = 0; at < wr.read_consumed;) { + uint32_t command; + memcpy(&command, buffer + at, 4); + unsigned size = _IOC_SIZE(command); + CHECK(at + 4 + size <= wr.read_consumed); + if (command == expected) { + struct binder_transaction_data tr; + memcpy(&tr, buffer + at + 4, sizeof tr); + return tr; + } + CHECK(command == BR_TRANSACTION_COMPLETE || command == BR_NOOP); + at += 4 + size; + } + } +} + +static void* serve(void* ignored) { + (void)ignored; + send_command(server, BC_ENTER_LOOPER, 0); + for (int i = 0; i < 100; ++i) { + struct binder_transaction_data tr = receive_transaction(server, BR_TRANSACTION); + CHECK(tr.target.ptr == 0x1234 && tr.cookie == 0x5678 && tr.code == 42); + CHECK(tr.data_size == sizeof(int) && tr.offsets_size == 0); + int value; + memcpy(&value, (void*)(uintptr_t)tr.data.ptr.buffer, sizeof value); + CHECK(value == i); + send_command(server, BC_FREE_BUFFER, &tr.data.ptr.buffer); + value *= 2; + struct binder_transaction_data reply = {.data_size = sizeof value}; + reply.data.ptr.buffer = (uintptr_t)&value; + send_command(server, BC_REPLY, &reply); + } + send_command(server, BC_EXIT_LOOPER, 0); + return 0; +} + +int main(void) { + server = open("/dev/binder", O_RDWR | O_CLOEXEC); + int client = open("/dev/binder", O_RDWR | O_CLOEXEC); + CHECK(server >= 0 && client >= 0); + struct binder_version version = {0}; + CHECK(ioctl(client, BINDER_VERSION, &version) == 0 && version.protocol_version == 8); + CHECK(mmap(0, 1024 * 1024, PROT_READ, MAP_PRIVATE, server, 0) != MAP_FAILED); + CHECK(mmap(0, 1024 * 1024, PROT_READ, MAP_PRIVATE, client, 0) != MAP_FAILED); + struct flat_binder_object manager = {.hdr.type = BINDER_TYPE_BINDER, .binder = 0x1234, .cookie = 0x5678}; + CHECK(ioctl(server, BINDER_SET_CONTEXT_MGR_EXT, &manager) == 0); + errno = 0; + CHECK(ioctl(client, BINDER_SET_CONTEXT_MGR_EXT, &manager) == -1 && errno == EBUSY); + pthread_t worker; + CHECK(pthread_create(&worker, 0, serve, 0) == 0); + for (int i = 0; i < 100; ++i) { + struct binder_transaction_data tr = {.target.handle = 0, .code = 42, .data_size = sizeof i}; + tr.data.ptr.buffer = (uintptr_t)&i; + send_command(client, BC_TRANSACTION, &tr); + struct binder_transaction_data reply = receive_transaction(client, BR_REPLY); + int value; + CHECK(reply.data_size == sizeof value); + memcpy(&value, (void*)(uintptr_t)reply.data.ptr.buffer, sizeof value); + CHECK(value == i * 2); + send_command(client, BC_FREE_BUFFER, &reply.data.ptr.buffer); + } + CHECK(pthread_join(worker, 0) == 0); + close(client); + close(server); + puts("BINDER_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/media_codec_probe.c b/direct/tests/art/media_codec_probe.c new file mode 100644 index 0000000..6c77418 --- /dev/null +++ b/direct/tests/art/media_codec_probe.c @@ -0,0 +1,218 @@ +// AMediaExtractor + AMediaCodec on an MP4 asset (H.264 video, AAC audio), +// opened the way Unity's video player does: the APK's asset descriptor. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char report[8192]; +static void say(const char* format, ...) __attribute__((format(printf, 1, 2))); +static void say(const char* format, ...) { + va_list args; + va_start(args, format); + size_t used = strlen(report); + vsnprintf(report + used, sizeof report - used, format, args); + va_end(args); +} + +struct Result { + int frames, width, height, rate, channels, varied; + long bytes; +}; + +static void inspect(const uint8_t* data, int size, struct Result* r) { + if (size <= 0) return; + uint8_t low = data[0], high = data[0]; + for (int i = 0; i < size; i += 7) { + if (data[i] < low) low = data[i]; + if (data[i] > high) high = data[i]; + } + if (high - low > 32) r->varied++; +} + +static void output_format(AMediaCodec* codec, struct Result* r) { + AMediaFormat* format = AMediaCodec_getOutputFormat(codec); + if (!format) return; + AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_WIDTH, &r->width); + AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_HEIGHT, &r->height); + AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &r->rate); + AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &r->channels); + AMediaFormat_delete(format); +} + +// Synchronous decoding of one track to its end. +static int decode(AMediaExtractor* x, size_t track, struct Result* r) { + AMediaFormat* format = AMediaExtractor_getTrackFormat(x, track); + const char* mime = NULL; + if (!format || !AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) return -1; + AMediaCodec* codec = AMediaCodec_createDecoderByType(mime); + if (!codec) { say("no decoder for %s; ", mime); return -2; } + if (AMediaCodec_configure(codec, format, NULL, NULL, 0) != AMEDIA_OK || AMediaCodec_start(codec) != AMEDIA_OK) return -3; + AMediaExtractor_selectTrack(x, track); + AMediaExtractor_seekTo(x, 0, AMEDIAEXTRACTOR_SEEK_PREVIOUS_SYNC); + int input_done = 0, output_done = 0; + for (int guard = 0; !output_done && guard < 100000; ++guard) { + if (!input_done) { + ssize_t in = AMediaCodec_dequeueInputBuffer(codec, 1000); + if (in >= 0) { + size_t capacity = 0; + uint8_t* buffer = AMediaCodec_getInputBuffer(codec, (size_t)in, &capacity); + ssize_t n = AMediaExtractor_readSampleData(x, buffer, capacity); + if (n < 0) { + AMediaCodec_queueInputBuffer(codec, (size_t)in, 0, 0, 0, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); + input_done = 1; + } else { + AMediaCodec_queueInputBuffer(codec, (size_t)in, 0, (size_t)n, (uint64_t)AMediaExtractor_getSampleTime(x), 0); + AMediaExtractor_advance(x); + } + } + } + AMediaCodecBufferInfo info; + ssize_t out = AMediaCodec_dequeueOutputBuffer(codec, &info, 1000); + if (out >= 0) { + size_t size = 0; + uint8_t* buffer = AMediaCodec_getOutputBuffer(codec, (size_t)out, &size); + if (info.size > 0 && buffer) { + r->frames++; + r->bytes += info.size; + inspect(buffer + info.offset, info.size, r); + } + if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) output_done = 1; + AMediaCodec_releaseOutputBuffer(codec, (size_t)out, false); + } else if (out == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { + output_format(codec, r); + } + } + AMediaExtractor_unselectTrack(x, track); + AMediaCodec_stop(codec); + AMediaCodec_delete(codec); + AMediaFormat_delete(format); + return output_done ? 0 : -4; +} + +// Asynchronous decoding (callbacks on the codec's thread). +struct Async { + AMediaExtractor* x; + struct Result r; + pthread_mutex_t lock; + int input_done, output_done; +}; +static void on_input(AMediaCodec* codec, void* user, int32_t index) { + struct Async* a = user; + pthread_mutex_lock(&a->lock); + size_t capacity = 0; + uint8_t* buffer = AMediaCodec_getInputBuffer(codec, (size_t)index, &capacity); + ssize_t n = a->input_done ? -1 : AMediaExtractor_readSampleData(a->x, buffer, capacity); + if (n < 0) { + AMediaCodec_queueInputBuffer(codec, (size_t)index, 0, 0, 0, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); + a->input_done = 1; + } else { + AMediaCodec_queueInputBuffer(codec, (size_t)index, 0, (size_t)n, (uint64_t)AMediaExtractor_getSampleTime(a->x), 0); + AMediaExtractor_advance(a->x); + } + pthread_mutex_unlock(&a->lock); +} +static void on_output(AMediaCodec* codec, void* user, int32_t index, AMediaCodecBufferInfo* info) { + struct Async* a = user; + size_t size = 0; + uint8_t* buffer = AMediaCodec_getOutputBuffer(codec, (size_t)index, &size); + pthread_mutex_lock(&a->lock); + if (info->size > 0 && buffer) { + a->r.frames++; + a->r.bytes += info->size; + inspect(buffer + info->offset, info->size, &a->r); + } + if (info->flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) a->output_done = 1; + pthread_mutex_unlock(&a->lock); + AMediaCodec_releaseOutputBuffer(codec, (size_t)index, false); +} +static void on_format(AMediaCodec* codec, void* user, AMediaFormat* format) { + struct Async* a = user; + AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &a->r.rate); + AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &a->r.channels); +} +static void on_error(AMediaCodec* codec, void* user, media_status_t error, int32_t code, const char* detail) { + say("async error %d %s; ", error, detail); + ((struct Async*)user)->output_done = -1; +} + +static int decode_async(AMediaExtractor* x, size_t track, struct Result* r) { + AMediaFormat* format = AMediaExtractor_getTrackFormat(x, track); + const char* mime = NULL; + AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime); + AMediaCodec* codec = AMediaCodec_createDecoderByType(mime); + if (!codec) return -2; + static struct Async a; + memset(&a, 0, sizeof a); + a.x = x; + pthread_mutex_init(&a.lock, NULL); + AMediaCodecOnAsyncNotifyCallback callback = {on_input, on_output, on_format, on_error}; + if (AMediaCodec_setAsyncNotifyCallback(codec, callback, &a) != AMEDIA_OK) return -3; + if (AMediaCodec_configure(codec, format, NULL, NULL, 0) != AMEDIA_OK) return -4; + AMediaExtractor_selectTrack(x, track); + AMediaExtractor_seekTo(x, 0, AMEDIAEXTRACTOR_SEEK_PREVIOUS_SYNC); + if (AMediaCodec_start(codec) != AMEDIA_OK) return -5; + for (int i = 0; i < 1000 && !a.output_done; ++i) { + struct timespec pause = {0, 10000000}; + nanosleep(&pause, NULL); + } + AMediaCodec_stop(codec); + AMediaCodec_delete(codec); + AMediaExtractor_unselectTrack(x, track); + AMediaFormat_delete(format); + *r = a.r; + return a.output_done == 1 ? 0 : -6; +} + +JNIEXPORT jstring JNICALL Java_com_axrb_mediaprobe_MediaCodecProbe_run(JNIEnv* env, jclass cls, jobject assets) { + report[0] = 0; + AAssetManager* manager = AAssetManager_fromJava(env, assets); + AAsset* asset = manager ? AAssetManager_open(manager, "probe.mp4", AASSET_MODE_RANDOM) : NULL; + off_t start = 0, length = 0; + int fd = asset ? AAsset_openFileDescriptor(asset, &start, &length) : -1; + if (fd < 0) return (*env)->NewStringUTF(env, "MEDIA_CODEC_PROBE_FAIL no asset descriptor"); + AMediaExtractor* x = AMediaExtractor_new(); + media_status_t opened = AMediaExtractor_setDataSourceFd(x, fd, start, length); + close(fd); + AAsset_close(asset); + if (opened != AMEDIA_OK) { + say("MEDIA_CODEC_PROBE_FAIL setDataSourceFd %d", opened); + return (*env)->NewStringUTF(env, report); + } + size_t tracks = AMediaExtractor_getTrackCount(x); + say("tracks=%zu; ", tracks); + int ok = tracks == 2; + for (size_t t = 0; t < tracks; ++t) { + AMediaFormat* format = AMediaExtractor_getTrackFormat(x, t); + const char* mime = "?"; + AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime); + int video = strncmp(mime, "video/", 6) == 0; + void* csd = NULL; + size_t csd_size = 0; + AMediaFormat_getBuffer(format, "csd-0", &csd, &csd_size); + say("%s csd0=%zu; ", mime, csd_size); + struct Result r = {0}; + int status = decode(x, t, &r); + say("%s sync: status=%d frames=%d bytes=%ld size=%dx%d rate=%d ch=%d varied=%d; ", mime, status, r.frames, r.bytes, + r.width, r.height, r.rate, r.channels, r.varied); + if (video) { + ok &= status == 0 && r.frames == 30 && r.width >= 320 && r.height >= 240 && r.varied >= 25 && csd_size > 0; + } else { + ok &= status == 0 && r.rate == 44100 && r.channels == 2 && r.bytes > 160000 && r.bytes < 200000 && r.varied > 10; + struct Result async = {0}; + status = decode_async(x, t, &async); + say("%s async: status=%d frames=%d bytes=%ld rate=%d; ", mime, status, async.frames, async.bytes, async.rate); + ok &= status == 0 && async.bytes == r.bytes && async.rate == 44100; + } + AMediaFormat_delete(format); + } + AMediaExtractor_delete(x); + say(ok ? "MEDIA_CODEC_PROBE_PASS" : "MEDIA_CODEC_PROBE_FAIL"); + return (*env)->NewStringUTF(env, report); +} diff --git a/direct/tests/art/opensles_selftest.c b/direct/tests/art/opensles_selftest.c new file mode 100644 index 0000000..45d61af --- /dev/null +++ b/direct/tests/art/opensles_selftest.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include + +#define CHECK(call) \ + do { \ + SLresult r = (call); \ + if (r) { \ + fprintf(stderr, "%s failed: %u\n", #call, r); \ + return 1; \ + } \ + } while (0) +static short samples[960]; +static atomic_int callbacks; +static atomic_int callbackError; +static void refill(SLAndroidSimpleBufferQueueItf queue, void *context) { + (void)context; + if (atomic_fetch_add(&callbacks, 1) < 8 && + (*queue)->Enqueue(queue, samples, sizeof samples)) + atomic_store(&callbackError, 1); +} +int main(void) { + SLObjectItf engineObject = 0, mix = 0, player = 0; + SLEngineItf engine = 0; + SLPlayItf play = 0; + SLAndroidSimpleBufferQueueItf queue = 0; + CHECK(slCreateEngine(&engineObject, 0, 0, 0, 0, 0)); + CHECK((*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE)); + CHECK((*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engine)); + CHECK((*engine)->CreateOutputMix(engine, &mix, 0, 0, 0)); + CHECK((*mix)->Realize(mix, SL_BOOLEAN_FALSE)); + SLDataLocator_AndroidSimpleBufferQueue locator = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 3}; + SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, + 2, + SL_SAMPLINGRATE_48, + 16, + 16, + SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, + SL_BYTEORDER_LITTLEENDIAN}; + SLDataSource source = {&locator, &pcm}; + SLDataLocator_OutputMix output = {SL_DATALOCATOR_OUTPUTMIX, mix}; + SLDataSink sink = {&output, 0}; + SLInterfaceID ids[] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME}; + SLboolean required[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + CHECK((*engine)->CreateAudioPlayer(engine, &player, &source, &sink, 2, ids, + required)); + CHECK((*player)->Realize(player, SL_BOOLEAN_FALSE)); + CHECK((*player)->GetInterface(player, SL_IID_PLAY, &play)); + CHECK( + (*player)->GetInterface(player, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &queue)); + CHECK((*queue)->RegisterCallback(queue, refill, 0)); + CHECK((*queue)->Enqueue(queue, samples, sizeof samples)); + CHECK((*play)->SetPlayState(play, SL_PLAYSTATE_PLAYING)); + for (int i = 0; i < 300 && atomic_load(&callbacks) < 8; ++i) + usleep(10000); + if (atomic_load(&callbacks) < 8 || atomic_load(&callbackError)) { + fprintf(stderr, "queue callback failed\n"); + return 1; + } + CHECK((*play)->SetPlayState(play, SL_PLAYSTATE_PAUSED)); + CHECK((*queue)->Clear(queue)); + SLAndroidSimpleBufferQueueState state; + CHECK((*queue)->GetState(queue, &state)); + if (state.count) + return 1; + CHECK((*play)->SetPlayState(play, SL_PLAYSTATE_STOPPED)); + (*player)->Destroy(player); + (*mix)->Destroy(mix); + (*engineObject)->Destroy(engineObject); + puts("OPENSL_ES_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/timeout_selftest.c b/direct/tests/art/timeout_selftest.c new file mode 100644 index 0000000..023df14 --- /dev/null +++ b/direct/tests/art/timeout_selftest.c @@ -0,0 +1,60 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static _Atomic int word; +static void* wake_later(void* unused) { + (void)unused; + struct timespec pause = {0, 50000000}; + nanosleep(&pause, 0); + atomic_store(&word, 1); + syscall(SYS_futex, &word, FUTEX_WAKE_PRIVATE, 1, 0, 0, 0); + return 0; +} +static int check_wait(int op, struct timespec deadline) { + atomic_store(&word, 0); + pthread_t worker; + if (pthread_create(&worker, 0, wake_later, 0)) return 1; + errno = 0; + long result = syscall(SYS_futex, &word, op, 0, &deadline, 0, FUTEX_BITSET_MATCH_ANY); + int error = errno, woke = atomic_load(&word); + pthread_join(worker, 0); + if (!woke || (result != 0 && error != EAGAIN)) { + fprintf(stderr, "long wait failed: op=%x sec=%lld result=%ld errno=%d woke=%d\n", + op, (long long)deadline.tv_sec, result, error, woke); + return 1; + } + return 0; +} +int main(void) { + struct timespec mono, real; + clock_gettime(CLOCK_MONOTONIC, &mono); + clock_gettime(CLOCK_REALTIME, &real); + mono.tv_sec += 946080000; // 30-year parking deadline. + real.tv_sec += 946080000; + if (check_wait(FUTEX_WAIT_BITSET_PRIVATE, mono)) return 1; + if (check_wait(FUTEX_WAIT_BITSET_PRIVATE | FUTEX_CLOCK_REALTIME, real)) return 2; + if (check_wait(FUTEX_WAIT_PRIVATE, (struct timespec){946080000, 123})) return 3; + if (check_wait(FUTEX_WAIT_BITSET_PRIVATE, (struct timespec){INT64_MAX, 999999999})) return 4; + atomic_store(&word, 0); + struct timespec short_wait = {0, 20000000}, start, end; + clock_gettime(CLOCK_MONOTONIC, &start); + if (syscall(SYS_futex, &word, FUTEX_WAIT_PRIVATE, 0, &short_wait, 0, 0) != -1 || errno != ETIMEDOUT) return 5; + clock_gettime(CLOCK_MONOTONIC, &end); + int64_t ns = (end.tv_sec-start.tv_sec)*1000000000LL + end.tv_nsec-start.tv_nsec; + if (ns < 15000000 || ns > 2000000000LL) return 6; + struct timespec invalid = {-1, 0}; + if (syscall(SYS_futex, &word, FUTEX_WAIT_PRIVATE, 0, &invalid, 0, 0) != -1 || errno != EINVAL) return 7; + struct timespec expired = {0, 0}; + if (syscall(SYS_futex, &word, FUTEX_WAIT_BITSET_PRIVATE, 0, &expired, 0, FUTEX_BITSET_MATCH_ANY) != -1 || errno != ETIMEDOUT) return 8; + puts("TIMEOUT_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/translator_benchmark.c b/direct/tests/art/translator_benchmark.c new file mode 100644 index 0000000..8e1cb9d --- /dev/null +++ b/direct/tests/art/translator_benchmark.c @@ -0,0 +1,150 @@ +// Deterministic translator throughput probe, not a game FPS measurement. +// Each line is one construct games lean on, in nanoseconds per operation. +#include +#include +#include +#include +#include +#include +#include +static double now(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} +#define NOINLINE __attribute__((noinline)) +static volatile uint64_t sink; + +static NOINLINE uint64_t leaf(uint64_t x) { __asm__ volatile(""); return x + 1; } +static NOINLINE uint64_t leaf_a(uint64_t x) { __asm__ volatile(""); return x + 3; } +static NOINLINE uint64_t leaf_b(uint64_t x) { __asm__ volatile(""); return x ^ 5; } +static NOINLINE uint64_t leaf_c(uint64_t x) { __asm__ volatile(""); return x * 7; } +static NOINLINE uint64_t leaf_d(uint64_t x) { __asm__ volatile(""); return x - 9; } +typedef uint64_t (*Fn)(uint64_t); +static Fn table[4] = {leaf_a, leaf_b, leaf_c, leaf_d}; +// One function returning to many call sites, as accessors and allocators do. +static NOINLINE uint64_t sites(uint64_t x) { + x = leaf(x) + 1; x = leaf(x) ^ 2; x = leaf(x) + 3; x = leaf(x) ^ 4; + x = leaf(x) + 5; x = leaf(x) ^ 6; x = leaf(x) + 7; x = leaf(x) ^ 8; + return x; +} +static NOINLINE uint64_t deep(uint64_t x, int depth) { + __asm__ volatile(""); + return depth ? deep(x + 1, depth - 1) + 1 : x; +} +struct Node { struct Node* next; uint64_t value; uint64_t pad[2]; }; +struct Vec4 { float v[4]; }; +static NOINLINE void transform(const float* restrict m, const struct Vec4* restrict in, struct Vec4* restrict out, int n) { + for (int i = 0; i < n; ++i) + for (int r = 0; r < 4; ++r) + out[i].v[r] = m[r * 4] * in[i].v[0] + m[r * 4 + 1] * in[i].v[1] + m[r * 4 + 2] * in[i].v[2] + m[r * 4 + 3] * in[i].v[3]; +} +static void report(const char* name, double seconds, double operations) { + printf("%-22s %8.2f ns\n", name, seconds * 1e9 / operations); + fflush(stdout); +} + +int main(void) { + double start; + uint64_t x = 1; + const int n = 20000000; + + float sum = 1.0f, multiplier = 0.999999f, addend = 0.000123f; + start = now(); + for (int i = 0; i < n; ++i) + __asm__ volatile("fmadd %s0,%s0,%s1,%s2" : "+w"(sum) : "w"(multiplier), "w"(addend)); + report("fmadd", now() - start, n); + + uint64_t value = 123; + start = now(); + for (int i = 0; i < n; ++i) + __asm__ volatile("eor %0,%0,%0,lsl #13\n eor %0,%0,%0,lsr #7\n eor %0,%0,%0,lsl #17" : "+r"(value)); + report("integer xorshift", now() - start, n); + + start = now(); + for (int i = 0; i < n; ++i) x = leaf(x); + report("call+return", now() - start, n); + + start = now(); + for (int i = 0; i < n / 8; ++i) x = sites(x); + report("return, 8 sites", now() - start, n); + + start = now(); + for (int i = 0; i < n; ++i) x = table[i & 3](x); + report("indirect call, 4", now() - start, n); + + start = now(); + for (int i = 0; i < n; ++i) x = table[(x >> 7) & 3](x + i); + report("indirect call, random", now() - start, n); + + start = now(); + for (int i = 0; i < n / 16; ++i) x = deep(x, 15); + report("recursion, depth 16", now() - start, n); + + static struct Node nodes[4096]; + for (int i = 0; i < 4096; ++i) { nodes[i].next = &nodes[(i * 1237 + 1) & 4095]; nodes[i].value = i; } + struct Node* node = nodes; + start = now(); + for (int i = 0; i < n; ++i) { node->value += x; node = node->next; } + report("pointer chase+store", now() - start, n); + + static char a[4096], b[4096]; + start = now(); + for (int i = 0; i < n; ++i) { memcpy(a + (i & 63), b + (i & 31), 48); __asm__ volatile("" : : "r"(a)); } + report("memcpy 48", now() - start, n); + start = now(); + for (int i = 0; i < n / 20; ++i) { memcpy(a, b, 4096); __asm__ volatile("" : : "r"(a)); } + report("memcpy 4096", now() - start, n / 20); + start = now(); + for (int i = 0; i < n / 4; ++i) { memset(a, i, 256); __asm__ volatile("" : : "r"(a)); } + report("memset 256", now() - start, n / 4); + + static struct Vec4 in[256], out[256]; + static float matrix[16]; + for (int i = 0; i < 16; ++i) matrix[i] = 0.01f * (i + 1); + for (int i = 0; i < 256; ++i) for (int k = 0; k < 4; ++k) in[i].v[k] = i * 0.5f + k; + start = now(); + for (int i = 0; i < n / 256; ++i) { transform(matrix, in, out, 256); in[i & 255].v[0] = out[0].v[1]; } + report("mat4*vec4", now() - start, (n / 256) * 256.0); + + double d = 1.5; + start = now(); + for (int i = 0; i < n; ++i) { d = d / 1.0000001 + 0.25; __asm__ volatile("fsqrt %d0,%d0" : "+w"(d)); } + report("fdiv+fsqrt", now() - start, n); + + float f = 3.7f; int converted = 0; + start = now(); + for (int i = 0; i < n; ++i) { converted += (int)(f + (float)i * 0.001f); __asm__ volatile("" : : "r"(converted)); } + report("int<->float", now() - start, n); + + static _Atomic uint64_t counter; + start = now(); + for (int i = 0; i < n; ++i) atomic_fetch_add(&counter, 1); + report("atomic add", now() - start, n); + uint64_t expected = 0; + start = now(); + for (int i = 0; i < n; ++i) { expected = i; atomic_compare_exchange_strong(&counter, &expected, (uint64_t)i + 1); atomic_store(&counter, (uint64_t)i + 1); } + report("compare+swap", now() - start, n); + + pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + start = now(); + for (int i = 0; i < n; ++i) { pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); } + report("mutex lock+unlock", now() - start, n); + + start = now(); + for (int i = 0; i < n / 4; ++i) { void* p = malloc(64 + (i & 255)); *(volatile char*)p = 1; free(p); } + report("malloc+free", now() - start, n / 4); + + struct timespec ts; + start = now(); + for (int i = 0; i < n / 4; ++i) clock_gettime(CLOCK_MONOTONIC, &ts); + report("clock_gettime", now() - start, n / 4); + + static __thread uint64_t local; + start = now(); + for (int i = 0; i < n; ++i) { local += x; __asm__ volatile("" : : "r"(&local)); } + report("thread-local add", now() - start, n); + + sink = x + value + converted + (uint64_t)sum + (uint64_t)d + local + node->value + counter; + return 0; +} diff --git a/direct/tests/art/translator_cond_selftest.c b/direct/tests/art/translator_cond_selftest.c new file mode 100644 index 0000000..0047cc3 --- /dev/null +++ b/direct/tests/art/translator_cond_selftest.c @@ -0,0 +1,77 @@ +// Every condition code after SUBS, ADDS and ANDS, read by B.cond, CSET/CSEL and +// CCMP, against NZCV computed with plain arithmetic. Run repeatedly so every +// translation tier executes it; the checksum line must match across translators. +#include +#include + +static const char* const names[14] = {"eq", "ne", "cs", "cc", "mi", "pl", "vs", "vc", "hi", "ls", "ge", "lt", "gt", "le"}; + +static int holds(int cond, int n, int z, int c, int v) { + switch (cond) { + case 0: return z; case 1: return !z; case 2: return c; case 3: return !c; + case 4: return n; case 5: return !n; case 6: return v; case 7: return !v; + case 8: return c & !z; case 9: return !c | z; + case 10: return n == v; case 11: return n != v; + case 12: return !z & (n == v); default: return z | (n != v); + } +} + +#define CONDITIONS(X) X(eq, 0) X(ne, 1) X(cs, 2) X(cc, 3) X(mi, 4) X(pl, 5) X(vs, 6) X(vc, 7) \ + X(hi, 8) X(ls, 9) X(ge, 10) X(lt, 11) X(gt, 12) X(le, 13) + +// op 0: SUBS, 1: ADDS, 2: ANDS. Returns bit 0: B.cond taken, bit 1: CSET, +// bit 2: CSEL chose the first, bit 3: CCMP ran its compare (then "eq" of 7==7). +#define DEFINE(cond, index) \ + static __attribute__((noinline)) unsigned run_##cond(int op, uint64_t a, uint64_t b) { \ + uint64_t taken = 0, set = 0, selected = 0, compared = 0, seven = 7, one = 1, zero = 0; \ + __asm__ volatile( \ + "cbz %w[op], 1f\n cmp %w[op], #1\n b.eq 2f\n" \ + "ands xzr, %[a], %[b]\n b 3f\n" \ + "1: subs xzr, %[a], %[b]\n b 3f\n" \ + "2: adds xzr, %[a], %[b]\n" \ + "3: cset %[set], " #cond "\n" \ + "csel %[selected], %[one], %[zero], " #cond "\n" \ + "mrs x9, nzcv\n" \ + "ccmp %[seven], #7, #0, " #cond "\n" \ + "cset %[compared], eq\n" \ + "msr nzcv, x9\n" \ + "b." #cond " 4f\n b 5f\n 4: mov %[taken], #1\n 5:\n" \ + : [taken] "+r"(taken), [set] "+r"(set), [selected] "+r"(selected), [compared] "+r"(compared) \ + : [op] "r"(op), [a] "r"(a), [b] "r"(b), [seven] "r"(seven), [one] "r"(one), [zero] "r"(zero) \ + : "x9", "cc"); \ + return (unsigned)(taken | set << 1 | selected << 2 | compared << 3); \ + } +CONDITIONS(DEFINE) +typedef unsigned (*Run)(int, uint64_t, uint64_t); +#define ENTRY(cond, index) run_##cond, +static const Run runs[14] = {CONDITIONS(ENTRY)}; + +int main(void) { + static const uint64_t values[] = {0, 1, 2, 7, 0x7fffffffffffffffull, 0x8000000000000000ull, 0x8000000000000001ull, + 0xffffffffffffffffull, 0xfffffffffffffffeull, 0x100000000ull, 0xffffffffull, 0x5555555555555555ull, 0xaaaaaaaaaaaaaaaaull}; + const int count = sizeof values / sizeof *values; + int failures = 0; + uint64_t checksum = 1469598103934665603ull; + for (int round = 0; round < 40; ++round) { + for (int i = 0; i < count; ++i) for (int j = 0; j < count; ++j) for (int op = 0; op < 3; ++op) { + const uint64_t a = values[i], b = values[j]; + uint64_t r; + int c, v; + if (op == 0) { r = a - b; c = a >= b; v = (int)(((a ^ b) & (a ^ r)) >> 63); } + else if (op == 1) { r = a + b; c = r < a; v = (int)((~(a ^ b) & (a ^ r)) >> 63); } + else { r = a & b; c = 0; v = 0; } + const int n = (int)(r >> 63), z = r == 0; + for (int cond = 0; cond < 14; ++cond) { + const unsigned got = runs[cond](op, a, b), want = holds(cond, n, z, c, v) ? 15u : 0u; + checksum = (checksum ^ got) * 1099511628211ull; + if (got != want && failures++ < 20) + printf("FAIL %s op=%d a=%llx b=%llx got=%x want=%x round=%d\n", names[cond], op, + (unsigned long long)a, (unsigned long long)b, got, want, round); + } + } + } + printf("checksum %016llx\n", (unsigned long long)checksum); + if (failures) { printf("TRANSLATOR_COND_SELFTEST_FAIL %d\n", failures); return 1; } + puts("TRANSLATOR_COND_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/translator_mmap_selftest.c b/direct/tests/art/translator_mmap_selftest.c new file mode 100644 index 0000000..6ba5a24 --- /dev/null +++ b/direct/tests/art/translator_mmap_selftest.c @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +static pthread_barrier_t barrier; +static unsigned char* code; +enum { pages = 2048, threads = 4 }; +static void* execute_pages(void* opaque) { + uintptr_t worker = (uintptr_t)opaque; + pthread_barrier_wait(&barrier); + for (unsigned i = 0; i < pages / threads; ++i) { + unsigned page = i * threads + worker; + int (*function)(void) = (int (*)(void))(code + page * 4096); + assert(function() == 42); + } + return NULL; +} +int main(void) { + char path[] = "/data/local/tmp/translator-pages-XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + unlink(path); + uint32_t page[1024] = {0xd2800540, 0xd65f03c0}; // mov x0,42; ret + for (unsigned i = 0; i < pages; ++i) assert(write(fd, page, sizeof page) == sizeof page); + pthread_barrier_init(&barrier, NULL, threads); + for (int round = 0; round < 4; ++round) { + // Shared mappings use the lazy path even for this small fixture. + code = mmap(NULL, pages * 4096, PROT_READ | PROT_EXEC, MAP_SHARED, fd, 0); + assert(code != MAP_FAILED); + pthread_t workers[threads]; + for (uintptr_t i = 0; i < threads; ++i) assert(!pthread_create(&workers[i], NULL, execute_pages, (void*)i)); + for (int i = 0; i < threads; ++i) assert(!pthread_join(workers[i], NULL)); + assert(!munmap(code, pages * 4096)); + } + pthread_barrier_destroy(&barrier); + close(fd); + puts("TRANSLATOR_MMAP_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/translator_round_selftest.c b/direct/tests/art/translator_round_selftest.c new file mode 100644 index 0000000..d26e7e3 --- /dev/null +++ b/direct/tests/art/translator_round_selftest.c @@ -0,0 +1,106 @@ +// FRINT* (round to integral) in every rounding mode, scalar and vector, single +// and double, against integer arithmetic; FRINTX must also report inexact. +#include +#include +#include +#include + +enum { NEAREST, PLUS, MINUS, ZERO, AWAY }; + +static double reference(double v, int mode, double limit) { + if (v != v || fabs(v) >= limit) return v; + double t = (double)(int64_t)v, frac = v - t, sign = v < 0 ? -1 : 1; + switch (mode) { + case NEAREST: + if (fabs(frac) > 0.5 || (fabs(frac) == 0.5 && ((int64_t)t & 1))) t += sign; + break; + case PLUS: if (frac > 0) t += 1; break; + case MINUS: if (frac < 0) t -= 1; break; + case AWAY: if (fabs(frac) >= 0.5) t += sign; break; + } + return copysign(t, v); +} + +static void set_fpcr(uint64_t v) { __asm__ volatile("msr fpcr, %0" : : "r"(v)); } +static uint64_t fpsr(void) { uint64_t v; __asm__ volatile("mrs %0, fpsr" : "=r"(v)); return v; } +static void clear_fpsr(void) { uint64_t zero = 0; __asm__ volatile("msr fpsr, %0" : : "r"(zero)); } + +#define SCALAR(name, insn, type, reg) \ + static type __attribute__((noinline)) name(type v) { __asm__ volatile(insn " %" reg "0, %" reg "0" : "+w"(v)); return v; } +SCALAR(sx, "frintx", float, "s") SCALAR(si, "frinti", float, "s") SCALAR(sn, "frintn", float, "s") +SCALAR(sp, "frintp", float, "s") SCALAR(sm, "frintm", float, "s") SCALAR(sz, "frintz", float, "s") SCALAR(sa, "frinta", float, "s") +SCALAR(dx, "frintx", double, "d") SCALAR(di, "frinti", double, "d") SCALAR(dn, "frintn", double, "d") +SCALAR(dp, "frintp", double, "d") SCALAR(dm, "frintm", double, "d") SCALAR(dz, "frintz", double, "d") SCALAR(da, "frinta", double, "d") + +#define VECTOR(name, insn, type, arrangement) \ + static void __attribute__((noinline)) name(type* v) { \ + __asm__ volatile("ldr q0, [%0]\n" insn " v0." arrangement ", v0." arrangement "\n str q0, [%0]" : : "r"(v) : "v0", "memory"); } +VECTOR(vsx, "frintx", float, "4s") VECTOR(vsi, "frinti", float, "4s") VECTOR(vsn, "frintn", float, "4s") +VECTOR(vsp, "frintp", float, "4s") VECTOR(vsm, "frintm", float, "4s") VECTOR(vsz, "frintz", float, "4s") VECTOR(vsa, "frinta", float, "4s") +VECTOR(vdx, "frintx", double, "2d") VECTOR(vdi, "frinti", double, "2d") VECTOR(vdn, "frintn", double, "2d") +VECTOR(vdp, "frintp", double, "2d") VECTOR(vdm, "frintm", double, "2d") VECTOR(vdz, "frintz", double, "2d") VECTOR(vda, "frinta", double, "2d") + +static int failures; +static void check32(const char* what, int fpcr_mode, float in, float got, float want) { + uint32_t a, b; + memcpy(&a, &got, 4); memcpy(&b, &want, 4); + if (a != b && !(got != got && want != want) && failures++ < 20) + printf("FAIL %s mode=%d in=%a got=%a want=%a\n", what, fpcr_mode, in, got, want); +} +static void check64(const char* what, int fpcr_mode, double in, double got, double want) { + uint64_t a, b; + memcpy(&a, &got, 8); memcpy(&b, &want, 8); + if (a != b && !(got != got && want != want) && failures++ < 20) + printf("FAIL %s mode=%d in=%a got=%a want=%a\n", what, fpcr_mode, in, got, want); +} + +int main(void) { + static const double values[] = {0.0, -0.0, 0.25, -0.25, 0.5, -0.5, 0.75, -0.75, 1.0, -1.0, 1.5, -1.5, 2.5, -2.5, + 3.5, -3.5, 1e-30, -1e-30, 123456.5, -123456.5, 8388607.5, -8388607.5, 8388608.0, 16777216.0, 1e15, -1e15, + 4503599627370495.5, -4503599627370495.5, 1e30, -1e30, INFINITY, -INFINITY, NAN, 0.49999997, 0.9999999}; + // FPCR RMode (bits 23:22) for each rounding: RN, RP, RM, RZ. + static const int fpcr_bits[] = {0, 1, 2, 3}; + static const int fpcr_rounding[] = {NEAREST, PLUS, MINUS, ZERO}; + for (int m = 0; m < 4; ++m) { + set_fpcr((uint64_t)fpcr_bits[m] << 22); + const int current = fpcr_rounding[m]; + for (size_t i = 0; i < sizeof values / sizeof *values; ++i) { + const float f = (float)values[i]; + const double d = values[i]; + struct { const char* name; float (*s)(float); double (*dd)(double); void (*vs)(float*); void (*vd)(double*); int mode; } ops[] = { + {"frintx", sx, dx, vsx, vdx, current}, {"frinti", si, di, vsi, vdi, current}, {"frintn", sn, dn, vsn, vdn, NEAREST}, + {"frintp", sp, dp, vsp, vdp, PLUS}, {"frintm", sm, dm, vsm, vdm, MINUS}, {"frintz", sz, dz, vsz, vdz, ZERO}, + {"frinta", sa, da, vsa, vda, AWAY}}; + for (size_t o = 0; o < sizeof ops / sizeof *ops; ++o) { + const float want32 = (float)reference(f, ops[o].mode, 8388608.0); + const double want64 = reference(d, ops[o].mode, 4503599627370496.0); + check32(ops[o].name, m, f, ops[o].s(f), want32); + check64(ops[o].name, m, d, ops[o].dd(d), want64); + float v32[4] = {f, -f, f, 1.0f}; + ops[o].vs(v32); + check32(ops[o].name, m, f, v32[0], want32); + check32(ops[o].name, m, f, v32[2], want32); + check32(ops[o].name, m, 1.0f, v32[3], 1.0f); + double v64[2] = {d, 1.0}; + ops[o].vd(v64); + check64(ops[o].name, m, d, v64[0], want64); + check64(ops[o].name, m, 1.0, v64[1], 1.0); + } + } + } + set_fpcr(0); + // Inexact (FPSR.IXC, bit 4): FRINTX reports it, and only for a changed value. + clear_fpsr(); + volatile float exact = sx(2.0f); + if (fpsr() & 16) { printf("FAIL frintx reported inexact for 2.0\n"); ++failures; } + volatile float inexact = sx(2.25f); + if (!(fpsr() & 16)) { printf("FAIL frintx did not report inexact for 2.25\n"); ++failures; } + clear_fpsr(); + float lanes[4] = {1.0f, 2.0f, 3.25f, 4.0f}; + vsx(lanes); + if (!(fpsr() & 16)) { printf("FAIL vector frintx did not report inexact\n"); ++failures; } + (void)exact; (void)inexact; + if (failures) { printf("TRANSLATOR_ROUND_SELFTEST_FAIL %d\n", failures); return 1; } + puts("TRANSLATOR_ROUND_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/translator_selftest.c b/direct/tests/art/translator_selftest.c new file mode 100644 index 0000000..1bc8ea7 --- /dev/null +++ b/direct/tests/art/translator_selftest.c @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#include + +static _Atomic uint64_t counter; +static void* worker(void* unused) { + (void)unused; + for (int i = 0; i < 10000; ++i) atomic_fetch_add(&counter, 1); + return NULL; +} +int main(void) { + pthread_t threads[4]; + for (int i = 0; i < 4; ++i) assert(!pthread_create(&threads[i], NULL, worker, NULL)); + for (int i = 0; i < 4; ++i) assert(!pthread_join(threads[i], NULL)); + assert(counter == 40000); + uint64_t value = 10, old, increment = 7; + __asm__ volatile("ldaddal %x[increment], %x[old], [%x[ptr]]" + : [old] "=&r"(old) : [increment] "r"(increment), [ptr] "r"(&value) : "memory"); + assert(old == 10 && value == 17); + uint64_t expected = 17, replacement = 42; + __asm__ volatile("casal %x[expected], %x[replacement], [%x[ptr]]" + : [expected] "+r"(expected) : [replacement] "r"(replacement), [ptr] "r"(&value) : "memory"); + assert(expected == 17 && value == 42); + puts("TRANSLATOR_ATOMICS_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/translator_tbi_selftest.c b/direct/tests/art/translator_tbi_selftest.c new file mode 100644 index 0000000..ffacb60 --- /dev/null +++ b/direct/tests/art/translator_tbi_selftest.c @@ -0,0 +1,101 @@ +// Top-byte-ignore: ARM64 loads and stores disregard bits 56-63 of the address. +// The same code runs first with plain pointers, often enough for every +// translation tier to have compiled it, then with tagged ones, on this thread +// and on one that was already running. Results must not depend on the tag. +#include +#include +#include +#include +#include +#include + +enum { kWords = 64 }; + +static uint64_t* tag(uint64_t* p, uint64_t value) { return (uint64_t*)(((uintptr_t)p & 0x00ffffffffffffffull) | value << 56); } + +// Every addressing form, with live registers across each access. +static __attribute__((noinline)) uint64_t work(uint64_t* p, uint64_t seed) { + uint64_t sum = seed, a, b; + uint64_t* q = p; + for (int i = 0; i < kWords; ++i) p[i] = seed * (uint64_t)(i + 1) + (sum ^ (uint64_t)i); + for (int i = 0; i < kWords; ++i) sum = sum * 31 + p[i]; + sum += ((uint8_t*)p)[3] + ((uint16_t*)p)[5] + (uint64_t)((int32_t*)p)[7] + (uint64_t)((int8_t*)p)[9]; + ((uint8_t*)p)[11] = (uint8_t)sum; + ((uint16_t*)p)[13] = (uint16_t)sum; + ((uint32_t*)p)[15] = (uint32_t)sum; + __asm__ volatile( + "ldr %[a], [%[q]], #8\n" // post-index + "ldr %[b], [%[q], #8]!\n" // pre-index + "add %[sum], %[sum], %[a]\n" + "eor %[sum], %[sum], %[b]\n" + "ldp %[a], %[b], [%[q], #16]\n" + "add %[sum], %[sum], %[a], lsl #1\n" + "sub %[sum], %[sum], %[b]\n" + "stp %[sum], %[a], [%[q], #32]!\n" // pre-index pair store + "ldp %[a], %[b], [%[q]], #16\n" // post-index pair load + "add %[sum], %[sum], %[b]\n" + "mov x9, #24\n" + "ldr %[a], [%[q], x9]\n" // register offset + "eor %[sum], %[sum], %[a]\n" + "str %[sum], [%[q], x9, lsl #0]\n" + "ld1 {v16.16b}, [%[q]]\n" // vector load and store + "ldr q17, [%[q], #16]\n" + "add v16.2d, v16.2d, v17.2d\n" + "st1 {v16.16b}, [%[q]]\n" + "str q16, [%[q], #48]\n" + "ldr d18, [%[q], #8]\n" + "fmov %[a], d18\n" + "add %[sum], %[sum], %[a]\n" + "1: ldxr %[a], [%[q]]\n" // exclusive pair + "add %[a], %[a], #3\n" + "stxr w9, %[a], [%[q]]\n" + "cbnz w9, 1b\n" + "ldrsw %[b], [%[q], #4]\n" + "add %[sum], %[sum], %[b]\n" + : [sum] "+r"(sum), [q] "+r"(q), [a] "=&r"(a), [b] "=&r"(b) + : + : "x9", "v16", "v17", "v18", "memory"); + sum += (uint64_t)(q - p); + sum += atomic_fetch_add((_Atomic uint64_t*)(p + 40), sum); + for (int i = 0; i < kWords; ++i) sum = (sum << 7 | sum >> 57) ^ p[i]; + return sum; +} + +static uint64_t run(uint64_t* p, int rounds) { + uint64_t sum = 1469598103934665603ull; + for (int i = 0; i < rounds; ++i) sum = work(p, sum + (uint64_t)i); + return sum; +} + +static atomic_int stage; +static uint64_t thread_plain, thread_tagged; + +static void* thread_main(void* argument) { + uint64_t* buffer = argument; + thread_plain = run(buffer, 3000); + atomic_store(&stage, 1); + while (atomic_load(&stage) != 2) {} + thread_tagged = run(tag(buffer, 0x5a), 3000); + return NULL; +} + +int main(void) { + uint64_t* buffer = aligned_alloc(64, kWords * sizeof(uint64_t)); + uint64_t* other = aligned_alloc(64, kWords * sizeof(uint64_t)); + pthread_t thread; + if (!buffer || !other || pthread_create(&thread, NULL, thread_main, other)) return 2; + const uint64_t plain = run(buffer, 3000); + while (atomic_load(&stage) != 1) {} + const uint64_t tagged = run(tag(buffer, 0xb4), 3000); + const uint64_t again = run(tag(buffer, 0xff), 3000); + atomic_store(&stage, 2); + pthread_join(thread, NULL); + printf("checksum %016llx %016llx\n", (unsigned long long)plain, (unsigned long long)thread_plain); + if (plain != tagged || plain != again || thread_plain != thread_tagged || plain != thread_plain) { + printf("TRANSLATOR_TBI_SELFTEST_FAIL %016llx %016llx %016llx %016llx\n", (unsigned long long)tagged, + (unsigned long long)again, (unsigned long long)thread_plain, (unsigned long long)thread_tagged); + return 1; + } + puts("TRANSLATOR_TBI_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/art/vulkan_images_selftest.c b/direct/tests/art/vulkan_images_selftest.c new file mode 100644 index 0000000..d9ea917 --- /dev/null +++ b/direct/tests/art/vulkan_images_selftest.c @@ -0,0 +1,155 @@ +#include +#include +#include +#include + +#define CHECK(x) do { VkResult r = (x); if (r != VK_SUCCESS) { fprintf(stderr, "%s: %d\n", #x, r); return 1; } } while (0) +#define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "Failed: %s\n", #x); return 1; } } while (0) + +int main(int argc, char** argv) { + const int khr = argc > 1 && !strcmp(argv[1], "khr"); + VkApplicationInfo app = { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .apiVersion = khr ? VK_API_VERSION_1_1 : VK_API_VERSION_1_3 }; + VkInstanceCreateInfo ici = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app }; + VkInstance instance; + CHECK(vkCreateInstance(&ici, NULL, &instance)); + uint32_t count = 64; + VkPhysicalDevice devices[64]; + CHECK(vkEnumeratePhysicalDevices(instance, &count, devices)); + REQUIRE(count); + VkPhysicalDevice physical = devices[0]; + VkFormatProperties formatProperties; + vkGetPhysicalDeviceFormatProperties(physical, VK_FORMAT_ASTC_4x4_UNORM_BLOCK, &formatProperties); + const VkFormatFeatureFlags linearFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | + VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT; + REQUIRE((formatProperties.linearTilingFeatures & linearFeatures) == linearFeatures); + VkFormatProperties2 formatProperties2 = { .sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2 }; + vkGetPhysicalDeviceFormatProperties2(physical, VK_FORMAT_ASTC_4x4_UNORM_BLOCK, &formatProperties2); + REQUIRE(formatProperties2.formatProperties.linearTilingFeatures == formatProperties.linearTilingFeatures); + VkImageFormatProperties linearProperties; + CHECK(vkGetPhysicalDeviceImageFormatProperties(physical, VK_FORMAT_ASTC_4x4_UNORM_BLOCK, + VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_LINEAR, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + 0, &linearProperties)); + VkPhysicalDeviceImageFormatInfo2 linearQuery = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, + .format = VK_FORMAT_ASTC_4x4_UNORM_BLOCK, .type = VK_IMAGE_TYPE_2D, .tiling = VK_IMAGE_TILING_LINEAR, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT }; + VkImageFormatProperties2 linearProperties2 = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2 }; + CHECK(vkGetPhysicalDeviceImageFormatProperties2(physical, &linearQuery, &linearProperties2)); + REQUIRE(linearProperties.maxExtent.width == linearProperties2.imageFormatProperties.maxExtent.width); + VkPhysicalDeviceMemoryProperties memory; + vkGetPhysicalDeviceMemoryProperties(physical, &memory); + uint32_t familyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, NULL); + VkQueueFamilyProperties families[64]; + REQUIRE(familyCount && familyCount <= 64); + vkGetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, families); + uint32_t family = 0; + while (family < familyCount && !(families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT)) ++family; + REQUIRE(family < familyCount); + float priority = 1; + VkDeviceQueueCreateInfo queue = { .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .queueFamilyIndex = family, .queueCount = 1, .pQueuePriorities = &priority }; + VkPhysicalDeviceSynchronization2Features sync = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES }; + VkPhysicalDeviceMaintenance4Features maintenance = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_4_FEATURES, .pNext = &sync }; + VkPhysicalDeviceFeatures2 features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &maintenance }; + vkGetPhysicalDeviceFeatures2(physical, &features); + VkDeviceCreateInfo dci = { .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .pNext = &maintenance, .queueCreateInfoCount = 1, .pQueueCreateInfos = &queue }; + const char* extensions[] = { VK_KHR_MAINTENANCE_4_EXTENSION_NAME, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, + VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME, VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, + VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME }; + if (khr) { dci.enabledExtensionCount = sizeof(extensions) / sizeof(extensions[0]); dci.ppEnabledExtensionNames = extensions; } + VkDevice device; + CHECK(vkCreateDevice(physical, &dci, NULL, &device)); + PFN_vkGetDeviceImageMemoryRequirements query = (PFN_vkGetDeviceImageMemoryRequirements)vkGetDeviceProcAddr(device, + khr ? "vkGetDeviceImageMemoryRequirementsKHR" : "vkGetDeviceImageMemoryRequirements"); + PFN_vkBindImageMemory2 bind = (PFN_vkBindImageMemory2)vkGetDeviceProcAddr(device, + khr ? "vkBindImageMemory2KHR" : "vkBindImageMemory2"); + REQUIRE(query); + for (int kind = 0; kind < 4; ++kind) { + VkImageCreateInfo ci = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = kind == 2 ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D, + .format = VK_FORMAT_ASTC_4x4_UNORM_BLOCK, .extent = {16, 16, kind == 2 ? 4 : 1}, + .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = kind == 0 ? VK_IMAGE_TILING_LINEAR : VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .initialLayout = kind == 0 ? VK_IMAGE_LAYOUT_PREINITIALIZED : VK_IMAGE_LAYOUT_UNDEFINED }; + VkFormat formats[] = { VK_FORMAT_ASTC_4x4_UNORM_BLOCK, VK_FORMAT_ASTC_4x4_SRGB_BLOCK }; + if (kind == 3) ci.format = VK_FORMAT_R8G8B8A8_UNORM; + VkImageFormatListCreateInfo list = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, + .viewFormatCount = 2, .pViewFormats = formats }; + if (kind == 1) { ci.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; ci.pNext = &list; } + VkMemoryRequirements2 predicted = { .sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2 }; + VkDeviceImageMemoryRequirements input = { .sType = VK_STRUCTURE_TYPE_DEVICE_IMAGE_MEMORY_REQUIREMENTS, .pCreateInfo = &ci }; + if (maintenance.maintenance4) { + query(device, &input, &predicted); + REQUIRE(predicted.memoryRequirements.size && predicted.memoryRequirements.memoryTypeBits); + } + VkImage image; + CHECK(vkCreateImage(device, &ci, NULL, &image)); + VkMemoryRequirements requirements; + vkGetImageMemoryRequirements(device, image, &requirements); + REQUIRE(requirements.size && requirements.memoryTypeBits); + if (maintenance.maintenance4) REQUIRE(predicted.memoryRequirements.size == requirements.size); + uint32_t type = 0; + while (type < memory.memoryTypeCount && (!(requirements.memoryTypeBits & (1u << type)) || + (kind == 0 && !(memory.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)))) ++type; + REQUIRE(type < memory.memoryTypeCount); + VkMemoryAllocateInfo allocation = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = requirements.size, .memoryTypeIndex = type }; + VkDeviceMemory backing; + CHECK(vkAllocateMemory(device, &allocation, NULL, &backing)); + VkBindImageMemoryInfo binding = { .sType = VK_STRUCTURE_TYPE_BIND_IMAGE_MEMORY_INFO, .image = image, .memory = backing }; + REQUIRE(bind); + CHECK(bind(device, 1, &binding)); + if (kind == 0) { + VkImageSubresource subresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT }; + VkSubresourceLayout layout; + vkGetImageSubresourceLayout(device, image, &subresource, &layout); + REQUIRE(layout.rowPitch == 64 && layout.size == 256 && layout.offset + layout.size <= requirements.size); + void* mapped; + CHECK(vkMapMemory(device, backing, 0, VK_WHOLE_SIZE, 0, &mapped)); + memset((char*)mapped + layout.offset, 0, layout.size); + vkUnmapMemory(device, backing); + } + VkImageViewCreateInfo vci = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = image, + .viewType = kind == 2 ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D, + .format = kind == 1 ? VK_FORMAT_ASTC_4x4_SRGB_BLOCK : ci.format, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1} }; + VkImageView view; + CHECK(vkCreateImageView(device, &vci, NULL, &view)); + vkDestroyImageView(device, view, NULL); + vkDestroyImage(device, image, NULL); + vkFreeMemory(device, backing, NULL); + } + REQUIRE(sync.synchronization2); + PFN_vkCmdPipelineBarrier2 barrier = (PFN_vkCmdPipelineBarrier2)vkGetDeviceProcAddr(device, + khr ? "vkCmdPipelineBarrier2KHR" : "vkCmdPipelineBarrier2"); + PFN_vkQueueSubmit2 submit = (PFN_vkQueueSubmit2)vkGetDeviceProcAddr(device, + khr ? "vkQueueSubmit2KHR" : "vkQueueSubmit2"); + REQUIRE(barrier && submit); + VkCommandPoolCreateInfo pci = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .queueFamilyIndex = family }; + VkCommandPool pool; + CHECK(vkCreateCommandPool(device, &pci, NULL, &pool)); + VkCommandBufferAllocateInfo cai = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .commandPool = pool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1 }; + VkCommandBuffer command; + CHECK(vkAllocateCommandBuffers(device, &cai, &command)); + VkCommandBufferBeginInfo begin = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + CHECK(vkBeginCommandBuffer(command, &begin)); + VkDependencyInfo dependency = { .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO }; + barrier(command, &dependency); + CHECK(vkEndCommandBuffer(command)); + VkQueue q; + vkGetDeviceQueue(device, family, 0, &q); + VkCommandBufferSubmitInfo commandInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = command }; + VkSubmitInfo2 submission = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .commandBufferInfoCount = 1, .pCommandBufferInfos = &commandInfo }; + CHECK(submit(q, 1, &submission, VK_NULL_HANDLE)); + CHECK(vkQueueWaitIdle(q)); + vkDestroyCommandPool(device, pool, NULL); + vkDestroyDevice(device, NULL); + vkDestroyInstance(instance, NULL); + puts("VULKAN_IMAGES_SELFTEST_PASS"); + return 0; +} diff --git a/direct/tests/patch_probe.ps1 b/direct/tests/patch_probe.ps1 new file mode 100644 index 0000000..3537ac6 --- /dev/null +++ b/direct/tests/patch_probe.ps1 @@ -0,0 +1,15 @@ +$ErrorActionPreference = 'Stop' +$tokens=$null; $errors=$null +$ast=[Management.Automation.Language.Parser]::ParseFile((Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'scripts/build/direct_deps.ps1'),[ref]$tokens,[ref]$errors) +if($errors) { throw ($errors|Out-String) } +$function=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Test-AxrbPatchApplied'},$true) +Invoke-Expression $function.Extent.Text +$fixture=(Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) ('out/patch-check-' + [guid]::NewGuid().ToString('N'))) +New-Item -ItemType Directory -Force $fixture | Out-Null +[IO.File]::WriteAllText((Join-Path $fixture 'git.cmd'),"@echo expected reverse-check failure 1>&2`r`n@exit /b 1`r`n") +$env:PATH=$fixture+';'+$env:PATH +if(Test-AxrbPatchApplied 'repo with spaces' 'patch with spaces') { throw 'Expected a false probe result' } +if($ErrorActionPreference -ne 'Stop') { throw 'Error preference was not restored' } +[IO.File]::WriteAllText((Join-Path $fixture 'git.cmd'),"@exit /b 0`r`n") +if(!(Test-AxrbPatchApplied 'repo with spaces' 'patch with spaces')) { throw 'Expected a true probe result' } +Write-Output 'Windows PowerShell expected-stderr probe handling passed.' diff --git a/direct/tests/test_art_session.py b/direct/tests/test_art_session.py new file mode 100644 index 0000000..e0be1b0 --- /dev/null +++ b/direct/tests/test_art_session.py @@ -0,0 +1,95 @@ +"""Launcher preparation regressions; no JDK, SDK or device is needed.""" +import sys +import tempfile +import unittest +import zipfile +from unittest.mock import patch +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) +from prepare_art_session import stage_libraries, prepare + + +class ArtSessionTest(unittest.TestCase): + def test_package_path_is_shared_by_apk_libraries_and_bootstrap(self): + with patch('prepare_art_session.required', side_effect=lambda p: Path(p).resolve()), \ + patch('prepare_art_session.boot_classpath', return_value='boot'), \ + patch('prepare_art_session.stage_libraries', return_value=Path('libraries')): + args = prepare(apk='game.apk', activity='com.example.game/.Main', sysroot='root', + data='data', bootstrap='boot.dex', native='native', proxies='proxies') + root = '/data/app/com.example.game-axrb' + self.assertEqual(args[-2:], [root + '/base.apk', 'com.example.game.Main']) + mounts = [args[i + 1].split('=', 1)[0] for i, a in enumerate(args) if a == '--mount-ro'] + self.assertIn(root + '/base.apk', mounts) + self.assertIn(root + '/lib/arm64', mounts) + self.assertIn(root + '/lib/arm64/libopenxr_runtime.so', mounts) + self.assertIn('/data/app/axrb/lib/arm64/libopenxr_runtime.so', mounts) + with self.assertRaises(ValueError): + prepare(apk='game.apk', activity='../escape/.Main', sysroot='root', + data='data', bootstrap='boot.dex', native='native', proxies='proxies') + + def test_legacy_oculus_compatibility_is_opt_in(self): + def run(**options): + with patch('prepare_art_session.required', side_effect=lambda p: Path(p).resolve()), \ + patch('prepare_art_session.boot_classpath', return_value='boot'), \ + patch('prepare_art_session.stage_libraries', return_value=Path('libraries')): + args = prepare(apk='game.apk', activity='com.camouflaj.salmon/.Main', sysroot='root', data='data', + bootstrap='boot.dex', native='native', proxies='proxies', **options) + mounts = dict(args[i + 1].split('=', 1) for i, a in enumerate(args) if a == '--mount-ro') + props = dict(args[i + 1].split('=', 1) for i, a in enumerate(args) if a == '--prop') + return mounts, props + mounts, props = run() + self.assertFalse(any('systemdriver' in m for m in mounts)) + self.assertNotIn('ro.axrb.legacy_oculus_driver', props) + self.assertNotIn('ro.product.manufacturer', props) + + mounts, props = run(legacy_oculus_driver='driver.apk', quest_identity=True, controllers_only=True) + root = '/data/app/com.oculus.systemdriver-axrb' + self.assertEqual(Path(mounts[root + '/base.apk']), Path('driver.apk').resolve()) + self.assertIn(root + '/lib/arm64', mounts) + # The driver loads the session's runtime, not a copy from another build. + self.assertEqual(Path(mounts[root + '/lib/arm64/libopenxr_runtime.so']), + Path(mounts['/data/app/axrb/lib/arm64/libopenxr_runtime.so'])) + self.assertEqual(props['ro.axrb.legacy_oculus_driver'], root + '/base.apk') + self.assertEqual(props['persist.sys.axrb.legacy.controllers_only.com.camouflaj.salmon'], '1') + self.assertEqual((props['ro.product.manufacturer'], props['ro.product.vendor.manufacturer'], + props['ro.product.brand'], props['ro.product.model']), ('Oculus', 'Oculus', 'oculus', 'Quest 3')) + with self.assertRaises(ValueError): + run(controllers_only=True) + + def test_equal_size_library_update_does_not_reuse_old_code(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + apk = root / 'game.apk' + for value in (b'old!', b'new!'): + with zipfile.ZipFile(apk, 'w') as archive: + archive.writestr('lib/arm64-v8a/libgame.so', value) + libraries = stage_libraries(apk, root / 'data') + self.assertEqual((libraries / 'libgame.so').read_bytes(), value) + self.assertEqual(stage_libraries(apk, root / 'data'), libraries) + + def test_apk_paths_cannot_escape_extraction(self): + for name in ('../escape.so', 'a/b.so', 'C:stream', 'x\\evil.so'): + with self.subTest(name=name), tempfile.TemporaryDirectory() as root: + root = Path(root) + apk = root / 'game.apk' + with zipfile.ZipFile(apk, 'w') as archive: + archive.writestr('lib/arm64-v8a/' + name, b'bad') + with self.assertRaises(ValueError): + stage_libraries(apk, root / 'data') + self.assertFalse((root / 'escape.so').exists()) + + def test_failed_zip_extract_cannot_publish_complete_receipt(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + apk = root / 'game.apk' + with zipfile.ZipFile(apk, 'w', compression=zipfile.ZIP_STORED) as archive: + archive.writestr('lib/arm64-v8a/libgame.so', b'payload123') + apk.write_bytes(apk.read_bytes().replace(b'payload123', b'corrupt123')) + with self.assertRaises(zipfile.BadZipFile): + stage_libraries(apk, root / 'data') + self.assertFalse(list(root.rglob('.complete.json'))) + + +if __name__ == '__main__': + unittest.main() diff --git a/direct/tests/test_companion_apks.py b/direct/tests/test_companion_apks.py new file mode 100644 index 0000000..647905d --- /dev/null +++ b/direct/tests/test_companion_apks.py @@ -0,0 +1,52 @@ +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) +from prepare_art_session import prepare + +class CompanionApkTest(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + for name in ['sysroot/system/bin/app_process64','bootstrap.dex', + *['native/'+n for n in ('libandroid.so','libaxrbndk.so','libmediandk.so','libaxrbmndk.so','libaxrb_art_bridge.so','libOpenSLES.so')], + *['proxies/'+n for n in ('libvulkan.so','libaaudio.so','libEGL.so','libGLESv1_CM.so','libGLESv2.so','libopenxr_runtime.so')]]: + target=self.root/name; target.parent.mkdir(parents=True,exist_ok=True); target.write_bytes(b'fixture') + self.game=self.apk('game',b'game') + self.first=self.apk('first',b'first') + self.second=self.apk('second',b'second') + def tearDown(self): + self.temporary.cleanup() + def apk(self,name,value): + path=self.root/(name+'.apk') + with zipfile.ZipFile(path,'w') as archive: archive.writestr('lib/arm64-v8a/libshared.so',value) + return path + def prepare(self,**options): + with patch('prepare_art_session.boot_classpath',return_value='fixture'): + return prepare(apk=self.game,activity='com.example.game/.Main',sysroot=self.root/'sysroot', + data=self.root/'data',bootstrap=self.root/'bootstrap.dex',native=self.root/'native',proxies=self.root/'proxies',**options) + def test_companion_libraries_are_separate_and_read_only(self): + original=self.game.read_bytes() + args=self.prepare(companion_apks=[self.first,self.second]) + mounts=dict(args[i+1].split('=',1) for i,arg in enumerate(args) if arg=='--mount-ro') + props=dict(args[i+1].split('=',1) for i,arg in enumerate(args) if arg=='--prop') + self.assertEqual(props['ro.axrb.companion_count'],'2') + for index,value in enumerate((b'first',b'second')): + guest=f'/data/app/axrb-companion-{index}' + self.assertEqual(props[f'ro.axrb.companion_apk.{index}'],guest+'/base.apk') + self.assertEqual((Path(mounts[guest+'/lib/arm64'])/'libshared.so').read_bytes(),value) + self.assertNotIn('--mount-rw',args) + self.assertEqual(self.game.read_bytes(),original) + def test_empty_companion_configuration_keeps_original_session(self): + self.assertEqual(self.prepare(),self.prepare(companion_apks=[])) + def test_duplicates_game_driver_missing_and_excess_are_rejected(self): + driver=self.apk('driver',b'driver') + cases=[dict(companion_apks=[self.first,self.first]),dict(companion_apks=[self.game]), + dict(companion_apks=[driver],legacy_oculus_driver=driver), + dict(companion_apks=[self.root/'missing.apk']),dict(companion_apks=[self.first]*9)] + for options in cases: + with self.subTest(options=options),self.assertRaises(ValueError): self.prepare(**options) +if __name__=='__main__': unittest.main() diff --git a/direct/tests/time_math.cpp b/direct/tests/time_math.cpp new file mode 100644 index 0000000..6df7daf --- /dev/null +++ b/direct/tests/time_math.cpp @@ -0,0 +1,20 @@ +#include "time_math.h" +#include +#include +#include + +int main() { + using axrb::scale_time; + using axrb::add_time; + constexpr int64_t maximum = std::numeric_limits::max(); + if (scale_time(946080000000000000LL, 10000000, 1000000000) != 9460800000000000LL) return 1; + if (scale_time(9460800000000000LL, 10000000, 10000000) != 9460800000000000LL) return 2; + if (scale_time(maximum, 1000000000, 1) != maximum) return 3; + if (scale_time(maximum, maximum, maximum) != maximum) return 4; + if (add_time(maximum - 10, 11) != maximum || add_time(20, 30) != 50) return 5; + for (int64_t frequency : {10000000LL, 24000000LL, 3579545LL, 3000000000LL}) { + for (int64_t ns = 1; ns < 100000; ns += 97) + if (scale_time(ns, frequency, 1000000000) != ns * frequency / 1000000000) return 6; + } + std::puts("TIME_MATH_PASS"); +} diff --git a/direct/tools/build_art_bootstrap.py b/direct/tools/build_art_bootstrap.py new file mode 100644 index 0000000..b52d7b1 --- /dev/null +++ b/direct/tools/build_art_bootstrap.py @@ -0,0 +1,36 @@ +"""Compile the Direct AOSP bootstrap using developer Android/JDK tools.""" +import argparse +from pathlib import Path +import subprocess +import shutil +import tempfile + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--android-jar', type=Path, required=True) + p.add_argument('--d8', type=Path, required=True) + p.add_argument('--output', type=Path, required=True) + a = p.parse_args() + javac, java = shutil.which('javac'), shutil.which('java') + if not javac or not java: + p.error('Developer JDK must be on PATH') + source = Path(__file__).resolve().parents[1] / 'art/java' + dex = a.output / 'dex' + dex.mkdir(parents=True, exist_ok=True) + # A failed compile must not publish a stale or partially rebuilt bootstrap. + with tempfile.TemporaryDirectory(prefix='bootstrap-', dir=a.output) as staging: + classes, built = Path(staging) / 'classes', Path(staging) / 'dex' + classes.mkdir() + built.mkdir() + subprocess.run([javac, '--release', '8', '-Xlint:-options', '-cp', str(a.android_jar), + '-d', str(classes), *map(str, sorted(source.rglob('*.java')))], check=True) + subprocess.run([java, '-cp', str(a.d8), 'com.android.tools.r8.D8', '--min-api', '26', + '--lib', str(a.android_jar), '--output', str(built), + *map(str, sorted(classes.rglob('*.class')))], check=True) + (built / 'classes.dex').replace(dex / 'classes.dex') + print(dex / 'classes.dex') + + +if __name__ == '__main__': + main() diff --git a/direct/tools/build_art_native.py b/direct/tools/build_art_native.py new file mode 100644 index 0000000..7f8358b --- /dev/null +++ b/direct/tools/build_art_native.py @@ -0,0 +1,141 @@ +"""Build the real-ART host bridge and matched AOSP NDK forwarding adapters.""" +import argparse +from pathlib import Path +import re +import struct +import subprocess +import shutil + + +def run(args): + subprocess.run(list(map(str, args)), check=True) + + +def exports(nm, path): + """(name, version, is_function) for each defined dynamic export.""" + output = subprocess.check_output([str(nm), '-D', '--defined-only', str(path)], text=True) + result = [] + for line in output.splitlines(): + parts = line.split() + if len(parts) < 3 or parts[1] not in ('T', 'W', 'D', 'B', 'R', 'V'): + continue + name, *version = parts[-1].split('@@') + # Control-flow-integrity bookkeeping, not API: the linker requires + # __cfi_check to be page-aligned code of the library itself. + if name.startswith('__cfi'): + continue + result.append((name, version[0] if version else None, parts[1] in ('T', 'W'))) + return result + + +def rename_soname(original, expected, replacement): + """The library with only its SONAME string changed (same length or shorter).""" + data = bytearray(original.read_bytes()) + if data[:6] != b'\x7fELF\x02\x01': + raise ValueError('Expected an ELF64 little-endian AOSP NDK library') + if len(replacement) > len(expected): + raise ValueError('The replacement SONAME must not be longer than the original') + offset = struct.unpack_from('= 3} + symbols = exports(nm, original) + functions = [name for name, _, function in symbols if function and name not in overrides] + data = [name for name, _, function in symbols if not function and name not in overrides] + for name in functions + data: + if not re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', name): + raise ValueError('Unsupported ELF export ' + name) + assembly = ['.text'] + loader = ['#include ', '#include ', '#include '] + for i, name in enumerate(functions): + assembly += [f'.global {name}', f'.type {name}, %function', f'{name}:', + f'adrp x16, axrb_forward_{i}', f'ldr x16, [x16, :lo12:axrb_forward_{i}]', 'br x16', f'.size {name}, .-{name}'] + loader += [f'__attribute__((visibility("hidden"))) void* axrb_forward_{i};'] + # Data exports (pointer-sized constants such as AMEDIAFORMAT_KEY_*) are + # copied from the loaded original before any dependent library runs. + loader += [f'void* {name};' for name in data] + loader += ['__attribute__((constructor)) static void initialize(void) {', + f'void* library = dlopen("/system/lib64/{renamed}", RTLD_NOW | RTLD_LOCAL);', + f'if (!library) {{ fprintf(stderr, "AOSP {soname} forwarding library: %s\\n", dlerror()); abort(); }}'] + if data: + loader += ['void** value;'] + for i, name in enumerate(functions): + loader += [f'axrb_forward_{i} = dlsym(library, "{name}");', + f'if (!axrb_forward_{i}) {{ fprintf(stderr, "Missing AOSP export: {name}\\n"); abort(); }}'] + for name in data: + loader += [f'value = (void**)dlsym(library, "{name}");', + f'if (!value) {{ fprintf(stderr, "Missing AOSP export: {name}\\n"); abort(); }}', + f'{name} = *value;'] + loader += ['}'] + versions = {} + for name, version, _ in symbols: + versions.setdefault(version or default_version, []).append(name) + Path(f'{work}_forward.S').write_text('\n'.join(assembly) + '\n') + Path(f'{work}_forward.c').write_text('\n'.join(loader) + '\n') + Path(f'{work}.map').write_text('\n'.join(name + ' { global: ' + '; '.join(names) + '; local: *; };' for name, names in versions.items())) + run([clang, *flags, '-c', f'{work}_forward.c', '-o', f'{work}_forward.o']) + run([cxx, *flags, '-shared', overrides_object, f'{work}_forward.o', f'{work}_forward.S', + '-Wl,-soname,' + soname, f'-Wl,--version-script={work}.map', + '-L' + str(output.parent), '-Wl,--no-as-needed', '-l:' + renamed, *libs, '-ldl', '-static-libstdc++', '-o', output]) + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + for name in ('ndk', 'sysroot', 'output'): + p.add_argument('--' + name, type=Path, required=True) + a = p.parse_args() + a.output.mkdir(parents=True, exist_ok=True) + bin = a.ndk / 'toolchains/llvm/prebuilt/windows-x86_64/bin' + clang, cxx, nm = [bin / name for name in ('clang.exe', 'clang++.exe', 'llvm-nm.exe')] + source = Path(__file__).resolve().parents[1] / 'art' + flags = ['--target=aarch64-linux-android33', '-O2', '-Wall', '-Werror', '-fPIC'] + run([clang, *flags, '-shared', source.parent / 'guest/proxy/libOpenSLES.c', + '-laaudio', '-lm', '-Wl,-soname,libOpenSLES.so', '-o', a.output / 'libOpenSLES.so']) + run([clang, *flags, '-c', source / 'art_bridge.c', '-o', a.output / 'art_bridge.o']) + run([clang, *flags, '-c', source / 'art_media.c', '-o', a.output / 'art_media.o']) + run([cxx, *flags, '-shared', source / 'art_binder.cpp', a.output / 'art_bridge.o', a.output / 'art_media.o', + '-landroid', '-ldl', '-lbinder_ndk', '-static-libstdc++', '-o', a.output / 'libaxrb_art_bridge.so']) + shutil.copyfile(a.sysroot / 'system/etc/NOTICE.xml.gz', a.output / 'AOSP-NOTICE.xml.gz') + tools = (clang, cxx, nm) + # The matched AOSP libraries keep their asset, input, window, JNI and + # AMediaFormat implementations; only the overridden functions change. + run([cxx, *flags, '-Wno-deprecated-declarations', '-c', source / 'ndk_host.cpp', '-o', a.output / 'ndk_host.o']) + forwarding_proxy(tools, flags, a.sysroot / 'system/lib64/libandroid.so', 'libaxrbndk.so', a.output / 'ndk_host.o', + a.output / 'libandroid.so', [], 'LIBANDROID') + run([cxx, *flags, '-c', source / 'ndk_media.cpp', '-o', a.output / 'ndk_media.o']) + forwarding_proxy(tools, flags, a.sysroot / 'system/lib64/libmediandk.so', 'libaxrbmndk.so', a.output / 'ndk_media.o', + a.output / 'libmediandk.so', ['-llog'], 'LIBMEDIANDK') + print(a.output) + + +if __name__ == '__main__': + main() diff --git a/direct/tools/dexgap.py b/direct/tools/dexgap.py new file mode 100644 index 0000000..4acb241 --- /dev/null +++ b/direct/tools/dexgap.py @@ -0,0 +1,164 @@ +"""Lists the framework/library classes, methods and fields an APK's DEX code +references that AXRB Direct's boot class path does not define. + +usage: dexgap.py APK BOOT_DEX +""" +import struct +import sys +import zipfile + + +class Dex: + def __init__(self, data): + self.d = data + u = lambda o: struct.unpack_from(' + +Writes + direct/guest/proxy/vk_thunks.inc guest libvulkan.so: one exported function per + command that packs its arguments into uint64_t + slots and traps to the host ("vk.call", id) + direct/src/vk_entries.inc host: command names, dispatch level and an + invoker that unpacks the slots into the host + driver's function + +Both sides index commands by their position in the same list, so they must be +regenerated together. Guest and host structures share their layout (LP64 and +LLP64 agree on every Vulkan type: no `long`), so only the arguments are +marshalled; pointers are identity-mapped. +""" +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +HEADERS = ["vulkan/vulkan_core.h", "vulkan/vulkan_android.h"] +# The loader's own entry points: written by hand on both sides. +MANUAL = {"vkGetInstanceProcAddr", "vkGetDeviceProcAddr"} + +PROTO = re.compile(r"VKAPI_ATTR\s+(.+?)\s+VKAPI_CALL\s+(vk\w+)\s*\((.*?)\)\s*;", re.S) + + +def parse_param(text): + text = " ".join(text.split()) + m = re.match(r"(.*?)\b(\w+)\s*((?:\[\w+\])*)$", text) + if not m: + raise SystemExit("cannot parse parameter: " + text) + ctype, name, dims = m.group(1).strip(), m.group(2), m.group(3) + if dims: + ctype += "*" + return ctype, name + + +def level(params): + if not params: + return "kGlobal" + first = params[0][0] + if first in ("VkInstance", "VkPhysicalDevice"): + return "kInstance" + if first in ("VkDevice", "VkQueue", "VkCommandBuffer"): + return "kDevice" + return "kGlobal" + + +def main(): + inc = sys.argv[1] + commands = [] + seen = set() + for header in HEADERS: + text = open(os.path.join(inc, header), encoding="utf-8").read() + for ret, name, params in PROTO.findall(text): + if name in MANUAL or name in seen: + continue + seen.add(name) + plist = [] if params.strip() in ("", "void") else [parse_param(p) for p in params.split(",")] + commands.append((" ".join(ret.split()), name, plist)) + + banner = "// Generated by direct/tools/gen_vulkan.py from the NDK's Vulkan headers. Do not edit.\n" + + g = [banner, "// %d commands\n" % len(commands)] + for i, (ret, name, params) in enumerate(commands): + decl = ", ".join("%s %s" % p for p in params) or "void" + slots = ", ".join("u64(%s)" % p[1] for p in params) or "0" + g.append('extern "C" VKAPI_ATTR %s VKAPI_CALL %s(%s) {\n' % (ret, name, decl)) + g.append(" const uint64_t a[] = {%s};\n" % slots) + if ret == "void": + g.append(" axrb_vk_call(%d, a);\n}\n" % i) + else: + g.append(" return from_u64<%s>(axrb_vk_call(%d, a));\n}\n" % (ret, i)) + g.append("\nstatic const VkCommand kCommands[] = {\n") + for i, (ret, name, params) in enumerate(commands): + g.append(' {"%s", reinterpret_cast(%s), %s},\n' % (name, name, level(params))) + g.append("};\n") + open(os.path.join(ROOT, "direct/guest/proxy/vk_thunks.inc"), "w", encoding="utf-8", newline="\n").write("".join(g)) + + h = [banner, "// %d commands\n" % len(commands), "static const VkCommand kCommands[] = {\n"] + for i, (ret, name, params) in enumerate(commands): + h.append(' {"%s", %s, &Invoker::call},\n' % (name, level(params), name)) + h.append("};\n") + open(os.path.join(ROOT, "direct/src/vk_entries.inc"), "w", encoding="utf-8", newline="\n").write("".join(h)) + print("%d commands" % len(commands)) + + +main() diff --git a/direct/tools/make_sysroot.py b/direct/tools/make_sysroot.py new file mode 100644 index 0000000..6445100 --- /dev/null +++ b/direct/tools/make_sysroot.py @@ -0,0 +1,263 @@ +"""Builds AXRB Direct's Android system root from the SDK's arm64 system image. + +AXRB Direct runs the unmodified AOSP arm64 bionic, linker and system libraries +under Dynarmic. They come from Google's Android SDK system image +(sys-img/android/arm64-v8a-36_r02.zip), which the launcher downloads on the +player's machine under the Android SDK license; this script extracts the parts +the runner needs (about 250 MB): + + system/lib64, system/etc, system/bin/linker64, system/usr/share/zoneinfo, + system/build.prop + apex/com.android.{runtime,i18n,tzdata} (unpacked from their .apex payloads) + vendor-build.prop (the vendor partition's build.prop) + .symlinks.json (symbolic links, recreated by the runner) + +Usage: make_sysroot.py IMAGE OUTPUT + IMAGE is the downloaded zip, or the system.img inside it. OUTPUT must not exist. +Only the Python standard library is used (the launcher's embedded Python). +""" + +import io +import argparse +import json +import os +import shutil +import struct +import sys +import tempfile +import zipfile + +S_IFMT, S_IFDIR, S_IFLNK = 0xF000, 0x4000, 0xA000 +FT_REG, FT_DIR, FT_LNK = 1, 2, 7 +ROOT_INODE = 2 + + +class Ext4: + """Read-only access to an ext4 file system inside an image file.""" + + def __init__(self, f, base): + self.f, self.base = f, base + sb = self.read(1024, 1024) + (self.magic,) = struct.unpack_from('= 64 else 0 + return lo | (hi << 32) + + def inode(self, n): + group, index = divmod(n - 1, self.inodes_per_group) + return self.read(self.inode_table(group) * self.block_size + index * self.inode_size, self.inode_size) + + def extents(self, node): + magic, entries, _, depth = struct.unpack_from(' 32768: # uninitialized extent + length -= 32768 + out.append((logical, length, lo | (hi << 32))) + else: + _, lo, hi = struct.unpack_from('= size: + continue + chunk = self.read(physical * self.block_size, length * self.block_size) + buf[start:start + len(chunk)] = chunk[:size - start] + return bytes(buf) + + def entries(self, n): + d, o, out = self.data(n), 0, {} + while o < len(d): + ino, rec_len, name_len, file_type = struct.unpack_from(' 8: + raise ValueError('At most eight companion APKs are supported') + if len(set(companions)) != len(companions) or apk in companions or (legacy_oculus_driver and Path(legacy_oculus_driver).resolve() in companions): + raise ValueError('Companion APKs must be distinct from each other, the game and the legacy driver') + companion_paths = [] + for index, companion in enumerate(companions): + guest = f'/data/app/axrb-companion-{index}' + mount(guest + '/base.apk', companion) + mount(guest + '/lib/arm64', stage_libraries(companion, data), True) + companion_paths.append(guest + '/base.apk') + if companion_paths: + args.extend(['--prop', 'ro.axrb.companion_count=' + str(len(companion_paths))]) + for index, path in enumerate(companion_paths): + args.extend(['--prop', f'ro.axrb.companion_apk.{index}=' + path]) + if controllers_only: + if not legacy_oculus_driver: + raise ValueError('The controller-only policy is applied by the legacy Oculus driver') + if app_root == '/data/app/axrb': + raise ValueError('The controller-only policy needs the application package') + args.extend(['--prop', f'persist.sys.axrb.legacy.controllers_only.{package}=1']) + if quest_identity: + for key, value in QUEST_IDENTITY.items(): + for scope in ('', 'system.', 'vendor.', 'product.', 'odm.', 'system_ext.'): + args.extend(['--prop', f'ro.product.{scope}{key}={value}']) + for prop in ('ro.hardware.egl=axrb', 'servicemanager.ready=true', + 'dalvik.vm.usejit=false', 'dalvik.vm.usejitprofiles=false'): + args.extend(['--prop', prop]) + for env in ('LD_LIBRARY_PATH=/apex/com.android.art/lib64:/apex/com.android.os.statsd/lib64:/apex/com.android.runtime/lib64/bionic:/system/lib64', + 'BOOTCLASSPATH=' + boot, 'CLASSPATH=/system/framework/axrb-bootstrap.dex'): + args.extend(['--env', env]) + args += ['/system/bin/app_process64', '-Xint', '-Xnoimage-dex2oat', '-Ximage:/nonexistent.art'] + if check_jni: + args += ['-Xcheck:jni'] + args += ['/system/bin', 'com.axrb.runtime.AppProcess', app_root + '/base.apk', activity] + if diagnostics: + args += ['--diagnostics'] + return args + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('apk', 'sysroot', 'data', 'bootstrap', 'native', 'proxies'): + parser.add_argument('--' + name, type=Path, required=True) + parser.add_argument('--activity', required=True) + parser.add_argument('--runtime', type=Path) + parser.add_argument('--assets', type=Path) + parser.add_argument('--check-jni', action='store_true') + parser.add_argument('--legacy-oculus-driver', type=Path) + parser.add_argument('--quest-identity', action='store_true') + parser.add_argument('--controllers-only', action='store_true') + parser.add_argument('--companion-apk', dest='companion_apks', type=Path, action='append') + parser.add_argument('--diagnostics', action='store_true') + print(json.dumps(prepare(**vars(parser.parse_args())), ensure_ascii=True)) + + +if __name__ == '__main__': + main() diff --git a/direct/tools/probe_app.py b/direct/tools/probe_app.py new file mode 100644 index 0000000..54d33e7 --- /dev/null +++ b/direct/tools/probe_app.py @@ -0,0 +1,47 @@ +"""Build a one-activity probe APK and run it in a Direct session (developer-only). +JDK and Android build tools are needed to compile the fixture; the run itself +uses only the staged Direct runtime. +""" +import subprocess +import time +import zipfile +from pathlib import Path +from prepare_art_session import prepare +from test_art import run + + +def build_probe(work, android_jar, build_tools, package, activity, source): + """Compiles source (one Activity) into /.apk.""" + classes, dex = work / 'classes', work / 'dex' + classes.mkdir(parents=True, exist_ok=True); dex.mkdir(exist_ok=True) + run(['javac', '--release', '8', '-classpath', android_jar, '-d', classes, source]) + run(['java', '-cp', build_tools / 'lib/d8.jar', 'com.android.tools.r8.D8', '--min-api', '29', '--lib', android_jar, + '--output', dex, *classes.rglob('*.class')]) + manifest = work / 'AndroidManifest.xml' + manifest.write_text(f'') + apk = work / (package + '.apk') + run([build_tools / 'aapt2.exe', 'link', '-I', android_jar, '--manifest', manifest, '-o', apk]) + with zipfile.ZipFile(apk, 'a', zipfile.ZIP_DEFLATED) as archive: + archive.write(dex / 'classes.dex', 'classes.dex') + return apk + + +def run_probe(direct, sysroot, work, apk, package, activity, marker, **options): + """Runs the probe until it prints marker (plus a moment for late log lines); returns its log text.""" + direct = Path(direct).resolve() + command = prepare(apk=apk, activity=f'{package}/{package}.{activity}', sysroot=sysroot, data=work / 'data', + bootstrap=direct / 'art/axrb-bootstrap.dex', native=direct / 'art/lib64', proxies=direct / 'guest/lib64', **options) + log = work / (activity + '.log') + with log.open('wb') as output: + child = subprocess.Popen([str(direct / 'axrb-direct.exe'), *map(str, command)], stdout=output, stderr=subprocess.STDOUT, + creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) + deadline = time.monotonic() + 240 + while child.poll() is None and time.monotonic() < deadline: + if marker in log.read_text(encoding='utf-8', errors='replace'): + time.sleep(2) + break + time.sleep(0.2) + if child.poll() is None: + child.kill() + child.wait() + return log.read_text(encoding='utf-8', errors='replace'), log diff --git a/direct/tools/probe_art.py b/direct/tools/probe_art.py new file mode 100644 index 0000000..0e88519 --- /dev/null +++ b/direct/tools/probe_art.py @@ -0,0 +1,79 @@ +"""Exercise a real ART DEX program in an isolated Direct runtime directory.""" +import argparse +from pathlib import Path +import subprocess +import shutil +import re + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--runner', type=Path, required=True) + p.add_argument('--sysroot', type=Path, required=True) + p.add_argument('--data', type=Path, required=True) + p.add_argument('--log', type=Path, required=True) + p.add_argument('--timeout', type=int, default=120) + p.add_argument('--main', default='ArtSmoke') + p.add_argument('--dex', type=Path, help='DEX containing the selected main class') + p.add_argument('--arg', action='append', default=[], help='Argument for the Java entry point') + p.add_argument('--expect', default='ART_SMOKE_PASS') + p.add_argument('--mount-ro', action='append', default=[]) + p.add_argument('--prop', action='append', default=[]) + p.add_argument('--host-graphics', action='store_true') + p.add_argument('--framework', action='store_true') + p.add_argument('--check-jni', action='store_true') + a = p.parse_args() + # Android rejects writable dynamically loaded DEX files. Put the fixture + # on the runner's read-only system mount, as installed application code is. + fixture = (a.dex or a.data / 'data/local/tmp/classes.dex').resolve() + boot = [f'/apex/com.android.art/javalib/{n}.jar' for n in + ('core-oj', 'core-libart', 'okhttp', 'bouncycastle', 'apache-xml')] + boot += ['/apex/com.android.i18n/javalib/core-icu4j.jar', + '/apex/com.android.conscrypt/javalib/conscrypt.jar'] + if a.framework: + fragments = [a.sysroot / 'system/etc/classpaths/bootclasspath.pb'] + fragments += sorted(a.sysroot.glob('apex/*/etc/classpaths/bootclasspath.pb')) + for fragment in fragments: + for entry in re.findall(rb'/(?:apex|system)/[\w./-]+\.jar', fragment.read_bytes()): + entry = entry.decode('ascii') + if entry not in boot: + boot.append(entry) + args = [str(a.runner.resolve()), '--low-memory', '--sysroot', str(a.sysroot.resolve()), + '--data', str(a.data.resolve()), '--env', + 'LD_LIBRARY_PATH=/apex/com.android.art/lib64:/apex/com.android.os.statsd/lib64:/apex/com.android.runtime/lib64/bionic:/system/lib64', + '/apex/com.android.art/bin/dalvikvm64', '-Xint', '-Xnoimage-dex2oat', + '-Ximage:/nonexistent.art', '-Xbootclasspath:' + ':'.join(boot), + '-cp', '/system/framework/axrb-art-smoke.dex', a.main] + if a.framework: + split = args.index('/apex/com.android.art/bin/dalvikvm64') + args = args[:split] + ['--prop', 'dalvik.vm.usejit=false', '--prop', 'dalvik.vm.usejitprofiles=false', + '--env', 'BOOTCLASSPATH=' + ':'.join(boot), + '--env', 'CLASSPATH=/system/framework/axrb-art-smoke.dex', + '/system/bin/app_process64', '-Xint', '-Xnoimage-dex2oat', + '-Ximage:/nonexistent.art', '/system/bin', a.main] + args += a.arg + if a.check_jni: + executable = '/system/bin/app_process64' if a.framework else '/apex/com.android.art/bin/dalvikvm64' + args.insert(args.index(executable) + 1, '-Xcheck:jni') + if a.host_graphics: + args[1:1] = ['--host-graphics'] + args[1:1] = ['--mount-ro', '/system/framework/axrb-art-smoke.dex=' + str(fixture)] + for mount in a.mount_ro: + args[1:1] = ['--mount-ro', mount] + for prop in a.prop: + args[1:1] = ['--prop', prop] + a.log.parent.mkdir(parents=True, exist_ok=True) + with a.log.open('wb') as log: + try: + result = subprocess.run(args, stdout=log, stderr=subprocess.STDOUT, + timeout=a.timeout, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) + except subprocess.TimeoutExpired: + raise SystemExit(f'ART probe timed out; see {a.log}') + output = a.log.read_text(encoding='utf-8', errors='replace') + print(output[-6000:]) + if result.returncode or a.expect not in output: + raise SystemExit(f'ART probe failed ({result.returncode}); see {a.log}') + + +if __name__ == '__main__': + main() diff --git a/direct/tools/probe_game_art.py b/direct/tools/probe_game_art.py new file mode 100644 index 0000000..087877c --- /dev/null +++ b/direct/tools/probe_game_art.py @@ -0,0 +1,43 @@ +"""Stage an APK into an isolated real-ART session and capture its startup log. + +Activity creation is only a diagnostic milestone, not a successful game test. +""" +import argparse +from pathlib import Path +import subprocess + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + for name in ('runner', 'sysroot', 'data', 'bootstrap', 'apk', 'log'): + p.add_argument('--' + name, type=Path, required=True) + p.add_argument('--activity', required=True) + p.add_argument('--assets', type=Path) + p.add_argument('--timeout', type=int, default=60) + p.add_argument('--proxies', type=Path, required=True) + p.add_argument('--native', type=Path, required=True) + p.add_argument('--runtime', type=Path) + p.add_argument('--check-jni', action='store_true') + a = p.parse_args() + from prepare_art_session import prepare + args = prepare(apk=a.apk, activity=a.activity, sysroot=a.sysroot, data=a.data, + bootstrap=a.bootstrap, native=a.native, proxies=a.proxies, + runtime=a.runtime, assets=a.assets, check_jni=a.check_jni, + diagnostics=True) + a.log.parent.mkdir(parents=True, exist_ok=True) + with a.log.open('wb') as log: + try: + result = subprocess.run([str(a.runner.resolve()), *args], stdout=log, + stderr=subprocess.STDOUT, timeout=a.timeout, + creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) + except subprocess.TimeoutExpired: + raise SystemExit(f'ART probe timed out; see {a.log}') + output = a.log.read_text(encoding='utf-8', errors='replace') + print(output[-6000:]) + if result.returncode or 'AXRB_AOSP_ACTIVITY_CREATED' not in output: + raise SystemExit(f'ART probe failed ({result.returncode}); see {a.log}') + + + +if __name__ == '__main__': + main() diff --git a/direct/tools/test_art.py b/direct/tools/test_art.py new file mode 100644 index 0000000..9f09ef6 --- /dev/null +++ b/direct/tools/test_art.py @@ -0,0 +1,119 @@ +"""Build and run isolated ARM64 ART probes (developer SDK/NDK required). + +The sysroot must be a disposable extraction made with make_sysroot.py --art. +This does not install a launcher or verify that an Android game can launch. +""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import sys + + +def run(args, *, log=None, marker=None, timeout=120): + args = [str(arg) for arg in args] + if log is None: + subprocess.run(args, check=True, timeout=timeout) + return + with log.open('wb') as output: + result = subprocess.run(args, stdout=output, stderr=subprocess.STDOUT, + timeout=timeout, + creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) + text = log.read_text(encoding='utf-8', errors='replace') + if result.returncode or (marker and marker not in text): + print(text[-6000:]) + raise RuntimeError(f'Probe failed ({result.returncode}): {log}') + print(f'PASS: {marker} ({log})', flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--runner', type=Path, required=True) + parser.add_argument('--sysroot', type=Path, required=True) + parser.add_argument('--work', type=Path, required=True) + parser.add_argument('--ndk', type=Path, required=True) + parser.add_argument('--build-tools', type=Path, required=True) + parser.add_argument('--framework', action='store_true', + help='Also require real app_process framework startup (currently needs Binder support)') + parser.add_argument('--host-bridges', type=Path, + help='Staged out/direct directory: test real Vulkan images and OpenSL ES output') + args = parser.parse_args() + runner, root, work = (p.resolve() for p in (args.runner, args.sysroot, args.work)) + clang = args.ndk.resolve() / 'toolchains/llvm/prebuilt/windows-x86_64/bin/clang.exe' + d8 = args.build_tools.resolve() / 'lib/d8.jar' + for file in (runner, clang, d8, root / 'apex/com.android.art/bin/dalvikvm64'): + if not file.is_file(): + parser.error(f'Missing required file: {file}') + javac, java = shutil.which('javac'), shutil.which('java') + if not javac or not java: + parser.error('The developer probes require a JDK on PATH') + source = Path(__file__).resolve().parents[1] / 'tests/art' + classes = work / 'classes' + dex = work / 'data/data/local/tmp' + for directory in (classes, dex): + directory.mkdir(parents=True, exist_ok=True) + run([javac, '--release', '8', '-Xlint:-options', '-d', classes, source / 'ArtSmoke.java']) + run([java, '-cp', d8, 'com.android.tools.r8.D8', '--min-api', '26', + '--output', dex, classes / 'ArtSmoke.class']) + flags = [clang, '--target=aarch64-linux-android29', '-O2', '-Wall', '-Werror'] + run(flags + ['-fPIC', '-shared', source / 'art_smoke_jni.c', '-o', + root / 'system/lib64/libaxrb_art_smoke.so']) + native = work / 'art_linux_selftest' + run(flags + [source / 'art_linux_selftest.c', '-o', native]) + run([runner, '--low-memory', '--sysroot', root, '--data', work / 'data', native], + log=work / 'linux-selftest.log', marker='ART_LINUX_SELFTEST_PASS') + timeout_probe = work / 'timeout_selftest' + run(flags + [source / 'timeout_selftest.c', '-o', timeout_probe]) + run([runner, '--sysroot', root, '--data', work / 'data', timeout_probe], + log=work / 'timeout-selftest.log', marker='TIMEOUT_SELFTEST_PASS', timeout=20) + translator = work / 'translator_selftest' + run(flags + ['-march=armv8.1-a', source / 'translator_selftest.c', '-o', translator]) + run([runner, '--sysroot', root, '--data', work / 'data', translator], + log=work / 'translator-selftest.log', marker='TRANSLATOR_ATOMICS_SELFTEST_PASS') + rounding = work / 'translator_round_selftest' + run(flags + [source / 'translator_round_selftest.c', '-lm', '-o', rounding]) + # Digitalis does not pass yet: FRINTX/FRINTI ignore a non-default FPCR + # rounding mode, and single-precision FRINTA rounds 0.49999997 up. + if os.environ.get('AXRB_DIRECT_TRANSLATOR', 'dynarmic') == 'dynarmic': + run([runner, '--sysroot', root, '--data', work / 'data', rounding], + log=work / 'translator-round-selftest.log', marker='TRANSLATOR_ROUND_SELFTEST_PASS') + for name, marker in (('cond', 'TRANSLATOR_COND_SELFTEST_PASS'), ('tbi', 'TRANSLATOR_TBI_SELFTEST_PASS')): + binary = work / f'translator_{name}_selftest' + run(flags + ['-march=armv8.1-a', source / f'translator_{name}_selftest.c', '-o', binary]) + run([runner, '--sysroot', root, '--data', work / 'data', binary], + log=work / f'translator-{name}-selftest.log', marker=marker) + mapped = work / 'translator_mmap_selftest' + run(flags + [source / 'translator_mmap_selftest.c', '-o', mapped]) + run([runner, '--sysroot', root, '--data', work / 'data', mapped], + log=work / 'translator-mmap-selftest.log', marker='TRANSLATOR_MMAP_SELFTEST_PASS') + binder = work / 'binder_selftest' + run(flags + [source / 'binder_selftest.c', '-o', binder]) + run([runner, '--sysroot', root, '--data', work / 'data', binder], + log=work / 'binder-selftest.log', marker='BINDER_SELFTEST_PASS') + probe = [sys.executable, Path(__file__).with_name('probe_art.py'), '--runner', runner, + '--sysroot', root, '--data', work / 'data'] + run(probe + ['--log', work / 'art-jni-smoke.log'], timeout=150) + if args.framework: + run(probe + ['--framework', '--log', work / 'framework-smoke.log'], timeout=150) + if args.host_bridges: + bridges = args.host_bridges.resolve() + for name, link, marker in [('opensles', 'OpenSLES', 'OPENSL_ES_SELFTEST_PASS'), + ('vulkan_images', 'vulkan', 'VULKAN_IMAGES_SELFTEST_PASS')]: + binary = work / (name + '-selftest') + run(flags + [source / (name + '_selftest.c'), '-l' + link, '-o', binary]) + mounts = ['--mount-ro', '/system/lib64/libaaudio.so=' + str(bridges / 'guest/lib64/libaaudio.so'), + '--mount-ro', '/system/lib64/libvulkan.so=' + str(bridges / 'guest/lib64/libvulkan.so'), + '--mount-ro', '/system/lib64/libOpenSLES.so=' + str(bridges / 'art/lib64/libOpenSLES.so')] + run([runner, '--host-graphics', '--sysroot', root, '--data', work / 'data', *mounts, binary], + log=work / (name + '-selftest.log'), marker=marker, timeout=30) + if name == 'vulkan_images': + run([runner, '--host-graphics', '--sysroot', root, '--data', work / 'data', *mounts, binary, 'khr'], + log=work / (name + '-khr-selftest.log'), marker=marker, timeout=30) + + +if __name__ == '__main__': + try: + main() + except (RuntimeError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: + sys.exit(str(error)) diff --git a/direct/tools/test_audio_devices.py b/direct/tools/test_audio_devices.py new file mode 100644 index 0000000..09a2d31 --- /dev/null +++ b/direct/tools/test_audio_devices.py @@ -0,0 +1,40 @@ +"""Build a probe APK and verify real AOSP audio-device discovery and callbacks. +Developer-only: JDK and Android build tools are needed to compile the fixture. +The tested runtime itself uses only the bundled Direct runtime. +""" +import argparse +from pathlib import Path +import zipfile +from prepare_art_session import prepare +from test_art import run + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('direct', 'sysroot', 'work', 'android-jar', 'build-tools'): + parser.add_argument('--' + name, type=Path, required=True) + args = parser.parse_args() + work = args.work.resolve() + work.mkdir(parents=True, exist_ok=True) + data = work / 'data' + if data.exists(): + parser.error('Use a new work folder for an isolated probe') + classes, dex = work / 'classes', work / 'dex' + classes.mkdir(); dex.mkdir() + source = Path(__file__).resolve().parents[1] / 'tests/art/AudioDeviceProbe.java' + run(['javac', '--release', '8', '-classpath', args.android_jar, '-d', classes, source]) + run(['java', '-cp', args.build_tools / 'lib/d8.jar', 'com.android.tools.r8.D8', '--min-api', '29', '--lib', args.android_jar, '--output', dex, *classes.rglob('*.class')]) + manifest = work / 'AndroidManifest.xml' + manifest.write_text('') + apk = work / 'audio-probe.apk' + run([args.build_tools / 'aapt2.exe', 'link', '-I', args.android_jar, '--manifest', manifest, '-o', apk]) + with zipfile.ZipFile(apk, 'a', zipfile.ZIP_DEFLATED) as archive: + archive.write(dex / 'classes.dex', 'classes.dex') + direct = args.direct.resolve() + command = prepare(apk=apk, activity='com.axrb.audioprobe/com.axrb.audioprobe.AudioDeviceProbe', sysroot=args.sysroot, data=data, + bootstrap=direct / 'art/axrb-bootstrap.dex', native=direct / 'art/lib64', proxies=direct / 'guest/lib64') + run([direct / 'axrb-direct.exe', *command], log=work / 'audio.log', + marker='AUDIO_DEVICE_PROBE_PASS', timeout=120) + +if __name__ == '__main__': + main() diff --git a/direct/tools/test_companion_apks.py b/direct/tools/test_companion_apks.py new file mode 100644 index 0000000..7e825f6 --- /dev/null +++ b/direct/tools/test_companion_apks.py @@ -0,0 +1,26 @@ +"""Compile/run a Direct companion-package metadata and Java code-loading probe.""" +import argparse +from pathlib import Path +from probe_app import build_probe, run_probe + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + for name in ('direct','sysroot','work','android-jar','build-tools'): + parser.add_argument('--'+name,type=Path,required=True) + parser.add_argument('--build-only',action='store_true') + args=parser.parse_args() + source=Path(__file__).resolve().parents[1]/'tests/art' + work=args.work.resolve(); work.mkdir(parents=True,exist_ok=True) + companion=work/'companion'; companion.mkdir(exist_ok=True) + app=work/'app'; app.mkdir(exist_ok=True) + helper=build_probe(companion,args.android_jar,args.build_tools,'com.axrb.companionfixture','CompanionHelper',source/'CompanionHelper.java') + apk=build_probe(app,args.android_jar,args.build_tools,'com.axrb.companionprobe','CompanionApkProbe',source/'CompanionApkProbe.java') + if args.build_only: + print('Companion probe APKs compiled; native execution was not requested.') + return + text,log=run_probe(args.direct,args.sysroot,app,apk,'com.axrb.companionprobe','CompanionApkProbe', + 'COMPANION_APK_PROBE',companion_apks=[helper]) + if 'COMPANION_APK_PROBE visible=true context=true class=true main=true' not in text: + raise SystemExit(f'Companion package runtime probe failed: {log}') + print('COMPANION_APK_RUNTIME_PROBE_PASS') +if __name__=='__main__': main() diff --git a/direct/tools/test_keyboard.py b/direct/tools/test_keyboard.py new file mode 100644 index 0000000..df1a392 --- /dev/null +++ b/direct/tools/test_keyboard.py @@ -0,0 +1,34 @@ +"""Build a probe APK and verify soft-keyboard requests in a Direct session. +Developer-only: JDK and Android build tools are needed to compile the fixture. +Direct mode has no input method: showing the keyboard must report that it was +not shown, as on a device without one, rather than end the app. +""" +import argparse +import re +from pathlib import Path +from probe_app import build_probe, run_probe + +PACKAGE = 'com.axrb.keyboardprobe' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('direct', 'sysroot', 'work', 'android-jar', 'build-tools'): + parser.add_argument('--' + name, type=Path, required=True) + args = parser.parse_args() + work = args.work.resolve() + work.mkdir(parents=True, exist_ok=True) + source = Path(__file__).resolve().parents[1] / 'tests/art/KeyboardProbe.java' + apk = build_probe(work, args.android_jar, args.build_tools, PACKAGE, 'KeyboardProbe', source) + text, log = run_probe(args.direct, args.sysroot, work, apk, PACKAGE, 'KeyboardProbe', 'KEYBOARD_PROBE') + result = re.search(r'KEYBOARD_PROBE (shown=\w+ hidden=\w+)', text) + # Hiding is answered by the app's own InputMethodManager; either result is fine. + if not result or not result.group(1).startswith('shown=false ') or 'AXRB_SERVICE_UNIMPLEMENTED' in text: + unimplemented = re.findall(r'AXRB_SERVICE_UNIMPLEMENTED (\S+)', text) + raise SystemExit(f'KEYBOARD_SELFTEST_FAIL: {result.group(1) if result else "no probe result"} ' + f'unimplemented={unimplemented} ({log})') + print('KEYBOARD_SELFTEST_PASS', result.group(1)) + + +if __name__ == '__main__': + main() diff --git a/direct/tools/test_legacy_driver.py b/direct/tools/test_legacy_driver.py new file mode 100644 index 0000000..6e4b388 --- /dev/null +++ b/direct/tools/test_legacy_driver.py @@ -0,0 +1,36 @@ +"""Build a probe APK and verify Direct mode's legacy Oculus driver package. +Developer-only: JDK and Android build tools are needed to compile the fixture. +The probe loads AXRB's runtime the way older Oculus OpenXR loaders do, through +com.oculus.systemdriver, with the Quest identity and controller-only policy on. +""" +import argparse +import re +from pathlib import Path +from probe_app import build_probe, run_probe + +PACKAGE = 'com.axrb.legacyprobe' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('direct', 'sysroot', 'work', 'android-jar', 'build-tools'): + parser.add_argument('--' + name, type=Path, required=True) + args = parser.parse_args() + work = args.work.resolve() + work.mkdir(parents=True, exist_ok=True) + source = Path(__file__).resolve().parents[1] / 'tests/art/LegacyDriverProbe.java' + apk = build_probe(work, args.android_jar, args.build_tools, PACKAGE, 'LegacyDriverProbe', source) + text, log = run_probe(args.direct, args.sysroot, work, apk, PACKAGE, 'LegacyDriverProbe', 'LEGACY_DRIVER_PROBE', + legacy_oculus_driver=args.direct.resolve() / 'guest/axrb-systemdriver.apk', + quest_identity=True, controllers_only=True) + result = re.search(r'LEGACY_DRIVER_PROBE (manufacturer=.*)', text) + expected = 'manufacturer=Oculus brand=oculus model=Quest 3 gipa=true negotiate=true initialize=true missing=false' + if not result or result.group(1).strip() != expected: + raise SystemExit(f'LEGACY_DRIVER_SELFTEST_FAIL: {result.group(1).strip() if result else "no probe result"} ({log})') + if 'controller-only compatibility=1' not in text: + raise SystemExit(f'LEGACY_DRIVER_SELFTEST_FAIL: the controller-only policy did not reach the driver ({log})') + print('LEGACY_DRIVER_SELFTEST_PASS', result.group(1).strip()) + + +if __name__ == '__main__': + main() diff --git a/direct/tools/test_media_codec.py b/direct/tools/test_media_codec.py new file mode 100644 index 0000000..d00e237 --- /dev/null +++ b/direct/tools/test_media_codec.py @@ -0,0 +1,54 @@ +"""Build a probe APK and verify AMediaExtractor/AMediaCodec on the host's Media +Foundation backend: an H.264 + AAC MP4 asset, decoded synchronously and with +asynchronous callbacks. Developer-only: JDK, Android build tools, the NDK and +ffmpeg (for the fixture) are needed. The tested runtime is the staged out/direct. +""" +import argparse +from pathlib import Path +import zipfile +from prepare_art_session import prepare +from test_art import run + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('direct', 'sysroot', 'work', 'android-jar', 'build-tools', 'ndk', 'ffmpeg'): + parser.add_argument('--' + name, type=Path, required=True) + args = parser.parse_args() + work = args.work.resolve() + work.mkdir(parents=True, exist_ok=True) + data = work / 'data' + if data.exists(): + parser.error('Use a new work folder for an isolated probe') + tests = Path(__file__).resolve().parents[1] / 'tests/art' + # One second: 30 frames of 320x240 H.264 and 44.1 kHz stereo AAC. + clip = work / 'probe.mp4' + run([args.ffmpeg, '-hide_banner', '-loglevel', 'error', '-y', '-f', 'lavfi', '-i', 'testsrc=size=320x240:rate=30:duration=1', + '-f', 'lavfi', '-i', 'sine=frequency=1000:sample_rate=44100:duration=1', '-c:v', 'libx264', '-profile:v', 'baseline', + '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-ac', '2', '-shortest', '-movflags', '+faststart', clip]) + native = work / 'libmediaprobe.so' + clang = args.ndk / 'toolchains/llvm/prebuilt/windows-x86_64/bin/clang.exe' + run([clang, '--target=aarch64-linux-android29', '-O2', '-Wall', '-Werror', '-fPIC', '-shared', tests / 'media_codec_probe.c', + '-lmediandk', '-landroid', '-o', native]) + classes, dex = work / 'classes', work / 'dex' + classes.mkdir(); dex.mkdir() + run(['javac', '--release', '8', '-classpath', args.android_jar, '-d', classes, tests / 'MediaCodecProbe.java']) + run(['java', '-cp', args.build_tools / 'lib/d8.jar', 'com.android.tools.r8.D8', '--min-api', '29', '--lib', args.android_jar, + '--output', dex, *classes.rglob('*.class')]) + manifest = work / 'AndroidManifest.xml' + manifest.write_text('') + apk = work / 'media-probe.apk' + run([args.build_tools / 'aapt2.exe', 'link', '-I', args.android_jar, '--manifest', manifest, '-o', apk]) + with zipfile.ZipFile(apk, 'a') as archive: + archive.write(dex / 'classes.dex', 'classes.dex', zipfile.ZIP_DEFLATED) + archive.write(native, 'lib/arm64-v8a/libmediaprobe.so', zipfile.ZIP_DEFLATED) + # Uncompressed, so AAsset_openFileDescriptor can hand out the APK itself. + archive.write(clip, 'assets/probe.mp4', zipfile.ZIP_STORED) + direct = args.direct.resolve() + command = prepare(apk=apk, activity='com.axrb.mediaprobe/com.axrb.mediaprobe.MediaCodecProbe', sysroot=args.sysroot, data=data, + bootstrap=direct / 'art/axrb-bootstrap.dex', native=direct / 'art/lib64', proxies=direct / 'guest/lib64') + run([direct / 'axrb-direct.exe', *command], log=work / 'media.log', marker='MEDIA_CODEC_PROBE_PASS', timeout=180) + + +if __name__ == '__main__': + main() diff --git a/direct/tools/test_permissions.py b/direct/tools/test_permissions.py new file mode 100644 index 0000000..798e11b --- /dev/null +++ b/direct/tools/test_permissions.py @@ -0,0 +1,40 @@ +"""Build a probe APK and verify real AOSP permission-request callbacks. +Developer-only: JDK and Android build tools are needed to compile the fixture. +The tested runtime itself uses only the bundled Direct runtime. +""" +import argparse +from pathlib import Path +import zipfile +from prepare_art_session import prepare +from test_art import run + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('direct', 'sysroot', 'work', 'android-jar', 'build-tools'): + parser.add_argument('--' + name, type=Path, required=True) + args = parser.parse_args() + work = args.work.resolve() + work.mkdir(parents=True, exist_ok=True) + data = work / 'data' + if data.exists(): + parser.error('Use a new work folder for an isolated probe') + classes, dex = work / 'classes', work / 'dex' + classes.mkdir(); dex.mkdir() + source = Path(__file__).resolve().parents[1] / 'tests/art/PermissionProbe.java' + run(['javac', '--release', '8', '-classpath', args.android_jar, '-d', classes, source]) + run(['java', '-cp', args.build_tools / 'lib/d8.jar', 'com.android.tools.r8.D8', '--min-api', '29', '--lib', args.android_jar, '--output', dex, *classes.rglob('*.class')]) + manifest = work / 'AndroidManifest.xml' + manifest.write_text('') + apk = work / 'permission-probe.apk' + run([args.build_tools / 'aapt2.exe', 'link', '-I', args.android_jar, '--manifest', manifest, '-o', apk]) + with zipfile.ZipFile(apk, 'a', zipfile.ZIP_DEFLATED) as archive: + archive.write(dex / 'classes.dex', 'classes.dex') + direct = args.direct.resolve() + command = prepare(apk=apk, activity='com.axrb.permissionprobe/com.axrb.permissionprobe.PermissionProbe', sysroot=args.sysroot, data=data, + bootstrap=direct / 'art/axrb-bootstrap.dex', native=direct / 'art/lib64', proxies=direct / 'guest/lib64') + run([direct / 'axrb-direct.exe', *command], log=work / 'permission.log', + marker='PERMISSION_PROBE_PASS', timeout=120) + +if __name__ == '__main__': + main() diff --git a/direct/tools/test_storage.py b/direct/tools/test_storage.py new file mode 100644 index 0000000..4877dfb --- /dev/null +++ b/direct/tools/test_storage.py @@ -0,0 +1,89 @@ +"""Build a probe APK and verify real AOSP saves across process restarts. +Developer-only: JDK and Android build tools are needed to compile the fixture. +The tested runtime itself uses only the bundled Direct runtime. +""" +import argparse +import ctypes +import os +import time +import uuid +import json +from pathlib import Path +import subprocess +import zipfile +from prepare_art_session import prepare +from test_art import run + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('direct', 'sysroot', 'work', 'android-jar', 'build-tools'): + parser.add_argument('--' + name, type=Path, required=True) + args = parser.parse_args() + work = args.work.resolve() + work.mkdir(parents=True, exist_ok=True) + data = work / 'data' + if data.exists(): + parser.error('Use a new work folder so the write test starts with empty app storage') + classes, dex = work / 'classes', work / 'dex' + classes.mkdir(); dex.mkdir() + source = Path(__file__).resolve().parents[1] / 'tests/art/StorageProbe.java' + run(['javac', '--release', '8', '-classpath', args.android_jar, '-d', classes, source]) + run(['java', '-cp', args.build_tools / 'lib/d8.jar', 'com.android.tools.r8.D8', '--min-api', '29', '--lib', args.android_jar, '--output', dex, *classes.rglob('*.class')]) + manifest = work / 'AndroidManifest.xml' + manifest.write_text('') + apk = work / 'storage-probe.apk' + run([args.build_tools / 'aapt2.exe', 'link', '-I', args.android_jar, '--manifest', manifest, '-o', apk]) + with zipfile.ZipFile(apk, 'a', zipfile.ZIP_DEFLATED) as archive: + archive.write(dex / 'classes.dex', 'classes.dex') + direct = args.direct.resolve() + command = prepare(apk=apk, activity='com.axrb.storageprobe/com.axrb.storageprobe.StorageProbe', sysroot=args.sysroot, data=data, + bootstrap=direct / 'art/axrb-bootstrap.dex', native=direct / 'art/lib64', proxies=direct / 'guest/lib64') + for phase in ('write', 'stop', 'read'): + if phase == 'stop': + # Exercise exactly the named stop event used by run_direct_game.ps1. + kernel = ctypes.WinDLL('kernel32', use_last_error=True) + kernel.CreateEventW.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_wchar_p] + kernel.CreateEventW.restype = ctypes.c_void_p + kernel.SetEvent.argtypes = [ctypes.c_void_p] + kernel.CloseHandle.argtypes = [ctypes.c_void_p] + event_name = 'Local\\AXRB.DirectStop.' + uuid.uuid4().hex + event = kernel.CreateEventW(None, 1, 0, event_name) + if not event: + raise ctypes.WinError(ctypes.get_last_error()) + log = work / 'stop.log' + child = None + try: + with log.open('wb') as output: + child = subprocess.Popen([str(direct / 'axrb-direct.exe'), '--env', 'AXRB_STORAGE_TEST_PHASE=stop', *map(str, command)], + env={**os.environ, 'AXRB_DIRECT_STOP_EVENT': event_name}, stdout=output, stderr=subprocess.STDOUT, + creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) + deadline = time.monotonic() + 240 + while 'STORAGE_PROBE_READY_FOR_STOP' not in log.read_text(encoding='utf-8', errors='replace'): + if child.poll() is not None or time.monotonic() > deadline: + raise RuntimeError('Storage lifecycle startup failed: ' + str(log)) + time.sleep(0.1) + if not kernel.SetEvent(event): + raise ctypes.WinError(ctypes.get_last_error()) + assert child.wait(timeout=25) == 0, log + transcript = log.read_text(encoding='utf-8', errors='replace') + assert 'STORAGE_PROBE_ASYNC_SAVE_QUEUED' in transcript and 'AXRB_AOSP_SESSION_CLOSED' in transcript, transcript[-2000:] + print('PASS: graceful onStop and session shutdown', flush=True) + finally: + if child and child.poll() is None: + child.kill(); child.wait() + kernel.CloseHandle(event) + continue + run([direct / 'axrb-direct.exe', '--env', 'AXRB_STORAGE_TEST_PHASE=' + phase, *command], + log=work / (phase + '.log'), marker='STORAGE_PROBE_' + phase.upper() + '_PASS', timeout=240) + saves = list(data.rglob('save.json')) + list(data.rglob('external-save.txt')) + assert len(saves) == 2, saves + for save in saves: + assert save.read_text(encoding='utf-8') == 'AXRB save: \u00e4 \u65e5\u672c 12345' + report = {'passed': True, 'tests': ['private file', 'UTF-8', 'fsync', 'rename', 'SharedPreferences', 'onStop asynchronous apply and graceful shutdown', 'SQLite WAL transaction', 'external app file', 'cache write', 'delete', 'process restart', 'Windows file verification'], 'windowsSaves': [str(p) for p in saves]} + (work / 'report.json').write_text(json.dumps(report, indent=2)) + print('STORAGE_PERSISTENCE_PASS', flush=True) + + +if __name__ == '__main__': + main() diff --git a/direct/tools/unbundle.py b/direct/tools/unbundle.py new file mode 100644 index 0000000..4ea82a3 --- /dev/null +++ b/direct/tools/unbundle.py @@ -0,0 +1,33 @@ +"""Splits a bundle of source files ("=== relative/path" header lines) into files. + +Usage: unbundle.py +""" +import os +import sys + + +def main(): + bundle, root = sys.argv[1], sys.argv[2] + path, lines = None, [] + + def flush(): + if path is None: + return + target = os.path.join(root, path) + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, "w", encoding="utf-8", newline="\n") as f: + f.write("".join(lines).strip("\n") + "\n") + print("wrote", path) + + with open(bundle, encoding="utf-8") as f: + for line in f: + if line.startswith("=== "): + flush() + path, lines = line[4:].strip(), [] + else: + lines.append(line) + flush() + + +if __name__ == "__main__": + main() diff --git a/direct/vm/CMakeLists.txt b/direct/vm/CMakeLists.txt new file mode 100644 index 0000000..c9bc543 --- /dev/null +++ b/direct/vm/CMakeLists.txt @@ -0,0 +1,19 @@ +# The managed side of an Android app: Androitainer's DEX interpreter, core +# library and framework subset (snapshot, see SNAPSHOT.md), built as static +# libraries for axrb-direct. +set(AXRB_VM_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_SAVED "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") +if(NOT CMAKE_RUNTIME_OUTPUT_DIRECTORY) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/vm") +endif() + +add_compile_options(/utf-8 /EHsc /GR /W3 /wd4100 /wd4244 /wd4267 /wd4146) +add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS) + +add_subdirectory(diagnostics) +add_subdirectory(vfs) +add_subdirectory(package) +add_subdirectory(manifest) +add_subdirectory(resources) +add_subdirectory(managed) +add_subdirectory(framework-host) diff --git a/direct/vm/SNAPSHOT.md b/direct/vm/SNAPSHOT.md new file mode 100644 index 0000000..3926b0f --- /dev/null +++ b/direct/vm/SNAPSHOT.md @@ -0,0 +1,17 @@ +# Androitainer snapshot + +`direct/vm` is a copy of the managed runtime and framework modules of +Androitainer (`../androitainer`), taken from commit +`eeb1963eb1765aa4d54e2e878ae4d53a9441a2cd` plus that tree's uncommitted work as +of 2026-10-02: + +| here | Androitainer | +|---|---| +| `managed/` | `src/managed` (DEX interpreter, core library, framework Java) | +| `diagnostics/`, `package/`, `manifest/`, `resources/`, `framework-host/` | `src/` | +| `vfs/` | `src/vfs` (re-implemented on AXRB Direct's guest file system) | +| `tools/` | `tools/build-bootclasspath.ps1`, `build-framework-res.ps1`, `toolchain.json` | + +`package/src/mapped_file.cc` is `MappedFile` from Androitainer's `src/elf`; the +rest of the ELF loader is not used (AXRB Direct runs the Android linker itself). +Changes made here for AXRB Direct are listed in the git history of this directory. diff --git a/direct/vm/diagnostics/CMakeLists.txt b/direct/vm/diagnostics/CMakeLists.txt new file mode 100644 index 0000000..8374e67 --- /dev/null +++ b/direct/vm/diagnostics/CMakeLists.txt @@ -0,0 +1,5 @@ +add_library(arun_diagnostics STATIC + src/status.cc + src/log.cc +) +target_include_directories(arun_diagnostics PUBLIC include) diff --git a/direct/vm/diagnostics/include/arun/log.h b/direct/vm/diagnostics/include/arun/log.h new file mode 100644 index 0000000..0f685d3 --- /dev/null +++ b/direct/vm/diagnostics/include/arun/log.h @@ -0,0 +1,65 @@ +// Ownership: global sink, process-wide. +// Thread affinity: all functions are thread-safe (a mutex serialises the sink). +// Error propagation: logging never fails and never throws. +// Pointers: formatted values only; no host/guest pointer ownership implied. +// +// Categories exist so hot paths (spec 22: "do not flood hot paths by default") +// can be silenced independently of control-plane logging. +#pragma once + +#include +#include +#include +#include + +namespace arun::log { + +enum class Level : int { kTrace = 0, kDebug = 1, kInfo = 2, kWarn = 3, kError = 4, kOff = 5 }; + +enum class Category : std::uint32_t { + kGeneral, + kInstall, + kManifest, + kVfs, // hot + kMemory, // hot + kElf, + kAbi, // hot + kThreads, + kRuntime, // ART + kJni, // hot + kLifecycle, + kGraphics, // hot + kAudio, // hot + kInput, // hot + kSecurity, + kCount, +}; + +const char* CategoryName(Category c); + +// Default: kInfo for control-plane categories, kOff for the hot ones above. +void SetLevel(Category category, Level level); +void SetGlobalLevel(Level level); +bool Enabled(Category category, Level level); + +// Writes to stderr, plus the session log file when one is open. +void Emit(Category category, Level level, std::string_view message); + +// Opens an additional file sink. Returns false if the file cannot be created; +// logging continues to stderr in that case. +bool OpenFileSink(const std::wstring& path); +void CloseFileSink(); + +template +void Write(Category category, Level level, std::format_string fmt, Args&&... args) { + if (!Enabled(category, level)) return; + Emit(category, level, std::format(fmt, std::forward(args)...)); +} + +} // namespace arun::log + +#define ARUN_LOG_TRACE(cat, ...) ::arun::log::Write((cat), ::arun::log::Level::kTrace, __VA_ARGS__) +#define ARUN_LOG_DEBUG(cat, ...) ::arun::log::Write((cat), ::arun::log::Level::kDebug, __VA_ARGS__) +#define ARUN_LOG_INFO(cat, ...) ::arun::log::Write((cat), ::arun::log::Level::kInfo, __VA_ARGS__) +#define ARUN_LOG_WARN(cat, ...) ::arun::log::Write((cat), ::arun::log::Level::kWarn, __VA_ARGS__) +#define ARUN_LOG_ERROR(cat, ...) ::arun::log::Write((cat), ::arun::log::Level::kError, __VA_ARGS__) diff --git a/direct/vm/diagnostics/include/arun/status.h b/direct/vm/diagnostics/include/arun/status.h new file mode 100644 index 0000000..a83da58 --- /dev/null +++ b/direct/vm/diagnostics/include/arun/status.h @@ -0,0 +1,161 @@ +// Ownership: value type, no allocation beyond the message string. +// Thread affinity: none (immutable after construction). +// Error propagation: every fallible internal API returns Status or Result. +// Pointers: none held. +// +// Reason codes are stable, machine-readable identifiers. The UI and the JSON CLI +// surface (spec 21.1) must switch on `ReasonCode`, never on the English message. +#pragma once + +#include +#include +#include +#include +#include + +namespace arun { + +// Stable numeric values: they appear in JSON output and process exit codes. +// Never renumber an existing entry; append only. +enum class ReasonCode : std::uint32_t { + kOk = 0, + + // Generic + kInvalidArgument = 1, + kNotImplemented = 2, // honestly unimplemented; never returned as success + kUnsupported = 3, // understood, deliberately out of profile + kInternal = 4, + kNotFound = 5, + kAlreadyExists = 6, + kPermissionDenied = 7, + kResourceExhausted = 8, + kBusy = 9, + + // Host OS + kHostOsError = 20, + kHostMemoryError = 21, + + // ELF loader (spec 8) + kElfMalformed = 40, + kElfUnsupportedClass = 41, // not ELF64 little-endian x86-64 + kElfUnsupportedType = 42, // not ET_DYN + kElfUnsupportedRelocation = 43, + kElfUnresolvedSymbol = 44, + kElfMissingDependency = 45, + kElfTextRelocation = 46, + kElfExecutableStack = 47, + kElfBoundsViolation = 48, + kElfDuplicateLoad = 49, + + // Guest memory (spec 10.4) + kMemReservationFailed = 60, + kMemCommitFailed = 61, + kMemProtectFailed = 62, + kMemRegionOverlap = 63, + kMemBadAlignment = 64, + + // Guest ABI / execution (spec 9, 10) + kAbiUnsupportedSignature = 80, + kAbiThunkFailure = 81, + kTlsUnavailable = 82, + kGuestFault = 83, + + // Package / manifest (spec 13) + kPackageMalformed = 100, + kPackageSignatureInvalid = 101, + kPackageAbiIncompatible = 102, + kPackageUnsupportedContainer = 103, + kManifestMalformed = 104, // binary AndroidManifest (AXML) or resources.arsc + kManifestNoLauncher = 105, // no MAIN/LAUNCHER activity to start + + // Managed runtime (DEX interpreter, ADR 0004) + kDexMalformed = 120, // structurally invalid DEX; rejected before execution + kDexUnsupported = 121, // valid DEX using a format/opcode this runtime lacks + kVmClassNotFound = 122, + kVmLinkageError = 123, // unresolvable method/field, incompatible change + kVmUncaughtException = 124, // a Java exception escaped to the host boundary + kVmStackOverflow = 125, + kVmOutOfMemory = 126, +}; + +const char* ReasonCodeName(ReasonCode code); + +class [[nodiscard]] Status { + public: + Status() = default; // ok + Status(ReasonCode code, std::string message) + : code_(code), message_(std::move(message)) {} + + static Status Ok() { return Status{}; } + + bool ok() const { return code_ == ReasonCode::kOk; } + explicit operator bool() const { return ok(); } + + ReasonCode code() const { return code_; } + const std::string& message() const { return message_; } + + // "kElfMalformed: " — for logs, never for programmatic dispatch. + std::string ToString() const; + + // Adds outer context while preserving the original reason code. + Status WithContext(std::string_view context) const; + + private: + ReasonCode code_ = ReasonCode::kOk; + std::string message_; +}; + +// Convenience constructors. +Status Err(ReasonCode code, std::string message); +// Captures GetLastError() at the call site. +Status Win32Err(std::string_view what, unsigned long last_error); + +template +class [[nodiscard]] Result { + public: + Result(T value) : value_(std::move(value)) {} + Result(Status status) : status_(std::move(status)) { + // An ok Status carries no value; that is a programming error. + } + + bool ok() const { return status_.ok() && value_.has_value(); } + explicit operator bool() const { return ok(); } + + const Status& status() const { return status_; } + ReasonCode code() const { return status_.code(); } + + T& operator*() { return *value_; } + const T& operator*() const { return *value_; } + T* operator->() { return &*value_; } + const T* operator->() const { return &*value_; } + + T value_or(T fallback) const { return value_ ? *value_ : std::move(fallback); } + + private: + Status status_; + std::optional value_; +}; + +} // namespace arun + +// Propagate an error out of the current function. +#define ARUN_RETURN_IF_ERROR(expr) \ + do { \ + ::arun::Status _arun_st = (expr); \ + if (!_arun_st.ok()) return _arun_st; \ + } while (0) + +// Two levels of indirection are required: `a##__LINE__` pastes the literal text +// "__LINE__" instead of the line number, which silently collapses every use in a +// function to one name. +#define ARUN_CONCAT_INNER(a, b) a##b +#define ARUN_CONCAT(a, b) ARUN_CONCAT_INNER(a, b) + +#define ARUN_ASSIGN_OR_RETURN_IMPL(tmp, decl, expr) \ + auto tmp = (expr); \ + if (!(tmp).ok()) return (tmp).status(); \ + decl = std::move(*(tmp)) + +// Assign or propagate. `decl` is the full declaration, e.g. `auto x`. +#define ARUN_ASSIGN_OR_RETURN(decl, expr) \ + ARUN_ASSIGN_OR_RETURN_IMPL(ARUN_CONCAT(_arun_r_, __LINE__), decl, expr) diff --git a/direct/vm/diagnostics/src/log.cc b/direct/vm/diagnostics/src/log.cc new file mode 100644 index 0000000..95ad1eb --- /dev/null +++ b/direct/vm/diagnostics/src/log.cc @@ -0,0 +1,151 @@ +#include "arun/log.h" + +#include + +#include +#include +#include +#include + +namespace arun::log { +namespace { + +constexpr std::size_t kCategoryCount = static_cast(Category::kCount); + +// Hot-path categories start silenced (spec 22). +constexpr Level kDefaultLevel(Category c) { + switch (c) { + case Category::kVfs: + case Category::kMemory: + case Category::kAbi: + case Category::kJni: + case Category::kGraphics: + case Category::kAudio: + case Category::kInput: + return Level::kWarn; + default: + return Level::kInfo; + } +} + +struct State { + std::mutex mutex; + std::array levels{}; + HANDLE file = INVALID_HANDLE_VALUE; + + State() { + for (std::size_t i = 0; i < kCategoryCount; ++i) { + levels[i] = kDefaultLevel(static_cast(i)); + } + } +}; + +State& state() { + static State s; + return s; +} + +const char* LevelName(Level l) { + switch (l) { + case Level::kTrace: return "TRACE"; + case Level::kDebug: return "DEBUG"; + case Level::kInfo: return "INFO "; + case Level::kWarn: return "WARN "; + case Level::kError: return "ERROR"; + case Level::kOff: return "OFF "; + } + return "?????"; +} + +} // namespace + +const char* CategoryName(Category c) { + switch (c) { + case Category::kGeneral: return "general"; + case Category::kInstall: return "install"; + case Category::kManifest: return "manifest"; + case Category::kVfs: return "vfs"; + case Category::kMemory: return "memory"; + case Category::kElf: return "elf"; + case Category::kAbi: return "abi"; + case Category::kThreads: return "threads"; + case Category::kRuntime: return "runtime"; + case Category::kJni: return "jni"; + case Category::kLifecycle: return "lifecycle"; + case Category::kGraphics: return "graphics"; + case Category::kAudio: return "audio"; + case Category::kInput: return "input"; + case Category::kSecurity: return "security"; + case Category::kCount: break; + } + return "unknown"; +} + +void SetLevel(Category category, Level level) { + State& s = state(); + std::lock_guard lock(s.mutex); + s.levels[static_cast(category)] = level; +} + +void SetGlobalLevel(Level level) { + State& s = state(); + std::lock_guard lock(s.mutex); + s.levels.fill(level); +} + +bool Enabled(Category category, Level level) { + State& s = state(); + std::lock_guard lock(s.mutex); + return static_cast(level) >= static_cast(s.levels[static_cast(category)]); +} + +void Emit(Category category, Level level, std::string_view message) { + using namespace std::chrono; + const auto now = system_clock::now(); + const auto ms = duration_cast(now.time_since_epoch()) % 1000; + const std::time_t t = system_clock::to_time_t(now); + std::tm tm{}; + localtime_s(&tm, &t); + + char line[4096]; + const int n = std::snprintf( + line, sizeof(line), "%02d:%02d:%02d.%03lld [%s] %-9s tid=%lu %.*s\n", + tm.tm_hour, tm.tm_min, tm.tm_sec, static_cast(ms.count()), + LevelName(level), CategoryName(category), GetCurrentThreadId(), + static_cast(message.size()), message.data()); + if (n <= 0) return; + const std::size_t len = (static_cast(n) < sizeof(line)) + ? static_cast(n) + : sizeof(line) - 1; + + State& s = state(); + std::lock_guard lock(s.mutex); + DWORD written = 0; + WriteFile(GetStdHandle(STD_ERROR_HANDLE), line, static_cast(len), &written, nullptr); + if (s.file != INVALID_HANDLE_VALUE) { + WriteFile(s.file, line, static_cast(len), &written, nullptr); + } +} + +bool OpenFileSink(const std::wstring& path) { + HANDLE h = CreateFileW(path.c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (h == INVALID_HANDLE_VALUE) return false; + State& s = state(); + std::lock_guard lock(s.mutex); + if (s.file != INVALID_HANDLE_VALUE) CloseHandle(s.file); + s.file = h; + return true; +} + +void CloseFileSink() { + State& s = state(); + std::lock_guard lock(s.mutex); + if (s.file != INVALID_HANDLE_VALUE) { + CloseHandle(s.file); + s.file = INVALID_HANDLE_VALUE; + } +} + +} // namespace arun::log diff --git a/direct/vm/diagnostics/src/status.cc b/direct/vm/diagnostics/src/status.cc new file mode 100644 index 0000000..dbbd62c --- /dev/null +++ b/direct/vm/diagnostics/src/status.cc @@ -0,0 +1,122 @@ +#include "arun/status.h" + +#include + +#include +#include + +namespace arun { +namespace { + +struct Entry { + ReasonCode code; + const char* name; +}; + +constexpr std::array kNames{ + Entry{ReasonCode::kOk, "ok"}, + Entry{ReasonCode::kInvalidArgument, "invalid_argument"}, + Entry{ReasonCode::kNotImplemented, "not_implemented"}, + Entry{ReasonCode::kUnsupported, "unsupported"}, + Entry{ReasonCode::kInternal, "internal"}, + Entry{ReasonCode::kNotFound, "not_found"}, + Entry{ReasonCode::kAlreadyExists, "already_exists"}, + Entry{ReasonCode::kPermissionDenied, "permission_denied"}, + Entry{ReasonCode::kResourceExhausted, "resource_exhausted"}, + Entry{ReasonCode::kBusy, "busy"}, + Entry{ReasonCode::kHostOsError, "host_os_error"}, + Entry{ReasonCode::kHostMemoryError, "host_memory_error"}, + Entry{ReasonCode::kElfMalformed, "elf_malformed"}, + Entry{ReasonCode::kElfUnsupportedClass, "elf_unsupported_class"}, + Entry{ReasonCode::kElfUnsupportedType, "elf_unsupported_type"}, + Entry{ReasonCode::kElfUnsupportedRelocation, "elf_unsupported_relocation"}, + Entry{ReasonCode::kElfUnresolvedSymbol, "elf_unresolved_symbol"}, + Entry{ReasonCode::kElfMissingDependency, "elf_missing_dependency"}, + Entry{ReasonCode::kElfTextRelocation, "elf_text_relocation"}, + Entry{ReasonCode::kElfExecutableStack, "elf_executable_stack"}, + Entry{ReasonCode::kElfBoundsViolation, "elf_bounds_violation"}, + Entry{ReasonCode::kElfDuplicateLoad, "elf_duplicate_load"}, + Entry{ReasonCode::kMemReservationFailed, "mem_reservation_failed"}, + Entry{ReasonCode::kMemCommitFailed, "mem_commit_failed"}, + Entry{ReasonCode::kMemProtectFailed, "mem_protect_failed"}, + Entry{ReasonCode::kMemRegionOverlap, "mem_region_overlap"}, + Entry{ReasonCode::kMemBadAlignment, "mem_bad_alignment"}, + Entry{ReasonCode::kAbiUnsupportedSignature, "abi_unsupported_signature"}, + Entry{ReasonCode::kAbiThunkFailure, "abi_thunk_failure"}, + Entry{ReasonCode::kTlsUnavailable, "tls_unavailable"}, + Entry{ReasonCode::kGuestFault, "guest_fault"}, + Entry{ReasonCode::kPackageMalformed, "package_malformed"}, + Entry{ReasonCode::kPackageSignatureInvalid, "package_signature_invalid"}, + Entry{ReasonCode::kPackageAbiIncompatible, "package_abi_incompatible"}, + Entry{ReasonCode::kPackageUnsupportedContainer, "package_unsupported_container"}, + Entry{ReasonCode::kManifestMalformed, "manifest_malformed"}, + Entry{ReasonCode::kManifestNoLauncher, "manifest_no_launcher"}, + Entry{ReasonCode::kDexMalformed, "dex_malformed"}, + Entry{ReasonCode::kDexUnsupported, "dex_unsupported"}, + Entry{ReasonCode::kVmClassNotFound, "vm_class_not_found"}, + Entry{ReasonCode::kVmLinkageError, "vm_linkage_error"}, + Entry{ReasonCode::kVmUncaughtException, "vm_uncaught_exception"}, + Entry{ReasonCode::kVmStackOverflow, "vm_stack_overflow"}, + Entry{ReasonCode::kVmOutOfMemory, "vm_out_of_memory"}, +}; + +} // namespace + +const char* ReasonCodeName(ReasonCode code) { + for (const Entry& e : kNames) { + if (e.code == code) return e.name; + } + return "unknown_reason_code"; +} + +std::string Status::ToString() const { + if (ok()) return "ok"; + std::string out = ReasonCodeName(code_); + out += ": "; + out += message_; + return out; +} + +Status Status::WithContext(std::string_view context) const { + if (ok()) return *this; + std::string combined(context); + combined += ": "; + combined += message_; + return Status(code_, std::move(combined)); +} + +Status Err(ReasonCode code, std::string message) { + return Status(code, std::move(message)); +} + +Status Win32Err(std::string_view what, unsigned long last_error) { + char* buffer = nullptr; + const DWORD len = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buffer), 0, nullptr); + + std::string detail; + if (len != 0 && buffer != nullptr) { + detail.assign(buffer, len); + while (!detail.empty() && + (detail.back() == '\n' || detail.back() == '\r' || detail.back() == '.')) { + detail.pop_back(); + } + } + if (buffer != nullptr) LocalFree(buffer); + + char prefix[64]; + std::snprintf(prefix, sizeof(prefix), " (win32 error %lu)", last_error); + + std::string message(what); + if (!detail.empty()) { + message += ": "; + message += detail; + } + message += prefix; + return Status(ReasonCode::kHostOsError, std::move(message)); +} + +} // namespace arun diff --git a/direct/vm/framework-host/CMakeLists.txt b/direct/vm/framework-host/CMakeLists.txt new file mode 100644 index 0000000..eb44193 --- /dev/null +++ b/direct/vm/framework-host/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(arun_framework_host STATIC + src/framework_host.cc + src/render.cc + src/resources_host.cc + src/graphics_natives.cc +) +target_include_directories(arun_framework_host PUBLIC include PRIVATE src) +# d2d1/dwrite: rendering and text. windowscodecs/ole32: image decoding/encoding (WIC). +# gdi32/user32: the window. +target_link_libraries(arun_framework_host PUBLIC arun_managed arun_package arun_resources + PRIVATE user32.lib gdi32.lib ole32.lib windowscodecs.lib d2d1.lib dwrite.lib) diff --git a/direct/vm/framework-host/include/arun/framework_host.h b/direct/vm/framework-host/include/arun/framework_host.h new file mode 100644 index 0000000..3889389 --- /dev/null +++ b/direct/vm/framework-host/include/arun/framework_host.h @@ -0,0 +1,88 @@ +// Host side of the in-tree Android framework (ADR 0004): the natives behind +// android.os.MessageQueue, SystemClock, Process, util.Log, content.res +// (assets, display metrics), graphics.Canvas/Paint and view.ViewRootImpl. +// +// The application's main thread is the Win32 UI thread. MessageQueue's native +// poll pumps the thread's Win32 messages while it waits, the window procedure +// only records input, and recorded input is delivered to Java after the wait +// (ViewRootImpl.dispatch*), the way Android's native Looper services its input +// channel. In headless mode there is no window: the surface is an off-screen +// buffer and a script supplies input when the queue is idle. +// +// Ownership: FrameworkHost owns the surfaces, the window and the queue table. +// Thread affinity: natives may run on any attached thread; window and surface +// operations happen on the UI thread (the one that calls ActivityThread.main). +// Error propagation: natives leave Java exceptions pending; Create returns Status. +#pragma once + +#include +#include +#include +#include +#include + +#include "arun/status.h" + +namespace arun::pkg { +class ZipReader; +} +namespace arun::vm { +class Runtime; +class Thread; +struct Object; +} // namespace arun::vm + +namespace arun::fw { + +struct HostConfig { + // Window client area / headless surface size in device pixels. + int width_px = 360; + int height_px = 640; + // Android densityDpi (160 = mdpi). Derived from the host DPI by the runner. + int density_dpi = 160; + bool headless = false; + // Headless (or scripted window) input, one command per entry, run when the + // main queue is idle: "tap X Y", "wait MS", "screenshot PATH", "back", "close". + std::vector script; + // Session wall-clock limit; 0 = none. On expiry the window is asked to close. + std::uint32_t timeout_ms = 0; + // Guest process id reported by android.os.Process.myPid (spec 11: a guest id). + int guest_pid = 4200; + // The installed, unmodified APK (assets and resources are read from it). + std::shared_ptr apk; + // The runtime's framework resource package (package id 0x01). + std::shared_ptr framework_res; +}; + +struct SessionReport { + int frames_presented = 0; + bool timed_out = false; + std::string script_error; // non-empty if a script command failed + int script_commands_run = 0; +}; + +class FrameworkHost { + public: + static Result> Create(HostConfig config); + ~FrameworkHost(); + FrameworkHost(const FrameworkHost&) = delete; + FrameworkHost& operator=(const FrameworkHost&) = delete; + + // Registers the framework natives. Call once, before any framework class runs. + void RegisterNatives(vm::Runtime* rt); + // Marks the calling host thread as the UI thread (the Java main thread). + void BindUiThread(); + // The application's class loader object (ActivityThread.nativeAppClassLoader). + // Must stay reachable through the runtime (a ClassLoaderRt's java_object is). + void SetAppClassLoader(vm::Object* loader); + + SessionReport report() const; + + struct Impl; + + private: + explicit FrameworkHost(std::unique_ptr impl); + std::unique_ptr impl_; +}; + +} // namespace arun::fw diff --git a/direct/vm/framework-host/src/framework_host.cc b/direct/vm/framework-host/src/framework_host.cc new file mode 100644 index 0000000..a36a162 --- /dev/null +++ b/direct/vm/framework-host/src/framework_host.cc @@ -0,0 +1,988 @@ +#include "arun/framework_host.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "arun/vm/runtime.h" +#include "arun/zip_reader.h" +#include "graphics_natives.h" +#include "render.h" +#include "resources_host.h" + +namespace arun::fw { + +using vm::Array; +using vm::ArgFloat; +using vm::ArgInt; +using vm::ArgLong; +using vm::ArgRef; +using vm::Frame; +using vm::JValue; +using vm::Object; +using vm::RootScope; +using vm::Runtime; +using vm::RtClass; +using vm::RtMethod; +using vm::ScopedBlocking; +using vm::Thread; + +namespace { + +constexpr std::uint64_t kMaxAssetBytes = 256ull << 20; +// The script runs only when the main queue has nothing due this soon. +constexpr int kIdleThresholdMs = 250; +constexpr std::uint32_t kWindowBackground = 0xFFFAFAFA; + +// MotionEvent / KeyEvent actions. +constexpr int kActionDown = 0; +constexpr int kActionUp = 1; +constexpr int kActionMove = 2; +constexpr int kActionCancel = 3; + +// android.view.KeyEvent key codes the host produces. +constexpr int kKeycodeUnknown = 0; +constexpr int kKeycodeBack = 4; +constexpr int kKeycodeDpadUp = 19; +constexpr int kKeycodeDpadDown = 20; +constexpr int kKeycodeDpadLeft = 21; +constexpr int kKeycodeDpadRight = 22; +constexpr int kKeycodeTab = 61; +constexpr int kKeycodeSpace = 62; +constexpr int kKeycodeEnter = 66; +constexpr int kKeycodeDel = 67; +constexpr int kKeycodeEscape = 111; +constexpr int kKeycodeForwardDel = 112; +constexpr int kKeycodeMoveHome = 122; +constexpr int kKeycodeMoveEnd = 123; +constexpr int kKeycodePageUp = 92; +constexpr int kKeycodePageDown = 93; +constexpr int kMetaShiftOn = 0x1; +constexpr int kMetaCtrlOn = 0x1000; + +std::int64_t UptimeMillis() { + ULONGLONG t = 0; // 100 ns units, excluding time asleep: uptimeMillis semantics + QueryUnbiasedInterruptTime(&t); + return static_cast(t / 10000); +} + +std::wstring Widen(std::string_view utf8) { + if (utf8.empty()) return {}; + const int n = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); + std::wstring out(static_cast(std::max(n, 0)), L'\0'); + if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), out.data(), n); + return out; +} + +struct HostEvent { + enum Kind { kPointer, kResize, kClose, kBack, kKey, kScroll } kind; + int action = 0; + float x = 0, y = 0; + std::int64_t time = 0; + int width = 0, height = 0; + int key_code = 0; + int unicode = 0; + int meta = 0; + float scroll = 0; // kScroll: lines, positive = away from the user (up) +}; + +struct NativeQueue { + HANDLE wake = nullptr; // auto-reset + bool ui = false; +}; + +const wchar_t kWindowClass[] = L"AndroitainerAppWindow"; + +// Key code for a non-character virtual key, or -1. +int KeyCodeForVk(WPARAM vk) { + switch (vk) { + case VK_BACK: return kKeycodeDel; + case VK_RETURN: return kKeycodeEnter; + case VK_TAB: return kKeycodeTab; + case VK_LEFT: return kKeycodeDpadLeft; + case VK_RIGHT: return kKeycodeDpadRight; + case VK_UP: return kKeycodeDpadUp; + case VK_DOWN: return kKeycodeDpadDown; + case VK_DELETE: return kKeycodeForwardDel; + case VK_HOME: return kKeycodeMoveHome; + case VK_END: return kKeycodeMoveEnd; + case VK_PRIOR: return kKeycodePageUp; + case VK_NEXT: return kKeycodePageDown; + default: return -1; + } +} + +int CurrentMeta() { + int meta = 0; + if (GetKeyState(VK_SHIFT) & 0x8000) meta |= kMetaShiftOn; + if (GetKeyState(VK_CONTROL) & 0x8000) meta |= kMetaCtrlOn; + return meta; +} + +} // namespace + +struct FrameworkHost::Impl { + HostConfig config; + std::unique_ptr resources; + Graphics* gfx = nullptr; + Runtime* runtime = nullptr; + DWORD ui_thread = 0; + Object* app_loader = nullptr; + + // Main-queue handles and their pids/tids. + std::mutex queue_lock; + std::unordered_map> queues; + std::int64_t next_queue = 1; + std::mutex tid_lock; + std::unordered_map guest_tids; + int next_tid = 0; + + // Window and surface (UI thread only). + NativeBitmap* surface = nullptr; // pinned: owned by the host + bool surface_open = false; + std::vector present_pixels; + int present_width = 0, present_height = 0; + HWND hwnd = nullptr; + bool pointer_down = false; + + // Input recorded by the window procedure or the script (UI thread only). + std::deque events; + + // Script state (UI thread only). + std::size_t script_pos = 0; + std::int64_t script_wait_until = 0; + std::int64_t session_deadline = 0; + bool close_requested_by_timeout = false; + + std::atomic frames{0}; + mutable std::mutex report_lock; + SessionReport report; + + // Cached callbacks into android.view.ViewRootImpl. + RtMethod* dispatch_pointer = nullptr; + RtMethod* dispatch_resize = nullptr; + RtMethod* dispatch_close = nullptr; + RtMethod* dispatch_back = nullptr; + RtMethod* dispatch_key = nullptr; + RtMethod* dispatch_scroll = nullptr; + + bool OnUiThread() const { return GetCurrentThreadId() == ui_thread; } + + NativeQueue* Queue(std::int64_t id) { + std::lock_guard hold(queue_lock); + auto it = queues.find(id); + return it == queues.end() ? nullptr : it->second.get(); + } + + void SetScriptError(std::string error) { + std::lock_guard hold(report_lock); + if (report.script_error.empty()) report.script_error = std::move(error); + } + + // --- window ------------------------------------------------------------------- + + static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + bool OpenWindow(const std::wstring& title, std::string* error); + void Present(); + + void PushPointer(int action, LPARAM lp) { + HostEvent e{HostEvent::kPointer}; + e.action = action; + e.x = static_cast(static_cast(LOWORD(lp))); + e.y = static_cast(static_cast(HIWORD(lp))); + e.time = UptimeMillis(); + events.push_back(e); + } + + void PushKey(int key_code, int unicode, int meta) { + const std::int64_t now = UptimeMillis(); + HostEvent down{HostEvent::kKey}; + down.action = kActionDown; + down.key_code = key_code; + down.unicode = unicode; + down.meta = meta; + down.time = now; + HostEvent up = down; + up.action = kActionUp; + events.push_back(down); + events.push_back(up); + } + + // --- script --------------------------------------------------------------------- + + // Runs script commands while the queue is idle. Returns after queuing input + // or when the script must wait. + void RunScript(); + bool ScriptPending() const { return script_pos < config.script.size(); } +}; + +// --- window ------------------------------------------------------------------------ + +LRESULT CALLBACK FrameworkHost::Impl::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + auto* self = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + if (self == nullptr) return DefWindowProcW(hwnd, msg, wp, lp); + switch (msg) { + case WM_PAINT: { + PAINTSTRUCT ps; + HDC dc = BeginPaint(hwnd, &ps); + const int w = self->present_width, h = self->present_height; + if (w > 0 && h > 0 && self->present_pixels.size() == static_cast(w) * static_cast(h)) { + BITMAPINFO bmi{}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = w; + bmi.bmiHeader.biHeight = -h; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + SetDIBitsToDevice(dc, 0, 0, static_cast(w), static_cast(h), 0, 0, 0, static_cast(h), + self->present_pixels.data(), &bmi, DIB_RGB_COLORS); + } + EndPaint(hwnd, &ps); + return 0; + } + case WM_ERASEBKGND: + return 1; + case WM_SIZE: { + if (wp == SIZE_MINIMIZED || self->surface == nullptr) return 0; + const int w = LOWORD(lp), h = HIWORD(lp); + if (w > 0 && h > 0 && (w != self->gfx->Width(self->surface) || h != self->gfx->Height(self->surface))) { + std::string error; + if (self->gfx->Resize(self->surface, w, h, kWindowBackground, &error)) { + HostEvent e{HostEvent::kResize}; + e.width = w; + e.height = h; + self->events.push_back(e); + } + } + return 0; + } + case WM_LBUTTONDOWN: + SetCapture(hwnd); + SetFocus(hwnd); + self->pointer_down = true; + self->PushPointer(kActionDown, lp); + return 0; + case WM_MOUSEMOVE: + if (self->pointer_down && (wp & MK_LBUTTON) != 0) self->PushPointer(kActionMove, lp); + return 0; + case WM_LBUTTONUP: + if (self->pointer_down) { + self->pointer_down = false; + self->PushPointer(kActionUp, lp); + } + ReleaseCapture(); + return 0; + case WM_CAPTURECHANGED: + if (self->pointer_down && reinterpret_cast(lp) != hwnd) { + self->pointer_down = false; + self->PushPointer(kActionCancel, 0); + } + return 0; + case WM_MOUSEWHEEL: { + POINT pt{static_cast(LOWORD(lp)), static_cast(HIWORD(lp))}; + ScreenToClient(hwnd, &pt); + HostEvent e{HostEvent::kScroll}; + e.x = static_cast(pt.x); + e.y = static_cast(pt.y); + e.scroll = static_cast(GET_WHEEL_DELTA_WPARAM(wp)) / WHEEL_DELTA; + e.time = UptimeMillis(); + self->events.push_back(e); + return 0; + } + case WM_KEYDOWN: + if (wp == VK_ESCAPE || wp == VK_BROWSER_BACK) { + self->events.push_back(HostEvent{HostEvent::kBack}); + return 0; + } + if (const int code = KeyCodeForVk(wp); code >= 0) { + self->PushKey(code, 0, CurrentMeta()); + return 0; + } + break; + case WM_CHAR: + // Printable characters; control characters come through WM_KEYDOWN. + if (wp >= 0x20 && wp != 0x7f) self->PushKey(wp == ' ' ? kKeycodeSpace : kKeycodeUnknown, static_cast(wp), CurrentMeta()); + return 0; + case WM_CLOSE: + // The application decides (Activity.finish); DestroyWindow follows from + // ViewRootImpl.nativeCloseWindow. + self->events.push_back(HostEvent{HostEvent::kClose}); + return 0; + case WM_DPICHANGED: { + const RECT* r = reinterpret_cast(lp); + SetWindowPos(hwnd, nullptr, r->left, r->top, r->right - r->left, r->bottom - r->top, + SWP_NOZORDER | SWP_NOACTIVATE); + return 0; + } + default: + break; + } + return DefWindowProcW(hwnd, msg, wp, lp); +} + +bool FrameworkHost::Impl::OpenWindow(const std::wstring& title, std::string* error) { + if (surface == nullptr) { + surface = gfx->CreateBitmap(config.width_px, config.height_px, false, error); + if (surface == nullptr) return false; + gfx->Pin(surface); + } + if (!gfx->Resize(surface, config.width_px, config.height_px, kWindowBackground, error)) return false; + surface_open = true; + if (config.headless) return true; + HINSTANCE instance = GetModuleHandleW(nullptr); + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = WndProc; + wc.hInstance = instance; + wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wc.lpszClassName = kWindowClass; + if (RegisterClassExW(&wc) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + *error = std::format("RegisterClassExW failed ({})", GetLastError()); + return false; + } + const DWORD style = WS_OVERLAPPEDWINDOW; + RECT rc{0, 0, config.width_px, config.height_px}; + AdjustWindowRectExForDpi(&rc, style, FALSE, 0, GetDpiForSystem()); + hwnd = CreateWindowExW(0, kWindowClass, title.c_str(), style, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, + rc.bottom - rc.top, nullptr, nullptr, instance, nullptr); + if (hwnd == nullptr) { + *error = std::format("CreateWindowExW failed ({})", GetLastError()); + return false; + } + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(this)); + // The client area may differ from the request (screen too small): use it. + RECT client{}; + GetClientRect(hwnd, &client); + if (client.right > 0 && client.bottom > 0) gfx->Resize(surface, client.right, client.bottom, kWindowBackground, error); + ShowWindow(hwnd, SW_SHOW); + UpdateWindow(hwnd); + return true; +} + +void FrameworkHost::Impl::Present() { + frames.fetch_add(1, std::memory_order_relaxed); + if (hwnd != nullptr && surface != nullptr) { + present_width = gfx->Width(surface); + present_height = gfx->Height(surface); + gfx->CopyOpaque(surface, &present_pixels); + InvalidateRect(hwnd, nullptr, FALSE); + UpdateWindow(hwnd); + } +} + +// --- script ------------------------------------------------------------------------ + +namespace { + +struct NamedKey { + const char* name; + int code; +}; + +constexpr NamedKey kNamedKeys[] = { + {"ENTER", kKeycodeEnter}, {"DEL", kKeycodeDel}, {"BACKSPACE", kKeycodeDel}, + {"TAB", kKeycodeTab}, {"SPACE", kKeycodeSpace}, {"LEFT", kKeycodeDpadLeft}, + {"RIGHT", kKeycodeDpadRight}, {"UP", kKeycodeDpadUp}, {"DOWN", kKeycodeDpadDown}, + {"FORWARD_DEL", kKeycodeForwardDel}, {"HOME", kKeycodeMoveHome}, {"END", kKeycodeMoveEnd}, + {"ESCAPE", kKeycodeEscape}, {"PAGE_UP", kKeycodePageUp}, {"PAGE_DOWN", kKeycodePageDown}, +}; + +// Decodes UTF-8 into UTF-16 code units (invalid bytes become U+FFFD). +std::u16string Utf16(std::string_view s) { + std::wstring w = Widen(s); + return std::u16string(w.begin(), w.end()); +} + +} // namespace + +void FrameworkHost::Impl::RunScript() { + while (ScriptPending()) { + if (UptimeMillis() < script_wait_until) return; + const std::string& line = config.script[script_pos]; + ++script_pos; + { + std::lock_guard hold(report_lock); + ++report.script_commands_run; + } + std::vector words; + std::string rest; // everything after the command word + for (std::size_t i = 0; i < line.size();) { + while (i < line.size() && line[i] == ' ') ++i; + std::size_t j = i; + while (j < line.size() && line[j] != ' ') ++j; + if (j > i) words.push_back(line.substr(i, j - i)); + if (words.size() == 1) { + std::size_t k = j; + if (k < line.size() && line[k] == ' ') ++k; + rest = k < line.size() ? line.substr(k) : std::string(); + if (words[0] == "screenshot" || words[0] == "type") { + while (!rest.empty() && rest.front() == ' ' && words[0] == "screenshot") rest.erase(rest.begin()); + if (!rest.empty()) words.push_back(rest); + break; + } + } + i = j; + } + if (words.empty()) continue; + auto number = [&](std::size_t idx, float* out) { + if (idx >= words.size()) return false; + const std::string& w = words[idx]; + auto [p, ec] = std::from_chars(w.data(), w.data() + w.size(), *out); + return ec == std::errc() && p == w.data() + w.size(); + }; + const std::string& cmd = words[0]; + if (cmd == "tap") { + float x = 0, y = 0; + if (!number(1, &x) || !number(2, &y)) { + SetScriptError("bad script command: " + line); + continue; + } + const std::int64_t now = UptimeMillis(); + HostEvent down{HostEvent::kPointer}; + down.action = kActionDown; + down.x = x; + down.y = y; + down.time = now; + HostEvent up = down; + up.action = kActionUp; + up.time = now + 50; + events.push_back(down); + events.push_back(up); + return; // let the application process the tap first + } + if (cmd == "longpress") { + float x = 0, y = 0; + if (!number(1, &x) || !number(2, &y)) { + SetScriptError("bad script command: " + line); + continue; + } + HostEvent down{HostEvent::kPointer}; + down.action = kActionDown; + down.x = x; + down.y = y; + down.time = UptimeMillis(); + events.push_back(down); + // The release comes after the long-press timeout, as a separate command. + config.script.insert(config.script.begin() + static_cast(script_pos), + {"wait 700", std::format("__up {} {}", x, y)}); + return; + } + if (cmd == "__up") { + float x = 0, y = 0; + number(1, &x); + number(2, &y); + HostEvent up{HostEvent::kPointer}; + up.action = kActionUp; + up.x = x; + up.y = y; + up.time = UptimeMillis(); + events.push_back(up); + return; + } + if (cmd == "swipe") { + float x0 = 0, y0 = 0, x1 = 0, y1 = 0, ms = 300; + if (!number(1, &x0) || !number(2, &y0) || !number(3, &x1) || !number(4, &y1) || + (words.size() > 5 && !number(5, &ms))) { + SetScriptError("bad script command: " + line); + continue; + } + const std::int64_t now = UptimeMillis(); + constexpr int kSteps = 10; + for (int s = 0; s <= kSteps + 1; ++s) { + HostEvent e{HostEvent::kPointer}; + const float f = std::min(1.0f, static_cast(s) / kSteps); + e.action = s == 0 ? kActionDown : s == kSteps + 1 ? kActionUp : kActionMove; + e.x = x0 + (x1 - x0) * f; + e.y = y0 + (y1 - y0) * f; + e.time = now + static_cast(ms * f); + events.push_back(e); + } + return; + } + if (cmd == "scroll") { + float x = 0, y = 0, lines = 0; + if (!number(1, &x) || !number(2, &y) || !number(3, &lines)) { + SetScriptError("bad script command: " + line); + continue; + } + HostEvent e{HostEvent::kScroll}; + e.x = x; + e.y = y; + e.scroll = lines; + e.time = UptimeMillis(); + events.push_back(e); + return; + } + if (cmd == "type") { + if (words.size() < 2) { + SetScriptError("bad script command: " + line); + continue; + } + for (char16_t c : Utf16(words[1])) PushKey(c == u' ' ? kKeycodeSpace : kKeycodeUnknown, c, 0); + return; + } + if (cmd == "key") { + int code = -1; + if (words.size() >= 2) { + for (const NamedKey& k : kNamedKeys) { + if (words[1] == k.name) code = k.code; + } + } + if (code < 0) { + SetScriptError("unknown key in script command: " + line); + continue; + } + PushKey(code, 0, 0); + return; + } + if (cmd == "wait") { + float ms = 0; + if (!number(1, &ms) || ms < 0) { + SetScriptError("bad script command: " + line); + continue; + } + script_wait_until = UptimeMillis() + static_cast(ms); + return; + } + if (cmd == "screenshot") { + if (words.size() < 2) { + SetScriptError("bad script command: " + line); + continue; + } + std::string err = surface_open && surface != nullptr + ? gfx->WritePng(surface, std::filesystem::path(Widen(words[1]))) + : std::string("no window is open"); + if (!err.empty()) SetScriptError("screenshot " + words[1] + ": " + err); + continue; + } + if (cmd == "back") { + events.push_back(HostEvent{HostEvent::kBack}); + return; + } + if (cmd == "close") { + events.push_back(HostEvent{HostEvent::kClose}); + return; + } + SetScriptError("unknown script command: " + line); + } +} + +// --- natives --------------------------------------------------------------------------- + +namespace { + +FrameworkHost::Impl* g_host = nullptr; + +Runtime* Rt(Thread* self) { return self->runtime(); } + +bool CheckSurface(Thread* self, std::int64_t handle) { + FrameworkHost::Impl* h = g_host; + if (h->surface == nullptr || handle != Graphics::Handle(h->surface) || !h->surface_open || !h->OnUiThread()) { + Rt(self)->ThrowNew(self, "Ljava/lang/IllegalStateException;", + "no such surface, or not on the thread that owns it"); + return false; + } + return true; +} + +// android.util.Log +void Log_println_native(Thread* self, Frame* a, JValue* r) { + Runtime* rt = Rt(self); + const std::string tag = rt->GetStringUtf8(ArgRef(a, 2)); + const std::string msg = rt->GetStringUtf8(ArgRef(a, 3)); + rt->Log(ArgInt(a, 1), tag, msg); + r->i = static_cast(msg.size()); +} + +// android.os.SystemClock +void SystemClock_uptimeMillis(Thread*, Frame*, JValue* r) { r->j = UptimeMillis(); } + +// android.os.Process +void Process_nativeMyPid(Thread*, Frame*, JValue* r) { r->i = g_host->config.guest_pid; } + +void Process_nativeMyTid(Thread*, Frame*, JValue* r) { + // Guest thread ids: the main thread's equals the pid, as on Android. + const DWORD host = GetCurrentThreadId(); + if (host == g_host->ui_thread) { + r->i = g_host->config.guest_pid; + return; + } + std::lock_guard hold(g_host->tid_lock); + auto [it, inserted] = g_host->guest_tids.try_emplace(host, 0); + if (inserted) it->second = g_host->config.guest_pid + (++g_host->next_tid); + r->i = it->second; +} + +// android.os.MessageQueue +void MessageQueue_nativeInit(Thread*, Frame*, JValue* r) { + auto q = std::make_unique(); + q->wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); + q->ui = g_host->OnUiThread(); + std::lock_guard hold(g_host->queue_lock); + const std::int64_t id = g_host->next_queue++; + g_host->queues.emplace(id, std::move(q)); + r->j = id; +} + +void MessageQueue_nativeDestroy(Thread*, Frame* a, JValue*) { + std::unique_ptr q; + { + std::lock_guard hold(g_host->queue_lock); + auto it = g_host->queues.find(ArgLong(a, 0)); + if (it == g_host->queues.end()) return; + q = std::move(it->second); + g_host->queues.erase(it); + } + CloseHandle(q->wake); +} + +void MessageQueue_nativeWake(Thread*, Frame* a, JValue*) { + NativeQueue* q = g_host->Queue(ArgLong(a, 0)); + if (q != nullptr) SetEvent(q->wake); +} + +// Delivers recorded host input to ViewRootImpl. Stops at the first exception. +void DispatchEvents(Thread* self) { + Runtime* rt = Rt(self); + FrameworkHost::Impl* h = g_host; + if (h->dispatch_pointer == nullptr) { + RtClass* c = rt->class_linker().FindClass(self, "Landroid/view/ViewRootImpl;", nullptr); + if (c == nullptr) return; + h->dispatch_pointer = rt->FindMethod(c, "dispatchPointer", "(IFFJ)V"); + h->dispatch_resize = rt->FindMethod(c, "dispatchResize", "(II)V"); + h->dispatch_close = rt->FindMethod(c, "dispatchCloseRequest", "()V"); + h->dispatch_back = rt->FindMethod(c, "dispatchBack", "()V"); + h->dispatch_key = rt->FindMethod(c, "dispatchKey", "(IIIIJ)V"); + h->dispatch_scroll = rt->FindMethod(c, "dispatchScroll", "(FFFJ)V"); + if (h->dispatch_pointer == nullptr || h->dispatch_resize == nullptr || h->dispatch_close == nullptr || + h->dispatch_back == nullptr || h->dispatch_key == nullptr || h->dispatch_scroll == nullptr) { + rt->ThrowNew(self, "Ljava/lang/NoSuchMethodError;", "ViewRootImpl host callbacks"); + return; + } + } + while (!h->events.empty() && self->exception() == nullptr) { + const HostEvent e = h->events.front(); + h->events.pop_front(); + JValue result{}; + switch (e.kind) { + case HostEvent::kPointer: { + JValue args[4]; + args[0].i = e.action; + args[1].f = e.x; + args[2].f = e.y; + args[3].j = e.time; + rt->Invoke(self, h->dispatch_pointer, nullptr, args, &result); + break; + } + case HostEvent::kResize: { + JValue args[2]; + args[0].i = e.width; + args[1].i = e.height; + rt->Invoke(self, h->dispatch_resize, nullptr, args, &result); + break; + } + case HostEvent::kClose: + rt->Invoke(self, h->dispatch_close, nullptr, {}, &result); + break; + case HostEvent::kBack: + rt->Invoke(self, h->dispatch_back, nullptr, {}, &result); + break; + case HostEvent::kKey: { + JValue args[5]; + args[0].i = e.action; + args[1].i = e.key_code; + args[2].i = e.unicode; + args[3].i = e.meta; + args[4].j = e.time; + rt->Invoke(self, h->dispatch_key, nullptr, args, &result); + break; + } + case HostEvent::kScroll: { + JValue args[4]; + args[0].f = e.x; + args[1].f = e.y; + args[2].f = e.scroll; + args[3].j = e.time; + rt->Invoke(self, h->dispatch_scroll, nullptr, args, &result); + break; + } + } + } +} + +void MessageQueue_nativePollOnce(Thread* self, Frame* a, JValue*) { + FrameworkHost::Impl* h = g_host; + NativeQueue* q = h->Queue(ArgLong(a, 0)); + if (q == nullptr) { + Rt(self)->ThrowNew(self, "Ljava/lang/IllegalStateException;", "message queue has been disposed"); + return; + } + const std::int32_t timeout = ArgInt(a, 2); + if (!q->ui) { + ScopedBlocking blocking(self); + WaitForSingleObject(q->wake, timeout < 0 ? INFINITE : static_cast(timeout)); + return; + } + + const std::int64_t start = UptimeMillis(); + const bool idle = timeout < 0 || timeout >= kIdleThresholdMs; + { + ScopedBlocking blocking(self); + for (;;) { + MSG msg; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + const std::int64_t now = UptimeMillis(); + if (h->session_deadline != 0 && now >= h->session_deadline && !h->close_requested_by_timeout) { + h->close_requested_by_timeout = true; + { + std::lock_guard hold(h->report_lock); + h->report.timed_out = true; + } + h->events.push_back(HostEvent{HostEvent::kClose}); + } + if (idle && h->events.empty() && h->ScriptPending()) h->RunScript(); + if (!h->events.empty()) break; + if (timeout == 0) break; + std::int64_t wait = timeout < 0 ? -1 : start + timeout - now; + if (timeout >= 0 && wait <= 0) break; + auto limit = [&](std::int64_t until) { + if (until <= 0) return; + const std::int64_t d = std::max(0, until - now); + wait = wait < 0 ? d : std::min(wait, d); + }; + if (idle && h->ScriptPending()) limit(h->script_wait_until > now ? h->script_wait_until : now + 1); + if (h->session_deadline != 0 && !h->close_requested_by_timeout) limit(h->session_deadline); + const DWORD r = MsgWaitForMultipleObjectsEx(1, &q->wake, wait < 0 ? INFINITE : static_cast(wait), + QS_ALLINPUT, MWMO_INPUTAVAILABLE); + if (r == WAIT_OBJECT_0) break; // woken: a message was enqueued + } + } + DispatchEvents(self); +} + +// android.content.res.ResourcesHost +void ResourcesHost_nativeDisplayInfo(Thread* self, Frame* a, JValue*) { + auto* out = static_cast(ArgRef(a, 0)); + if (out == nullptr || out->length < 3) { + Rt(self)->ThrowNew(self, "Ljava/lang/IllegalArgumentException;", "display info array"); + return; + } + FrameworkHost::Impl* h = g_host; + const bool open = h->surface_open && h->surface != nullptr; + out->elements()[0] = open ? h->gfx->Width(h->surface) : h->config.width_px; + out->elements()[1] = open ? h->gfx->Height(h->surface) : h->config.height_px; + out->elements()[2] = h->config.density_dpi; +} + +// android.content.res.AssetManager (assets/) +bool AssetName(Thread* self, Frame* a, std::string* name) { + *name = Rt(self)->GetStringUtf8(ArgRef(a, 0)); + // Asset names are relative paths inside assets/; nothing may climb out. + if (name->find('\0') != std::string::npos) return false; + std::size_t start = 0; + while (start <= name->size()) { + std::size_t end = name->find('/', start); + if (end == std::string::npos) end = name->size(); + const std::string_view part(name->data() + start, end - start); + if (part == "..") return false; + start = end + 1; + } + while (!name->empty() && name->front() == '/') name->erase(name->begin()); + return true; +} + +void AssetManager_nativeOpenAsset(Thread* self, Frame* a, JValue* r) { + Runtime* rt = Rt(self); + r->l = nullptr; + std::string name; + if (!AssetName(self, a, &name) || name.empty() || g_host->config.apk == nullptr) return; + const pkg::ZipEntry* e = g_host->config.apk->Find("assets/" + name); + if (e == nullptr || e->is_directory) return; + if (e->uncompressed_size > kMaxAssetBytes) { + rt->ThrowNew(self, "Ljava/io/IOException;", std::format("asset {} is too large ({} bytes)", name, + e->uncompressed_size)); + return; + } + std::vector data; + { + ScopedBlocking blocking(self); + auto bytes = g_host->config.apk->Extract(*e); + if (bytes.ok()) data = std::move(*bytes); + else name = std::string(bytes.status().message()); + } + if (data.empty() && e->uncompressed_size != 0) { + rt->ThrowNew(self, "Ljava/io/IOException;", "cannot read asset: " + name); + return; + } + Array* arr = rt->heap().AllocArray(self, rt->wk.byte_array, static_cast(data.size())); + if (arr == nullptr) return; + if (!data.empty()) std::memcpy(arr->data(), data.data(), data.size()); + r->l = arr; +} + +void AssetManager_nativeList(Thread* self, Frame* a, JValue* r) { + Runtime* rt = Rt(self); + std::string dir; + if (!AssetName(self, a, &dir)) { + r->l = rt->heap().AllocArray(self, rt->wk.string_array, 0); + return; + } + while (!dir.empty() && dir.back() == '/') dir.pop_back(); + const std::string prefix = dir.empty() ? "assets/" : "assets/" + dir + "/"; + std::set names; + if (g_host->config.apk != nullptr) { + for (const pkg::ZipEntry& e : g_host->config.apk->entries()) { + if (e.name.size() <= prefix.size() || e.name.compare(0, prefix.size(), prefix) != 0) continue; + std::string rest = e.name.substr(prefix.size()); + const std::size_t slash = rest.find('/'); + if (slash != std::string::npos) rest.resize(slash); + if (!rest.empty()) names.insert(rest); + } + } + RootScope roots(self); + auto arr = roots.Add(rt->heap().AllocArray(self, rt->wk.string_array, static_cast(names.size()))); + if (arr.Get() == nullptr) return; + std::size_t i = 0; + for (const std::string& n : names) { + Object* s = rt->NewStringUtf8(self, n); + if (s == nullptr) return; + static_cast(arr.Get())->elements()[i++] = s; + } + r->l = arr.Get(); +} + +// android.view.ViewRootImpl +void ViewRootImpl_nativeOpenWindow(Thread* self, Frame* a, JValue* r) { + Runtime* rt = Rt(self); + FrameworkHost::Impl* h = g_host; + auto* size = static_cast(ArgRef(a, 1)); + if (size == nullptr || size->length < 2) { + rt->ThrowNew(self, "Ljava/lang/IllegalArgumentException;", "size array"); + return; + } + if (!h->OnUiThread()) { + rt->ThrowNew(self, "Ljava/lang/IllegalStateException;", "windows can only be opened on the main thread"); + return; + } + if (h->surface_open) { + rt->ThrowNew(self, "Ljava/lang/IllegalStateException;", "only one window per session is supported"); + return; + } + const std::string title = rt->GetStringUtf8(ArgRef(a, 0)); + std::string error; + if (!h->OpenWindow(Widen(title), &error)) { + rt->ThrowNew(self, "Ljava/lang/RuntimeException;", "cannot open the host window: " + error); + return; + } + size->elements()[0] = h->gfx->Width(h->surface); + size->elements()[1] = h->gfx->Height(h->surface); + r->j = Graphics::Handle(h->surface); +} + +void ViewRootImpl_nativeSetTitle(Thread* self, Frame* a, JValue*) { + if (!CheckSurface(self, ArgLong(a, 0))) return; + if (g_host->hwnd != nullptr) SetWindowTextW(g_host->hwnd, Widen(Rt(self)->GetStringUtf8(ArgRef(a, 2))).c_str()); +} + +void ViewRootImpl_nativePresent(Thread* self, Frame* a, JValue*) { + if (!CheckSurface(self, ArgLong(a, 0))) return; + g_host->Present(); +} + +void ViewRootImpl_nativeCloseWindow(Thread* self, Frame* a, JValue*) { + if (!CheckSurface(self, ArgLong(a, 0))) return; + FrameworkHost::Impl* h = g_host; + if (h->hwnd != nullptr) { + SetWindowLongPtrW(h->hwnd, GWLP_USERDATA, 0); + DestroyWindow(h->hwnd); + h->hwnd = nullptr; + } + h->surface_open = false; + h->events.clear(); +} + +// android.app.ActivityThread +void ActivityThread_nativeAppClassLoader(Thread*, Frame*, JValue* r) { r->l = g_host->app_loader; } + +// android.content.ClipboardManager: the host clipboard is deliberately not +// shared with the guest (spec 11); the guest gets a session-local one in Java. + +} // namespace + +// --- FrameworkHost ------------------------------------------------------------------------- + +FrameworkHost::FrameworkHost(std::unique_ptr impl) : impl_(std::move(impl)) {} + +FrameworkHost::~FrameworkHost() { + if (g_host == impl_.get()) g_host = nullptr; + if (impl_->hwnd != nullptr) DestroyWindow(impl_->hwnd); + for (auto& [id, q] : impl_->queues) CloseHandle(q->wake); +} + +Result> FrameworkHost::Create(HostConfig config) { + if (g_host != nullptr) return Status(ReasonCode::kBusy, "a framework host already exists in this process"); + if (config.width_px <= 0 || config.height_px <= 0 || config.width_px > 16384 || config.height_px > 16384) { + return Status(ReasonCode::kInvalidArgument, "window size out of range"); + } + if (config.density_dpi < 60 || config.density_dpi > 960) { + return Status(ReasonCode::kInvalidArgument, "density out of range"); + } + auto impl = std::make_unique(); + impl->config = std::move(config); + ARUN_ASSIGN_OR_RETURN(impl->resources, ResourcesHost::Create(impl->config.framework_res, impl->config.apk)); + std::string error; + impl->gfx = Graphics::Get(&error); + if (impl->gfx == nullptr) return Status(ReasonCode::kHostOsError, "graphics: " + error); + if (impl->config.timeout_ms != 0) impl->session_deadline = UptimeMillis() + impl->config.timeout_ms; + auto host = std::unique_ptr(new FrameworkHost(std::move(impl))); + g_host = host->impl_.get(); + return host; +} + +void FrameworkHost::BindUiThread() { impl_->ui_thread = GetCurrentThreadId(); } + +void FrameworkHost::SetAppClassLoader(Object* loader) { impl_->app_loader = loader; } + +SessionReport FrameworkHost::report() const { + std::lock_guard hold(impl_->report_lock); + SessionReport r = impl_->report; + r.frames_presented = impl_->frames.load(std::memory_order_relaxed); + return r; +} + +void FrameworkHost::RegisterNatives(Runtime* rt) { + impl_->runtime = rt; + impl_->resources->RegisterNatives(rt); + (void)RegisterGraphicsNatives(rt, impl_->config.density_dpi); // Graphics::Get already succeeded in Create + auto reg = [rt](const char* key, vm::NativeFn fn) { rt->RegisterNative(key, fn); }; + reg("Landroid/util/Log;.println_native(IILjava/lang/String;Ljava/lang/String;)I", Log_println_native); + reg("Landroid/os/SystemClock;.uptimeMillis()J", SystemClock_uptimeMillis); + reg("Landroid/os/Process;.nativeMyPid()I", Process_nativeMyPid); + reg("Landroid/os/Process;.nativeMyTid()I", Process_nativeMyTid); + reg("Landroid/os/MessageQueue;.nativeInit()J", MessageQueue_nativeInit); + reg("Landroid/os/MessageQueue;.nativeDestroy(J)V", MessageQueue_nativeDestroy); + reg("Landroid/os/MessageQueue;.nativePollOnce(JI)V", MessageQueue_nativePollOnce); + reg("Landroid/os/MessageQueue;.nativeWake(J)V", MessageQueue_nativeWake); + reg("Landroid/content/res/ResourcesHost;.nativeDisplayInfo([I)V", ResourcesHost_nativeDisplayInfo); + reg("Landroid/content/res/AssetManager;.nativeOpenAsset(Ljava/lang/String;)[B", AssetManager_nativeOpenAsset); + reg("Landroid/content/res/AssetManager;.nativeList(Ljava/lang/String;)[Ljava/lang/String;", AssetManager_nativeList); + reg("Landroid/view/ViewRootImpl;.nativeOpenWindow(Ljava/lang/String;[I)J", ViewRootImpl_nativeOpenWindow); + reg("Landroid/view/ViewRootImpl;.nativeSetTitle(JLjava/lang/String;)V", ViewRootImpl_nativeSetTitle); + reg("Landroid/view/ViewRootImpl;.nativePresent(J)V", ViewRootImpl_nativePresent); + reg("Landroid/view/ViewRootImpl;.nativeCloseWindow(J)V", ViewRootImpl_nativeCloseWindow); + reg("Landroid/app/ActivityThread;.nativeAppClassLoader()Ljava/lang/ClassLoader;", + ActivityThread_nativeAppClassLoader); +} + +} // namespace arun::fw diff --git a/direct/vm/framework-host/src/graphics_natives.cc b/direct/vm/framework-host/src/graphics_natives.cc new file mode 100644 index 0000000..aa8809a --- /dev/null +++ b/direct/vm/framework-host/src/graphics_natives.cc @@ -0,0 +1,388 @@ +// Natives behind android.graphics.{Canvas, Paint, Bitmap, BitmapFactory}. Every +// bitmap handle from Java is validated (Graphics::Validate) before use. +#include "graphics_natives.h" + +#include +#include +#include +#include + +#include "arun/vm/runtime.h" +#include "render.h" + +namespace arun::fw { + +using vm::Array; +using vm::ArgFloat; +using vm::ArgInt; +using vm::ArgLong; +using vm::ArgRef; +using vm::Frame; +using vm::JValue; +using vm::Object; +using vm::Runtime; +using vm::Thread; + +namespace { + +Graphics* g_gfx = nullptr; +int g_device_density = 160; + +Runtime* Rt(Thread* self) { return self->runtime(); } + +void Throw(Thread* self, const char* cls, std::string_view msg) { Rt(self)->ThrowNew(self, cls, msg); } + +NativeBitmap* Bmp(Thread* self, std::int64_t handle) { + NativeBitmap* b = g_gfx->Validate(handle); + if (b == nullptr) Throw(self, "Ljava/lang/IllegalArgumentException;", "not a live bitmap"); + return b; +} + +// A float[] argument with at least `min` elements. +const float* Floats(Thread* self, Frame* a, std::uint32_t reg, std::int32_t min) { + auto* arr = static_cast(ArgRef(a, reg)); + if (arr == nullptr || arr->klass->descriptor != "[F" || arr->length < min) { + Throw(self, "Ljava/lang/IllegalArgumentException;", "float[] argument"); + return nullptr; + } + return arr->elements(); +} + +// The optional shader description (null -> empty span). +std::span Shader(Frame* a, std::uint32_t reg) { + auto* arr = static_cast(ArgRef(a, reg)); + if (arr == nullptr || arr->klass->descriptor != "[F") return {}; + return std::span(arr->elements(), static_cast(arr->length)); +} + +bool State(Thread* self, Frame* a, std::uint32_t reg, DrawState* s) { + const float* f = Floats(self, a, reg, 11); + if (f == nullptr) return false; + for (int i = 0; i < 6; ++i) s->m[i] = f[i]; + for (int i = 0; i < 4; ++i) { + const float v = f[6 + i]; + s->clip[i] = std::isfinite(v) ? static_cast(std::clamp(v, -1e6f, 1e6f)) : 0; + } + s->alpha = std::clamp(f[10], 0.0f, 1.0f); + return true; +} + +// Paint arguments starting at `reg`: color, flags, strokeWidth, miter, shader. +PaintSpec Paint(Frame* a, std::uint32_t reg) { + PaintSpec p; + p.color = static_cast(ArgInt(a, reg)); + p.flags = ArgInt(a, reg + 1); + p.stroke_width = ArgFloat(a, reg + 2); + p.miter = ArgFloat(a, reg + 3); + if (!std::isfinite(p.stroke_width) || p.stroke_width < 0) p.stroke_width = 0; + if (!std::isfinite(p.miter)) p.miter = 4; + p.shader = Shader(a, reg + 4); + return p; +} + +FontSpec Font(Frame* a, std::uint32_t reg) { + FontSpec f; + f.size = ArgFloat(a, reg); + if (!std::isfinite(f.size) || f.size <= 0) f.size = 1; + f.size = std::min(f.size, 2048.0f); + f.weight = ArgInt(a, reg + 1); + f.family = ArgInt(a, reg + 2); + f.italic = ArgInt(a, reg + 3) != 0; + return f; +} + +// --- Canvas --------------------------------------------------------------------------- +// Static natives: (long target) occupies registers 0-1, the state array is 2. + +void Canvas_nativeClear(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + g_gfx->Clear(t, s, static_cast(ArgInt(a, 3)), ArgInt(a, 4) != 0); +} + +void Canvas_nativeDrawRect(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + g_gfx->DrawRect(t, s, ArgFloat(a, 3), ArgFloat(a, 4), ArgFloat(a, 5), ArgFloat(a, 6), ArgFloat(a, 7), ArgFloat(a, 8), + Paint(a, 9)); +} + +void Canvas_nativeDrawOval(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + g_gfx->DrawOval(t, s, ArgFloat(a, 3), ArgFloat(a, 4), ArgFloat(a, 5), ArgFloat(a, 6), Paint(a, 7)); +} + +void Canvas_nativeDrawArc(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + g_gfx->DrawArc(t, s, ArgFloat(a, 3), ArgFloat(a, 4), ArgFloat(a, 5), ArgFloat(a, 6), ArgFloat(a, 7), ArgFloat(a, 8), + ArgInt(a, 9) != 0, Paint(a, 10)); +} + +void Canvas_nativeDrawLine(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + g_gfx->DrawLine(t, s, ArgFloat(a, 3), ArgFloat(a, 4), ArgFloat(a, 5), ArgFloat(a, 6), Paint(a, 7)); +} + +void Canvas_nativeDrawPath(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + const std::int32_t count = ArgInt(a, 4); + const float* ops = Floats(self, a, 3, std::max(count, 0)); + if (ops == nullptr || count < 0) return; + g_gfx->DrawPath(t, s, std::span(ops, static_cast(count)), ArgInt(a, 5) != 0, Paint(a, 6)); +} + +void Canvas_nativeDrawBitmap(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + NativeBitmap* src = Bmp(self, ArgLong(a, 3)); + if (src == nullptr) return; + const float sr[4] = {ArgFloat(a, 5), ArgFloat(a, 6), ArgFloat(a, 7), ArgFloat(a, 8)}; + const float dr[4] = {ArgFloat(a, 9), ArgFloat(a, 10), ArgFloat(a, 11), ArgFloat(a, 12)}; + g_gfx->DrawBitmap(t, s, src, sr, dr, ArgInt(a, 13), ArgInt(a, 14) != 0); +} + +void Canvas_nativeDrawText(Thread* self, Frame* a, JValue*) { + DrawState s; + NativeBitmap* t = Bmp(self, ArgLong(a, 0)); + if (t == nullptr || !State(self, a, 2, &s)) return; + const std::u16string text = Rt(self)->GetString(ArgRef(a, 3)); + PaintSpec p; + p.color = static_cast(ArgInt(a, 10)); + p.flags = ArgInt(a, 11); + p.shader = Shader(a, 12); + g_gfx->DrawText(t, s, text, ArgFloat(a, 4), ArgFloat(a, 5), Font(a, 6), p); +} + +// --- Paint ------------------------------------------------------------------------------ + +void Paint_nativeMeasureText(Thread* self, Frame* a, JValue* r) { + const std::u16string text = Rt(self)->GetString(ArgRef(a, 0)); + r->f = g_gfx->MeasureText(text, Font(a, 1)); +} + +void Paint_nativeGetTextWidths(Thread* self, Frame* a, JValue*) { + const std::u16string text = Rt(self)->GetString(ArgRef(a, 0)); + auto* out = static_cast(ArgRef(a, 5)); + if (out == nullptr || out->klass->descriptor != "[F" || out->length < static_cast(text.size())) { + Throw(self, "Ljava/lang/ArrayIndexOutOfBoundsException;", "widths array too short"); + return; + } + std::vector widths(text.size()); + g_gfx->TextWidths(text, Font(a, 1), widths.data()); + std::memcpy(out->data(), widths.data(), widths.size() * sizeof(float)); +} + +void Paint_nativeFontMetrics(Thread* self, Frame* a, JValue*) { + auto* out = static_cast(ArgRef(a, 4)); + if (out == nullptr || out->klass->descriptor != "[F" || out->length < 5) { + Throw(self, "Ljava/lang/IllegalArgumentException;", "font metrics array"); + return; + } + const FontMetrics m = g_gfx->Metrics(Font(a, 0)); + float* f = out->elements(); + f[0] = m.top; + f[1] = m.ascent; + f[2] = m.descent; + f[3] = m.bottom; + f[4] = m.leading; +} + +// --- Bitmap ----------------------------------------------------------------------------- + +void FreeBitmap(void* data) { g_gfx->Free(static_cast(data)); } + +void Bitmap_nativeCreate(Thread* self, Frame* a, JValue* r) { + std::string error; + NativeBitmap* b = g_gfx->CreateBitmap(ArgInt(a, 0), ArgInt(a, 1), ArgInt(a, 2) != 0, &error); + if (b == nullptr) { + Throw(self, "Ljava/lang/IllegalArgumentException;", error); + return; + } + r->j = Graphics::Handle(b); +} + +void Bitmap_nativeCopy(Thread* self, Frame* a, JValue* r) { + NativeBitmap* src = Bmp(self, ArgLong(a, 0)); + if (src == nullptr) return; + std::string error; + NativeBitmap* b = g_gfx->Copy(src, ArgInt(a, 2), ArgInt(a, 3), ArgInt(a, 4), ArgInt(a, 5), ArgInt(a, 6), + ArgInt(a, 7), ArgInt(a, 8) != 0, &error); + if (b == nullptr) { + Throw(self, "Ljava/lang/IllegalArgumentException;", error); + return; + } + r->j = Graphics::Handle(b); +} + +// Bitmap.nativeRegisterCleanup(Bitmap owner, long ptr, long bytes) +void Bitmap_nativeRegisterCleanup(Thread* self, Frame* a, JValue*) { + Object* owner = ArgRef(a, 0); + NativeBitmap* b = Bmp(self, ArgLong(a, 1)); + if (b == nullptr) return; + if (owner == nullptr || g_gfx->Pinned(b)) { + Throw(self, "Ljava/lang/IllegalArgumentException;", "bitmap cannot be owned"); + return; + } + // Ownership moves to the collector exactly once: pin it so no second + // registration (reflection) can double-free it. + g_gfx->Pin(b); + Rt(self)->heap().RegisterNativeAllocation(owner, FreeBitmap, b, + static_cast(std::max(0, ArgLong(a, 3)))); +} + +bool PixelArgs(Thread* self, Frame* a, NativeBitmap** b, Array** pixels) { + *b = Bmp(self, ArgLong(a, 0)); + if (*b == nullptr) return false; + *pixels = static_cast(ArgRef(a, 2)); + const std::int32_t offset = ArgInt(a, 3), stride = ArgInt(a, 4), w = ArgInt(a, 7), h = ArgInt(a, 8); + if (*pixels == nullptr || (*pixels)->klass->descriptor != "[I" || w < 0 || h < 0 || offset < 0 || stride < w) { + Throw(self, "Ljava/lang/IllegalArgumentException;", "pixel arguments"); + return false; + } + if (w == 0 || h == 0) return false; + const std::int64_t last = static_cast(offset) + static_cast(h - 1) * stride + w; + if (last > (*pixels)->length) { + Throw(self, "Ljava/lang/ArrayIndexOutOfBoundsException;", "pixels"); + return false; + } + return true; +} + +// Bitmap.nativeGetPixels(long ptr, int[] pixels, int offset, int stride, int x, int y, int w, int h) +void Bitmap_nativeGetPixels(Thread* self, Frame* a, JValue*) { + NativeBitmap* b; + Array* pixels; + if (!PixelArgs(self, a, &b, &pixels)) return; + if (!g_gfx->GetPixels(b, ArgInt(a, 5), ArgInt(a, 6), ArgInt(a, 7), ArgInt(a, 8), + pixels->elements() + ArgInt(a, 3), ArgInt(a, 4))) { + Throw(self, "Ljava/lang/IllegalArgumentException;", "pixel region out of range"); + } +} + +void Bitmap_nativeSetPixels(Thread* self, Frame* a, JValue*) { + NativeBitmap* b; + Array* pixels; + if (!PixelArgs(self, a, &b, &pixels)) return; + if (!g_gfx->SetPixels(b, ArgInt(a, 5), ArgInt(a, 6), ArgInt(a, 7), ArgInt(a, 8), + pixels->elements() + ArgInt(a, 3), ArgInt(a, 4))) { + Throw(self, "Ljava/lang/IllegalArgumentException;", "pixel region out of range"); + } +} + +void Bitmap_nativeErase(Thread* self, Frame* a, JValue*) { + NativeBitmap* b = Bmp(self, ArgLong(a, 0)); + if (b != nullptr) g_gfx->EraseColor(b, static_cast(ArgInt(a, 2))); +} + +void Bitmap_nativeHasAlpha(Thread* self, Frame* a, JValue* r) { + NativeBitmap* b = Bmp(self, ArgLong(a, 0)); + r->i = b != nullptr && g_gfx->HasAlpha(b) ? 1 : 0; +} + +void Bitmap_nativeSetHasAlpha(Thread* self, Frame* a, JValue*) { + NativeBitmap* b = Bmp(self, ArgLong(a, 0)); + if (b != nullptr) g_gfx->SetHasAlpha(b, ArgInt(a, 2) != 0); +} + +void Bitmap_nativeGeneration(Thread* self, Frame* a, JValue* r) { + NativeBitmap* b = Bmp(self, ArgLong(a, 0)); + r->j = b != nullptr ? static_cast(g_gfx->Generation(b)) : 0; +} + +void Bitmap_nativeCompress(Thread* self, Frame* a, JValue* r) { + r->l = nullptr; + NativeBitmap* b = Bmp(self, ArgLong(a, 0)); + if (b == nullptr) return; + std::string error; + std::vector data = g_gfx->Encode(b, ArgInt(a, 2), ArgInt(a, 3), &error); + if (data.empty()) return; + Runtime* rt = Rt(self); + Array* arr = rt->heap().AllocArray(self, rt->wk.byte_array, static_cast(data.size())); + if (arr == nullptr) return; + std::memcpy(arr->data(), data.data(), data.size()); + r->l = arr; +} + +// --- BitmapFactory ------------------------------------------------------------------------- + +std::span ByteRange(Thread* self, Frame* a, std::uint32_t reg) { + auto* arr = static_cast(ArgRef(a, reg)); + const std::int32_t off = ArgInt(a, reg + 1), len = ArgInt(a, reg + 2); + if (arr == nullptr || arr->klass->descriptor != "[B" || off < 0 || len < 0 || + static_cast(off) + len > arr->length) { + Throw(self, "Ljava/lang/ArrayIndexOutOfBoundsException;", "image data range"); + return {}; + } + return std::span(arr->data() + off, static_cast(len)); +} + +void BitmapFactory_nativeDecodeBounds(Thread* self, Frame* a, JValue* r) { + r->i = 0; + auto bytes = ByteRange(self, a, 0); + auto* out = static_cast(ArgRef(a, 3)); + if (bytes.empty() || out == nullptr || out->klass->descriptor != "[I" || out->length < 2) return; + int w = 0, h = 0; + if (!g_gfx->DecodeBounds(bytes, &w, &h)) return; + out->elements()[0] = w; + out->elements()[1] = h; + r->i = 1; +} + +void BitmapFactory_nativeDecode(Thread* self, Frame* a, JValue* r) { + r->j = 0; + auto bytes = ByteRange(self, a, 0); + if (bytes.empty()) return; + std::string error; + NativeBitmap* b = g_gfx->Decode(bytes, ArgInt(a, 3), ArgInt(a, 4), &error); + if (b != nullptr) r->j = Graphics::Handle(b); +} + +void DisplayMetrics_nativeDeviceDensity(Thread*, Frame*, JValue* r) { r->i = g_device_density; } + +} // namespace + +Status RegisterGraphicsNatives(Runtime* rt, int device_density) { + std::string error; + g_gfx = Graphics::Get(&error); + if (g_gfx == nullptr) return Status(ReasonCode::kHostOsError, "graphics: " + error); + g_device_density = device_density; + auto reg = [rt](const char* key, vm::NativeFn fn) { rt->RegisterNative(key, fn); }; + reg("Landroid/graphics/Canvas;.nativeClear(J[FIZ)V", Canvas_nativeClear); + reg("Landroid/graphics/Canvas;.nativeDrawRect(J[FFFFFFFIIFF[F)V", Canvas_nativeDrawRect); + reg("Landroid/graphics/Canvas;.nativeDrawOval(J[FFFFFIIFF[F)V", Canvas_nativeDrawOval); + reg("Landroid/graphics/Canvas;.nativeDrawArc(J[FFFFFFFZIIFF[F)V", Canvas_nativeDrawArc); + reg("Landroid/graphics/Canvas;.nativeDrawLine(J[FFFFFIIFF[F)V", Canvas_nativeDrawLine); + reg("Landroid/graphics/Canvas;.nativeDrawPath(J[F[FIZIIFF[F)V", Canvas_nativeDrawPath); + reg("Landroid/graphics/Canvas;.nativeDrawBitmap(J[FJFFFFFFFFIZ)V", Canvas_nativeDrawBitmap); + reg("Landroid/graphics/Canvas;.nativeDrawText(J[FLjava/lang/String;FFFIIZII[F)V", Canvas_nativeDrawText); + reg("Landroid/graphics/Paint;.nativeMeasureText(Ljava/lang/String;FIIZ)F", Paint_nativeMeasureText); + reg("Landroid/graphics/Paint;.nativeGetTextWidths(Ljava/lang/String;FIIZ[F)V", Paint_nativeGetTextWidths); + reg("Landroid/graphics/Paint;.nativeFontMetrics(FIIZ[F)V", Paint_nativeFontMetrics); + reg("Landroid/graphics/Bitmap;.nativeCreate(IIZ)J", Bitmap_nativeCreate); + reg("Landroid/graphics/Bitmap;.nativeCopy(JIIIIIIZ)J", Bitmap_nativeCopy); + reg("Landroid/graphics/Bitmap;.nativeRegisterCleanup(Landroid/graphics/Bitmap;JJ)V", Bitmap_nativeRegisterCleanup); + reg("Landroid/graphics/Bitmap;.nativeGetPixels(J[IIIIIII)V", Bitmap_nativeGetPixels); + reg("Landroid/graphics/Bitmap;.nativeSetPixels(J[IIIIIII)V", Bitmap_nativeSetPixels); + reg("Landroid/graphics/Bitmap;.nativeErase(JI)V", Bitmap_nativeErase); + reg("Landroid/graphics/Bitmap;.nativeHasAlpha(J)Z", Bitmap_nativeHasAlpha); + reg("Landroid/graphics/Bitmap;.nativeSetHasAlpha(JZ)V", Bitmap_nativeSetHasAlpha); + reg("Landroid/graphics/Bitmap;.nativeGeneration(J)J", Bitmap_nativeGeneration); + reg("Landroid/graphics/Bitmap;.nativeCompress(JII)[B", Bitmap_nativeCompress); + reg("Landroid/graphics/BitmapFactory;.nativeDecodeBounds([BII[I)Z", BitmapFactory_nativeDecodeBounds); + reg("Landroid/graphics/BitmapFactory;.nativeDecode([BIIII)J", BitmapFactory_nativeDecode); + reg("Landroid/util/DisplayMetrics;.nativeDeviceDensity()I", DisplayMetrics_nativeDeviceDensity); + return Status::Ok(); +} + +} // namespace arun::fw diff --git a/direct/vm/framework-host/src/graphics_natives.h b/direct/vm/framework-host/src/graphics_natives.h new file mode 100644 index 0000000..2f64eff --- /dev/null +++ b/direct/vm/framework-host/src/graphics_natives.h @@ -0,0 +1,15 @@ +// Registers the android.graphics natives (Canvas, Paint, Bitmap, BitmapFactory, +// DisplayMetrics.DENSITY_DEVICE_STABLE). Internal to arun_framework_host. +#pragma once + +#include "arun/status.h" + +namespace arun::vm { +class Runtime; +} + +namespace arun::fw { + +Status RegisterGraphicsNatives(vm::Runtime* rt, int device_density); + +} // namespace arun::fw \ No newline at end of file diff --git a/direct/vm/framework-host/src/render.cc b/direct/vm/framework-host/src/render.cc new file mode 100644 index 0000000..abacb68 --- /dev/null +++ b/direct/vm/framework-host/src/render.cc @@ -0,0 +1,1010 @@ +#include "render.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace arun::fw { +namespace { + +template +class Com { + public: + Com() = default; + Com(const Com& o) : p_(o.p_) { + if (p_ != nullptr) p_->AddRef(); + } + Com(Com&& o) noexcept : p_(o.p_) { o.p_ = nullptr; } + ~Com() { reset(); } + Com& operator=(Com o) noexcept { + std::swap(p_, o.p_); + return *this; + } + T* get() const { return p_; } + T* operator->() const { return p_; } + T** put() { + reset(); + return &p_; + } + void reset() { + if (p_ != nullptr) { + p_->Release(); + p_ = nullptr; + } + } + explicit operator bool() const { return p_ != nullptr; } + + private: + T* p_ = nullptr; +}; + +void EnsureCom() { + static thread_local bool done = false; + if (!done) { + // Multithreaded apartment: graphics objects are used from any thread. A + // thread that already chose another apartment keeps it. + (void)CoInitializeEx(nullptr, COINIT_MULTITHREADED); + done = true; + } +} + +std::string Hr(const char* what, HRESULT hr) { + return std::format("{} failed (HRESULT 0x{:08x})", what, static_cast(hr)); +} + +D2D1_COLOR_F ColorF(std::uint32_t argb, float alpha_scale) { + return D2D1_COLOR_F{((argb >> 16) & 0xff) / 255.0f, ((argb >> 8) & 0xff) / 255.0f, (argb & 0xff) / 255.0f, + ((argb >> 24) & 0xff) / 255.0f * alpha_scale}; +} + +std::uint32_t FloatBits(float f) { + std::uint32_t u; + std::memcpy(&u, &f, 4); + return u; +} + +bool Finite(std::initializer_list values) { + for (float v : values) { + if (!std::isfinite(v)) return false; + } + return true; +} + +} // namespace + +struct Upload { + ID2D1RenderTarget* target; + std::uint64_t generation; + Com bitmap; +}; + +struct NativeBitmap { + int width = 0; + int height = 0; + bool has_alpha = true; + bool pinned = false; + Com wic; + Com target; + Com brush; + bool drawing = false; + std::uint64_t generation = 1; + std::vector uploads; // this bitmap uploaded to other targets +}; + +struct Graphics::Impl { + std::mutex lock; + Com d2d; + Com dw; + Com wic; + Com fonts; + std::unordered_set live; + std::map, Com> formats; + std::map, DWRITE_FONT_METRICS> metrics; + std::map, Com> strokes; + + static const wchar_t* FamilyName(int family) { + return family == 1 ? L"Georgia" : family == 2 ? L"Consolas" : L"Segoe UI"; + } + + // --- targets ----------------------------------------------------------------- + + bool EnsureTarget(NativeBitmap* b) { + if (b->target) return true; + const D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), + 96.0f, 96.0f); + if (FAILED(d2d->CreateWicBitmapRenderTarget(b->wic.get(), props, b->target.put()))) return false; + b->target->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + return SUCCEEDED(b->target->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0, 1), b->brush.put())); + } + + // Starts (or continues) a drawing batch on `b`. + ID2D1RenderTarget* Begin(NativeBitmap* b) { + if (!EnsureTarget(b)) return nullptr; + if (!b->drawing) { + b->target->BeginDraw(); + b->drawing = true; + } + ++b->generation; + return b->target.get(); + } + + // Ends the drawing batch so the pixels can be read or used as a source. + void Flush(NativeBitmap* b) { + if (!b->drawing) return; + b->drawing = false; + const HRESULT hr = b->target->EndDraw(); + if (hr == D2DERR_RECREATE_TARGET) { + b->target.reset(); + b->brush.reset(); + } + } + + void DropUploadsFor(ID2D1RenderTarget* target) { + for (NativeBitmap* other : live) { + auto& u = other->uploads; + u.erase(std::remove_if(u.begin(), u.end(), [&](const Upload& x) { return x.target == target; }), u.end()); + } + } + + ID2D1Bitmap* UploadTo(NativeBitmap* src, ID2D1RenderTarget* target) { + Flush(src); + for (Upload& u : src->uploads) { + if (u.target == target) { + if (u.generation == src->generation) return u.bitmap.get(); + u.bitmap.reset(); + if (FAILED(target->CreateBitmapFromWicBitmap(src->wic.get(), nullptr, u.bitmap.put()))) return nullptr; + u.generation = src->generation; + return u.bitmap.get(); + } + } + Upload u{target, src->generation, {}}; + if (FAILED(target->CreateBitmapFromWicBitmap(src->wic.get(), nullptr, u.bitmap.put()))) return nullptr; + if (src->uploads.size() > 8) src->uploads.erase(src->uploads.begin()); + src->uploads.push_back(std::move(u)); + return src->uploads.back().bitmap.get(); + } + + // --- state --------------------------------------------------------------------- + + // Pushes the clip and transform; false if nothing can be drawn. + bool Apply(ID2D1RenderTarget* rt, const DrawState& s, bool antialias) { + if (s.clip[0] >= s.clip[2] || s.clip[1] >= s.clip[3] || s.alpha <= 0) return false; + if (!Finite({s.m[0], s.m[1], s.m[2], s.m[3], s.m[4], s.m[5]})) return false; + rt->SetTransform(D2D1::Matrix3x2F::Identity()); + rt->PushAxisAlignedClip(D2D1::RectF(static_cast(s.clip[0]), static_cast(s.clip[1]), + static_cast(s.clip[2]), static_cast(s.clip[3])), + D2D1_ANTIALIAS_MODE_ALIASED); + rt->SetTransform(D2D1::Matrix3x2F(s.m[0], s.m[1], s.m[2], s.m[3], s.m[4], s.m[5])); + rt->SetAntialiasMode(antialias ? D2D1_ANTIALIAS_MODE_PER_PRIMITIVE : D2D1_ANTIALIAS_MODE_ALIASED); + return true; + } + + static void Unapply(ID2D1RenderTarget* rt) { rt->PopAxisAlignedClip(); } + + // A brush for `p`: the target's solid brush, or a new gradient brush. + Com Brush(NativeBitmap* t, const DrawState& s, const PaintSpec& p) { + const float paint_alpha = ((p.color >> 24) & 0xff) / 255.0f; + if (p.shader.size() >= 9) { + // [kind, tile, n, m0..m5, geometry..., colors(n as int bits), positions(n)] + const int kind = static_cast(p.shader[0]); + const int tile = static_cast(p.shader[1]); + const int n = static_cast(p.shader[2]); + const std::size_t geom = kind == 1 ? 4 : 3; + if ((kind == 1 || kind == 2) && n >= 2 && n <= 64 && p.shader.size() >= 9 + geom + 2ull * n) { + std::vector stops(static_cast(n)); + const float* colors = p.shader.data() + 9 + geom; + const float* positions = colors + n; + for (int i = 0; i < n; ++i) { + stops[i].color = ColorF(FloatBits(colors[i]), 1.0f); + float pos = positions[i]; + if (!std::isfinite(pos) || pos < 0) pos = static_cast(i) / static_cast(n - 1); + stops[i].position = std::clamp(pos, 0.0f, 1.0f); + } + const D2D1_EXTEND_MODE extend = + tile == 1 ? D2D1_EXTEND_MODE_WRAP : tile == 2 ? D2D1_EXTEND_MODE_MIRROR : D2D1_EXTEND_MODE_CLAMP; + Com collection; + if (SUCCEEDED(t->target->CreateGradientStopCollection(stops.data(), static_cast(n), D2D1_GAMMA_2_2, + extend, collection.put()))) { + const float* g = p.shader.data() + 9; + const D2D1_BRUSH_PROPERTIES bp{paint_alpha * s.alpha, + D2D1::Matrix3x2F(p.shader[3], p.shader[4], p.shader[5], p.shader[6], + p.shader[7], p.shader[8])}; + if (kind == 1) { + Com lg; + if (SUCCEEDED(t->target->CreateLinearGradientBrush( + D2D1::LinearGradientBrushProperties(D2D1::Point2F(g[0], g[1]), D2D1::Point2F(g[2], g[3])), bp, + collection.get(), lg.put()))) { + Com out; + lg->QueryInterface(IID_PPV_ARGS(out.put())); + return out; + } + } else { + Com rg; + if (SUCCEEDED(t->target->CreateRadialGradientBrush( + D2D1::RadialGradientBrushProperties(D2D1::Point2F(g[0], g[1]), D2D1::Point2F(0, 0), g[2], g[2]), + bp, collection.get(), rg.put()))) { + Com out; + rg->QueryInterface(IID_PPV_ARGS(out.put())); + return out; + } + } + } + } + } + t->brush->SetColor(ColorF(p.color, s.alpha)); + t->brush->SetOpacity(1.0f); + Com out; + t->brush->QueryInterface(IID_PPV_ARGS(out.put())); + return out; + } + + ID2D1StrokeStyle* Stroke(const PaintSpec& p) { + const int cap = (p.flags >> kPaintCapShift) & 3; + const int join = (p.flags >> kPaintJoinShift) & 3; + const int miter = static_cast(std::clamp(p.miter, 1.0f, 100.0f) * 16); + auto key = std::make_tuple(cap, join, miter); + auto it = strokes.find(key); + if (it != strokes.end()) return it->second.get(); + const D2D1_CAP_STYLE c = cap == 1 ? D2D1_CAP_STYLE_ROUND : cap == 2 ? D2D1_CAP_STYLE_SQUARE : D2D1_CAP_STYLE_FLAT; + const D2D1_LINE_JOIN j = + join == 1 ? D2D1_LINE_JOIN_ROUND : join == 2 ? D2D1_LINE_JOIN_BEVEL : D2D1_LINE_JOIN_MITER_OR_BEVEL; + Com style; + d2d->CreateStrokeStyle(D2D1::StrokeStyleProperties(c, c, c, j, miter / 16.0f), nullptr, 0, style.put()); + ID2D1StrokeStyle* raw = style.get(); + strokes.emplace(key, std::move(style)); + return raw; + } + + static float StrokeWidth(const PaintSpec& p) { return p.stroke_width > 0 ? p.stroke_width : 1.0f; } + static int Style(const PaintSpec& p) { return (p.flags >> kPaintStyleShift) & 3; } + + // Fills and/or strokes `geometry` per the paint style. + void Paint(NativeBitmap* t, const DrawState& s, ID2D1Geometry* geometry, const PaintSpec& p) { + ID2D1RenderTarget* rt = t->target.get(); + Com brush = Brush(t, s, p); + if (!brush) return; + const int style = Style(p); + if (style == 0 || style == 2) rt->FillGeometry(geometry, brush.get()); + if (style == 1 || style == 2) rt->DrawGeometry(geometry, brush.get(), StrokeWidth(p), Stroke(p)); + } + + // --- text ------------------------------------------------------------------------ + + IDWriteTextFormat* Format(const FontSpec& f) { + const int size64 = std::clamp(static_cast(std::lround(f.size * 64)), 64, 2048 * 64); + const int weight = std::clamp(f.weight, 100, 900); + auto key = std::make_tuple(f.family, weight, f.italic ? 1 : 0, size64); + auto it = formats.find(key); + if (it != formats.end()) return it->second.get(); + Com format; + if (FAILED(dw->CreateTextFormat(FamilyName(f.family), nullptr, static_cast(weight), + f.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, size64 / 64.0f, L"en-us", format.put()))) { + return nullptr; + } + format->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); + if (formats.size() > 512) formats.clear(); + IDWriteTextFormat* raw = format.get(); + formats.emplace(key, std::move(format)); + return raw; + } + + Com Layout(std::u16string_view text, const FontSpec& f) { + Com layout; + IDWriteTextFormat* format = Format(f); + if (format == nullptr || text.size() > (1u << 20)) return layout; + dw->CreateTextLayout(reinterpret_cast(text.data()), static_cast(text.size()), format, + 1e6f, 1e6f, layout.put()); + return layout; + } + + DWRITE_FONT_METRICS FontMetricsFor(const FontSpec& f) { + const int weight = std::clamp(f.weight, 100, 900); + auto key = std::make_tuple(f.family, weight, f.italic ? 1 : 0); + auto it = metrics.find(key); + if (it != metrics.end()) return it->second; + DWRITE_FONT_METRICS m{}; + m.designUnitsPerEm = 2048; + m.ascent = 2210; + m.descent = 514; + UINT32 index = 0; + BOOL exists = FALSE; + Com family; + Com font; + if (fonts && SUCCEEDED(fonts->FindFamilyName(FamilyName(f.family), &index, &exists)) && exists && + SUCCEEDED(fonts->GetFontFamily(index, family.put())) && + SUCCEEDED(family->GetFirstMatchingFont(static_cast(weight), DWRITE_FONT_STRETCH_NORMAL, + f.italic ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL, + font.put()))) { + font->GetMetrics(&m); + } + metrics.emplace(key, m); + return m; + } +}; + +// --- Graphics ------------------------------------------------------------------------ + +Graphics* Graphics::Get(std::string* error) { + static std::mutex init_lock; + static Graphics* instance = nullptr; + static std::string init_error; + std::lock_guard hold(init_lock); + EnsureCom(); + if (instance != nullptr) return instance; + if (!init_error.empty()) { + *error = init_error; + return nullptr; + } + auto impl = std::make_unique(); + D2D1_FACTORY_OPTIONS options{}; + HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, __uuidof(ID2D1Factory), &options, + reinterpret_cast(impl->d2d.put())); + if (FAILED(hr)) { + init_error = Hr("D2D1CreateFactory", hr); + } else if (FAILED(hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), + reinterpret_cast(impl->dw.put())))) { + init_error = Hr("DWriteCreateFactory", hr); + } else if (FAILED(hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(impl->wic.put())))) { + init_error = Hr("CoCreateInstance(WICImagingFactory)", hr); + } else { + impl->dw->GetSystemFontCollection(impl->fonts.put(), FALSE); + } + if (!init_error.empty()) { + *error = init_error; + return nullptr; + } + // Deliberately leaked: bitmaps may be freed by the collector at any point + // until the process exits. + instance = new Graphics(); + instance->impl_ = std::move(impl); + return instance; +} + +NativeBitmap* Graphics::CreateBitmap(int width, int height, bool has_alpha, std::string* error) { + EnsureCom(); + if (width <= 0 || height <= 0 || width > 16384 || height > 16384) { + *error = std::format("bitmap size {}x{} out of range", width, height); + return nullptr; + } + std::lock_guard hold(impl_->lock); + auto b = std::make_unique(); + HRESULT hr = impl_->wic->CreateBitmap(static_cast(width), static_cast(height), + GUID_WICPixelFormat32bppPBGRA, WICBitmapCacheOnLoad, b->wic.put()); + if (FAILED(hr)) { + *error = Hr("IWICImagingFactory::CreateBitmap", hr); + return nullptr; + } + b->width = width; + b->height = height; + b->has_alpha = has_alpha; + NativeBitmap* raw = b.release(); + impl_->live.insert(raw); + return raw; +} + +NativeBitmap* Graphics::Decode(std::span bytes, int scale_w, int scale_h, std::string* error) { + EnsureCom(); + if (bytes.empty() || bytes.size() > (1u << 30)) { + *error = "no image data"; + return nullptr; + } + std::lock_guard hold(impl_->lock); + IWICImagingFactory* wic = impl_->wic.get(); + Com stream; + Com decoder; + Com frame; + HRESULT hr = wic->CreateStream(stream.put()); + if (SUCCEEDED(hr)) { + hr = stream->InitializeFromMemory(const_cast(bytes.data()), static_cast(bytes.size())); + } + if (SUCCEEDED(hr)) hr = wic->CreateDecoderFromStream(stream.get(), nullptr, WICDecodeMetadataCacheOnDemand, decoder.put()); + if (SUCCEEDED(hr)) hr = decoder->GetFrame(0, frame.put()); + UINT w = 0, h = 0; + if (SUCCEEDED(hr)) hr = frame->GetSize(&w, &h); + if (FAILED(hr) || w == 0 || h == 0 || w > 16384 || h > 16384) { + *error = FAILED(hr) ? Hr("image decoding", hr) : std::format("image size {}x{} out of range", w, h); + return nullptr; + } + bool has_alpha = true; + WICPixelFormatGUID format{}; + if (SUCCEEDED(frame->GetPixelFormat(&format))) { + Com info; + Com pixel; + BOOL transparency = TRUE; + if (SUCCEEDED(wic->CreateComponentInfo(format, info.put())) && + SUCCEEDED(info->QueryInterface(IID_PPV_ARGS(pixel.put()))) && + SUCCEEDED(pixel->SupportsTransparency(&transparency))) { + has_alpha = transparency != FALSE; + } + } + Com converter; + hr = wic->CreateFormatConverter(converter.put()); + if (SUCCEEDED(hr)) { + hr = converter->Initialize(frame.get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0, + WICBitmapPaletteTypeMedianCut); + } + IWICBitmapSource* source = converter.get(); + Com scaler; + if (SUCCEEDED(hr) && scale_w > 0 && scale_h > 0 && (static_cast(scale_w) != w || static_cast(scale_h) != h)) { + if (scale_w > 16384 || scale_h > 16384) { + *error = "scaled image too large"; + return nullptr; + } + hr = wic->CreateBitmapScaler(scaler.put()); + if (SUCCEEDED(hr)) { + hr = scaler->Initialize(converter.get(), static_cast(scale_w), static_cast(scale_h), + WICBitmapInterpolationModeFant); + } + source = scaler.get(); + w = static_cast(scale_w); + h = static_cast(scale_h); + } + auto b = std::make_unique(); + if (SUCCEEDED(hr)) hr = wic->CreateBitmapFromSource(source, WICBitmapCacheOnLoad, b->wic.put()); + if (FAILED(hr)) { + *error = Hr("image conversion", hr); + return nullptr; + } + b->width = static_cast(w); + b->height = static_cast(h); + b->has_alpha = has_alpha; + NativeBitmap* raw = b.release(); + impl_->live.insert(raw); + return raw; +} + +bool Graphics::DecodeBounds(std::span bytes, int* width, int* height) { + EnsureCom(); + if (bytes.empty()) return false; + std::lock_guard hold(impl_->lock); + Com stream; + Com decoder; + Com frame; + UINT w = 0, h = 0; + if (FAILED(impl_->wic->CreateStream(stream.put())) || + FAILED(stream->InitializeFromMemory(const_cast(bytes.data()), static_cast(bytes.size()))) || + FAILED(impl_->wic->CreateDecoderFromStream(stream.get(), nullptr, WICDecodeMetadataCacheOnDemand, + decoder.put())) || + FAILED(decoder->GetFrame(0, frame.put())) || FAILED(frame->GetSize(&w, &h))) { + return false; + } + *width = static_cast(w); + *height = static_cast(h); + return true; +} + +NativeBitmap* Graphics::Copy(NativeBitmap* src, int x, int y, int w, int h, int dst_w, int dst_h, bool filter, + std::string* error) { + EnsureCom(); + if (x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > src->width || y + h > src->height || dst_w <= 0 || dst_h <= 0 || + dst_w > 16384 || dst_h > 16384) { + *error = "bitmap region out of range"; + return nullptr; + } + std::lock_guard hold(impl_->lock); + impl_->Flush(src); + IWICImagingFactory* wic = impl_->wic.get(); + Com clipper; + HRESULT hr = wic->CreateBitmapClipper(clipper.put()); + const WICRect rect{x, y, w, h}; + if (SUCCEEDED(hr)) hr = clipper->Initialize(src->wic.get(), &rect); + IWICBitmapSource* source = clipper.get(); + Com scaler; + if (SUCCEEDED(hr) && (dst_w != w || dst_h != h)) { + hr = wic->CreateBitmapScaler(scaler.put()); + if (SUCCEEDED(hr)) { + hr = scaler->Initialize(clipper.get(), static_cast(dst_w), static_cast(dst_h), + filter ? WICBitmapInterpolationModeFant : WICBitmapInterpolationModeNearestNeighbor); + } + source = scaler.get(); + } + auto b = std::make_unique(); + if (SUCCEEDED(hr)) hr = wic->CreateBitmapFromSource(source, WICBitmapCacheOnLoad, b->wic.put()); + if (FAILED(hr)) { + *error = Hr("bitmap copy", hr); + return nullptr; + } + b->width = dst_w; + b->height = dst_h; + b->has_alpha = src->has_alpha; + NativeBitmap* raw = b.release(); + impl_->live.insert(raw); + return raw; +} + +void Graphics::Free(NativeBitmap* b) { + std::lock_guard hold(impl_->lock); + if (impl_->live.erase(b) == 0) return; + if (b->drawing) impl_->Flush(b); + if (b->target) impl_->DropUploadsFor(b->target.get()); + delete b; +} + +NativeBitmap* Graphics::Validate(std::int64_t handle) { + auto* b = reinterpret_cast(static_cast(handle)); + std::lock_guard hold(impl_->lock); + return impl_->live.count(b) != 0 ? b : nullptr; +} + +int Graphics::Width(NativeBitmap* b) { + std::lock_guard hold(impl_->lock); + return b->width; +} + +int Graphics::Height(NativeBitmap* b) { + std::lock_guard hold(impl_->lock); + return b->height; +} + +bool Graphics::HasAlpha(NativeBitmap* b) { + std::lock_guard hold(impl_->lock); + return b->has_alpha; +} + +void Graphics::SetHasAlpha(NativeBitmap* b, bool has_alpha) { + std::lock_guard hold(impl_->lock); + b->has_alpha = has_alpha; +} + +std::uint64_t Graphics::Generation(NativeBitmap* b) { + std::lock_guard hold(impl_->lock); + return b->generation; +} + +void Graphics::Pin(NativeBitmap* b) { + std::lock_guard hold(impl_->lock); + b->pinned = true; +} + +bool Graphics::Pinned(NativeBitmap* b) { + std::lock_guard hold(impl_->lock); + return b->pinned; +} + +bool Graphics::Resize(NativeBitmap* b, int width, int height, std::uint32_t argb, std::string* error) { + EnsureCom(); + if (width <= 0 || height <= 0 || width > 16384 || height > 16384) { + *error = "size out of range"; + return false; + } + { + std::lock_guard hold(impl_->lock); + impl_->Flush(b); + if (b->target) impl_->DropUploadsFor(b->target.get()); + b->target.reset(); + b->brush.reset(); + b->uploads.clear(); + Com fresh; + const HRESULT hr = impl_->wic->CreateBitmap(static_cast(width), static_cast(height), + GUID_WICPixelFormat32bppPBGRA, WICBitmapCacheOnLoad, fresh.put()); + if (FAILED(hr)) { + *error = Hr("IWICImagingFactory::CreateBitmap", hr); + return false; + } + b->wic = fresh; + b->width = width; + b->height = height; + ++b->generation; + } + return EraseColor(b, argb); +} + +namespace { + +// Runs `fn(bytes, stride)` with the bitmap's pixels locked. +template +bool WithPixels(IWICBitmap* wic, int width, int height, DWORD flags, Fn&& fn) { + const WICRect all{0, 0, width, height}; + Com lock; + if (FAILED(wic->Lock(&all, flags, lock.put()))) return false; + UINT stride = 0, size = 0; + BYTE* data = nullptr; + if (FAILED(lock->GetStride(&stride)) || FAILED(lock->GetDataPointer(&size, &data)) || data == nullptr) return false; + fn(data, stride); + return true; +} + +std::uint32_t Premultiply(std::uint32_t argb) { + const std::uint32_t a = argb >> 24; + if (a == 255) return argb; + if (a == 0) return 0; + auto ch = [&](int shift) { return ((((argb >> shift) & 0xff) * a + 127) / 255) << shift; }; + return (a << 24) | ch(16) | ch(8) | ch(0); +} + +std::uint32_t Unpremultiply(std::uint32_t p) { + const std::uint32_t a = p >> 24; + if (a == 255) return p; + if (a == 0) return 0; + auto ch = [&](int shift) { return std::min(255, (((p >> shift) & 0xff) * 255 + a / 2) / a) << shift; }; + return (a << 24) | ch(16) | ch(8) | ch(0); +} + +} // namespace + +bool Graphics::GetPixels(NativeBitmap* b, int x, int y, int w, int h, std::uint32_t* out, int stride) { + std::lock_guard hold(impl_->lock); + if (x < 0 || y < 0 || w < 0 || h < 0 || x + w > b->width || y + h > b->height) return false; + impl_->Flush(b); + return WithPixels(b->wic.get(), b->width, b->height, WICBitmapLockRead, [&](BYTE* data, UINT row_bytes) { + for (int row = 0; row < h; ++row) { + const auto* src = reinterpret_cast(data + static_cast(y + row) * row_bytes) + x; + std::uint32_t* dst = out + static_cast(row) * stride; + for (int col = 0; col < w; ++col) { + const std::uint32_t p = Unpremultiply(src[col]); + dst[col] = b->has_alpha ? p : (p | 0xff000000u); + } + } + }); +} + +bool Graphics::SetPixels(NativeBitmap* b, int x, int y, int w, int h, const std::uint32_t* in, int stride) { + std::lock_guard hold(impl_->lock); + if (x < 0 || y < 0 || w < 0 || h < 0 || x + w > b->width || y + h > b->height) return false; + impl_->Flush(b); + ++b->generation; + return WithPixels(b->wic.get(), b->width, b->height, WICBitmapLockWrite, [&](BYTE* data, UINT row_bytes) { + for (int row = 0; row < h; ++row) { + auto* dst = reinterpret_cast(data + static_cast(y + row) * row_bytes) + x; + const std::uint32_t* src = in + static_cast(row) * stride; + for (int col = 0; col < w; ++col) { + dst[col] = Premultiply(b->has_alpha ? src[col] : (src[col] | 0xff000000u)); + } + } + }); +} + +bool Graphics::EraseColor(NativeBitmap* b, std::uint32_t argb) { + std::lock_guard hold(impl_->lock); + impl_->Flush(b); + ++b->generation; + const std::uint32_t p = Premultiply(b->has_alpha ? argb : (argb | 0xff000000u)); + return WithPixels(b->wic.get(), b->width, b->height, WICBitmapLockWrite, [&](BYTE* data, UINT row_bytes) { + for (int row = 0; row < b->height; ++row) { + auto* dst = reinterpret_cast(data + static_cast(row) * row_bytes); + std::fill(dst, dst + b->width, p); + } + }); +} + +bool Graphics::CopyOpaque(NativeBitmap* b, std::vector* out) { + std::lock_guard hold(impl_->lock); + impl_->Flush(b); + out->resize(static_cast(b->width) * static_cast(b->height)); + return WithPixels(b->wic.get(), b->width, b->height, WICBitmapLockRead, [&](BYTE* data, UINT row_bytes) { + for (int row = 0; row < b->height; ++row) { + const auto* src = reinterpret_cast(data + static_cast(row) * row_bytes); + std::uint32_t* dst = out->data() + static_cast(row) * static_cast(b->width); + for (int col = 0; col < b->width; ++col) dst[col] = src[col] | 0xff000000u; + } + }); +} + +std::vector Graphics::Encode(NativeBitmap* b, int format, int quality, std::string* error) { + EnsureCom(); + std::lock_guard hold(impl_->lock); + impl_->Flush(b); + IWICImagingFactory* wic = impl_->wic.get(); + Com stream; + Com encoder; + Com frame; + Com props; + Com converter; + const bool jpeg = format == 0; + HRESULT hr = CreateStreamOnHGlobal(nullptr, TRUE, stream.put()); + if (SUCCEEDED(hr)) hr = wic->CreateEncoder(jpeg ? GUID_ContainerFormatJpeg : GUID_ContainerFormatPng, nullptr, encoder.put()); + if (SUCCEEDED(hr)) hr = encoder->Initialize(stream.get(), WICBitmapEncoderNoCache); + if (SUCCEEDED(hr)) hr = encoder->CreateNewFrame(frame.put(), props.put()); + if (SUCCEEDED(hr) && jpeg && props) { + PROPBAG2 option{}; + option.pstrName = const_cast(L"ImageQuality"); + VARIANT v; + VariantInit(&v); + v.vt = VT_R4; + v.fltVal = std::clamp(quality, 0, 100) / 100.0f; + props->Write(1, &option, &v); + } + if (SUCCEEDED(hr)) hr = frame->Initialize(props.get()); + if (SUCCEEDED(hr)) hr = frame->SetSize(static_cast(b->width), static_cast(b->height)); + WICPixelFormatGUID pf = jpeg ? GUID_WICPixelFormat24bppBGR : GUID_WICPixelFormat32bppBGRA; + if (SUCCEEDED(hr)) hr = frame->SetPixelFormat(&pf); + if (SUCCEEDED(hr)) hr = wic->CreateFormatConverter(converter.put()); + if (SUCCEEDED(hr)) { + hr = converter->Initialize(b->wic.get(), pf, WICBitmapDitherTypeNone, nullptr, 0, WICBitmapPaletteTypeCustom); + } + if (SUCCEEDED(hr)) hr = frame->WriteSource(converter.get(), nullptr); + if (SUCCEEDED(hr)) hr = frame->Commit(); + if (SUCCEEDED(hr)) hr = encoder->Commit(); + std::vector out; + HGLOBAL global = nullptr; + if (SUCCEEDED(hr)) hr = GetHGlobalFromStream(stream.get(), &global); + if (SUCCEEDED(hr)) { + STATSTG stat{}; + stream->Stat(&stat, STATFLAG_NONAME); + const void* data = GlobalLock(global); + if (data != nullptr) { + const auto n = static_cast(stat.cbSize.QuadPart); + out.assign(static_cast(data), static_cast(data) + n); + GlobalUnlock(global); + } + } + if (FAILED(hr)) *error = Hr("image encoding", hr); + return out; +} + +std::string Graphics::WritePng(NativeBitmap* b, const std::filesystem::path& path) { + std::string error; + std::vector png = Encode(b, 1, 100, &error); + if (png.empty()) return error.empty() ? "PNG encoding failed" : error; + HANDLE f = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (f == INVALID_HANDLE_VALUE) return std::format("cannot create the file ({})", GetLastError()); + DWORD written = 0; + const BOOL ok = WriteFile(f, png.data(), static_cast(png.size()), &written, nullptr); + CloseHandle(f); + if (!ok || written != png.size()) return "cannot write the file"; + return {}; +} + +// --- drawing ------------------------------------------------------------------------------ + +void Graphics::Clear(NativeBitmap* t, const DrawState& s, std::uint32_t argb, bool src_mode) { + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr || s.clip[0] >= s.clip[2] || s.clip[1] >= s.clip[3]) return; + rt->SetTransform(D2D1::Matrix3x2F::Identity()); + const D2D1_RECT_F clip = D2D1::RectF(static_cast(s.clip[0]), static_cast(s.clip[1]), + static_cast(s.clip[2]), static_cast(s.clip[3])); + rt->PushAxisAlignedClip(clip, D2D1_ANTIALIAS_MODE_ALIASED); + if (src_mode) { + rt->Clear(ColorF(argb, s.alpha)); + } else { + t->brush->SetColor(ColorF(argb, s.alpha)); + rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + rt->FillRectangle(clip, t->brush.get()); + } + rt->PopAxisAlignedClip(); +} + +void Graphics::DrawRect(NativeBitmap* t, const DrawState& s, float l, float top, float r, float b, float rx, float ry, + const PaintSpec& p) { + if (!Finite({l, top, r, b, rx, ry})) return; + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr || !impl_->Apply(rt, s, (p.flags & kPaintAntiAlias) != 0)) return; + Com brush = impl_->Brush(t, s, p); + if (brush) { + const D2D1_RECT_F rect = D2D1::RectF(l, top, r, b); + const int style = Impl::Style(p); + if (rx > 0 && ry > 0) { + const D2D1_ROUNDED_RECT rr{rect, rx, ry}; + if (style == 0 || style == 2) rt->FillRoundedRectangle(rr, brush.get()); + if (style == 1 || style == 2) rt->DrawRoundedRectangle(rr, brush.get(), Impl::StrokeWidth(p), impl_->Stroke(p)); + } else { + if (style == 0 || style == 2) rt->FillRectangle(rect, brush.get()); + if (style == 1 || style == 2) rt->DrawRectangle(rect, brush.get(), Impl::StrokeWidth(p), impl_->Stroke(p)); + } + } + Impl::Unapply(rt); +} + +void Graphics::DrawOval(NativeBitmap* t, const DrawState& s, float l, float top, float r, float b, const PaintSpec& p) { + if (!Finite({l, top, r, b})) return; + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr || !impl_->Apply(rt, s, (p.flags & kPaintAntiAlias) != 0)) return; + Com brush = impl_->Brush(t, s, p); + if (brush) { + const D2D1_ELLIPSE e{D2D1::Point2F((l + r) / 2, (top + b) / 2), (r - l) / 2, (b - top) / 2}; + const int style = Impl::Style(p); + if (style == 0 || style == 2) rt->FillEllipse(e, brush.get()); + if (style == 1 || style == 2) rt->DrawEllipse(e, brush.get(), Impl::StrokeWidth(p), impl_->Stroke(p)); + } + Impl::Unapply(rt); +} + +void Graphics::DrawArc(NativeBitmap* t, const DrawState& s, float l, float top, float r, float b, float start, + float sweep, bool use_center, const PaintSpec& p) { + if (!Finite({l, top, r, b, start, sweep}) || sweep == 0) return; + if (sweep >= 360 || sweep <= -360) { + if (!use_center) { + DrawOval(t, s, l, top, r, b, p); + return; + } + sweep = sweep > 0 ? 359.99f : -359.99f; + } + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr || !impl_->Apply(rt, s, (p.flags & kPaintAntiAlias) != 0)) return; + const float cx = (l + r) / 2, cy = (top + b) / 2, rx = (r - l) / 2, ry = (b - top) / 2; + auto point = [&](float deg) { + const float rad = deg * std::numbers::pi_v / 180.0f; + return D2D1::Point2F(cx + rx * std::cos(rad), cy + ry * std::sin(rad)); + }; + Com geometry; + Com sink; + if (SUCCEEDED(impl_->d2d->CreatePathGeometry(geometry.put())) && SUCCEEDED(geometry->Open(sink.put()))) { + if (use_center) { + sink->BeginFigure(D2D1::Point2F(cx, cy), D2D1_FIGURE_BEGIN_FILLED); + sink->AddLine(point(start)); + } else { + sink->BeginFigure(point(start), D2D1_FIGURE_BEGIN_FILLED); + } + sink->AddArc(D2D1::ArcSegment(point(start + sweep), D2D1::SizeF(rx, ry), 0, + sweep > 0 ? D2D1_SWEEP_DIRECTION_CLOCKWISE : D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE, + std::fabs(sweep) > 180 ? D2D1_ARC_SIZE_LARGE : D2D1_ARC_SIZE_SMALL)); + sink->EndFigure(use_center ? D2D1_FIGURE_END_CLOSED : D2D1_FIGURE_END_OPEN); + if (SUCCEEDED(sink->Close())) impl_->Paint(t, s, geometry.get(), p); + } + Impl::Unapply(rt); +} + +void Graphics::DrawLine(NativeBitmap* t, const DrawState& s, float x0, float y0, float x1, float y1, + const PaintSpec& p) { + if (!Finite({x0, y0, x1, y1})) return; + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr || !impl_->Apply(rt, s, (p.flags & kPaintAntiAlias) != 0)) return; + Com brush = impl_->Brush(t, s, p); + if (brush) rt->DrawLine(D2D1::Point2F(x0, y0), D2D1::Point2F(x1, y1), brush.get(), Impl::StrokeWidth(p), impl_->Stroke(p)); + Impl::Unapply(rt); +} + +void Graphics::DrawPath(NativeBitmap* t, const DrawState& s, std::span ops, bool even_odd, + const PaintSpec& p) { + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr || !impl_->Apply(rt, s, (p.flags & kPaintAntiAlias) != 0)) return; + Com geometry; + Com sink; + if (SUCCEEDED(impl_->d2d->CreatePathGeometry(geometry.put())) && SUCCEEDED(geometry->Open(sink.put()))) { + sink->SetFillMode(even_odd ? D2D1_FILL_MODE_ALTERNATE : D2D1_FILL_MODE_WINDING); + bool open = false; + D2D1_POINT_2F last{0, 0}; + std::size_t i = 0; + bool bad = false; + auto need = [&](std::size_t n) { + if (i + n > ops.size()) bad = true; + return !bad; + }; + auto ensure_open = [&]() { + if (!open) { + sink->BeginFigure(last, D2D1_FIGURE_BEGIN_FILLED); + open = true; + } + }; + while (i < ops.size() && !bad) { + const int verb = static_cast(ops[i++]); + switch (verb) { + case 0: + if (!need(2)) break; + if (open) sink->EndFigure(D2D1_FIGURE_END_OPEN); + open = false; + last = D2D1::Point2F(ops[i], ops[i + 1]); + i += 2; + break; + case 1: + if (!need(2)) break; + ensure_open(); + last = D2D1::Point2F(ops[i], ops[i + 1]); + sink->AddLine(last); + i += 2; + break; + case 2: + if (!need(4)) break; + ensure_open(); + sink->AddQuadraticBezier(D2D1::QuadraticBezierSegment(D2D1::Point2F(ops[i], ops[i + 1]), + D2D1::Point2F(ops[i + 2], ops[i + 3]))); + last = D2D1::Point2F(ops[i + 2], ops[i + 3]); + i += 4; + break; + case 3: + if (!need(6)) break; + ensure_open(); + sink->AddBezier(D2D1::BezierSegment(D2D1::Point2F(ops[i], ops[i + 1]), D2D1::Point2F(ops[i + 2], ops[i + 3]), + D2D1::Point2F(ops[i + 4], ops[i + 5]))); + last = D2D1::Point2F(ops[i + 4], ops[i + 5]); + i += 6; + break; + case 4: + if (open) sink->EndFigure(D2D1_FIGURE_END_CLOSED); + open = false; + break; + default: + bad = true; + break; + } + } + if (open) sink->EndFigure(D2D1_FIGURE_END_OPEN); + if (SUCCEEDED(sink->Close()) && !bad) impl_->Paint(t, s, geometry.get(), p); + } + Impl::Unapply(rt); +} + +void Graphics::DrawBitmap(NativeBitmap* t, const DrawState& s, NativeBitmap* src, const float srcr[4], + const float dst[4], int alpha, bool filter) { + if (src == t || !Finite({srcr[0], srcr[1], srcr[2], srcr[3], dst[0], dst[1], dst[2], dst[3]})) return; + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr) return; + ID2D1Bitmap* bmp = impl_->UploadTo(src, rt); + if (bmp == nullptr || !impl_->Apply(rt, s, true)) return; + const D2D1_RECT_F s_rect = D2D1::RectF(srcr[0], srcr[1], srcr[2], srcr[3]); + rt->DrawBitmap(bmp, D2D1::RectF(dst[0], dst[1], dst[2], dst[3]), std::clamp(alpha, 0, 255) / 255.0f * s.alpha, + filter ? D2D1_BITMAP_INTERPOLATION_MODE_LINEAR : D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, + &s_rect); + Impl::Unapply(rt); +} + +void Graphics::DrawText(NativeBitmap* t, const DrawState& s, std::u16string_view text, float x, float y, + const FontSpec& f, const PaintSpec& p) { + if (text.empty() || !Finite({x, y, f.size})) return; + std::lock_guard hold(impl_->lock); + ID2D1RenderTarget* rt = impl_->Begin(t); + if (rt == nullptr) return; + Com layout = impl_->Layout(text, f); + if (!layout || !impl_->Apply(rt, s, true)) return; + const DWRITE_TEXT_RANGE all{0, static_cast(text.size())}; + if (p.flags & kPaintUnderline) layout->SetUnderline(TRUE, all); + if (p.flags & kPaintStrikeThru) layout->SetStrikethrough(TRUE, all); + DWRITE_LINE_METRICS line{}; + UINT32 lines = 0; + layout->GetLineMetrics(&line, 1, &lines); + Com brush = impl_->Brush(t, s, p); + if (brush) rt->DrawTextLayout(D2D1::Point2F(x, y - line.baseline), layout.get(), brush.get()); + Impl::Unapply(rt); +} + +// --- text ----------------------------------------------------------------------------------- + +float Graphics::MeasureText(std::u16string_view text, const FontSpec& f) { + if (text.empty()) return 0; + std::lock_guard hold(impl_->lock); + Com layout = impl_->Layout(text, f); + if (!layout) return 0; + DWRITE_TEXT_METRICS m{}; + layout->GetMetrics(&m); + return m.widthIncludingTrailingWhitespace; +} + +void Graphics::TextWidths(std::u16string_view text, const FontSpec& f, float* out) { + std::fill(out, out + text.size(), 0.0f); + if (text.empty()) return; + std::lock_guard hold(impl_->lock); + Com layout = impl_->Layout(text, f); + if (!layout) return; + UINT32 count = 0; + layout->GetClusterMetrics(nullptr, 0, &count); + std::vector clusters(count); + if (count == 0 || FAILED(layout->GetClusterMetrics(clusters.data(), count, &count))) return; + std::size_t pos = 0; + for (const DWRITE_CLUSTER_METRICS& c : clusters) { + if (pos >= text.size()) break; + out[pos] = c.width; + pos += c.length; + } +} + +FontMetrics Graphics::Metrics(const FontSpec& f) { + std::lock_guard hold(impl_->lock); + const DWRITE_FONT_METRICS m = impl_->FontMetricsFor(f); + const float scale = f.size / static_cast(m.designUnitsPerEm != 0 ? m.designUnitsPerEm : 2048); + FontMetrics out{}; + out.ascent = -static_cast(m.ascent) * scale; + out.descent = static_cast(m.descent) * scale; + out.top = out.ascent; + out.bottom = out.descent; + out.leading = static_cast(m.lineGap) * scale; + return out; +} + +} // namespace arun::fw diff --git a/direct/vm/framework-host/src/render.h b/direct/vm/framework-host/src/render.h new file mode 100644 index 0000000..304f657 --- /dev/null +++ b/direct/vm/framework-host/src/render.h @@ -0,0 +1,137 @@ +// Rendering for the framework host: Direct2D over WIC bitmaps, text through +// DirectWrite, image decoding and encoding through WIC. Internal to +// arun_framework_host. +// +// Every pixel buffer -- the window's surface and every android.graphics.Bitmap +// -- is a NativeBitmap (32bpp premultiplied BGRA). Java holds them as opaque +// handles; every handle crossing from Java is validated against the live set +// before use, so a forged handle is an exception, never a wild pointer. +// +// Ownership: Graphics owns every NativeBitmap until Free. Bitmaps owned by a +// Java object are freed by the collector (Heap::RegisterNativeAllocation). +// Thread affinity: thread-safe; one lock serialises all drawing. +// Error propagation: false/nullptr plus an error string. +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace arun::fw { + +struct NativeBitmap; + +// The canvas state a draw call runs under: an affine matrix in D2D order +// (m11 m12 m21 m22 dx dy), a device-space clip rectangle, a global alpha and +// the antialiasing flag. +struct DrawState { + float m[6] = {1, 0, 0, 1, 0, 0}; + int clip[4] = {0, 0, 0, 0}; + float alpha = 1; +}; + +// A paint as the Java side packs it. +struct PaintSpec { + std::uint32_t color = 0xff000000; + int flags = 0; // kPaint* below + float stroke_width = 0; + float miter = 4; + std::span shader; // empty: solid color +}; +constexpr int kPaintAntiAlias = 0x1; +constexpr int kPaintStyleShift = 8; // 0 fill, 1 stroke, 2 fill and stroke +constexpr int kPaintCapShift = 10; // 0 butt, 1 round, 2 square +constexpr int kPaintJoinShift = 12; // 0 miter, 1 round, 2 bevel +constexpr int kPaintUnderline = 0x10000; +constexpr int kPaintStrikeThru = 0x20000; + +struct FontSpec { + float size = 14; + int weight = 400; + int family = 0; // 0 sans-serif, 1 serif, 2 monospace + bool italic = false; +}; + +struct FontMetrics { + float top, ascent, descent, bottom, leading; +}; + +class Graphics { + public: + // The process's graphics; nullptr (with `error`) if Direct2D is unavailable. + static Graphics* Get(std::string* error); + + // --- bitmaps ------------------------------------------------------------- + NativeBitmap* CreateBitmap(int width, int height, bool has_alpha, std::string* error); + // Decodes any image WIC can read. `max_w`/`max_h` > 0 scale the result to + // exactly that size (density scaling, inSampleSize). + NativeBitmap* Decode(std::span bytes, int scale_w, int scale_h, std::string* error); + // Image size without decoding pixels; false if not an image. + bool DecodeBounds(std::span bytes, int* width, int* height); + NativeBitmap* Copy(NativeBitmap* src, int x, int y, int w, int h, int dst_w, int dst_h, bool filter, + std::string* error); + void Free(NativeBitmap* b); + // `handle` as a live bitmap, or nullptr. + NativeBitmap* Validate(std::int64_t handle); + static std::int64_t Handle(NativeBitmap* b) { return reinterpret_cast(b); } + + int Width(NativeBitmap* b); + int Height(NativeBitmap* b); + bool HasAlpha(NativeBitmap* b); + void SetHasAlpha(NativeBitmap* b, bool has_alpha); + std::uint64_t Generation(NativeBitmap* b); + // A pinned bitmap belongs to the host (the window surface): Java may draw + // into it but never takes ownership of it. + void Pin(NativeBitmap* b); + bool Pinned(NativeBitmap* b); + // Resizes (contents are lost; filled with `argb`). + bool Resize(NativeBitmap* b, int width, int height, std::uint32_t argb, std::string* error); + + // Unpremultiplied ARGB access. + bool GetPixels(NativeBitmap* b, int x, int y, int w, int h, std::uint32_t* out, int stride); + bool SetPixels(NativeBitmap* b, int x, int y, int w, int h, const std::uint32_t* in, int stride); + bool EraseColor(NativeBitmap* b, std::uint32_t argb); + // Opaque top-down 0xAARRGGBB copy (window presentation). + bool CopyOpaque(NativeBitmap* b, std::vector* out); + // format: 0 JPEG, 1 PNG. Empty on failure. + std::vector Encode(NativeBitmap* b, int format, int quality, std::string* error); + std::string WritePng(NativeBitmap* b, const std::filesystem::path& path); + + // --- drawing ------------------------------------------------------------- + void Clear(NativeBitmap* t, const DrawState& s, std::uint32_t argb, bool src_mode); + void DrawRect(NativeBitmap* t, const DrawState& s, float l, float top, float r, float b, float rx, float ry, + const PaintSpec& p); + void DrawOval(NativeBitmap* t, const DrawState& s, float l, float top, float r, float b, const PaintSpec& p); + void DrawArc(NativeBitmap* t, const DrawState& s, float l, float top, float r, float b, float start, float sweep, + bool use_center, const PaintSpec& p); + void DrawLine(NativeBitmap* t, const DrawState& s, float x0, float y0, float x1, float y1, const PaintSpec& p); + // Path ops: 0 move(x,y) 1 line(x,y) 2 quad(x1,y1,x2,y2) 3 cubic(x1..y3) 4 close. + void DrawPath(NativeBitmap* t, const DrawState& s, std::span ops, bool even_odd, const PaintSpec& p); + void DrawBitmap(NativeBitmap* t, const DrawState& s, NativeBitmap* src, const float srcr[4], const float dst[4], + int alpha, bool filter); + void DrawText(NativeBitmap* t, const DrawState& s, std::u16string_view text, float x, float y, + const FontSpec& f, const PaintSpec& p); + + // --- text ---------------------------------------------------------------- + float MeasureText(std::u16string_view text, const FontSpec& f); + // Advance of every UTF-16 unit (0 for the trailing units of a cluster). + void TextWidths(std::u16string_view text, const FontSpec& f, float* out); + FontMetrics Metrics(const FontSpec& f); + + private: + Graphics() = default; + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace arun::fw diff --git a/direct/vm/framework-host/src/resources_host.cc b/direct/vm/framework-host/src/resources_host.cc new file mode 100644 index 0000000..1028a5d --- /dev/null +++ b/direct/vm/framework-host/src/resources_host.cc @@ -0,0 +1,386 @@ +#include "resources_host.h" + +#include +#include + +#include "arun/axml.h" +#include "arun/res_string_pool.h" +#include "arun/vm/runtime.h" +#include "arun/zip_reader.h" + +namespace arun::fw { + +using vm::Array; +using vm::ArgInt; +using vm::ArgRef; +using vm::Frame; +using vm::JValue; +using vm::Object; +using vm::RootScope; +using vm::Runtime; +using vm::RtClass; +using vm::ScopedBlocking; +using vm::Thread; + +namespace { + +constexpr std::uint64_t kMaxResourceFileBytes = 64ull << 20; +constexpr int kEventInts = 6; +constexpr int kAttrInts = 6; + +ResourcesHost* g_res = nullptr; + +Runtime* Rt(Thread* self) { return self->runtime(); } + +// An int[] argument of at least `min` elements, or nullptr with an exception +// pending (null is allowed when `nullable`). +Array* IntArray(Thread* self, Frame* a, std::uint32_t reg, std::int32_t min, bool nullable = false) { + auto* arr = static_cast(ArgRef(a, reg)); + if (arr == nullptr) { + if (!nullable) Rt(self)->ThrowNew(self, "Ljava/lang/NullPointerException;", "int[] argument"); + return nullptr; + } + if (arr->klass->descriptor != "[I" || arr->length < min) { + Rt(self)->ThrowNew(self, "Ljava/lang/IllegalArgumentException;", "int[] argument too short"); + return nullptr; + } + return arr; +} + +Array* NewIntArray(Thread* self, const std::vector& values) { + Runtime* rt = Rt(self); + RtClass* int_array = rt->class_linker().FindClass(self, "[I", nullptr); + if (int_array == nullptr) return nullptr; + Array* arr = rt->heap().AllocArray(self, int_array, static_cast(values.size())); + if (arr != nullptr && !values.empty()) std::memcpy(arr->data(), values.data(), values.size() * 4); + return arr; +} + +void ThrowStatus(Thread* self, const Status& s) { + Rt(self)->ThrowNew(self, "Ljava/lang/RuntimeException;", std::string(s.message())); +} + +void WriteResolved(const res::Resolved& v, std::int32_t* out) { + out[0] = v.value.type; + out[1] = static_cast(v.value.data); + out[2] = v.cookie; + out[3] = static_cast(v.resid); + out[4] = static_cast(v.spec_flags); + out[5] = v.density; + out[6] = 0; +} + +// The XML attributes passed as three parallel int[] (ids, types, data). +bool XmlAttrs(Thread* self, Frame* a, std::uint32_t reg, std::vector* out) { + out->clear(); + auto* ids = static_cast(ArgRef(a, reg)); + if (ids == nullptr) return true; + Array* types = IntArray(self, a, reg + 1, ids->length); + Array* data = IntArray(self, a, reg + 2, ids->length); + if (IntArray(self, a, reg, 0) == nullptr || types == nullptr || data == nullptr) return false; + for (std::int32_t i = 0; i < ids->length; ++i) { + res::XmlAttr x; + x.resource_id = static_cast(ids->elements()[i]); + x.value.type = static_cast(types->elements()[i]); + x.value.data = static_cast(data->elements()[i]); + out->push_back(x); + } + return true; +} + +// AssetManager.nativeSetConfiguration(mcc, mnc, locale, orientation, touchscreen, +// density, keyboard, keyboardHidden, navigation, screenWidth, screenHeight, +// smallestScreenWidthDp, screenWidthDp, screenHeightDp, screenLayout, uiMode, +// colorMode, majorVersion) +void AssetManager_nativeSetConfiguration(Thread* self, Frame* a, JValue*) { + res::Config c; + c.mcc = static_cast(ArgInt(a, 0)); + c.mnc = static_cast(ArgInt(a, 1)); + const std::string locale = Rt(self)->GetStringUtf8(ArgRef(a, 2)); + const std::size_t sep = locale.find_first_of("-_"); + c.language = locale.substr(0, sep); + if (sep != std::string::npos) { + c.country = locale.substr(sep + 1); + if (const std::size_t more = c.country.find_first_of("-_"); more != std::string::npos) c.country.resize(more); + } + c.orientation = static_cast(ArgInt(a, 3)); + c.touchscreen = static_cast(ArgInt(a, 4)); + c.density = static_cast(ArgInt(a, 5)); + c.keyboard = static_cast(ArgInt(a, 6)); + c.input_flags = static_cast(ArgInt(a, 7) & 0x03); + c.navigation = static_cast(ArgInt(a, 8)); + c.screen_width = static_cast(ArgInt(a, 9)); + c.screen_height = static_cast(ArgInt(a, 10)); + c.smallest_screen_width_dp = static_cast(ArgInt(a, 11)); + c.screen_width_dp = static_cast(ArgInt(a, 12)); + c.screen_height_dp = static_cast(ArgInt(a, 13)); + c.screen_layout = static_cast(ArgInt(a, 14)); + c.ui_mode = static_cast(ArgInt(a, 15)); + c.color_mode = static_cast(ArgInt(a, 16)); + c.sdk_version = static_cast(ArgInt(a, 17)); + g_res->assets().SetConfig(c); +} + +// AssetManager.nativeGetResourceValue(int resid, int density, boolean resolve, int[] out7) +void AssetManager_nativeGetResourceValue(Thread* self, Frame* a, JValue* r) { + r->i = 0; + Array* out = IntArray(self, a, 3, res::kStyleNumEntries); + if (out == nullptr) return; + auto v = g_res->assets().GetResource(static_cast(ArgInt(a, 0)), true, + static_cast(ArgInt(a, 1))); + if (!v.ok()) return ThrowStatus(self, v.status()); + if (!*v) return; + res::Resolved value = **v; + if (ArgInt(a, 2) != 0) { + if (Status s = g_res->assets().ResolveReference(&value); !s.ok()) return ThrowStatus(self, s); + } + WriteResolved(value, out->elements()); + r->i = 1; +} + +// AssetManager.nativeGetBag(int resid): [spec_flags, count, (key, type, data, cookie)*] or null. +void AssetManager_nativeGetBag(Thread* self, Frame* a, JValue* r) { + r->l = nullptr; + auto bag = g_res->assets().GetBag(static_cast(ArgInt(a, 0))); + if (!bag.ok()) return ThrowStatus(self, bag.status()); + if (*bag == nullptr) return; + std::vector v; + v.reserve(2 + 4 * (*bag)->entries.size()); + v.push_back(static_cast((*bag)->spec_flags)); + v.push_back(static_cast((*bag)->entries.size())); + for (const res::BagEntry& e : (*bag)->entries) { + v.push_back(static_cast(e.key)); + v.push_back(e.value.type); + v.push_back(static_cast(e.value.data)); + v.push_back(e.cookie); + } + r->l = NewIntArray(self, v); +} + +void AssetManager_nativeGetResourceName(Thread* self, Frame* a, JValue* r) { + r->l = nullptr; + auto name = g_res->assets().ResourceName(static_cast(ArgInt(a, 0))); + if (name) r->l = Rt(self)->NewStringUtf8(self, *name); +} + +void AssetManager_nativeGetIdentifier(Thread* self, Frame* a, JValue* r) { + Runtime* rt = Rt(self); + r->i = static_cast(g_res->assets().Identifier( + rt->GetStringUtf8(ArgRef(a, 0)), rt->GetStringUtf8(ArgRef(a, 1)), rt->GetStringUtf8(ArgRef(a, 2)))); +} + +void AssetManager_nativeGetPooledString(Thread* self, Frame* a, JValue* r) { + r->l = nullptr; + auto s = g_res->assets().PooledString(ArgInt(a, 0), static_cast(ArgInt(a, 1))); + if (!s.ok()) return ThrowStatus(self, s.status()); + r->l = Rt(self)->NewString(self, *s); +} + +// AssetManager.nativeThemeCreate(int[] styleAndForcePairs) +void AssetManager_nativeThemeCreate(Thread* self, Frame* a, JValue* r) { + Array* arr = IntArray(self, a, 0, 0); + if (arr == nullptr) return; + std::vector> styles; + for (std::int32_t i = 0; i + 1 < arr->length; i += 2) { + styles.emplace_back(static_cast(arr->elements()[i]), + arr->elements()[i + 1] != 0); + } + auto id = g_res->assets().Theme(styles); + if (!id.ok()) return ThrowStatus(self, id.status()); + r->i = *id; +} + +// AssetManager.nativeThemeGetAttribute(int theme, int attr, boolean resolve, int[] out7) +void AssetManager_nativeThemeGetAttribute(Thread* self, Frame* a, JValue* r) { + r->i = 0; + Array* out = IntArray(self, a, 3, res::kStyleNumEntries); + if (out == nullptr) return; + auto v = g_res->assets().ThemeAttribute(ArgInt(a, 0), static_cast(ArgInt(a, 1))); + if (!v.ok()) return ThrowStatus(self, v.status()); + if (!*v) return; + res::Resolved value = **v; + if (ArgInt(a, 2) != 0) { + if (Status s = g_res->assets().ResolveReference(&value); !s.ok()) return ThrowStatus(self, s); + } + WriteResolved(value, out->elements()); + r->i = 1; +} + +// AssetManager.nativeApplyStyle(theme, xmlIds, xmlTypes, xmlData, styleType, styleData, +// defStyleAttr, defStyleRes, attrs, outData, outIndices) +void AssetManager_nativeApplyStyle(Thread* self, Frame* a, JValue*) { + std::vector xml; + if (!XmlAttrs(self, a, 1, &xml)) return; + Array* attrs = IntArray(self, a, 8, 0); + if (attrs == nullptr) return; + Array* out_data = IntArray(self, a, 9, attrs->length * res::kStyleNumEntries); + Array* out_indices = IntArray(self, a, 10, attrs->length + 1); + if (out_data == nullptr || out_indices == nullptr) return; + std::vector ids(attrs->elements(), attrs->elements() + attrs->length); + std::vector data, indices; + const res::Value style{static_cast(ArgInt(a, 4)), static_cast(ArgInt(a, 5))}; + Status s = g_res->assets().ApplyStyle(ArgInt(a, 0), xml, style, static_cast(ArgInt(a, 6)), + static_cast(ArgInt(a, 7)), ids, &data, &indices); + if (!s.ok()) return ThrowStatus(self, s); + std::memcpy(out_data->data(), data.data(), data.size() * 4); + std::memcpy(out_indices->data(), indices.data(), indices.size() * 4); +} + +// AssetManager.nativeRetrieveAttributes(xmlIds, xmlTypes, xmlData, attrs, outData, outIndices) +void AssetManager_nativeRetrieveAttributes(Thread* self, Frame* a, JValue*) { + std::vector xml; + if (!XmlAttrs(self, a, 0, &xml)) return; + Array* attrs = IntArray(self, a, 3, 0); + if (attrs == nullptr) return; + Array* out_data = IntArray(self, a, 4, attrs->length * res::kStyleNumEntries); + Array* out_indices = IntArray(self, a, 5, attrs->length + 1); + if (out_data == nullptr || out_indices == nullptr) return; + std::vector ids(attrs->elements(), attrs->elements() + attrs->length); + std::vector data, indices; + Status s = g_res->assets().RetrieveAttributes(xml, ids, &data, &indices); + if (!s.ok()) return ThrowStatus(self, s); + std::memcpy(out_data->data(), data.data(), data.size() * 4); + std::memcpy(out_indices->data(), indices.data(), indices.size() * 4); +} + +// The bytes of `path` inside the package of `cookie`, or nullopt with an +// exception pending. +std::optional> ReadResourceFile(Thread* self, std::int32_t cookie, const std::string& path) { + Runtime* rt = Rt(self); + const pkg::ZipReader* zip = g_res->apk(cookie); + const pkg::ZipEntry* e = zip == nullptr ? nullptr : zip->Find(path); + if (e == nullptr || e->is_directory) { + rt->ThrowNew(self, "Ljava/io/FileNotFoundException;", path); + return std::nullopt; + } + if (e->uncompressed_size > kMaxResourceFileBytes) { + rt->ThrowNew(self, "Ljava/io/IOException;", std::format("{} is too large", path)); + return std::nullopt; + } + std::optional>> bytes; + { + ScopedBlocking blocking(self); + bytes.emplace(zip->Extract(*e)); + } + if (!bytes->ok()) { + rt->ThrowNew(self, "Ljava/io/IOException;", std::format("{}: {}", path, bytes->status().message())); + return std::nullopt; + } + return std::move(**bytes); +} + +// AssetManager.nativeOpenNonAsset(int cookie, String path) -> byte[] +void AssetManager_nativeOpenNonAsset(Thread* self, Frame* a, JValue* r) { + r->l = nullptr; + auto bytes = ReadResourceFile(self, ArgInt(a, 0), Rt(self)->GetStringUtf8(ArgRef(a, 1))); + if (!bytes) return; + Runtime* rt = Rt(self); + Array* arr = rt->heap().AllocArray(self, rt->wk.byte_array, static_cast(bytes->size())); + if (arr == nullptr) return; + if (!bytes->empty()) std::memcpy(arr->data(), bytes->data(), bytes->size()); + r->l = arr; +} + +// AssetManager.nativeOpenXmlBlock(int cookie, String path) -> +// Object[] {int[] events (6 per event), int[] attributes (6 per attribute), String[] strings} +void AssetManager_nativeOpenXmlBlock(Thread* self, Frame* a, JValue* r) { + r->l = nullptr; + Runtime* rt = Rt(self); + const std::string path = rt->GetStringUtf8(ArgRef(a, 1)); + auto bytes = ReadResourceFile(self, ArgInt(a, 0), path); + if (!bytes) return; + auto doc = axml::ParseEvents(*bytes); + if (!doc.ok()) { + rt->ThrowNew(self, "Lorg/xmlpull/v1/XmlPullParserException;", std::format("{}: {}", path, doc.status().message())); + return; + } + std::vector events, attrs; + events.reserve(doc->events.size() * kEventInts); + for (const axml::Event& e : doc->events) { + events.insert(events.end(), {static_cast(e.kind), static_cast(e.line), + static_cast(e.ns), static_cast(e.name), + static_cast(e.first_attribute), + static_cast(e.attribute_count)}); + } + attrs.reserve(doc->attributes.size() * kAttrInts); + for (const axml::EventAttribute& at : doc->attributes) { + attrs.insert(attrs.end(), {static_cast(at.ns), static_cast(at.name), + static_cast(at.resource_id), static_cast(at.raw), + static_cast(at.type), static_cast(at.data)}); + } + RootScope roots(self); + auto result = roots.Add(rt->heap().AllocArray(self, rt->wk.object_array, 3)); + if (result.Get() == nullptr) return; + Object* ev = NewIntArray(self, events); + if (ev == nullptr) return; + static_cast(result.Get())->elements()[0] = ev; + Object* at = NewIntArray(self, attrs); + if (at == nullptr) return; + static_cast(result.Get())->elements()[1] = at; + auto strings = roots.Add(rt->heap().AllocArray(self, rt->wk.string_array, static_cast(doc->pool.size()))); + if (strings.Get() == nullptr) return; + static_cast(result.Get())->elements()[2] = strings.Get(); + std::u16string s; + for (std::uint32_t i = 0; i < doc->pool.size(); ++i) { + if (Status st = doc->pool.Get16(i, &s); !st.ok()) { + rt->ThrowNew(self, "Lorg/xmlpull/v1/XmlPullParserException;", std::format("{}: {}", path, st.message())); + return; + } + Object* str = rt->NewString(self, s); + if (str == nullptr) return; + static_cast(strings.Get())->elements()[i] = str; + } + r->l = result.Get(); +} + +} // namespace + +Result> ResourcesHost::Create(std::shared_ptr framework, + std::shared_ptr app) { + if (framework == nullptr) return Status(ReasonCode::kInvalidArgument, "no framework resource package"); + std::unique_ptr h(new ResourcesHost()); + h->apks_[0] = std::move(framework); + h->apks_[1] = std::move(app); + for (int i = 0; i < 2; ++i) { + if (h->apks_[i] == nullptr) continue; + const pkg::ZipEntry* e = h->apks_[i]->Find("resources.arsc"); + if (e == nullptr) { + if (i == 0) return Status(ReasonCode::kInvalidArgument, "the framework package has no resources.arsc"); + continue; // an application without resources + } + ARUN_ASSIGN_OR_RETURN(std::vector bytes, h->apks_[i]->Extract(*e)); + ARUN_ASSIGN_OR_RETURN(h->tables_[i], res::ResourceTable::Load(bytes)); + ARUN_ASSIGN_OR_RETURN(std::int32_t cookie, h->assets_.AddTable(h->tables_[i].get())); + if (cookie != i) return Status(ReasonCode::kInternal, "unexpected resource cookie"); + } + return h; +} + +const pkg::ZipReader* ResourcesHost::apk(int cookie) const { + return cookie >= 0 && cookie < 2 ? apks_[cookie].get() : nullptr; +} + +void ResourcesHost::RegisterNatives(Runtime* rt) { + g_res = this; + auto reg = [rt](const char* key, vm::NativeFn fn) { rt->RegisterNative(key, fn); }; + reg("Landroid/content/res/AssetManager;.nativeSetConfiguration(IILjava/lang/String;IIIIIIIIIIIIIII)V", + AssetManager_nativeSetConfiguration); + reg("Landroid/content/res/AssetManager;.nativeGetResourceValue(IIZ[I)Z", AssetManager_nativeGetResourceValue); + reg("Landroid/content/res/AssetManager;.nativeGetBag(I)[I", AssetManager_nativeGetBag); + reg("Landroid/content/res/AssetManager;.nativeGetResourceName(I)Ljava/lang/String;", + AssetManager_nativeGetResourceName); + reg("Landroid/content/res/AssetManager;.nativeGetIdentifier(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + AssetManager_nativeGetIdentifier); + reg("Landroid/content/res/AssetManager;.nativeGetPooledString(II)Ljava/lang/String;", + AssetManager_nativeGetPooledString); + reg("Landroid/content/res/AssetManager;.nativeThemeCreate([I)I", AssetManager_nativeThemeCreate); + reg("Landroid/content/res/AssetManager;.nativeThemeGetAttribute(IIZ[I)Z", AssetManager_nativeThemeGetAttribute); + reg("Landroid/content/res/AssetManager;.nativeApplyStyle(I[I[I[IIIII[I[I[I)V", AssetManager_nativeApplyStyle); + reg("Landroid/content/res/AssetManager;.nativeRetrieveAttributes([I[I[I[I[I[I)V", + AssetManager_nativeRetrieveAttributes); + reg("Landroid/content/res/AssetManager;.nativeOpenNonAsset(ILjava/lang/String;)[B", AssetManager_nativeOpenNonAsset); + reg("Landroid/content/res/AssetManager;.nativeOpenXmlBlock(ILjava/lang/String;)[Ljava/lang/Object;", + AssetManager_nativeOpenXmlBlock); +} + +} // namespace arun::fw diff --git a/direct/vm/framework-host/src/resources_host.h b/direct/vm/framework-host/src/resources_host.h new file mode 100644 index 0000000..4108e27 --- /dev/null +++ b/direct/vm/framework-host/src/resources_host.h @@ -0,0 +1,40 @@ +// Natives behind android.content.res.AssetManager: the process's resource +// tables (framework package 0x01 = cookie 0, application package 0x7f = +// cookie 1), compiled XML blocks and non-asset files (res/...). Internal to +// arun_framework_host. +#pragma once + +#include + +#include "arun/asset_set.h" +#include "arun/res_table.h" +#include "arun/status.h" + +namespace arun::pkg { +class ZipReader; +} +namespace arun::vm { +class Runtime; +} + +namespace arun::fw { + +class ResourcesHost { + public: + // Loads resources.arsc from both packages. The application may have none; + // the framework package must. + static Result> Create(std::shared_ptr framework, + std::shared_ptr app); + void RegisterNatives(vm::Runtime* rt); + + res::AssetSet& assets() { return assets_; } + const pkg::ZipReader* apk(int cookie) const; + + private: + ResourcesHost() = default; + std::shared_ptr apks_[2]; + std::unique_ptr tables_[2]; + res::AssetSet assets_; +}; + +} // namespace arun::fw diff --git a/direct/vm/managed/CMakeLists.txt b/direct/vm/managed/CMakeLists.txt new file mode 100644 index 0000000..b810c3c --- /dev/null +++ b/direct/vm/managed/CMakeLists.txt @@ -0,0 +1,37 @@ +add_library(arun_managed STATIC + src/dex_file.cc + src/dex_instruction.cc + src/class.cc + src/class_linker.cc + src/heap.cc + src/monitor.cc + src/thread.cc + src/interpreter.cc + src/runtime.cc + src/natives_lang.cc + src/natives_reflect.cc + src/natives_thread.cc + src/natives_io.cc +) +target_include_directories(arun_managed PUBLIC include) +# icu.lib: the ICU that ships with Windows 10 1903+ (Unicode character data for +# java.lang.Character). bcrypt.lib: CSPRNG seeds (spec 11). +target_link_libraries(arun_managed PUBLIC arun_diagnostics arun_vfs arun_package icu.lib bcrypt.lib) + +# The boot class path (ADR 0004): the in-tree core library and framework, +# compiled with javac + d8 at build time. The runner loads bin/boot/arun-boot.dex. +file(GLOB_RECURSE ARUN_BOOT_JAVA CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/java/core/*.java + ${CMAKE_CURRENT_SOURCE_DIR}/java/framework/*.java + ${CMAKE_CURRENT_SOURCE_DIR}/framework-res/*) +set(ARUN_BOOT_DEX ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/boot/arun-boot.dex) +add_custom_command( + OUTPUT ${ARUN_BOOT_DEX} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/boot/arun-framework-res.apk + COMMAND powershell -NoProfile -ExecutionPolicy Bypass + -File ${AXRB_VM_DIR}/tools/build-bootclasspath.ps1 + -OutDir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/boot + DEPENDS ${ARUN_BOOT_JAVA} ${AXRB_VM_DIR}/tools/build-bootclasspath.ps1 + ${AXRB_VM_DIR}/tools/build-framework-res.ps1 ${AXRB_VM_DIR}/tools/toolchain.json + COMMENT "Building the framework resources (aapt2) and the boot class path (javac + d8)" + VERBATIM) +add_custom_target(arun_boot_dex DEPENDS ${ARUN_BOOT_DEX}) diff --git a/direct/vm/managed/framework-res/AndroidManifest.xml b/direct/vm/managed/framework-res/AndroidManifest.xml new file mode 100644 index 0000000..248d6b3 --- /dev/null +++ b/direct/vm/managed/framework-res/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/direct/vm/managed/framework-res/res/anim/accelerate_decelerate_interpolator.xml b/direct/vm/managed/framework-res/res/anim/accelerate_decelerate_interpolator.xml new file mode 100644 index 0000000..1306cec --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/accelerate_decelerate_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/accelerate_interpolator.xml b/direct/vm/managed/framework-res/res/anim/accelerate_interpolator.xml new file mode 100644 index 0000000..a7679f1 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/accelerate_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/anticipate_interpolator.xml b/direct/vm/managed/framework-res/res/anim/anticipate_interpolator.xml new file mode 100644 index 0000000..23ea239 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/anticipate_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/anticipate_overshoot_interpolator.xml b/direct/vm/managed/framework-res/res/anim/anticipate_overshoot_interpolator.xml new file mode 100644 index 0000000..9ac2555 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/anticipate_overshoot_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/bounce_interpolator.xml b/direct/vm/managed/framework-res/res/anim/bounce_interpolator.xml new file mode 100644 index 0000000..8d9161f --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/bounce_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/cycle_interpolator.xml b/direct/vm/managed/framework-res/res/anim/cycle_interpolator.xml new file mode 100644 index 0000000..9fda329 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/cycle_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/decelerate_interpolator.xml b/direct/vm/managed/framework-res/res/anim/decelerate_interpolator.xml new file mode 100644 index 0000000..d1ed594 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/decelerate_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/fade_in.xml b/direct/vm/managed/framework-res/res/anim/fade_in.xml new file mode 100644 index 0000000..6fdf467 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/fade_in.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/fade_out.xml b/direct/vm/managed/framework-res/res/anim/fade_out.xml new file mode 100644 index 0000000..29a4b80 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/fade_out.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/linear_interpolator.xml b/direct/vm/managed/framework-res/res/anim/linear_interpolator.xml new file mode 100644 index 0000000..61597b6 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/linear_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/overshoot_interpolator.xml b/direct/vm/managed/framework-res/res/anim/overshoot_interpolator.xml new file mode 100644 index 0000000..d1c9b38 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/overshoot_interpolator.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/anim/slide_in_left.xml b/direct/vm/managed/framework-res/res/anim/slide_in_left.xml new file mode 100644 index 0000000..d5cf75b --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/slide_in_left.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/anim/slide_out_right.xml b/direct/vm/managed/framework-res/res/anim/slide_out_right.xml new file mode 100644 index 0000000..e14d6f5 --- /dev/null +++ b/direct/vm/managed/framework-res/res/anim/slide_out_right.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/btn_colored_material_tint.xml b/direct/vm/managed/framework-res/res/color/btn_colored_material_tint.xml new file mode 100644 index 0000000..d2404e0 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/btn_colored_material_tint.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/btn_default_material_tint.xml b/direct/vm/managed/framework-res/res/color/btn_default_material_tint.xml new file mode 100644 index 0000000..8ee1696 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/btn_default_material_tint.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/control_activated_material.xml b/direct/vm/managed/framework-res/res/color/control_activated_material.xml new file mode 100644 index 0000000..5bffb81 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/control_activated_material.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/control_checkable_material.xml b/direct/vm/managed/framework-res/res/color/control_checkable_material.xml new file mode 100644 index 0000000..16a33ab --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/control_checkable_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/color/control_normal_material.xml b/direct/vm/managed/framework-res/res/color/control_normal_material.xml new file mode 100644 index 0000000..11058c0 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/control_normal_material.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/edit_text_underline_material.xml b/direct/vm/managed/framework-res/res/color/edit_text_underline_material.xml new file mode 100644 index 0000000..0dee2bd --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/edit_text_underline_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/color/list_divider_material.xml b/direct/vm/managed/framework-res/res/color/list_divider_material.xml new file mode 100644 index 0000000..81618a3 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/list_divider_material.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/color/primary_text_dark.xml b/direct/vm/managed/framework-res/res/color/primary_text_dark.xml new file mode 100644 index 0000000..ffff13a --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/primary_text_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/primary_text_light.xml b/direct/vm/managed/framework-res/res/color/primary_text_light.xml new file mode 100644 index 0000000..ebed384 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/primary_text_light.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/progress_track_material.xml b/direct/vm/managed/framework-res/res/color/progress_track_material.xml new file mode 100644 index 0000000..5475f7a --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/progress_track_material.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/color/secondary_progress_material.xml b/direct/vm/managed/framework-res/res/color/secondary_progress_material.xml new file mode 100644 index 0000000..3622383 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/secondary_progress_material.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/color/secondary_text_dark.xml b/direct/vm/managed/framework-res/res/color/secondary_text_dark.xml new file mode 100644 index 0000000..d9bb7e9 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/secondary_text_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/secondary_text_light.xml b/direct/vm/managed/framework-res/res/color/secondary_text_light.xml new file mode 100644 index 0000000..6e783ce --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/secondary_text_light.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/switch_thumb_material.xml b/direct/vm/managed/framework-res/res/color/switch_thumb_material.xml new file mode 100644 index 0000000..a35dfbd --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/switch_thumb_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/color/switch_track_material.xml b/direct/vm/managed/framework-res/res/color/switch_track_material.xml new file mode 100644 index 0000000..094b938 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/switch_track_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/color/tab_indicator_text.xml b/direct/vm/managed/framework-res/res/color/tab_indicator_text.xml new file mode 100644 index 0000000..d2c6049 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/tab_indicator_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/tertiary_text_dark.xml b/direct/vm/managed/framework-res/res/color/tertiary_text_dark.xml new file mode 100644 index 0000000..d9bb7e9 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/tertiary_text_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/tertiary_text_light.xml b/direct/vm/managed/framework-res/res/color/tertiary_text_light.xml new file mode 100644 index 0000000..6e783ce --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/tertiary_text_light.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/color/widget_edittext_dark.xml b/direct/vm/managed/framework-res/res/color/widget_edittext_dark.xml new file mode 100644 index 0000000..78804e7 --- /dev/null +++ b/direct/vm/managed/framework-res/res/color/widget_edittext_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/activated_background_material.xml b/direct/vm/managed/framework-res/res/drawable/activated_background_material.xml new file mode 100644 index 0000000..1b5f87d --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/activated_background_material.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/arrow_down_float.xml b/direct/vm/managed/framework-res/res/drawable/arrow_down_float.xml new file mode 100644 index 0000000..dce3e33 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/arrow_down_float.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/arrow_up_float.xml b/direct/vm/managed/framework-res/res/drawable/arrow_up_float.xml new file mode 100644 index 0000000..67f6c79 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/arrow_up_float.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/btn_borderless_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_borderless_material.xml new file mode 100644 index 0000000..b29e1af --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_borderless_material.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/btn_check_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_check_material.xml new file mode 100644 index 0000000..b111ac5 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_check_material.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/btn_check_off_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_check_off_material.xml new file mode 100644 index 0000000..474d666 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_check_off_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/btn_check_on_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_check_on_material.xml new file mode 100644 index 0000000..8519b11 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_check_on_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/btn_colored_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_colored_material.xml new file mode 100644 index 0000000..e3511e9 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_colored_material.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/btn_default.xml b/direct/vm/managed/framework-res/res/drawable/btn_default.xml new file mode 100644 index 0000000..33be9d6 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_default.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/btn_default_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_default_material.xml new file mode 100644 index 0000000..f743583 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_default_material.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/btn_radio_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_radio_material.xml new file mode 100644 index 0000000..36bd6fa --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_radio_material.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/btn_radio_off_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_radio_off_material.xml new file mode 100644 index 0000000..fecdf09 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_radio_off_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/btn_radio_on_material.xml b/direct/vm/managed/framework-res/res/drawable/btn_radio_on_material.xml new file mode 100644 index 0000000..88b5c3f --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_radio_on_material.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/btn_star_big_off.xml b/direct/vm/managed/framework-res/res/drawable/btn_star_big_off.xml new file mode 100644 index 0000000..ed19264 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_star_big_off.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/btn_star_big_on.xml b/direct/vm/managed/framework-res/res/drawable/btn_star_big_on.xml new file mode 100644 index 0000000..b663dc9 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/btn_star_big_on.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/dialog_background_material.xml b/direct/vm/managed/framework-res/res/drawable/dialog_background_material.xml new file mode 100644 index 0000000..a4ab3ae --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/dialog_background_material.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/divider_horizontal_bright.xml b/direct/vm/managed/framework-res/res/drawable/divider_horizontal_bright.xml new file mode 100644 index 0000000..086f479 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/divider_horizontal_bright.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/divider_horizontal_dark.xml b/direct/vm/managed/framework-res/res/drawable/divider_horizontal_dark.xml new file mode 100644 index 0000000..90679c0 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/divider_horizontal_dark.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/edit_text.xml b/direct/vm/managed/framework-res/res/drawable/edit_text.xml new file mode 100644 index 0000000..b440af3 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/edit_text.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/edit_text_material.xml b/direct/vm/managed/framework-res/res/drawable/edit_text_material.xml new file mode 100644 index 0000000..ac7526e --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/edit_text_material.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/ic_ab_back_material.xml b/direct/vm/managed/framework-res/res/drawable/ic_ab_back_material.xml new file mode 100644 index 0000000..beca8bd --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_ab_back_material.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_delete.xml b/direct/vm/managed/framework-res/res/drawable/ic_delete.xml new file mode 100644 index 0000000..f065597 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_delete.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_dialog_alert.xml b/direct/vm/managed/framework-res/res/drawable/ic_dialog_alert.xml new file mode 100644 index 0000000..f3b3a7c --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_dialog_alert.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_dialog_info.xml b/direct/vm/managed/framework-res/res/drawable/ic_dialog_info.xml new file mode 100644 index 0000000..fe190c1 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_dialog_info.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_info.xml b/direct/vm/managed/framework-res/res/drawable/ic_info.xml new file mode 100644 index 0000000..fe190c1 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_info.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_input_add.xml b/direct/vm/managed/framework-res/res/drawable/ic_input_add.xml new file mode 100644 index 0000000..76d0fb0 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_input_add.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_input_delete.xml b/direct/vm/managed/framework-res/res/drawable/ic_input_delete.xml new file mode 100644 index 0000000..8288640 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_input_delete.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_media_next.xml b/direct/vm/managed/framework-res/res/drawable/ic_media_next.xml new file mode 100644 index 0000000..72175e2 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_media_next.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_media_pause.xml b/direct/vm/managed/framework-res/res/drawable/ic_media_pause.xml new file mode 100644 index 0000000..63ded07 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_media_pause.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_media_play.xml b/direct/vm/managed/framework-res/res/drawable/ic_media_play.xml new file mode 100644 index 0000000..d4543b5 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_media_play.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_media_previous.xml b/direct/vm/managed/framework-res/res/drawable/ic_media_previous.xml new file mode 100644 index 0000000..d181794 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_media_previous.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_add.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_add.xml new file mode 100644 index 0000000..76d0fb0 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_add.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_close_clear_cancel.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_close_clear_cancel.xml new file mode 100644 index 0000000..2b3f4e1 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_close_clear_cancel.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_delete.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_delete.xml new file mode 100644 index 0000000..f065597 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_delete.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_edit.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_edit.xml new file mode 100644 index 0000000..03cf2e3 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_edit.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_info_details.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_info_details.xml new file mode 100644 index 0000000..fe190c1 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_info_details.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_more.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_more.xml new file mode 100644 index 0000000..5a0c430 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_more.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_save.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_save.xml new file mode 100644 index 0000000..9b702c8 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_save.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_search.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_search.xml new file mode 100644 index 0000000..c27e85b --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_search.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/ic_menu_send.xml b/direct/vm/managed/framework-res/res/drawable/ic_menu_send.xml new file mode 100644 index 0000000..fd0f4e2 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/ic_menu_send.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/item_background_borderless_material.xml b/direct/vm/managed/framework-res/res/drawable/item_background_borderless_material.xml new file mode 100644 index 0000000..1c436ba --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/item_background_borderless_material.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/item_background_material.xml b/direct/vm/managed/framework-res/res/drawable/item_background_material.xml new file mode 100644 index 0000000..7f30370 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/item_background_material.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/list_divider_material.xml b/direct/vm/managed/framework-res/res/drawable/list_divider_material.xml new file mode 100644 index 0000000..deaf5f5 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/list_divider_material.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/list_selector_background.xml b/direct/vm/managed/framework-res/res/drawable/list_selector_background.xml new file mode 100644 index 0000000..6b9354a --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/list_selector_background.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/progress_horizontal.xml b/direct/vm/managed/framework-res/res/drawable/progress_horizontal.xml new file mode 100644 index 0000000..e13cc00 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/progress_horizontal.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/progress_horizontal_material.xml b/direct/vm/managed/framework-res/res/drawable/progress_horizontal_material.xml new file mode 100644 index 0000000..0fba764 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/progress_horizontal_material.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/progress_medium_material.xml b/direct/vm/managed/framework-res/res/drawable/progress_medium_material.xml new file mode 100644 index 0000000..21e3392 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/progress_medium_material.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/screen_background_dark.xml b/direct/vm/managed/framework-res/res/drawable/screen_background_dark.xml new file mode 100644 index 0000000..67d37e0 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/screen_background_dark.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/screen_background_dark_transparent.xml b/direct/vm/managed/framework-res/res/drawable/screen_background_dark_transparent.xml new file mode 100644 index 0000000..ef5c209 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/screen_background_dark_transparent.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/screen_background_light.xml b/direct/vm/managed/framework-res/res/drawable/screen_background_light.xml new file mode 100644 index 0000000..924f737 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/screen_background_light.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/screen_background_light_transparent.xml b/direct/vm/managed/framework-res/res/drawable/screen_background_light_transparent.xml new file mode 100644 index 0000000..958d992 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/screen_background_light_transparent.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/drawable/seekbar_thumb_material.xml b/direct/vm/managed/framework-res/res/drawable/seekbar_thumb_material.xml new file mode 100644 index 0000000..76302cb --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/seekbar_thumb_material.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/spinner_background_material.xml b/direct/vm/managed/framework-res/res/drawable/spinner_background_material.xml new file mode 100644 index 0000000..daa0f9d --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/spinner_background_material.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/star_big_off.xml b/direct/vm/managed/framework-res/res/drawable/star_big_off.xml new file mode 100644 index 0000000..ed19264 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/star_big_off.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/star_big_on.xml b/direct/vm/managed/framework-res/res/drawable/star_big_on.xml new file mode 100644 index 0000000..b663dc9 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/star_big_on.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/star_off.xml b/direct/vm/managed/framework-res/res/drawable/star_off.xml new file mode 100644 index 0000000..ed19264 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/star_off.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/star_on.xml b/direct/vm/managed/framework-res/res/drawable/star_on.xml new file mode 100644 index 0000000..b663dc9 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/star_on.xml @@ -0,0 +1,4 @@ + + + + diff --git a/direct/vm/managed/framework-res/res/drawable/switch_thumb_material.xml b/direct/vm/managed/framework-res/res/drawable/switch_thumb_material.xml new file mode 100644 index 0000000..7f063e3 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/switch_thumb_material.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/switch_track_material.xml b/direct/vm/managed/framework-res/res/drawable/switch_track_material.xml new file mode 100644 index 0000000..cd289fc --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/switch_track_material.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/sym_def_app_icon.xml b/direct/vm/managed/framework-res/res/drawable/sym_def_app_icon.xml new file mode 100644 index 0000000..653f37c --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/sym_def_app_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/drawable/toast_frame.xml b/direct/vm/managed/framework-res/res/drawable/toast_frame.xml new file mode 100644 index 0000000..3959c63 --- /dev/null +++ b/direct/vm/managed/framework-res/res/drawable/toast_frame.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/direct/vm/managed/framework-res/res/interpolator/accelerate_cubic.xml b/direct/vm/managed/framework-res/res/interpolator/accelerate_cubic.xml new file mode 100644 index 0000000..5cdc554 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/accelerate_cubic.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/accelerate_decelerate.xml b/direct/vm/managed/framework-res/res/interpolator/accelerate_decelerate.xml new file mode 100644 index 0000000..1306cec --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/accelerate_decelerate.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/accelerate_quad.xml b/direct/vm/managed/framework-res/res/interpolator/accelerate_quad.xml new file mode 100644 index 0000000..bbd2b89 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/accelerate_quad.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/accelerate_quint.xml b/direct/vm/managed/framework-res/res/interpolator/accelerate_quint.xml new file mode 100644 index 0000000..5edb7ff --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/accelerate_quint.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/anticipate.xml b/direct/vm/managed/framework-res/res/interpolator/anticipate.xml new file mode 100644 index 0000000..23ea239 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/anticipate.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/anticipate_overshoot.xml b/direct/vm/managed/framework-res/res/interpolator/anticipate_overshoot.xml new file mode 100644 index 0000000..9ac2555 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/anticipate_overshoot.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/bounce.xml b/direct/vm/managed/framework-res/res/interpolator/bounce.xml new file mode 100644 index 0000000..8d9161f --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/bounce.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/cycle.xml b/direct/vm/managed/framework-res/res/interpolator/cycle.xml new file mode 100644 index 0000000..9fda329 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/cycle.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/decelerate_cubic.xml b/direct/vm/managed/framework-res/res/interpolator/decelerate_cubic.xml new file mode 100644 index 0000000..341c6e3 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/decelerate_cubic.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/decelerate_quad.xml b/direct/vm/managed/framework-res/res/interpolator/decelerate_quad.xml new file mode 100644 index 0000000..3fa2fdb --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/decelerate_quad.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/decelerate_quint.xml b/direct/vm/managed/framework-res/res/interpolator/decelerate_quint.xml new file mode 100644 index 0000000..9e77279 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/decelerate_quint.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/fast_out_linear_in.xml b/direct/vm/managed/framework-res/res/interpolator/fast_out_linear_in.xml new file mode 100644 index 0000000..9d07445 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/fast_out_linear_in.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/fast_out_slow_in.xml b/direct/vm/managed/framework-res/res/interpolator/fast_out_slow_in.xml new file mode 100644 index 0000000..2872002 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/fast_out_slow_in.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/linear.xml b/direct/vm/managed/framework-res/res/interpolator/linear.xml new file mode 100644 index 0000000..61597b6 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/linear.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/linear_out_slow_in.xml b/direct/vm/managed/framework-res/res/interpolator/linear_out_slow_in.xml new file mode 100644 index 0000000..a20b066 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/linear_out_slow_in.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/interpolator/overshoot.xml b/direct/vm/managed/framework-res/res/interpolator/overshoot.xml new file mode 100644 index 0000000..d1c9b38 --- /dev/null +++ b/direct/vm/managed/framework-res/res/interpolator/overshoot.xml @@ -0,0 +1,2 @@ + + diff --git a/direct/vm/managed/framework-res/res/layout/activity_list_item.xml b/direct/vm/managed/framework-res/res/layout/activity_list_item.xml new file mode 100644 index 0000000..663ee3c --- /dev/null +++ b/direct/vm/managed/framework-res/res/layout/activity_list_item.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/direct/vm/managed/framework-res/res/layout/alert_dialog_material.xml b/direct/vm/managed/framework-res/res/layout/alert_dialog_material.xml new file mode 100644 index 0000000..5b9567e --- /dev/null +++ b/direct/vm/managed/framework-res/res/layout/alert_dialog_material.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + +