Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
id: native-prefix-cache
uses: actions/cache/restore@v4
with:
path: gh-cache/
path: buildscripts/prefix/
key: native-prefix-${{ runner.os }}-${{ env.CACHE_IDENTIFIER }}
restore-keys: |
native-prefix-${{ runner.os }}-
Expand All @@ -95,16 +95,19 @@ jobs:
automake \
build-essential \
ccache \
dos2unix \
gcc g++ \
cmake \
gettext \
glslang-tools \
gperf \
libtool \
nasm \
ninja-build \
pkg-config \
python3 \
python3-pip \
spirv-tools \
unzip \
wget

Expand All @@ -118,6 +121,15 @@ jobs:
set -euxo pipefail
cd buildscripts
./download.sh

echo "Downloading latest Vulkan-Headers..."
if [ ! -d "deps/Vulkan-Headers" ]; then
git clone https://github.com/KhronosGroup/Vulkan-Headers.git deps/Vulkan-Headers
fi

echo "Normalizing line endings for all scripts..."
cd ..
find . -type f \( -name '*.sh' -o -name 'gradlew' \) -exec dos2unix {} +

- name: Save Android SDK cache
if: steps.android-sdk-cache.outputs.cache-hit != 'true'
Expand All @@ -130,7 +142,7 @@ jobs:
if: steps.native-prefix-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: gh-cache/
path: buildscripts/prefix/
key: native-prefix-${{ runner.os }}-${{ env.CACHE_IDENTIFIER }}

- name: Show ccache stats before build
Expand All @@ -151,7 +163,14 @@ jobs:
[ "$ENABLE_ARM_V9A" = "true" ] && arches+=(arm64-v9a)
[ "$ENABLE_X86_ARCH" = "true" ] && arches+=(x86 x86_64)
for arch in "${arches[@]}"; do
echo "Injecting Vulkan headers into prefix/$arch/include..."
mkdir -p "prefix/$arch/include"
cp -r deps/Vulkan-Headers/include/vulkan "prefix/$arch/include/"
cp -r deps/Vulkan-Headers/include/vk_video "prefix/$arch/include/"
./buildall.sh --arch "$arch" mpv
if [ "$arch" != "arm64-v9a" ]; then
./buildall.sh --arch "$arch" python
fi
done

./buildall.sh --clean mpv-android
Expand Down
23 changes: 21 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:
id: native-prefix-cache
uses: actions/cache/restore@v4
with:
path: gh-cache/
path: buildscripts/prefix/
key: native-prefix-${{ runner.os }}-${{ env.CACHE_IDENTIFIER }}
restore-keys: |
native-prefix-${{ runner.os }}-
Expand All @@ -147,15 +147,18 @@ jobs:
build-essential \
ccache \
cmake \
dos2unix \
gcc g++ \
gettext \
glslang-tools \
gperf \
libtool \
nasm \
ninja-build \
pkg-config \
python3 \
python3-pip \
spirv-tools \
unzip \
wget

Expand All @@ -169,6 +172,15 @@ jobs:
set -euxo pipefail
cd buildscripts
./download.sh

echo "Downloading latest Vulkan-Headers..."
if [ ! -d "deps/Vulkan-Headers" ]; then
git clone https://github.com/KhronosGroup/Vulkan-Headers.git deps/Vulkan-Headers
fi

echo "Normalizing line endings for all scripts..."
cd ..
find . -type f \( -name '*.sh' -o -name 'gradlew' \) -exec dos2unix {} +

- name: Save Android SDK cache
if: steps.android-sdk-cache.outputs.cache-hit != 'true'
Expand All @@ -181,7 +193,7 @@ jobs:
if: steps.native-prefix-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: gh-cache/
path: buildscripts/prefix/
key: native-prefix-${{ runner.os }}-${{ env.CACHE_IDENTIFIER }}

- name: Show ccache stats before build
Expand All @@ -202,7 +214,14 @@ jobs:
[ "$ENABLE_ARM_V9A" = "true" ] && arches+=(arm64-v9a)
[ "$ENABLE_X86_ARCH" = "true" ] && arches+=(x86 x86_64)
for arch in "${arches[@]}"; do
echo "Injecting Vulkan headers into prefix/$arch/include..."
mkdir -p "prefix/$arch/include"
cp -r deps/Vulkan-Headers/include/vulkan "prefix/$arch/include/"
cp -r deps/Vulkan-Headers/include/vk_video "prefix/$arch/include/"
./buildall.sh --arch "$arch" mpv
if [ "$arch" != "arm64-v9a" ]; then
./buildall.sh --arch "$arch" python
fi
done

./buildall.sh --clean mpv-android
Expand Down
6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ android {
buildConfig = true
}

sourceSets {
main {
jniLibs.srcDirs = ['src/main/libs']
}
}

// https://youtrack.jetbrains.com/issue/KT-55947
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
Expand Down
28 changes: 21 additions & 7 deletions app/src/main/java/is/xyz/mpv/AbiDetector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ object AbiDetector {
extractV9aLibraries(context, cacheDir)
}

// Ensure the standard C++ library is loaded into memory first
// This prevents strict Android linker namespace issues when loading libplayer.so from an absolute path
try {
System.loadLibrary("c++_shared")
} catch (e: Throwable) {
Log.w(TAG, "c++_shared not found via loadLibrary, continuing anyway")
}

// Load in dependency order
val loadOrder = arrayOf(
"libavutil.so",
Expand All @@ -181,7 +189,7 @@ object AbiDetector {
}
}
true
} catch (e: Exception) {
} catch (e: Throwable) {
Log.e(TAG, "Failed to load v9a libraries", e)
false
}
Expand All @@ -196,10 +204,10 @@ object AbiDetector {
val versionFile = File(cacheDir, "version.txt")
if (!versionFile.exists()) return false

// Check version matches current app version
// Check version matches current app install time
val cachedVersion = versionFile.readText().trim()
val currentVersion = try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime.toString()
} catch (e: Exception) { "" }

return cachedVersion == currentVersion
Expand Down Expand Up @@ -230,15 +238,16 @@ object AbiDetector {
input.copyTo(output, bufferSize = 65536)
}
}
// Libraries must be executable
// Libraries must be executable and read-only for Android 14+ DCL rules
outFile.setExecutable(true, false)
outFile.setReadable(true, false)
outFile.setWritable(false, false)
Log.d(TAG, "Extracted: $asset (${outFile.length()} bytes)")
}

// Write version marker
val currentVersion = try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "unknown"
context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime.toString()
} catch (e: Exception) { "unknown" }
File(cacheDir, "version.txt").writeText(currentVersion)

Expand Down Expand Up @@ -266,13 +275,18 @@ object AbiDetector {
val features = featuresLine.substringAfter(":").trim().split("\\s+".toRegex())
val hasSve2 = features.any { it.equals("sve2", ignoreCase = true) }
val hasI8mm = features.any { it.equals("i8mm", ignoreCase = true) }
val hasSve = features.any { it.equals("sve", ignoreCase = true) }

Log.d(TAG, "cpuinfo features: sve=$hasSve, sve2=$hasSve2, i8mm=$hasI8mm")
if (hasSve2 || hasI8mm) {
Log.d(TAG, "cpuinfo features (v9a relevant): sve2=$hasSve2, i8mm=$hasI8mm")
Log.d(TAG, "Full features: ${features.joinToString(" ")}")
}

hasSve2 || hasI8mm
// CRITICAL: We MUST require SVE2 specifically.
// i8mm alone is an ARMv8.6 extension and does NOT guarantee SVE2 hardware.
// Our v9a libraries are compiled with -march=armv9-a+sve2, so loading them
// on a device without SVE2 silicon causes SIGILL crash.
hasSve2
} catch (e: Exception) {
Log.w(TAG, "Failed to read /proc/cpuinfo", e)
false
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/is/xyz/mpv/BaseMPVView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ abstract class BaseMPVView(context: Context, attrs: AttributeSet) : SurfaceView(
} catch (e: Exception) {
Log.e(TAG, "Failed to set SSL_CERT_FILE", e)
}

// Adreno-optimized Vulkan defaults for vo=gpu-next
MPVLib.setOptionString("gpu-api", "vulkan")
MPVLib.setOptionString("vulkan-async-compute", "no")
MPVLib.setOptionString("vulkan-async-transfer", "no")
MPVLib.setOptionString("vulkan-queue-count", "1")
MPVLib.setOptionString("vd-lavc-film-grain", "gpu")

initOptions()

MPVLib.init()
Expand Down Expand Up @@ -74,7 +82,7 @@ abstract class BaseMPVView(context: Context, attrs: AttributeSet) : SurfaceView(
this.filePath = filePath
}

private var voInUse: String = "gpu"
private var voInUse: String = "gpu-next"

/**
* Sets the VO to use.
Expand Down
17 changes: 4 additions & 13 deletions app/src/main/java/is/xyz/mpv/MPVLib.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,10 @@ object MPVLib {
Log.i(TAG, "Native libraries loaded — ABI: ${AbiDetector.detectOptimalAbi().displayName}")
}

// Fallback for apps that don't call loadLibraries() with context
init {
try {
// Try standard loading — this works for v8a and when v9a isn't needed
val libs = arrayOf("mpv", "player")
for (lib in libs) {
System.loadLibrary(lib)
}
} catch (e: UnsatisfiedLinkError) {
// Libraries will be loaded by loadLibraries(context) instead
Log.d(TAG, "Deferred library loading — call MPVLib.loadLibraries(context) for v9a support")
}
}
// Note: Eager initialization via init {} was removed because it prematurely
// loads the v8a fallback libraries into memory. If v8a libraries are loaded first,
// Android's dlopen will ignore the absolute path v9a loading later and just reuse the v8a handle.
// Apps MUST call MPVLib.loadLibraries(context) during App.onCreate().

external fun create(appctx: Context)
external fun init()
Expand Down
11 changes: 6 additions & 5 deletions app/src/main/jni/abi_detect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ extern "C" {
* This is the most reliable method on Linux/Android — directly queries the kernel
* for CPU feature flags without parsing /proc/cpuinfo.
*
* Returns true if the CPU supports SVE2 or I8MM (both are mandatory ARMv9 features).
* Compatible SoCs: Cortex-X3+, Cortex-A720+, Snapdragon 8 Gen 2+, Dimensity 9200+, Exynos 2400+
* Returns true ONLY if the CPU supports SVE2.
* CRITICAL: i8mm alone is NOT sufficient — it's an ARMv8.6 extension that many
* non-SVE2 chips have. Our v9a libraries are compiled with -march=armv9-a+sve2,
* so they contain actual SVE2 instructions that SIGILL on non-SVE2 hardware.
*/
JNIEXPORT jboolean JNICALL
Java_is_xyz_mpv_AbiDetector_nativeCheckSve2Support(JNIEnv*, jclass) {
#ifdef __aarch64__
unsigned long hwcap2 = getauxval(AT_HWCAP2);
// SVE2 is the defining mandatory feature of ARMv9-A
// I8MM (Int8 Matrix Multiply) is also mandatory in ARMv9
return (jboolean)((hwcap2 & HWCAP2_SVE2) || (hwcap2 & HWCAP2_I8MM));
// Strictly require SVE2 — this is the only feature our v9a libs actually use
return (jboolean)((hwcap2 & HWCAP2_SVE2) != 0);
#else
return JNI_FALSE;
#endif
Expand Down
54 changes: 44 additions & 10 deletions buildscripts/buildall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,25 @@ loadarch () {
fi

# Base linker flags — 16KB page size support for modern Android
export LDFLAGS="-Wl,-O1,--icf=safe -Wl,-z,max-page-size=16384"
export LDFLAGS="-Wl,-O1,--icf=safe -Wl,-z,max-page-size=16384 -Wl,--gc-sections"

# === Architecture-specific optimization flags ===
if [ "$ARM_V9A" -eq 1 ]; then
# ARM v9a: SVE2 + enhanced NEON + crypto + I8MM
# Tuned for Cortex-X3/X4 (Snapdragon 8 Gen 2/3, Dimensity 9200/9300, Exynos 2400)
export CFLAGS="-march=armv9-a+sve2+sve2-bitperm+sme+sha3+sm4+lse+dotprod -mtune=cortex-x3 -O3 -flto=thin -ffast-math -fno-math-errno -fomit-frame-pointer"
# ARM v9a: SVE2 only. Devices where kernel exposes SVE2 (e.g. Pixel 9, Galaxy S24 Ultra).
# STRICTLY AVOID: +sve2-bitperm, +sha3, +sm4, +sme — these are OPTIONAL extensions
# that most SoCs (including Snapdragon 8 Gen 2/3, Dimensity 9200/9400) do NOT implement.
# Many OEMs also disable SVE2 entirely in the kernel (e.g. iQOO, Vivo, Xiaomi).
# Our AbiDetector.kt only loads these libs when /proc/cpuinfo actually lists "sve2".
export CFLAGS="-march=armv9-a+sve2+lse+dotprod -mtune=cortex-x3 -O2 -flto=thin -ffast-math -fno-math-errno -fomit-frame-pointer -fno-plt -fno-semantic-interposition -ffunction-sections -fdata-sections"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$LDFLAGS -flto=thin -fuse-ld=lld"
elif [[ "$ndk_triple" == "aarch64"* ]]; then
# ARM v8a base: NEON + CRC + crypto, tuned for Cortex-A76 class cores
# This gives 8-10% boost over the default NDK flags
export CFLAGS="-march=armv8-a+crypto+crc -mtune=cortex-a76 -O3 -flto=thin -ffast-math -fno-math-errno -fomit-frame-pointer"
# ARM v8a base: NEON + CRC. This is the UNIVERSAL path for ALL arm64 Android devices.
# Must support everything from Snapdragon 835 (2017) to Snapdragon 8s Gen 3 (2024).
# NO +crypto/+sha3/+dotprod/+lse — old budget SoCs (Helio P22, Exynos 7885) lack them.
# -mtune=cortex-a75 targets the median 2024 device profile (better scheduling than a76
# while generating compatible code for all armv8-a chips).
export CFLAGS="-march=armv8-a+simd+crc -mtune=cortex-a75 -O2 -flto=thin -ffast-math -fno-math-errno -fomit-frame-pointer -fno-plt -fno-semantic-interposition -ffunction-sections -fdata-sections"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$LDFLAGS -flto=thin -fuse-ld=lld"
fi
Expand All @@ -88,11 +94,29 @@ loadarch () {
export RANLIB=llvm-ranlib
}

to_meson_array () {
local flags=($1)
local result=""
for flag in "${flags[@]}"; do
if [ -n "$result" ]; then
result="$result, '$flag'"
else
result="'$flag'"
fi
done
echo "[$result]"
}

setup_prefix () {
if [ ! -d "$prefix_dir" ]; then
mkdir -p "$prefix_dir"
# enforce flat structure (/usr/local -> /)
mkdir -p "$prefix_dir"
# enforce flat structure (/usr/local -> /)
# Always re-create: cache restore may replace symlinks with real directories
if [ ! -L "$prefix_dir/usr" ]; then
rm -rf "$prefix_dir/usr"
ln -s . "$prefix_dir/usr"
fi
if [ ! -L "$prefix_dir/local" ]; then
rm -rf "$prefix_dir/local"
ln -s . "$prefix_dir/local"
fi

Expand All @@ -110,14 +134,24 @@ setup_prefix () {
cpu_tune="cortex-x3"
fi

# Convert CFLAGS and LDFLAGS into Meson-compatible array format
local c_args_meson=$(to_meson_array "$CFLAGS")
local cpp_args_meson=$(to_meson_array "$CXXFLAGS")
local link_args_meson=$(to_meson_array "$LDFLAGS")

# meson wants to be spoonfed this file, so create it ahead of time
# also define: release build, static libs and no source downloads at runtime(!!!)
cat >"$prefix_dir/crossfile.tmp" <<CROSSFILE
[built-in options]
buildtype = 'release'
optimization = '2'
default_library = 'static'
wrap_mode = 'nodownload'
prefix = '/usr/local'
c_args = $c_args_meson
cpp_args = $cpp_args_meson
c_link_args = $link_args_meson
cpp_link_args = $link_args_meson
[binaries]
c = '$CC'
cpp = '$CXX'
Expand Down
Loading
Loading