SIGN IN SIGN UP

Optimize text layout and PortalList widget performance (#1041)

* Android: fix nondeterminstic crashes when using a bad surface

Makepad apps on Android were randomly crashing within the `Cx::render_view`
callstack when the host Activity surface was recycled, e.g., on
background/foreground transitions, rotation, IME show/hide, etc.

Details of the change are generated below:
----

`SurfaceHolder.Callback.surfaceDestroyed` posted a fire-and-forget message
to the render thread and returned to Android immediately, leaving two
overlapping races:

1. The render thread could drain `SurfaceDestroyed` from the mpsc channel,
   call `destroy_surface()` (which does `eglMakeCurrent(NULL, NULL, NULL,
   NULL)` and nulls the EGL surface), then in the *same* `RenderLoop`
   iteration call `handle_drawing()` → `render_view()` and issue GL calls
   with no current EGL context.
2. Even when our Rust side was well-behaved, Android was free to recycle
   the underlying buffer queue the moment `surfaceDestroyed` returned to
   Java, while the render thread was still mid-frame against it.

**Layer 1 — Surface validity tracking.** Added `CxOs::surface_alive`,
flipped synchronously in `SurfaceCreated`/`SurfaceChanged`/`SurfaceDestroyed`
handlers, plus a `CxOs::has_drawable_surface()` helper that gates every
GL/Vulkan dispatch entry point: `main_loop`'s `handle_drawing()`,
`draw_pass_to_window_for_active_backend`, `draw_pass_to_texture_for_active_backend`,
`draw_pass_to_fullscreen`, and `eglSwapBuffers` in `present_window_for_active_backend`.

**Layer 2 — Synchronous Java↔Rust handshake.** `FromJavaMessage::SurfaceDestroyed`
now carries an `Arc<(Mutex<bool>, Condvar)>` ack channel. The JNI binding
blocks the Android UI thread on the condvar (2-second budget, well under
the 5-second ANR threshold) until the render thread confirms it has
released the surface. This is the same pattern `android.opengl.GLSurfaceView`
uses, and it closes the underlying-buffer-recycled-mid-frame race.

**Layer 3 — Defense-in-depth re-bind.** `draw_pass_to_fullscreen` now calls
a new fallible `try_make_current()` every frame, recovering from any GL
context drift caused by foreign code or our own teardown path, and
bailing cleanly if the bind fails instead of crashing inside the driver.

Verified all 5 gated entry points against the `openxr_render_loop` →
`openxr_handle_repaint` path:

- 4 entry points are unreachable in XR mode (XR uses its own swapchains
  and `xrEndFrame`, never `eglSwapBuffers` or the popup overlay path).
- `draw_pass_to_texture_for_active_backend` IS reachable (off-screen UI
  textures composited into the XR scene). To prevent these from being
  wrongly skipped in Vulkan+XR mode after the host surface is recycled,
  added an explicit XR escape hatch to `has_drawable_surface()`: when
  `in_xr_mode && openxr.session.is_some()`, return `true` based purely on
  `vulkan.is_some()`, ignoring the (intentionally nulled) `display.window`.
  This matches the existing `keep_xr_backend_alive` logic in the
  `SurfaceDestroyed` handler.

- `destroy_surface` is now idempotent (safe to call when already null).
- `make_current` asserts on null surface with a descriptive panic message
  instead of crashing inside EGL.
- Lifecycle handlers verify surface creation actually succeeded before
  flipping `surface_alive` to `true` (no false-positive ready signal).

Tested working on my OnePlus Open w/ Android 15.

-------------
* Other fixes: minor change to logging format to include level indicator
* Fix warning in cargo makepad android

* Fix Android platform errors, mostly no draw on start/resume

Also a small related optimization on iOS

------------

* Android: JNI method ID caching (`ndk_utils.rs`)
  * Rewrote the `call_method!` macro to cache `jmethodID` in a per-call-site `static AtomicPtr`. Previously, every JNI call allocated two `CString`s and called `GetObjectClass` + `GetMethodID` from scratch. Now these are resolved once and reused for all subsequent calls. Also deletes the local class reference after first resolution.

* Android: `to_java_update_tex_image` uses cached macro (`android_jni.rs`)
  * Replaced manual `GetObjectClass`/`CString::new`/`GetMethodID` calls with the now-cached `call_bool_method!` macro, eliminating per-frame JNI overhead for video texture updates.

* Android: Touch event coalescing (`android.rs`)
  * When draining pending messages before a RenderLoop frame, consecutive pure-Move touch events are now coalesced — only the last position is dispatched. Start/Stop events are never dropped. This reduces redundant event dispatch during active touch scrolling.

* Android: Black screen fix (`android.rs`)
  * Added `needs_first_draw` flag to `CxOs`. When a `RenderLoop` callback arrives but the surface isn't drawable, the flag is set. When the surface later becomes available, `redraw_all()` is called to ensure the first frame is painted. The flag is also set on `SurfaceDestroyed` so resuming from background always gets a guaranteed first frame.

* iOS: Remove unnecessary `passes_todo.clone()` (`ios.rs`)
  * Removed a redundant `Vec::clone()` in the popup pass draw loop — both the outer and inner loops borrow `passes_todo` immutably.

* Optimize text layout and PortalList widget performance

Text layout: eliminate redundant HarfBuzz reshaping (layouter.rs)
- Replace `can_fit()` reshaping of cumulative multi-word substrings (always cache misses) with summing pre-computed per-word widths
- Replace `fit()` reshaping with concatenation of cached per-word `ShapedText` results, adjusting cluster offsets
- Cache each word's `Rc<ShapedText>` in the Fitter constructor for reuse

PortalList: O(1) reusable item pool lookup (portal_list.rs)
- Change `reusable_items` from `Vec<WidgetItem>` (O(n) scan + O(n) remove) to `HashMap<LiveId, Vec<WidgetItem>>` (O(1) lookup + O(1) pop)

PortalList: skip touch cursor hit-testing (portal_list.rs)
- Gate `point_hits_interactive_item()` behind `!e.device.is_touch()` to skip expensive recursive widget-tree walk on touch devices where there is no cursor

* undo unnecessary change to inter-word/ligature width estimation

avoid problems with words'/ligatures' kerning
K
Kevin Boos committed
6102d4329bd17089afee3b776ecea35f13987970
Parent: e66d104
Committed by GitHub <noreply@github.com> on 4/15/2026, 9:47:47 AM