Skip to content
Closed
29 changes: 27 additions & 2 deletions draw/src/image_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down Expand Up @@ -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<u8> {
let palette = [0x00, 0x00, 0x00, 0xff, 0x00, 0x00];
let pixels = [0, 1, 1, 0];
Expand Down
6 changes: 6 additions & 0 deletions libs/shared_bytes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions platform/src/draw_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions platform/src/draw_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -53,10 +65,21 @@ pub struct DrawVars {
pub texture_slots: [Option<Texture>; DRAW_CALL_TEXTURE_SLOTS],
#[rust]
pub uniform_buffer_slots: [Option<UniformBuffer>; 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>(),
"DrawVars must end with dyn_instances (adjust DRAW_VARS_TAIL_PAD)"
);

impl ScriptHook for DrawVars {
fn on_after_apply(
&mut self,
Expand Down
7 changes: 5 additions & 2 deletions platform/src/os/linux/libc_sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 3 additions & 4 deletions tools/cargo_makepad/src/android/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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`:
*
* <pre>
* &lt;provider
* android:name="dev.makepad.android.MakepadFileProvider"
* android:authorities="{package_id}.robius.share"
* android:exported="false"
* android:grantUriPermissions="true"&gt;
* &lt;meta-data
* android:name="dev.makepad.android.MakepadFileProvider.cacheDirs"
* android:value="robius-share" /&gt;
* &lt;/provider&gt;
* </pre>
*
* `cacheDirs` is a comma-separated list of subdirectory names. The file
* `<cacheDir>/<dir>/<path>` is served as `content://<authority>/<dir>/<path>`, 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<String> 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<String> columns = new ArrayList<>();
ArrayList<Object> 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<String> 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);
}
}
}
16 changes: 14 additions & 2 deletions tools/cargo_makepad/src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<sys_dir>`). It differs from [`Self::clang`] on armv7,
/// whose clang wrappers are `armv7a-linux-androideabi<api>-clang`.
fn sys_dir(&self) -> &'static str {
match self {
Self::aarch64 => "aarch64-linux-android",
Self::x86_64 => "x86_64-linux-android",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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] {
Expand Down