diff --git a/draw/src/image_cache.rs b/draw/src/image_cache.rs index 82a1abaec7..f3d9f94e7a 100644 --- a/draw/src/image_cache.rs +++ b/draw/src/image_cache.rs @@ -198,11 +198,17 @@ impl PendingDecode { const MAX_IMAGE_DIMENSION: usize = 16384; /// Hard upper bound on a decoded image or animation atlas in bytes. Browser /// textures have a tighter admission limit; native retains the existing 1 GiB -/// cap (the RGBA size of a 16384x16384 image). +/// cap (the RGBA size of a 16384x16384 image). Other 32-bit targets (e.g., +/// armv7 Android) get 128 MiB (32 MP): decoding holds a few buffers of up to +/// this size at once, and several 1 GiB ones don't fit in a 32-bit address +/// space, so an oversized image would abort the app instead of failing. const WEB_MAX_IMAGE_DECODED_BYTES: usize = 64 * 1024 * 1024; +const NATIVE_32BIT_MAX_IMAGE_DECODED_BYTES: usize = 128 * 1024 * 1024; const NATIVE_MAX_IMAGE_DECODED_BYTES: usize = 1024 * 1024 * 1024; const MAX_IMAGE_DECODED_BYTES: usize = if cfg!(target_arch = "wasm32") { WEB_MAX_IMAGE_DECODED_BYTES +} else if cfg!(target_pointer_width = "32") { + NATIVE_32BIT_MAX_IMAGE_DECODED_BYTES } else { NATIVE_MAX_IMAGE_DECODED_BYTES }; @@ -1556,11 +1562,30 @@ mod tests { } #[test] - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), target_pointer_width = "64"))] fn native_decoded_image_policy_remains_one_gibibyte() { assert_eq!(MAX_IMAGE_DECODED_BYTES, NATIVE_MAX_IMAGE_DECODED_BYTES); } + #[test] + #[cfg(all(not(target_arch = "wasm32"), target_pointer_width = "32"))] + fn native_32bit_decoded_image_policy_is_128_mebibytes() { + assert_eq!(MAX_IMAGE_DECODED_BYTES, NATIVE_32BIT_MAX_IMAGE_DECODED_BYTES); + } + + #[test] + fn native_32bit_decoded_image_policy_boundaries() { + let limit = NATIVE_32BIT_MAX_IMAGE_DECODED_BYTES; + assert_eq!( + checked_pixel_count_with_limit(8192, 4096, limit).unwrap(), + 8192 * 4096 + ); + assert!(checked_pixel_count_with_limit(8192, 4097, limit).is_err()); + assert!(checked_pixel_count_with_limit(16384, 16384, limit).is_err()); + assert!(animation_atlas_layout_with_limit(32, 1024, 1024, limit).is_ok()); + assert!(animation_atlas_layout_with_limit(33, 1024, 1024, limit).is_err()); + } + fn single_frame_gif() -> Vec { let palette = [0x00, 0x00, 0x00, 0xff, 0x00, 0x00]; let pixels = [0, 1, 1, 0]; diff --git a/libs/shared_bytes/src/lib.rs b/libs/shared_bytes/src/lib.rs index ec5f294ffa..5e95f6c054 100644 --- a/libs/shared_bytes/src/lib.rs +++ b/libs/shared_bytes/src/lib.rs @@ -209,6 +209,12 @@ mod os { const MAP_FAILED: *mut c_void = !0usize as *mut c_void; unsafe extern "C" { + // On 32-bit bionic and glibc, the plain `mmap` symbol takes a 32-bit + // `off_t`; `mmap64` takes 64 bits on every ABI (on 64-bit, it's an alias). + #[cfg_attr( + any(target_os = "android", all(target_os = "linux", target_env = "gnu")), + link_name = "mmap64" + )] fn mmap( addr: *mut c_void, length: usize, diff --git a/platform/src/draw_list.rs b/platform/src/draw_list.rs index 1b62087357..67d97d88cc 100644 --- a/platform/src/draw_list.rs +++ b/platform/src/draw_list.rs @@ -3602,6 +3602,7 @@ mod uniform_generation_tests { dyn_uniforms: call.dyn_uniforms, texture_slots: call.texture_slots.clone(), uniform_buffer_slots: call.uniform_buffer_slots.clone(), + dyn_instances_pad: Default::default(), dyn_instances: [0.0; crate::draw_vars::DRAW_CALL_DYN_INSTANCES], }; vars.dyn_uniforms[0] += 1.0; diff --git a/platform/src/draw_vars.rs b/platform/src/draw_vars.rs index e1845b9981..92398d5de1 100644 --- a/platform/src/draw_vars.rs +++ b/platform/src/draw_vars.rs @@ -30,6 +30,18 @@ pub const DRAW_CALL_TEXTURE_SLOTS: usize = 17; pub const DRAW_CALL_UNIFORM_BUFFER_SLOTS: usize = 2; pub const DRAW_CALL_DYN_INSTANCES: usize = 32; +/// f32 lanes placed before `dyn_instances` so the array ends exactly where +/// `DrawVars` ends. The instance data a draw call uploads is the tail of +/// `dyn_instances` followed directly by the `#[live]` fields of the shader +/// struct that embeds `DrawVars` (`as_slice`), so any padding after the array +/// shifts every one of those fields by a lane. On wasm32 (4-byte pointers and +/// usize) the 8-byte-aligned struct ended 4 bytes past the array: the shader +/// read `char_index` where `texture_index` was, and web text drew as boxes. +#[cfg(target_pointer_width = "32")] +const DRAW_VARS_TAIL_PAD: usize = 1; +#[cfg(not(target_pointer_width = "32"))] +const DRAW_VARS_TAIL_PAD: usize = 0; + #[derive(Clone, Script, Debug)] #[repr(C)] pub struct DrawVars { @@ -53,10 +65,21 @@ pub struct DrawVars { pub texture_slots: [Option; DRAW_CALL_TEXTURE_SLOTS], #[rust] pub uniform_buffer_slots: [Option; DRAW_CALL_UNIFORM_BUFFER_SLOTS], + #[rust([0f32; DRAW_VARS_TAIL_PAD])] + pub dyn_instances_pad: [f32; DRAW_VARS_TAIL_PAD], #[rust([0f32; DRAW_CALL_DYN_INSTANCES])] pub dyn_instances: [f32; DRAW_CALL_DYN_INSTANCES], } +// See DRAW_VARS_TAIL_PAD: the instance fields must follow `dyn_instances` +// with no gap, on every target. +const _: () = assert!( + core::mem::offset_of!(DrawVars, dyn_instances) + + core::mem::size_of::<[f32; DRAW_CALL_DYN_INSTANCES]>() + == core::mem::size_of::(), + "DrawVars must end with dyn_instances (adjust DRAW_VARS_TAIL_PAD)" +); + impl ScriptHook for DrawVars { fn on_after_apply( &mut self, diff --git a/platform/src/os/linux/libc_sys.rs b/platform/src/os/linux/libc_sys.rs index 7317914675..ff7f931c6b 100644 --- a/platform/src/os/linux/libc_sys.rs +++ b/platform/src/os/linux/libc_sys.rs @@ -54,8 +54,11 @@ pub const RTLD_LOCAL: c_int = 0; pub const SYS_GETTID: c_long = 186; #[cfg(target_arch = "aarch64")] pub const SYS_GETTID: c_long = 178; -#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] -pub const SYS_GETTID: c_long = 178; // fallback, may need updating for other arches +// 32-bit ARM (EABI) and x86 use the legacy syscall table, where 178 is `rt_sigqueueinfo`. +#[cfg(any(target_arch = "arm", target_arch = "x86"))] +pub const SYS_GETTID: c_long = 224; +#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "arm", target_arch = "x86")))] +pub const SYS_GETTID: c_long = 178; // fallback (the asm-generic table, e.g. riscv64), may need updating for other arches extern "C" { pub fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; diff --git a/tools/cargo_makepad/src/android/compile.rs b/tools/cargo_makepad/src/android/compile.rs index 8ad97a935e..aab1fc4869 100644 --- a/tools/cargo_makepad/src/android/compile.rs +++ b/tools/cargo_makepad/src/android/compile.rs @@ -1137,6 +1137,7 @@ fn compile_java( makepad_java_classes_dir.join("VideoPlayerRunnable.java"), makepad_java_classes_dir.join("OesDecodeSurface.java"), makepad_java_classes_dir.join("H264Encoder.java"), + makepad_java_classes_dir.join("MakepadFileProvider.java"), build_paths.java_file.clone(), build_paths.xr_file.clone(), ]; @@ -1422,8 +1423,7 @@ fn bundle_ndk_shared_deps( // The NDK sysroot lib directory for this target triple (base dir, NOT the // API-level subdirectory). Files here that are real .so's (not linker // scripts / stubs) are NDK-provided and must be shipped inside the APK. - let clang_triple = android_target.clang(); - let sysroot_lib_dir = ndk_prebuilt_root.join("sysroot/usr/lib").join(clang_triple); + let sysroot_lib_dir = ndk_prebuilt_root.join("sysroot/usr/lib").join(android_target.sys_dir()); // Parse NEEDED entries from readelf output. Each relevant line looks like: // 0x0000000000000001 (NEEDED) Shared library: [libc++_shared.so] @@ -2100,8 +2100,7 @@ fn stage_ndk_shared_deps_for_so( readelf_path.to_str().unwrap(), &["-d", so_path.to_str().unwrap()], )?; - let clang_triple = android_target.clang(); - let sysroot_lib_dir = ndk_prebuilt_root.join("sysroot/usr/lib").join(clang_triple); + let sysroot_lib_dir = ndk_prebuilt_root.join("sysroot/usr/lib").join(android_target.sys_dir()); for line in output.lines() { if !line.contains("(NEEDED)") { continue; diff --git a/tools/cargo_makepad/src/android/java/dev/makepad/android/MakepadFileProvider.java b/tools/cargo_makepad/src/android/java/dev/makepad/android/MakepadFileProvider.java new file mode 100644 index 0000000000..0e9e4a6523 --- /dev/null +++ b/tools/cargo_makepad/src/android/java/dev/makepad/android/MakepadFileProvider.java @@ -0,0 +1,183 @@ +package dev.makepad.android; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; + +/** + * A read-only provider that hands other apps `content://` URIs for files in chosen + * subdirectories of the app's cache directory, like androidx's FileProvider but without + * the library. An app opts in by declaring it in its `AndroidManifest.xml.template`: + * + *
+ * <provider
+ *     android:name="dev.makepad.android.MakepadFileProvider"
+ *     android:authorities="{package_id}.robius.share"
+ *     android:exported="false"
+ *     android:grantUriPermissions="true">
+ *     <meta-data
+ *         android:name="dev.makepad.android.MakepadFileProvider.cacheDirs"
+ *         android:value="robius-share" />
+ * </provider>
+ * 
+ * + * `cacheDirs` is a comma-separated list of subdirectory names. The file + * `//` is served as `content:////`, and only to + * apps that were granted that URI (e.g., with `Intent.FLAG_GRANT_READ_URI_PERMISSION`). + */ +public class MakepadFileProvider extends ContentProvider { + private static final String CACHE_DIRS_META_DATA = + "dev.makepad.android.MakepadFileProvider.cacheDirs"; + private static final String[] DEFAULT_PROJECTION = { + OpenableColumns.DISPLAY_NAME, + OpenableColumns.SIZE, + }; + + private final HashSet cacheDirs = new HashSet<>(); + + @Override + public boolean onCreate() { + return true; + } + + // `resolveContentProvider(String, int)` is deprecated in API 33, but its replacement isn't + // available before that. + @SuppressWarnings("deprecation") + @Override + public void attachInfo(Context context, ProviderInfo info) { + super.attachInfo(context, info); + // As with FileProvider: exported, any app could read these files without a grant. + if (info.exported) { + throw new SecurityException("MakepadFileProvider must not be exported"); + } + if (!info.grantUriPermissions) { + throw new SecurityException("MakepadFileProvider must grant URI permissions"); + } + + // The ProviderInfo given here usually lacks meta-data, so look it up again. + Bundle metaData = info.metaData; + if (metaData == null) { + ProviderInfo withMetaData = context.getPackageManager().resolveContentProvider( + info.authority.split(";")[0], + PackageManager.GET_META_DATA); + metaData = withMetaData == null ? null : withMetaData.metaData; + } + String dirs = metaData == null ? null : metaData.getString(CACHE_DIRS_META_DATA); + if (dirs == null) { + return; + } + for (String dir : dirs.split(",")) { + dir = dir.trim(); + if (!dir.isEmpty() && !dir.equals(".") && !dir.equals("..") && dir.indexOf('/') < 0) { + cacheDirs.add(dir); + } + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + if (!"r".equals(mode)) { + throw new SecurityException("MakepadFileProvider only opens files for reading"); + } + return ParcelFileDescriptor.open(fileForUri(uri), ParcelFileDescriptor.MODE_READ_ONLY); + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + File file; + try { + file = fileForUri(uri); + } catch (FileNotFoundException e) { + return null; + } + + // Receivers ask for the name and size of a shared file; nothing else is known. + ArrayList columns = new ArrayList<>(); + ArrayList values = new ArrayList<>(); + for (String column : projection != null ? projection : DEFAULT_PROJECTION) { + if (OpenableColumns.DISPLAY_NAME.equals(column)) { + columns.add(column); + values.add(file.getName()); + } else if (OpenableColumns.SIZE.equals(column)) { + columns.add(column); + values.add(file.length()); + } + } + MatrixCursor cursor = new MatrixCursor(columns.toArray(new String[0]), 1); + cursor.addRow(values.toArray()); + return cursor; + } + + @Override + public String getType(Uri uri) { + String name = uri.getLastPathSegment(); + int dot = name == null ? -1 : name.lastIndexOf('.'); + if (dot >= 0) { + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + name.substring(dot + 1).toLowerCase(Locale.ROOT)); + if (mimeType != null) { + return mimeType; + } + } + return "application/octet-stream"; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("MakepadFileProvider is read-only"); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("MakepadFileProvider is read-only"); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("MakepadFileProvider is read-only"); + } + + private File fileForUri(Uri uri) throws FileNotFoundException { + List segments = uri.getPathSegments(); + if (segments.size() < 2 || !cacheDirs.contains(segments.get(0))) { + throw new FileNotFoundException("Not a shared file: " + uri); + } + try { + File root = new File(getContext().getCacheDir(), segments.get(0)).getCanonicalFile(); + File file = root; + for (String segment : segments.subList(1, segments.size())) { + file = new File(file, segment); + } + // Resolving `..` and symlinks must not lead out of the shared directory. + file = file.getCanonicalFile(); + if (!file.getPath().startsWith(root.getPath() + File.separator) || !file.isFile()) { + throw new FileNotFoundException("Not a shared file: " + uri); + } + return file; + } catch (IOException e) { + throw new FileNotFoundException("Not a shared file: " + uri); + } + } +} diff --git a/tools/cargo_makepad/src/android/mod.rs b/tools/cargo_makepad/src/android/mod.rs index 37c83af0ef..910e1aeffe 100644 --- a/tools/cargo_makepad/src/android/mod.rs +++ b/tools/cargo_makepad/src/android/mod.rs @@ -367,7 +367,10 @@ impl AndroidTarget { } return Ok(out); } - fn _sys_dir(&self) -> &'static str { + /// The triple the NDK sysroot uses for this ABI's library directory + /// (`sysroot/usr/lib/`). It differs from [`Self::clang`] on armv7, + /// whose clang wrappers are `armv7a-linux-androideabi-clang`. + fn sys_dir(&self) -> &'static str { match self { Self::aarch64 => "aarch64-linux-android", Self::x86_64 => "x86_64-linux-android", @@ -925,7 +928,7 @@ pub fn handle_android(mut args: &[String]) -> Result<(), String> { #[cfg(test)] mod tests { - use super::{AndroidVariant, ManifestArgs}; + use super::{AndroidTarget, AndroidVariant, ManifestArgs}; fn test_args<'a>(label: &'a str, url: &'a str) -> ManifestArgs<'a> { ManifestArgs { @@ -1020,6 +1023,15 @@ mod tests { assert!(xml.contains("android:debuggable=\"false\"")); } + #[test] + fn armv7_ndk_sysroot_dir_is_not_the_clang_prefix() { + assert_eq!(AndroidTarget::armv7.clang(), "armv7a-linux-androideabi"); + assert_eq!(AndroidTarget::armv7.sys_dir(), "arm-linux-androideabi"); + for target in [AndroidTarget::aarch64, AndroidTarget::x86_64, AndroidTarget::i686] { + assert_eq!(target.sys_dir(), target.clang()); + } + } + #[test] fn manifests_opt_into_predictive_back() { for variant in [AndroidVariant::Default, AndroidVariant::Quest] {