cranpose 0.1.149

Cranpose runtime and UI facade
Documentation
#![allow(unsafe_code)]

use std::{ffi::c_void, sync::OnceLock};

const FRAME_RATE_COMPATIBILITY_DEFAULT: i8 = 0;
const CHANGE_FRAME_RATE_ALWAYS: i8 = 1;

type SetFrameRateFn = unsafe extern "C" fn(*mut c_void, f32, i8) -> i32;
type SetFrameRateWithChangeStrategyFn = unsafe extern "C" fn(*mut c_void, f32, i8, i8) -> i32;

enum VoteSymbol {
    WithStrategy(SetFrameRateWithChangeStrategyFn),
    Plain(SetFrameRateFn),
    Absent,
}

unsafe fn resolve_native_window_symbol(name: &std::ffi::CStr) -> *mut c_void {
    let direct = unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr()) };
    if !direct.is_null() {
        return direct;
    }
    let library = unsafe { libc::dlopen(c"libnativewindow.so".as_ptr(), libc::RTLD_LAZY) };
    if library.is_null() {
        return std::ptr::null_mut();
    }
    unsafe { libc::dlsym(library, name.as_ptr()) }
}

fn vote_symbol() -> &'static VoteSymbol {
    static SYMBOL: OnceLock<VoteSymbol> = OnceLock::new();
    SYMBOL.get_or_init(|| {
        // SAFETY: symbols come from the loaded libnativewindow.so; the
        // transmutes target the NDK-documented signatures.
        unsafe {
            let with_strategy =
                resolve_native_window_symbol(c"ANativeWindow_setFrameRateWithChangeStrategy");
            if !with_strategy.is_null() {
                return VoteSymbol::WithStrategy(std::mem::transmute::<
                    *mut c_void,
                    SetFrameRateWithChangeStrategyFn,
                >(with_strategy));
            }
            let plain = resolve_native_window_symbol(c"ANativeWindow_setFrameRate");
            if !plain.is_null() {
                return VoteSymbol::Plain(std::mem::transmute::<*mut c_void, SetFrameRateFn>(
                    plain,
                ));
            }
            log::info!(
                "[android-frame-rate] ANativeWindow_setFrameRate needs API 30; \
                 frame-rate votes are disabled on this device"
            );
            VoteSymbol::Absent
        }
    })
}

pub(crate) fn panel_max_refresh_rate(app: &android_activity::AndroidApp) -> Option<f32> {
    static PANEL_MAX: OnceLock<Option<f32>> = OnceLock::new();
    *PANEL_MAX.get_or_init(|| match query_panel_max_refresh_rate(app) {
        Ok(rate) => {
            log::info!("[android-frame-rate] panel max refresh rate: {rate} Hz");
            Some(rate)
        }
        Err(error) => {
            log::warn!("[android-frame-rate] could not query display refresh rates: {error}");
            None
        }
    })
}

fn query_panel_max_refresh_rate(app: &android_activity::AndroidApp) -> Result<f32, String> {
    crate::android_jni::with_android_activity_env(app, |env, activity| {
        use jni::{jni_sig, jni_str, objects::JFloatArray};

        let describe = |env: &mut jni::Env<'_>, what: &str, error: jni::errors::Error| {
            crate::android_jni::clear_pending_android_jni_exception(env);
            format!("{what} failed: {error}")
        };
        let window_manager = env
            .call_method(
                &activity,
                jni_str!("getWindowManager"),
                jni_sig!("()Landroid/view/WindowManager;"),
                &[],
            )
            .and_then(|value| value.l())
            .map_err(|error| describe(env, "Activity.getWindowManager", error))?;
        let display = env
            .call_method(
                &window_manager,
                jni_str!("getDefaultDisplay"),
                jni_sig!("()Landroid/view/Display;"),
                &[],
            )
            .and_then(|value| value.l())
            .map_err(|error| describe(env, "WindowManager.getDefaultDisplay", error))?;
        let rates = env
            .call_method(
                &display,
                jni_str!("getSupportedRefreshRates"),
                jni_sig!("()[F"),
                &[],
            )
            .and_then(|value| value.l())
            .map_err(|error| describe(env, "Display.getSupportedRefreshRates", error))?;
        let rates = env
            .cast_local::<JFloatArray>(rates)
            .map_err(|error| describe(env, "float[] cast", error))?;
        let len = rates
            .len(env)
            .map_err(|error| describe(env, "float[] length", error))?;
        let mut buffer = vec![0.0f32; len];
        rates
            .get_region(env, 0, &mut buffer)
            .map_err(|error| describe(env, "float[] read", error))?;
        buffer
            .into_iter()
            .filter(|rate| rate.is_finite() && *rate > 0.0)
            .fold(None, |best: Option<f32>, rate| {
                Some(best.map_or(rate, |best| best.max(rate)))
            })
            .ok_or_else(|| "display reported no refresh rates".to_string())
    })
}

#[derive(Default)]
pub(crate) struct FrameRateVoter {
    last: Option<(usize, u32)>,
}

impl FrameRateVoter {
    pub(crate) fn apply(&mut self, app: &android_activity::AndroidApp, rate_hz: f32) {
        let symbol = vote_symbol();
        if matches!(symbol, VoteSymbol::Absent) {
            return;
        }
        let Some(window) = app.native_window() else {
            return;
        };
        let window_ptr = window.ptr().as_ptr().cast::<c_void>();
        let key = (window_ptr as usize, rate_hz.to_bits());
        if self.last == Some(key) {
            return;
        }
        // SAFETY: `window` keeps the ANativeWindow alive across the call; the
        // symbols carry their NDK signatures.
        let status = unsafe {
            match symbol {
                VoteSymbol::WithStrategy(set) => set(
                    window_ptr,
                    rate_hz,
                    FRAME_RATE_COMPATIBILITY_DEFAULT,
                    CHANGE_FRAME_RATE_ALWAYS,
                ),
                VoteSymbol::Plain(set) => {
                    set(window_ptr, rate_hz, FRAME_RATE_COMPATIBILITY_DEFAULT)
                }
                VoteSymbol::Absent => unreachable!(),
            }
        };
        if status == 0 {
            log::info!("[android-frame-rate] voted {rate_hz} Hz on window {window_ptr:?}");
            self.last = Some(key);
        } else {
            log::warn!("[android-frame-rate] setFrameRate({rate_hz}) returned {status}");
        }
    }
}