From bccf70b29ac3024e85ee00263afdb76fe9892d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Sun, 14 Jun 2026 01:43:37 -0400 Subject: [PATCH] android: populate dpi_scale from DisplayMetrics.density via JNI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream miniquad leaves dpi_scale at its 1.0 default on Android. As a result, macroquad's screen_width() / mouse_position() (which divide by dpi_scale to return density-independent units) return physical-pixel values instead of dp — so the same code that lays out UI correctly on macOS Retina has to special-case Android. This change queries the activity's Resources.getDisplayMetrics().density via JNI when init_with_activity sets up NativeDisplayData and again on every SurfaceChanged. With dpi_scale set, callers get consistent dp units across iOS / Android / macOS / Windows. query_display_density() is fail-soft — every nullable JNI lookup falls back to 1.0, matching upstream's behaviour for apps that don't yet expect a non-1 dpi_scale on Android. --- src/native/android.rs | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/native/android.rs b/src/native/android.rs index 3fae9bb0..9542f221 100644 --- a/src/native/android.rs +++ b/src/native/android.rs @@ -190,9 +190,11 @@ impl MainThreadState { }, Message::SurfaceChanged { width, height } => { { + let density = unsafe { query_display_density() }; let mut d = crate::native_display().lock().unwrap(); d.screen_width = width as _; d.screen_height = height as _; + d.dpi_scale = density; } self.event_handler.resize_event(width as _, height as _); } @@ -323,6 +325,56 @@ pub unsafe fn attach_jni_env() -> *mut ndk_sys::JNIEnv { env } +/// `Resources.getDisplayMetrics().density` — 1.0 = mdpi, 2.0 = xhdpi, +/// etc. Returns 1.0 on any JNI hiccup. +unsafe fn query_display_density() -> f32 { + if VM.is_null() || ACTIVITY.is_null() { + return 1.0; + } + let env = attach_jni_env(); + if env.is_null() { + return 1.0; + } + + let resources = ndk_utils::call_object_method!( + env, + ACTIVITY, + "getResources", + "()Landroid/content/res/Resources;" + ); + if resources.is_null() { + return 1.0; + } + + let metrics = ndk_utils::call_object_method!( + env, + resources, + "getDisplayMetrics", + "()Landroid/util/DisplayMetrics;" + ); + if metrics.is_null() { + return 1.0; + } + + let get_object_class = (**env).GetObjectClass.unwrap(); + let get_field_id = (**env).GetFieldID.unwrap(); + let get_float_field = (**env).GetFloatField.unwrap(); + + let metrics_class = get_object_class(env, metrics); + let density_field = get_field_id( + env, + metrics_class, + b"density\0".as_ptr() as _, + b"F\0".as_ptr() as _, + ); + if density_field.is_null() { + return 1.0; + } + + let density = get_float_field(env, metrics, density_field); + if density > 0.0 { density } else { 1.0 } +} + pub struct AndroidClipboard {} impl AndroidClipboard { pub fn new() -> AndroidClipboard { @@ -454,9 +506,11 @@ where let clipboard = Box::new(AndroidClipboard::new()); let tx_fn = Box::new(move |req| tx.send(Message::Request(req)).unwrap()); + let density = query_display_density(); crate::set_or_replace_display(NativeDisplayData { high_dpi: conf.high_dpi, blocking_event_loop: conf.platform.blocking_event_loop, + dpi_scale: density, ..NativeDisplayData::new(screen_width as _, screen_height as _, tx_fn, clipboard) });