NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist
Build fast native Android and iOS applications with PHP and PAM.
Last release today
07 Oct 2026
Ships on a steady schedule
a new release about every 1 weeks
Nearly every release is documented
notes for 58 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
2 months old
286 releases · first in 2026
One column per month.
Commits between frames: a batch that builds something (32 mutations or more: a page of older chat rows, a reel's chrome) and any batch while a virtual
beforeWaiting observer ordered afterActivityIndicator: the spinner is a plain View drawing the sameProgressBar, whose constructor loaded theTextView taken for a new list cell no longersetText defers to the singleRuntimeFrameMetrics.stats) are read from the engine onbatches/decodeNs/mountNs per commit without them; debug builds keep theLayout: aspect-ratio follows Yoga's rules with min/max constraints on both axes. In a flex container the main size comes from the cross size (the auth
aspect-ratio follows Yoga's rules with min/max constraints on bothwidth: 100%; max-height: 100%; aspect-ratio stage now narrows, keeping its ratio, whenYGAspectRatioTest reference cases.Modal: the navigation bar follows the app's light/dark appearancenavigationBarTranslucent orstatusBarTranslucent) modal gets RN's transparent, system-scrimmed barnavigationBarHidden hides it. Re-applied after each window configuration.StatusBar: barStyle (light-content/dark-content), hidden andanimated now drive the status bar like React Native. The activeStatusBar nodes (visible route, open or opening modals) stack in mountpreferredStatusBarStyle,prefersStatusBarHidden and preferredStatusBarUpdateAnimation, and eachsetNeedsStatusBarAppearanceUpdate() (inside an animationanimated). With UIViewControllerBasedStatusBarAppearance NO thePamStatusBarTests and thedocs/ios-parity.md must pass on a Mac.Android Modal : the StatusBar declared by a modal's content (or the screen's, for a modal without one) now reaches the modal's own window. A Dialog cr
Modal: the StatusBar declared by a modal's content (or theenableEdgeToEdge) reset its icons to the appstatusBarTranslucent modal keeps its transparent bar.Android VirtualizedList : a node of a mounted cell whose index changes under the same parent (a conditional sibling inserted or removed before it) kee
VirtualizedList: a node of a mounted cell whose index changesappearance.firstFrame ( "php" default, "window" ): with "window" the Android activity draws its themed window at once instead of holding the launch sc
appearance.firstFrame ("php" default, "window"): with "window"manifest.sha256 beside the pack) boots from that listingsrc/ or reading and fingerprinting each componentopcache.validate_timestamps=0 outside debug builds, which hot reload inVirtualizedList: history prepended above the rows (also when aPamParseCache; iOS: drag configs).ActivityIndicator no longer inflates the theme'sKeyboardAvoidingView works inside Modal and BottomSheet surfaces. On Android a modal is its own Dialog window: the activity window never receives that
KeyboardAvoidingView works inside Modal and BottomSheet surfaces.Dialog window: the activity window neverWindowInsetsAnimation, and noEngine::set_surface_keyboard_inset,pam_native_engine_set_surface_keyboard_inset). The resize (RNheight) and padding behaviors of a KAV inside a modal are laid out byrelativeKeyboardHeight,keyboardVerticalOffset): the content box shrinks, so flexiblepan KAVs inside a modal translate natively against the dialogBottomSheet keeps lifting itself and itsresize/padding/position KAVs inside aheight (= resize)position (= pan).Android: appending a page to a VirtualizedList (or raising the adaptive prefetch distance during a fling) no longer lays out and binds the whole prefe
VirtualizedList (or raising the adaptiveMediaLibrary::assets() and albums() sort through the SQL sortdate_added DESC, date_modified DESC, _id DESC). MediaProvider oncreationDate descending).Keyed VirtualizedList sections for tabs over one list: rows set listSection ( Element::listSection() ) and the list activeSection ( VirtualizedList::a
VirtualizedList sections for tabs over one list: rows setlistSection (Element::listSection()) and the list activeSectionVirtualizedList::activeSection()). Unsectioned rows (header, sticky tabListSection (524) and ListActiveSection (525) propertymount-scenarios adds chat-details tabs over oneVirtualizedList (header, sticky rail, 40 rows with a photo per tab,details) or with keyed sections (details-keep), with tap-to-commit andAndroid images decode at the display size: a source larger than its view is subsampled and then scaled in the same BitmapFactory pass to the smallest
BitmapFactory pass to theresizeMethod auto/resize),repeat) images only subsample; scale/none are unchanged.Bitmap.Config.HARDWARE on API 28+ (GPU memoryRGB_565 on API 26-27. Inline data: glyphs andARGB_8888. Software canvases (shared-elementActivityManager.memoryClass (1/8, 1/16 on low-RAM devices, 8-64 MiB)Image hidden by an ancestor (a coveredImageView (one ancestor re-measure per arriving photo), eachPamCommit.mutations, PamCommit.lists and PamCommit.layout traceasset://, pam-file:// and file://preparingForDisplay(). The encoded media memory cache (1/32 of RAM,Android lint ( lintDebug , warnings as errors) passes with zero findings.
lintDebug, warnings as errors) passes with zero findings.PamRuntime holds its Activity context onlydetach,PamRuntimeHostPamNotificationCredentials.replace validates and seals everyPamList.layout trace section is always closed, also whenWrongConstant on TextView.breakStrategy (Layout and LineBreakerUnusedResources for the CLI-managed splash drawables.cargo clippy --all-targets -D warnings is clean on the toolchain inif in appearance.rs, app_icon.rs and mobile.rs;double_ended_iterator_last in layout.rs). No behavior change.Android: a hidden route mounted outside a Choreographer frame (a direct commit, not an engine batch) is laid out on the frame after the one that lays
PamNavigationHost mounts a route straight through its
route controller and restores its position with
detachViewFromParent/attachViewToParent. A new screen enters the window
once instead of being attached, detached for its fragment and moved back
(three attaches and two detaches of the whole subtree).onGestureSettle after
onGestureEnd, never before it.TYPE_TEXT_FLAG_NO_SUGGESTIONS); the IME composed over
text the formatter rewrites on every keystroke. iOS: formatted fields turn
autocorrection and spell checking off.backdrop-filter container no longer redraws on every
frame of an idle screen. It re-records what is behind it only in frames
where something in the window changed; before, its own invalidation
scheduled the next frame forever (continuous rendering, and a main thread
that never went idle, which hung waitForIdleSync in tests).CADisplayLink (which retains
its target) runs only while the overlay is in a window, so a removed overlay
is released instead of ticking forever; showing it twice no longer adds a
second display link.testReducedMotionDragSettleIsReportedAfterTheRelease and
testVisibleDevToolsOverlayIsReleasedAfterLeavingItsWindow; validate on a
Mac. New Android instrumented tests
reducedMotionDragReportsItsSettleAfterTheEnd and
backdropFilterRedrawsOnlyWhenTheWindowChanges.awaitFrames) instead of waitForIdleSync/sleeps,
tests that observe motion in flight enable animations through
PamAnimationsEnabledRule, window captures wait for a stable composited
frame, the layout-origin test expects edge-snapped sizes (1 px at density
2.75), the status-bar and transition-layer tests follow the 1.25.0
transition timing, the double-tap test dispatches both taps in one main
thread message, the controlled-input echo test types into the field, the
rapid formatted input test waits until the IME serves the focused field
before its key burst, and the DevTools test no longer waits for an idle
main thread (its snapshot checks use JUnit instead of Kotlin assert, a
no-op on Android). The reload and crypto bridge tests no longer read the
PHP report list while the runtime thread appends to it (a
ConcurrentModificationException).Android device-gallery images (content://media/... photos and videos, including the Files collection MediaLibrary::assets() pages) up to 640 px load f
content://media/... photos and videos,
including the Files collection MediaLibrary::assets() pages) up to 640 px
load from the platform thumbnail cache (ContentResolver.loadThumbnail)
and are downscaled to cover the cell, instead of reading the whole file
into the heap and decoding it. Videos show a frame instead of failing to
decode after reading up to 16 MiB. Natural size events still report the
MediaStore width and height. Larger views and other sources keep the
regular decode.ph:// photo-library sources of video assets draw their poster frame
(previously an image error), matching Android.VirtualizedList/VirtualGrid cells include their root's margins, like React Native/Yoga: a content-sized cell's slot (row height, or column width in a
height (width horizontally) stays
the root's own height and the margins are added around it; rowHeight of
a horizontal list stays the slot extent. Applies to grid rows (the tallest
margin box) and fullSpan cells. Before, the margins were dropped from the
slot and the root sat at its top-left corner.ScrollView (and sticky virtual list cells on iOS)
pin and are pushed by their margin box, like React Native's sticky header
wrapper: a pinned header keeps its top margin above it.Navigator::prewarm($route, $params) mounts a screen ahead of navigation, hidden under the current one; a following push()/navigate() with the same rou
Navigator::prewarm($route, $params) mounts a screen ahead of navigation,
hidden under the current one; a following push()/navigate() with the
same route and params reuses it (same element, views and first layout), so
the transition starts on the next frame. One prewarmed screen is kept; it is
released after 4 s unused or by any other navigation. isPrewarmed(),
cancelPrewarm() and the static Navigator::isPrewarming() (true while a
prewarmed screen renders, so mount() can defer focus-only work to
navigationFocused()) complete the API. Call it from a row's on:pressIn.detachViewFromParent/attachViewToParent instead of
leaving the window, so a press in progress on the Inbox is no longer
cancelled and kept-alive routes keep their views and list layout.!isShown) creates its cells within 5 ms
per frame and finishes them on the following frames (10 ms once shown);
cell extents are laid out at once. The first layout after a commit starts
at initialScrollIndex instead of binding position 0 and scrolling a frame
later. A list resting at its start whose rows all fit is no longer treated
as "at its end" when its viewport shrinks (an Inbox banner no longer jumps
the list to its last rows).PamNavigationHost.reorderRoute
and PamRenderer.move reorder routes without removeFromSuperview, hidden
PamVirtualListViews materialize visible cells within 5 ms per frame, and
the outgoing route is rasterized during the transition. XCTest
testReorderingAPrewarmedRouteKeepsItInTheWindow; validate on a Mac.reorderingAPrewarmedRouteKeepsItAttachedAndItsLayout,
hiddenRouteIsNotLaidOutInTheFrameThatMountsIt); see
migration notes.Source commit: a3285bb99224c0b31af5490cc9602edbc1a3e098 ( [skip ci] ).
Source commit: a3285bb99224c0b31af5490cc9602edbc1a3e098 ([skip ci]).
PAM_NATIVE_FRAME_GC=0 keeps PHP's own scheduling, a number sets theuse function imports) andElement exposes its encoding fields as read-only public propertieskind, properties, events, children, elementKey, domIdentity,identitySlot, reusable); TreeEncoder::frameNodes() returns the lastiOS: PamHeldPanGestureRecognizer imports UIKit.UIGestureRecognizerSubclass , which declares the touchesBegan / touchesMoved / reset overrides it uses;
PamHeldPanGestureRecognizer imports UIKit.UIGestureRecognizerSubclass,touchesBegan/touchesMoved/reset overrides it uses;Notification action endpoints authenticate per account while the app is killed: ActionEndpoint::bearerFromCredential('user_id', 'recipient_user_id') r
ActionEndpoint::bearerFromCredential('user_id', 'recipient_user_id')Pam\Native\Notifications\NotificationCredentials storeset, remove, sync, clear).NotificationAction::$credentialMissing reports it. Credentials resolve onlySource commit: 3364058fe27eace31400965a9a1b2172e0075762 ( [skip ci] ).
Source commit: 3364058fe27eace31400965a9a1b2172e0075762 ([skip ci]).
Accepted after native regression, community project and application UI validation.
Android scroll views consume contentOffsetX/contentOffsetY requests once
each requested position is reached. Later child layouts or insertions no
longer replay an initial offset after scrolling to the last page. Requests
still clamped by insufficient content remain pending until content grows.
Explicit property changes can request a new position. End anchoring, keyboard
clearance and tokenized scroll requests keep their existing contracts.
native-ui (API 26) and native (API 36) projects passed launch,Rebuild the Android host after updating. PHP remains ^8.5; numeric protocol/
ABI identifiers and third-party dependencies are unchanged. The 1.22.6 image,
reload, cache-recovery and initial-property improvements remain included.
See migration notes.
No iOS source changes are included, and UIKit execution was unavailable on
Linux. Native correctness and emulator UI tests do not measure FPS. Final app
integration is validated separately from this framework release. Validation uses direct local commands,
without GitHub Actions.
Source commit: 2aff2f3b49d3558503a1c9aee86cf97e7436c50e ( [skip ci] ).
Source commit: 2aff2f3b49d3558503a1c9aee86cf97e7436c50e ([skip ci]).
The final V3 source passed the release gates and integrated Android cache-recovery QA.
ImageLoadEvent, ImageErrorEvent and ImageProgressEvent handlerscanOpenUrl result. Itsnative and native-ui projects passed first launch, edit/reload/The accepted initial-properties A/B kept all 157 PAM assets byte-identical.
The largest mount CPU interval changed from 21.020 to 18.609 ms, the largest
Recycler layout CPU interval from 44.064 to 39.893 ms, and its eleven binds
from 29.714 to 27.976 ms. The total observed mount CPU increased from 27.453
to 37.593 ms; this is evidence for reduced work in the selected intervals,
not a reduction in every opening cost.
Untraced steady scroll changed from 0/417 frames classified as jank (p95 10 ms)
to 1/420 (p95 11 ms), with GPU median/p95 at 4/5 ms in both. Cold p95 changed
from 133 to 105 ms while jank changed from 10/16 to 12/16 frames. A single pair,
shader work and scheduling limit attribution. Cold opening has not achieved
the 60 FPS target, and these samples do not establish universal FPS or
complete application parity.
The image-event application regression reproduced the old TypeError and
passed the two compiled editor/crop consumers on the candidate. A candidate
V1 device session also exported edited photos and confirmed one remote post
with HTTP 201. The final V3 APK then recovered an intentionally empty Feed
class on Android: 0 bytes became the original 22,665 bytes with the exact
original SHA-256, and Feed rendered after process restart without clearing
application data. Backend
media processing remains separate from SDK correctness and release acceptance.
Update the SDK, rebuild/precompile the application bundle and rebuild the
native host. PHP remains ^8.5; third-party dependencies and numeric protocol/
ABI identifiers are unchanged. Existing 1.22.5 list and zoom fixes remain
included. See the migration guide.
Validation uses direct local commands without GitHub Actions. The iOS bridge
implements the same request-scoped callback mapping, but iOS execution was
unavailable on the Linux host. Cache size recovery does not detect arbitrary
same-size corruption or rebuild a damaged source pack. The device cache recovery evidence and source hashes are recorded in the release manifest.
ImageLoadEvent, ImageErrorEvent or
ImageProgressEvent receive decoded event objects instead of wire strings.
Direct handlers, explicit event arguments and generated/interpreted template
expressions share the existing decoder. String, untyped and zero-argument
handlers retain their contracts; forwarded event objects retain identity.Source commit: 6a333fd4122adb9598c69ef9fe3f852ebf70ba04 ( [skip ci] ).
Source commit: 6a333fd4122adb9598c69ef9fe3f852ebf70ba04 ([skip ci]).
GestureEvent::nativeScale, nativeTranslationX andnativeTranslationY report the applied transform for committing interruptednative and native-ui projects passed installation, first launch,Rebuild the Android host after updating. No numeric protocol identifiers or
third-party dependency requirements change. See
the migration guide.
Validation used direct local commands without GitHub Actions. Three UIKit
regression tests are included, but Swift/UIKit execution was unavailable on the
Linux host. The iOS handoff covers CSS/program transforms on the shared photo
surface; a TapEffect owned by an ancestor and animating a child is outside this
coverage. End-to-end APNs/FCM delivery was not tested. Initial text/background
batching and rejected drawing experiments are not included in this release.
GestureEvent::nativeScale, nativeTranslationX and
nativeTranslationY report the applied native transform; existing relative
gesture deltas and positional constructor parameters remain compatible.Android keyboard-aware Scroll follows keyboard animations and focus changes while preserving the authored viewport height, including Android 26–29 wit
Android keyboard-aware Scroll follows keyboard animations and focus changes while preserving the authored viewport height, including Android 26–29 with adjustNothing.
keyboardVerticalOffset property adds focused-field clearance when keyboardInset is enabled. Default focused-field clearance remains 24 dp.No PHP API, protocol identifier or dependency requirement changes. Rebuild the Android host after updating the SDK. See docs/migration-1.22.4.md for guidance on using one keyboard-aware form container and retaining enough content extent.
Both community hosts passed on the final source commit after rebuild and installation.
native (API 36) and native-ui (API 26) projects installed their declared Composer dependencies, including development dependencies, after manifest/metadata/dry-run checks. Both passed first launch, edited-source reload, restored-source reload and counter interaction in the same process on the final Android host. Installed PHP SDK files are byte-identical to the final candidate.Source commit: f2f41c108382604e07707dc6db4c6b5be35aaeaa. The temporary Composer gate export contains 1,173 tracked source files and omits generated build output. Published packages contain no candidate path repository or rejected graphics experiment.
Validation runs directly through local commands, without GitHub Actions. Swift/iOS execution and end-to-end APNs/FCM delivery were not performed on this Linux host. This patch changes Android behavior only.
keyboardVerticalOffset adds focused-field clearance to keyboardInset.adjustNothing. Scroll and keyboard-avoiding overlays share one invisible,
non-interactive measurement window per root, released when unused. Dialogs
retain their own compatible window-inset path.Android media, initial interaction setup, font packaging and development reload fixes.
Android media, initial interaction setup, font packaging and development reload fixes.
No PHP API, protocol identifier or dependency requirement changes. Regenerate the Android host with pam dev after updating the SDK.
native and native-ui projects installed their declared Composer dependencies, including development dependencies, after manifest/metadata/dry-run validation. Native/API36 and Native-UI/API26 passed boot, source edit, source restoration and counter interaction in the same process, with two confirmed changed bundles, clean runtime logs and inspected PNG screenshots.2618ee88496e41d8f77d3871931359c52959804c; the clean export contains 1,168 files and excludes generated build outputs. The native template's earlier Composer export has byte-identical PHP SDK files; its Android host was rebuilt with the final functional fixes. Native-UI used the final export directly.Validation ran locally through direct commands, without GitHub Actions. iOS/Xcode execution and end-to-end APNs/FCM delivery were not performed in this Linux session. This patch changes Android behavior only.
Device bundles omit the official SDK's host sources and documentation while retaining PHP, resources and Composer dependencies. Fresh native/native-ui
Navigation sends removal notifications when route instances are actually released, including reset, stack pruning and interrupted transitions. Kept-al
p-intersect observes the drawn viewport through scrolling, detach and
reattach. It emits only visibility transitions, allowing media to release
decoders outside the viewport without PHP callbacks for every scroll pixel.ImageEditor::render(imageLayers: ...) composes private bitmap overlays,
including a static GIF frame, with normalized geometry and bounded sequential
decoding. Existing calls remain compatible; Android and iOS implementations
share the contract. iOS source is included, but was not executed on this Linux host.PAM_NATIVE_ANDROID_DEV_PORT for concurrent
projects. ADB maps the unchanged device port to the selected host port.App icons, a React Native-style splash and a shorter cold start. Zé Chat on a Galaxy S10 (Android 12, dark mode) showed ~1.7 s of a dark splash with n
App icons, a React Native-style splash and a shorter cold start. Zé Chat on a Galaxy S10 (Android 12, dark mode) showed ~1.7 s of a dark splash with no logo while React Native showed its own splash: the app declared no splash logo, and Android 12 draws the launcher icon there, which could only be PAM's generic icon.
android.icon (new): adaptive launcher icon from foreground,
background (colour or image) and monochrome layers. The manifest icon
is now @drawable/pam_launcher; without android.icon it is the bundled
PAM icon as before. Notification small icons keep pam_icon. See
App icon.ios.icon (new): a 1024x1024 PNG written as AppIcon.appiconset with
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon. iOS uncompiled on the
release machine; needs device validation.appearance.splash.android12Icon (new): "appIcon" keeps the launcher
icon on the Android 12+ splash (no windowSplashScreenAnimatedIcon), the
way a React Native/Expo theme does, and the logo only paints the API 26-30
starting window, so size may reach 288 dp (an xxhdpi 864 px bitmap).
Default "logo" is unchanged.PamErrorOverlay builds its toast, inspector and fallback on first
use. The module registry still starts with the runtime (the launch order
differs from Android).adb shell am start and notification launches get a
plain splashBackground for every app.Keep-alive routes give a stack navigator React Navigation tab-screen lifetime, and four Android fixes from the Zé Chat device QA on a Galaxy S10: caro
Keep-alive routes give a stack navigator React Navigation tab-screen lifetime, and four Android fixes from the Zé Chat device QA on a Galaxy S10: carousels page one page per fling, a swipe past the last loop no longer pauses it, and decoded images and video players stop blocking scroll frames.
Route::screen(...)->keepAlive() (Router::keepAlive()). An
app that draws its own bottom bar and switches tabs with navigate() lost
the Feed scroll offset on every tab switch: the popped route was destroyed
and rebuilt at offset zero. A kept-alive entry removed from the stack
(navigate(), pop(), popToTop(), replace()) is now parked mounted
and hidden, and a push or navigate with the same params reuses it with its
state, native views and scroll offsets, without a visible jump. Kept-alive
entries deeper in the stack stay mounted too. An optional RouteContext
predicate limits it to some entries (the own-profile tab, not visited
profiles); at most three entries per route are parked and reset()
releases them. Parked screens get blur/focus events, not
navigationRemoved(). PHP tests cover parking, reuse, deep entries,
params and the predicate; instrumented tests cover the native host.PamNavigationHost.insert() reorders a moved route view
instead of ignoring the move, so a parked route coming back on top is the
transition destination (Android already moved it). Uncompiled here: needs
Mac validation.ACTION_DOWN, so a paging
HorizontalScrollView/ScrollView first saw the gesture at the
intercepted move and paged from the dragged offset. A 700 px/250 ms fling
on a feed carousel skipped two pages; a stale fling flag also skipped the
snap of a later slow drag. The start page is now recorded in
onInterceptTouchEvent. iOS already reads it in
scrollViewWillBeginDragging.prepareToDraw() on the decode
thread, so the RenderThread texture upload (8.5 ms for a 1080x2042 photo)
no longer lands in the scroll frame that first draws them.MediaPlayer creation, data source, surface attach and
detach, commands, progress polling and release run in order on one worker
thread without a Looper (callbacks stay on the main thread). A feed settle
that swapped the playing video blocked the UI thread 95-97 ms in binder
calls to mediaserver. iOS is unaffected: AVPlayer item loading is already
asynchronous.Verified on the S10 (Zé Chat QA debug build, feed cold pass of 8 flings): the Feed keeps its offset across tab switches with no visible jump, and the remaining cold-pass jank is cell binding of the debug (JIT) build.
MediaPlayer on Android keeps the video's aspect ratio and a paused player stays paused. In Zé Chat on a Galaxy S10 a square (720x720) loop with resize
MediaPlayer on Android keeps the video's aspect ratio and a paused player
stays paused. In Zé Chat on a Galaxy S10 a square (720x720) loop with
resizeMode="contain" filled the whole portrait screen, stretched and
upscaled (React Native letterboxes it), and the warm neighbour of the Loops
pager (autoPlay false, muted) decoded and ran next to the visible loop:
dumpsys media.player listed both players RUNNING.
TextureView fills the player box, so every frame is
already stretched to the box; resolveVideoScale applied the fit scale on
top of that. The view scale is now the displayed size divided by the box:
contain letterboxes the whole frame, cover fills the box and crops the
overflow (it over-zoomed one axis), fill keeps the stretch and center
shows native pixels. Unit tests cover each mode.MediaPlayer.setPlaybackParams with a non-zero
speed starts a prepared or paused player, and every prepare applied the
playback rate. The rate is applied only when it differs from 1, and a player
that was not playing is paused again. Verified on the S10: one RUNNING
player per Loops page, the warm neighbour PREPARED.AVPlayerLayer.videoGravity already fits correctly and
AVPlayer.rate is only set while playing.A property that gives a node inside a virtualized-list cell its own native view, or takes it away, no longer rebuilds the whole cell on Android. In Zé
A property that gives a node inside a virtualized-list cell its own native view, or takes it away, no longer rebuilds the whole cell on Android. In Zé Chat on a Galaxy S10 a double tap on a feed post liked it and its like button started its pop animation: that View had been flattened (no host property until the animation appeared), so the renderer dematerialized and rebuilt the entire post card. The media flashed for a frame, a carousel jumped back to page 1 while its dots stayed on page 2, the header's "⋯" could vanish and the double-tap heart, already playing on the UI thread, was dropped.
update() of a host property such as
animation, nativeRef, opacity on a layout-only View) inside a mounted
cell whose root is hosted, only that node is promoted or demoted in place
(promote()/demote() with the hosted insertion index and the cell layout).
The rest of the cell keeps its decoded images, players, scroll offsets and
running motions. The cell is still rebuilt when the node is the cell root,
the cell root is itself flattened, or the cell is not mounted
(virtualCellHostingRepair, unit-tested). Verified on the S10: the
double-tap heart plays its full ~700 ms and a carousel keeps its page.Safe areas are per presentation surface. On Android a full-screen Modal that was not statusBarTranslucent/navigationBarTranslucent showed an extra sta
Safe areas are per presentation surface. On Android a full-screen Modal
that was not statusBarTranslucent/navigationBarTranslucent showed an
extra status-bar-tall gap at the top (and a doubled gap above the navigation
bar): its Dialog window already starts below the status bar, and the
SafeAreaView inside it padded the activity window's insets again. Zé Chat
worked around it by making its modals translucent.
Modal/BottomSheet is laid out on its own surface
(surface.rs). The surface viewport and the insets its SafeAreaViews
see come from the window insets and a host surface policy,
pam_native_engine_set_surface_policy (SurfacePolicy): 0 in-window
(iOS) - full-screen and dialog modals get every inset; 1 system windows
(Android 14 and older) - a non-translucent Dialog fits the system bars, so
its viewport excludes them and its insets are zero, a translucent one is
edge-to-edge and gets the real insets; 2 edge-to-edge windows (Android
15+ with target SDK 35+, where setDecorFitsSystemWindows(true) is
ignored) - every Dialog gets the real insets. Bottom sheets never get the
top inset (snap points resolve below the status bar), and neither does the
active route of a navigation formSheet (and, on iOS, modal, a page
sheet). position: fixed inside a modal now anchors to its surface.
Incremental layout keeps the surface insets.modalWindowSurfacePolicy) and passes it to the engine before the first
frame. iOS: the bridge sets policy 0 explicitly.surface::tests and layout tests (fitted, translucent,
enforced edge-to-edge, sheets, content-sized dialogs, incremental relayout,
sheet routes, FFI). Android PamSurfaceSafeAreaInstrumentedTest lays
modals out with the real engine (debug-only JNI PamEngineLayoutProbe) and
shows them in real Dialog windows: on API 31 the fitted window starts at the
status bar and the header sits there once (it was twice); on API 36 the
window is edge-to-edge and the SafeAreaView pads once; translucent and
not, full screen and sheet, top and bottom. JVM PamModalSurfacePolicyTest.
XCTest PamSurfaceSafeAreaTests (uncompiled on this release host).SafeAreaView keeps rendering the
same; the translucency is no longer needed to avoid the doubled gap.Inline icon images (data:image/*, the masks behind an app's icon component) paint in the frame that lays them out and can no longer vanish. In Zé Chat
Inline icon images (data:image/*, the masks behind an app's icon
component) paint in the frame that lays them out and can no longer vanish.
In Zé Chat on a Galaxy S10 (Android 12), the tab bar "Buscar" magnifier, the
Inbox search field magnifier and the ▶ badge of explore videos sometimes
stayed blank until the screen was rebuilt, and a reopened Profile painted its
first frame with no icons at all.
ConcurrentHashMap.computeIfAbsent and removed itself from the map in its
completion stage. A tiny inline decode on the idle inline lane can finish
before computeIfAbsent returns; the completion stage then runs inside the
map update, remove() throws "Recursive update", and the future stored for
that key is already failed and never removed. While the bitmap stayed in
memory the 32 ms retry hid it; once photos evicted it (inline glyphs shared
the 32 MiB photo cache), every later load of that key (same source, same
64 px bucket: the 22 dp tab magnifier and the 20 dp field magnifier share
one) failed through the poisoned future, and after two retries the image
stayed empty for good.putIfAbsent before
its work starts and unregisters itself by identity when it completes, on
any thread (InFlightImageLoads); a failed load is never reused. Inline
sources up to 16 KiB decode synchronously on the UI thread in the layout
pass (well under a millisecond for an icon mask), so an icon is in the
first frame like a font glyph; they live in their own 4 MiB memory cache,
which photos cannot evict and only a critical memory trim clears.data:image/* sources were not decoded at all (the image stayed
empty) and tintColor on <Image> was ignored. Inline images now decode
synchronously into their own cache (PamInlineImages), and tintColor
renders the bitmap as a template in that color (Android imageTintList).
Uncompiled on this release host; listed in docs/ios-parity.md for Mac
validation.InFlightImageLoadsTest (a load finishing before share returns,
a failed load, a throwing start, a shared pending load, the synchronous
inline threshold) with NativeImagePriorityTest; XCTest
PamInlineImageTests. Verified on the Galaxy S10 with Zé Chat: the
reopened Profile, the tab bar and the Inbox paint every icon in their
first frame.The Android and iOS PHP runtimes have neither ext-sodium nor ext-openssl, yet the SDK called them directly: Update\UpdateVerifier (Ed25519 signature o
The Android and iOS PHP runtimes have neither ext-sodium nor ext-openssl, yet
the SDK called them directly: Update\UpdateVerifier (Ed25519 signature of
signed OTA manifests) and LocalFirst\EncryptedJournal (AES-256-GCM) died
with "Call to undefined function" on the device only. Both now go through
Pam\Native\Crypto, which the native host backs on Android and iOS with the
same bytes and decisions as libsodium and OpenSSL.
Pam\Native\Crypto::ed25519Verify(), aes256GcmEncrypt()
(ciphertext . 16-byte tag) and aes256GcmDecrypt() (null when not
authentic). ext-sodium/ext-openssl are used when loaded (desktop, server,
tests); otherwise the host function pam_native_crypto(). With neither
(a device host older than 1.19.0) they throw CryptoUnavailableException:
UpdateVerifier refuses the update with InvalidSignature, and
EncryptedJournal throws. Envelopes and signatures are unchanged: a journal
sealed on desktop opens on a device and back.PamCrypto.kt behind PamRuntime.onNativeCrypto (JNI, synchronous
on the PHP worker). AES-256-GCM through the platform JCA/Conscrypt;
Ed25519 verified in Kotlin on every API level with libsodium's rules
(canonical S, no small-order R or public key, canonical public key,
cofactorless equation); the platform Ed25519 (API 33+) skips the
small-order checks, so it is not used. The 76 vectors take 235-291 ms on
x86_64 emulators (under 10 ms per full verification). R8 keeps the callback.PamCrypto.swift installs the provider from PamRuntime: CryptoKit
AES.GCM, and Curve25519.Signing after libsodium's encoding checks.
Uncompiled here (Linux); PamCryptoTests to run on a Mac
(docs/ios-parity.md).tests/runtime_audit.php keeps them so) and names the
device-ready replacement in findings, e.g. sodium_crypto_sign_verify_detached()
→ Pam\Native\Crypto::ed25519Verify(), openssl_encrypt() →
aes256GcmEncrypt(), sodium_bin2hex() → bin2hex().Store\EncryptedStatePersistence (XSalsa20-Poly1305
secretbox) still requires ext-sodium and throws on the device, as before.
HMAC/HKDF need nothing: hash_hmac()/hash_hkdf() are core.tests/Fixtures/crypto-vectors.json (scripts/generate-crypto-vectors.php:
RFC 8032, GCM spec case 16, 76 Ed25519 and 48 AES-256-GCM vectors including
S + L, every small-order encoding with and without the sign bit,
non-canonical and off-curve keys, and mixed-order keys that separate
cofactored from cofactorless verification) is replayed by
tests/native_crypto.php (extensions, a fake of the host function, seeded
random cross-checks, journal and OTA interop, fail-closed paths), the
Android JVM test PamCryptoTest, the instrumented NativeCryptoInstrumentedTest
and NativeCryptoBridgeInstrumentedTest (PHP → JNI → Kotlin through the
real runtime) and the iOS PamCryptoTests. tests/device/crypto_runtime.php
runs inside the Android PHP runtime. Passing on Android 8.0 (API 26) and
Android 16 (API 36) x86_64 emulators.First launch after installing or updating an Android app: the PHP bundle is one asset and the prebuilt component cache is one file. Zé Chat on a Galax
First launch after installing or updating an Android app: the PHP bundle is
one asset and the prebuilt component cache is one file. Zé Chat on a Galaxy
S10, release-optimized .perf build, process start → first commit (fresh
install each run; the phone was shared with other apps, so runs vary):
| 1.14.0 | 1.18.0 | |
|---|---|---|
Bundle install (bundleInstallMs) |
1,720-2,167 ms | 164-278 ms |
| First launch after install | 2,172-2,683 ms | 453-681 ms |
| Normal cold start | 268-322 ms | 233-313 ms |
| Files created on device by the install | 2,165 | 1,261 |
| APK size | 69.1 MB | 67.9 MB |
pam-native build writes assets/pam-bundle.pnb instead of
~2,000 assets/pam/** PHP assets (and drops 1.14's pam-files.txt): an
index (<sha256> <size> <p|a> <path>, c <compressed> <size> per chunk)
and the PHP code (*.php, *.pam, pam-prebuilt/) as independently
raw-deflated chunks of ~512 KB, stored uncompressed in the APK
(noCompress "pnb"). Images, fonts, CSS and every other file stay plain
assets/pam/ entries (asset://, fonts and Files.copyAsset read them
from the APK as before) and are listed in the index with their SHA-256.PamBundleInstallProvider,
before Application.onCreate and the Activity) instead of after the
Activity is created. One sequential read of the pack feeds compressed
chunks to 2-4 workers that inflate them and verify each file's size and
SHA-256 against the index before writing it; plain assets are copied on
their own thread; directories are created on first use. The index parser
is hand-written (Regex parsing alone cost ~110 ms on a cold process).
PamStartup (1.15.0) now waits for that install instead of running it.
Staging, content-addressed activation, release pruning, OTA bundles and
hot reload are unchanged. Debug/benchmark builds log bundleInstallMs
(the unpack itself) and bundleWaitMs (how long startup waited for it).PamPhpCompiler::prebuild() writes one
pam-prebuilt/components/components.pack (PNC1, JSON index with every
component's metadata, then class and template sources) instead of four
files per component (904 files in Zé Chat). A component's class and
template file (closures + template JSON in one opcode-cached file) are
written from the pack the first time they are used: beside the pack when
the bundle is writable (Android, per release), otherwise under
PAM_NATIVE_STATE_DIR (a read-only iOS app bundle), keeping only the
current pack's files. 1.14 prebuilt directories still load.PamBundle/ in place from the signed app bundle, so it
has no install step to speed up; it gets the same single-file prebuilt
cache (one bundle file instead of four per component). Uncompiled on this
release host; listed in docs/ios-parity.md for Mac validation.The mobile PHP runtime has no ext-mbstring (and no intl), so mb_* calls in apps and community packages (pam-native-calls's Calls::text()) died with "C
The mobile PHP runtime has no ext-mbstring (and no intl), so mb_* calls in
apps and community packages (pam-native-calls's Calls::text()) died with
"Call to undefined function" on the device only; Zé Chat carried its own
partial polyfill. PAM Native now covers it in the SDK and warns at build time
about the extensions it cannot cover.
src/Polyfill/mbstring.php is autoloaded by Composer before any
application code and defines 41 mb_* functions (every one in common use:
length/width, substr/strcut/str_split/strimwidth/str_pad/trim, the strpos
and strstr families, case conversion with all MB_CASE_* modes, ucfirst,
convert_encoding/variables, detect_encoding, check_encoding/scrub,
ord/chr, numeric entities, settings) and the MB_CASE_* constants, each
only when missing: a real ext-mbstring or an app's own polyfill loaded
first wins. Pam\Native\Polyfill\Mbstring follows PHP 8.5's
ext-mbstring, including malformed input, substitution modes, full Unicode
case mapping (final sigma, Turkish ISO-8859-9), East Asian widths and the
same ValueErrors, for UTF-8/16/32, UCS-2/4, ASCII, 8bit and every
single-byte code page. Not covered: SJIS/EUC/BIG-5/GB18030/ISO-2022/UTF-7,
mb_ereg*, mb_convert_kana, MIME headers, mb_send_mail..so on x86_64 after page alignment,
+0.63 MB compressed), measured with the runtime's NDK flags and no
Oniguruma; it would also need a new PAM runtime release and an iOS
XCFramework rebuilt on a Mac. intl (ICU, ~35 MB per ABI) stays out.Pam\Native\Tooling\MobileRuntimeAudit and prints a warning with file
and line for each function or class (new, ::, extends, implements) of an
extension the selected runtime does not compile (intl, iconv, zlib, gd,
curl, dom...) that no bundled file or polyfill declares. Guarded uses,
package tests/binaries and the SDK itself are skipped; findings never fail
the build.docs/platform-runtime.md#php-extensions lists what Android and iOS
have (identical sets), the polyfill coverage and the audit.docs/ios-parity.md for Mac validation.tests/mbstring_polyfill.php (recorded ext-mbstring results; with
ext-mbstring on the host, every function compared over seeded valid and
malformed strings in every encoding and substitution mode),
tests/runtime_audit.php, and
scripts/android-php-runtime-test.sh, which runs
tests/device/mbstring_runtime.php inside the real Android runtime: passing
on an Android 8.0 (API 26) x86_64 emulator and a Galaxy S10 (API 31,
arm64-v8a). All 1.1M code points were also compared with ext-mbstring
through every case mode, mb_strlen() and mb_strwidth().Plugins no longer declare a plugins.share.v1 capability for share. 1.16.0 accepted it at prepare, but the PHP runtime's Protocol::CAPABILITIES does no
plugins.share.v1 capability for share. 1.16.0
accepted it at prepare, but the PHP runtime's Protocol::CAPABILITIES does
not list it, so a plugin requiring it failed discovery at startup
(pam-native-share-extension 0.3.0). Prepare now rejects it like any other
unknown capability, and a test keeps the CLI's plugin capabilities within
the runtime's. Gate share with pamNative.minimum 1.16.0 instead.Plugins configure the content they receive from share sheets per application. Before, pam-native-share-extension hardcoded */* (Android SEND and SEND_
Plugins configure the content they receive from share sheets per
application. Before, pam-native-share-extension hardcoded */* (Android
SEND and SEND_MULTIPLE, iOS any file), so every app using it was offered
for every share, PDFs included.
share (configKey, default accept and
multiple) with the new plugins.share.v1 capability. Applications
override accept/multiple under plugins.<configKey> in
pam-native.json (new plugins section).SEND intent filter per accepted type to the
launcher activity, with SEND_MULTIPLE only for the types in multiple,
merged with android.shareTargets.NSExtensionActivationRule: text/URL, image and movie counts for
text/plain, image/*, video/* and */* (unchanged legacy rule), and a
SUBQUERY type-identifier predicate for specific types such as
application/pdf. Uncompiled on this release host; listed in
docs/ios-parity.md for Mac validation.plugins key no installed plugin declares, duplicate
configKeys, invalid MIME types and multiple types missing from
accept. JSON schemas and docs/plugins.md describe the contract.Telegram-style cold start: the system splash goes straight to the app's first PHP frame, with nothing in between, and that frame arrives sooner. Measu
Telegram-style cold start: the system splash goes straight to the app's first
PHP frame, with nothing in between, and that frame arrives sooner. Measured on
a Galaxy S10 (Android 12), release-optimized .perf Zé Chat build, START to
the first frame with content (screen recording) and Fully drawn, 3+ cold
starts each (the shared .perf install was logged out, so the measured screen
is Auth; it streams in two PHP frames like the Inbox snapshot streams in one):
| 1.14.x | 1.15.0 | |
|---|---|---|
| START → first content frame (video) | 316–399 ms | 269–302 ms |
| Displayed (first window draw) | 213–238 ms, an empty window | = first PHP frame |
| Fully drawn (first PHP frame) | 331–377 ms | 303–316 ms |
| PHP worker start (from process start) | after the first draw | 77–101 ms, in onCreate |
| First-frame view creation | 11–14 ms (spinner alone 10–12 ms) | pooled/warmed |
PamActivity.onCreate; on API 30+ PHP boots before the rest of
the native surface is built, with the window bounds and system-bar/cutout
insets from WindowMetrics (below API 30 it boots from the held first
pre-draw, once insets exist). The launch thread runs at display priority,
which the PHP worker inherits.ActivityIndicator's first instance cost 10–12 ms on the S10.docs/ios-parity.md.displayMetrics.heightPixels before the dialog existed and never re-sized.
It now estimates from the covered window and its insets and re-resolves on
the first layout (and on width changes) within the same traversal. A fitted
(base) sheet no longer subtracts the status bar twice and rests on the
navigation bar; an edge-to-edge sheet (navigationBarTranslucent /
statusBarTranslucent) resolves against the window minus the top inset and
reaches the screen bottom. iOS already re-sized on layout.Navigator::reset() followed one tick later by a push could
land natively on the reset route while PHP's current route was the pushed
one (a cold-start deep link opened the Inbox instead of the chat). When both
reach the host in one commit, the new routes are created in node-id order
and Android took the last inserted route as the destination, while the
FragmentManager re-added each route after the previously added fragment's
view. Route views now keep the engine's order and the destination is read
from it, whatever the commit timing. iOS already used the route order.PamBottomSheetSizingTest;
resetThenPushLandsOnThePushedRouteWhateverTheCommitTiming (fails on
1.14.2); XCTest testResetThenPushInOneCommitLandsOnThePushedRoute.An autoFocus input in a Modal/BottomSheet presented while an overlay before it closes gets the keyboard again. In Zé Chat, long-press on one's own mes
An autoFocus input in a Modal/BottomSheet presented while an overlay
before it closes gets the keyboard again. In Zé Chat, long-press on one's own
message → Editar opened the "Editar mensagem" sheet without the keyboard on a
Galaxy S10 (Android 12), 3 out of 3 times.
PamModalHost dismissed its Dialog and re-created the window 40 ms afterHIDE_UNSPECIFIED_WINDOW) and the new window had no focusedSOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored).move() leaves the view attached when its hosted position doesPamModalHost keeps its window across a detach that is undonePamAutoFocus).move() keeps the view in its superview when its sibling positiondocs/ios-parity.md for Mac validation.PamSheetKeyboardInstrumentedTest.autoFocusInASheetMovedByItsClosingOverlayKeepsWindowAndKeyboardfocus=false), passing with the rest of thetestMoveThatKeepsThePositionLeavesTheFocusedInputInPlace. Verified on thePAM Native 1.14.2 — autoFocus sheets keep the keyboard when a closing overlay moves them
Compare
Pinned sticky headers in VirtualizedList and VirtualGrid are real, interactive views (FlashList parity). Zé Chat's ChatDetails had to host its sticky
Pinned sticky headers in VirtualizedList and VirtualGrid are real,
interactive views (FlashList parity). Zé Chat's ChatDetails had to host its
sticky tab bar in a ScrollView because a tap on the pinned tabs reached the
row scrolling underneath.
zPosition only reorders drawing), so a
press on a pinned header never reaches the row under it. Uncompiled on
this release host; listed in docs/ios-parity.md for Mac validation.PamVirtualListStickyHeaderInstrumentedTest (press on the pinned
button, background tap that no row receives, pressed state, accessibility
visibility, push by the next header, VirtualGrid, stepping through the
list with no blank row and exactly one mounted header, a commit while
pinned), passing on an Android 12 (API 31) emulator;
PamStickyHeaderPositionsTest; XCTest
testPinnedVirtualListHeaderReceivesTouchesInsteadOfTheRowUnderIt.Per-component render cost and first launch after install (Zé Chat inbox on a Galaxy S10, release-optimized .perf build, against 1.13.3):
Per-component render cost and first launch after install (Zé Chat inbox on a
Galaxy S10, release-optimized .perf build, against 1.13.3):
| 1.13.3 | 1.14.0 | |
|---|---|---|
One inbox row (ChatListItem), render + encode |
1.79 ms | 0.44 ms |
| Inbox first render (12 rows), render + encode | 43.4 ms | 13.1 ms |
| PHP component discovery on the first launch after install | 2,066 ms | 28-38 ms |
| First launch after install, process start → first commit | 3,396 ms | ~1,750 ms |
Host (scripts/benchmark-inbox-render.php / benchmark-chat-render.php,
PHP without JIT): inbox first render 9.6 → 2.7 ms, realtime row update
7.7 → 1.9 ms, filter back 6.7 → 1.7 ms; chat first render 17.8 → 7.1 ms,
incoming message 8.9 → 2.7 ms. Encoded frames are byte-identical.
:class array literals) compile to plain PHPpam-native build and update bundles runPamPhpCompiler::prebuild() on the host, writing pam-prebuilt/componentsassets/pam-files.txt; the first launch copies theAssetManager.list() walk). Debug/benchmark builds logbundleInstallMs (PamNativePerf).on:pressIn, on:pressOut, on:pressMove, on:doubleTap, image, media,on:press: one double tap or press-in re-renders one memoized row insteadHoldPressable workaround needed).PAM Native 1.14.0 — per-component render cost, build-time component cache
Compare
Android: autoFocus on an input inside a presented BottomSheet opens the keyboard on Android 11-14 (Zé's "Editar mensagem" on a Galaxy S10, where 1.13.
autoFocus on an input inside a presented BottomSheet opens theadjustNothing with an unspecified soft-input state, so the system neverautoFocus input is pending, its modal window now asks forSOFT_INPUT_STATE_ALWAYS_VISIBLE (the platform itself shows the IME forPamAutoFocus logcat lines (focus gave up, waiting for window focus,PamSheetKeyboardInstrumentedTest: Zé's flow (kept sheet presented underautoFocus, a 1.2 s UI-thread stall), passing on anPAM Native 1.13.3 — sheet autoFocus keyboard on Android 11-14
Compare
Zé Chat cold start on Android: a position: absolute; top: -200px child of the root crashed every launch.
Zé Chat cold start on Android: a position: absolute; top: -200px child of
the root crashed every launch.
top: -200px, left: -34px, margin-top: -14px) put awidth/height must be finite andProtocolException("Layout value must be finite and non-negative");Layout::is_valid()/sanitized(). The layout engine sanitizes everydecode_batch rejects one (ProtocolError::InvalidLayout).PROTOCOL.md documents the rule.PamRenderer.resetTree()) and applies a full remount. AZé Chat device QA on 1.12.2 (Galaxy S10, Android 12).
Zé Chat device QA on 1.12.2 (Galaxy S10, Android 12).
autoFocus on an input inside a just-presented BottomSheet or
Modal opens the keyboard, also when the input is remounted (a new key per
open) in a re-presented or already-presented sheet. The renderer waited at
most 200 ms for window focus and asked the IME once; on Android 11-12 a
request from a window that is still gaining focus is dropped. It now waits
for the input's own window focus (OnWindowFocusChangeListener), asks that
window's insets controller (show(ime())) and the IMM, and re-asks until
the IME is reported visible. The modal no longer moves focus to its first
focusable (a header button) when an input already took it.<Modal transparent presentation="fullScreen"> whose flattened
full-height Column ends in a short options sheet (Zé's OptionDialog) is a
short sheet over the translucent backdrop again. Every child hosted by a
full-screen modal was forced to MATCH_PARENT, which drew the sheet as an
opaque full-screen window with its title at the top. Only a child spanning
the modal fills the window now; a bottom-anchored child keeps its height
at the window bottom and other children keep their frames.PamSheetKeyboardInstrumentedTest: remounted autoFocus input with a
focusable header button (first open, re-open, remount in an open sheet),
and a screenshot pixel test of the transparent options modal (backdrop
over the screen above, white sheet in its 44 % frame).autoFocus retried
after presentation; no layout-only flattening).Cold start (Zé Chat on a Galaxy S10, release build): first PAM frame 870 ms -> 290-370 ms; the logged-in inbox shows its rows ~470 ms after launch.
Cold start (Zé Chat on a Galaxy S10, release build): first PAM frame 870 ms -> 290-370 ms; the logged-in inbox shows its rows ~470 ms after launch.
.pam file on each boot.
An unchanged component (xxh128 of its source, the src/app.css location
and every stylesheet it imported) reuses its cached class/tag, and its
template tree is decoded lazily on first render. Zé Chat (103
components, 4.3 MB of source): 590 ms -> 16 ms per cold start. Editing a
component or any CSS it imports still recompiles it.WindowMetrics instance and
no longer invalidates and re-renders the whole tree. Android exports the
full style environment in PAM_BOOT_METRICS so the first event matches.android.benchmarkApplicationIdSuffix and pam-native build --benchmark:
the installable release-optimized variant (R8, non-debuggable, baseline
profile, signed with the local debug key) is written to
dist/<name>-<version>-android-benchmark.apk and can install beside
production for cold-start measurements; such builds omit Firebase.
run --release, benchmark and profile use the same package.dev.pam.nativeapp host.pm clear.manifest.sha256) and the full
PAM_BOOT_METRICS environment are ported. Uncompiled; needs Mac
validation.Android: a BottomSheet with keyboardBehavior="interactive" (Zé's "Editar mensagem") rides above its own keyboard again. The sheet window keeps adjustN
keyboardBehavior="interactive" (Zé's "Editar
mensagem") rides above its own keyboard again. The sheet window keeps
adjustNothing and read the IME from its content view, where a window
fitting system windows (Android 11-14, the Galaxy S10) had already
consumed the insets, so the field and Save button stayed behind the
keyboard. The IME is now read on the dialog's decor view and followed
frame by frame (WindowInsetsAnimation); the sheet's bottom edge is put
on the IME top from the real window geometry (edge-to-edge or not).autoFocus inside a sheet waits (up to 1 s) for the sheet window's focus
before opening the keyboard.PamSheetKeyboardInstrumentedTest: tapped and auto-focused sheet inputs
end directly above the IME with Save visible; the base composer and root
IME inset stay at 0; Back settles the sheet.keyboardWillChangeFrame with the keyboard's
duration and curve, the screen below ignores the keyboard of an input
inside a presented modal, and autoFocus inside a sheet runs once it is
presented. Uncompiled; needs Mac validation.Android: taps inside a Pressable nested in another Pressable reach the view under the finger again. Hit-slop delegates (every Pressable registers a mi
TouchDelegate, which re-centres each event in its target: a
Pressable panel inside a Pressable backdrop sent every tap to the child at
its centre, so all tiles of Zé's message-actions overlay fired the 5th
("react"). Delegates now keep the touch position (clamped onto the target
only for slop points) and large-enough targets are not delegated at all.
PamNestedPressableInstrumentedTest taps 12 tiles in nested Pressables and
in a re-opened Modal and expects 12 distinct ids.PamSheetNestedScrollInstrumentedTest.PamNestedPressableTests and the sheet change are uncompiled and need Mac
validation.Keyboard, safe-area, input and text-fit fixes from Zé Chat device QA (Galaxy S10, Android 12, three-button navigation).
Keyboard, safe-area, input and text-fit fixes from Zé Chat device QA (Galaxy S10, Android 12, three-button navigation).
pam_native_engine_set_keyboard_inset; Android WindowInsets IME,
iOS keyboardWillChangeFrame) and a KeyboardAvoidingView behavior="pan" that ends a vertical container (a chat composer after a
flexible timeline) is laid out directly above the keyboard while the
flexible siblings shrink, like adjustResize / react-native-keyboard-
controller. The composer is visible, tappable and reported at its real
bounds; the timeline keeps its last rows above the composer. Modal content
is excluded (modal windows resize themselves). See
docs/android-safe-area.md.z-index (the composer was drawn under a z-index: 1
timeline); the root host's inset handling keeps running while a KAV is
mounted; the IME height is reconciled from the settled window insets.on:blur handlers restore resting layout (the composer went
under the navigation bar).mostRecentEventCount; dropped
characters such as "qa teste" -> "qa test"); authored values still apply.
Plain inputs no longer remove a digit on Backspace ("J6pKK..." -> "JpK"):
the masked-input digit rule now applies only to formatted inputs. iOS has
the same echo guard.error_log() and logged errors reach logcat (tag PamPHP) for the
whole process lifetime, including re-attached Activities.PamKeyboardInstrumentedTest (composer above the IME, Back blur,
stale echoes, Backspace burst, unfocused window insets, overlay -> sheet in
one commit), PamTextClipInstrumentedTest (pixel tests), engine layout
tests, JVM and XCTest echo tests. iOS changes are not compiled here and
need Mac validation.:key (or key) on a p-for child is the loop identity exactly like p-key: the component instance, identity slot and native id follow the item, and dupli
:key (or key) on a p-for child is the loop identity exactly like
p-key: the component instance, identity slot and native id follow the
item, and duplicate keys are reported. Unkeyed loops keep one identity per
iteration index.p-for loops of components (:key, p-key,
unkeyed) through the runtime, decode the committed frames like a native
host and dispatch presses by native node id with a pointer payload: every
iteration runs its own callback on its own component instance across
reorders and conditional siblings.Unkeyed siblings keep their native identity when a conditional sibling appears or disappears (React-like reconciliation). Behaviour change: apps no lo
Unkeyed siblings keep their native identity when a conditional sibling
appears or disappears (React-like reconciliation). Behaviour change: apps no
longer need :key on every sibling of a p-if to avoid remounts.
p-if, the inactive p-if/p-else branch, If/Show/Match/
Await blocks, an empty p-for and null/false children passed to the
Element API (View, Column, Row, Screen, SafeAreaView, Pressable,
Grid, VirtualizedList, ...) leave a hole, so the siblings after them
keep their identity: a sheet Modal no longer inherits the native host of
a removed full-screen overlay Modal, and a VirtualizedList or
TextInput after a conditional banner is not remounted (no empty timeline
frame, focus and IME are kept).p-for children are identified by p-key (stable across reorders);
unkeyed loops with more than one item log a one-time development warning.Lifecycle and mount-cost fixes from Zé Chat device QA.
Lifecycle and mount-cost fixes from Zé Chat device QA.
PamActivity only
detaches its surface and the next Activity re-attaches to the live runtime
(renderer, modules and callbacks rebound) while the engine replays the
retained tree as a full mount (pam_native_engine_remount), in order with
the worker's batches. PHP state survives; nothing re-executes. Relaunch
renders in ~60 ms on the API 36 emulator
(PamActivityRelaunchInstrumentedTest, budget 1 s).emalloc) strings in PHP's persistent ini configuration hash and
php_embed_shutdown freed them with free()
(zend_hash_destroy ← php_shutdown_config). The ini defaults are now
persistent strings, and shutting the runtime down never blocks the UI
thread.CloseApp (Back at the root of a stack) follows React Native's
invokeDefaultOnBackPressed(): Android 12+ moves the root task to the
background instead of finishing it; older releases finish and re-attach.Failure with a clear message instead of being
dropped, on Android and iOS.PamVirtualListRebindInstrumentedTest).pam_native_runtime_remount),
PamRuntime.attach(hostView:)/detach(), PamNativeViewController
re-attaches to the live runtime; renderer uses binary sibling insertion,
dirty-list syncing and re-binds modal triggers only on structural or marker
changes.iOS SQLite reuses compiled statements per database (32-entry LRU keyed by SQL, oversized one-off statements excluded, finalized on close), matching An
SQLiteDatabase statement cache; pam-native-nitro upserts,
batches and paged reads no longer re-prepare SQL on every call. Statements
are reset right after use so cached reads never hold a WAL snapshot.onTextLayout and ellipsizeMode="marquee" on Text are owned by
PamTextView (1.9.0); the 1.8.0 UILabel fallback no longer duplicates
text-layout events or reconfigures PamTextView.SQLiteStatementCacheTests).iOS rendering parity with the Android 1.2.0–1.7.0 releases (CSS paint and effects, typography, layout, components and the error overlay). See docs/ios
iOS rendering parity with the Android 1.2.0–1.7.0 releases (CSS paint and effects, typography, layout, components and the error overlay). See docs/ios-parity.md. Swift and the iOS bridge were written on Linux: the bridge was syntax-checked with the PHP headers, the Swift sources and the new XCTest suites are uncompiled and need Mac validation.
Text draws with a CoreText layout (PamTextLayout) that the Rust
engine also uses to measure every text box (pam_native_ios_set_text_measurer,
installed before the first frame): React Native iOS line-height rule
(scaled by fontScale), pixel-ceiled sizes, first baselines,
numberOfLines ellipsis modes, adjustsFontSizeToFit, nested spans with
per-span press (SpanPress), text-shadow, font-feature-settings /
font-variant-numeric, text-align: justify, on:textLayout and marquee.
A bare fontFamily resolves bundled files by React Native conventions;
<Icon> fonts load from asset://.transform-origin and % translations, linear/radial/repeating gradients
with premultiplied stops clipped to the radius, border-image gradient
rings, multiple/inset/spread box-shadow as cached Core Animation shadow
paths (outer shadows of overflow: hidden views live in a sibling layer),
image filter/blurRadius (blur with opaque edges + color matrix),
backdrop-filter: blur(), <Shimmer>. Filters on non-image views and
backdrop color functions log a one-time debug diagnostic.PAM_BOOT_METRICS exported, native insets never
applied twice, geometry changes relayout), on:layout, ScrollView
content size (previously never set on iOS), sticky headers in ScrollView
and VirtualizedList, <ScrollView keyboardInset>, lists resting at their
end stay there, scroll targets use the real viewport, and scrolled-out cell
views are pooled by kind and property set.MediaReadyEvent natural size/duration, on:mediaLoadStart,
on:buffering; pressed-state translate in points; Toast::message()
card; Modal backdropColor/animationType (incl. slide-fade)/
transparent; bottom sheets slide while the backdrop fades and resolve
% snap points against the container minus the top inset.devErrorOverlay → PamDevErrorOverlay), coalesced events of removed
nodes are dropped, media cache identities are hashed once with a fast hex
encoder.appearance.splash builds the iOS launch screen
(App/PamLaunch.xcassets, light/dark background, PNG logo) and the host
keeps the logo until the first frame.iOS parity for the 1.5.0 gestures/animations and the 1.1.x native modules. See docs/gestures.md, docs/animations.md and docs/native-capabilities.md.
iOS parity for the 1.5.0 gestures/animations and the 1.1.x native modules. See docs/gestures.md, docs/animations.md and docs/native-capabilities.md.
PressEvent coordinates for press/long press, delayLongPress,
press-in/out delays (hold-to-record), on:doubleTap with deferred single
press and TapEffect (heart burst), Drag (rubber band, drivers on
nativeRef views, snaps with spring/timing settle, threshold/velocity
release, haptic tick, groups, dragSnap), Swipeable, animation programs
and presets, replayKey, per-property CSS transitions with spring(),
per-keyframe easing, scroll begin/end drag and momentum end with page
index, list pagingEnabled, on:textLayout, ellipsizeMode="marquee"
and the slide-fade modal. The spring solver matches Android/PHP
durations exactly.Http::multipart() and upload transfers with progress
and cancellation, Image::prefetch(), conversation notifications
(communication notifications with avatar, inline reply, mark as read),
durable Notifications::onAction() queue with native ActionEndpoint
delivery, PushRendering for background (content-available) pushes with
route suppression and PushMessage::$rendered.Screen::secure() on iOS: screenshots cannot be blocked, so secure mode
shields every window while the screen is recorded/mirrored
(UIScreen.isCaptured) and while the app is inactive (app switcher).remote-notification and INSendMessageIntent.LogBox-style runtime error overlay. See docs/error-overlay.md.
LogBox-style runtime error overlay. See docs/error-overlay.md.
/data/user/0/<pkg>/files/pam/releases/<hash>/ stripped), source
snippet, collapsible framework/vendor frames, wrapped monospace stack,
Dismiss / Copy / Reload, and ‹ 1 / N › for queued errors (repeats count
as ×N). Fatal render/boot errors open the inspector directly. Respects
safe areas and the light/dark appearance.pam-native.json
devErrorOverlay: true (default, debug only), false, or "always".App::onError(Closure(Throwable, RuntimeError)) / offError(): crash
reporting hook (Sentry, observability) for every uncaught error, with
phase (boot, render, event, module), fatal(), classified frames and a
fingerprint.SQLite::execute/query/executeMany/ transaction and Storage::get/set accept onError, and
NativeModules::call/callRaw accept onFailure, so failures such as
"Native module value is too large" go to that callback instead of the
overlay. Without one they surface as a non-fatal
Pam\Native\Modules\NativeModuleException (a RuntimeException).phase, fatal, frames, appFrame,
snippet, fingerprint); version 1 and plain-text errors still render.values-pt).Fixes for the React Native parity releases (1.4.0+), found on a Galaxy S10.
Fixes for the React Native parity releases (1.4.0+), found on a Galaxy S10.
PamRuntime.onMeasureText), so minified builds
silently fell back to the portable estimator (wrong text box heights, e.g.
line pitch not following fontScale). The callback is now kept, with a unit
test that every JNI callback looked up by the bridge has a keep rule.Text::rich()/<Span> and Icon no longer require ext-mbstring
(absent from the Android PHP runtime).lineHeight × fontScale 1.1 parity, ProGuard JNI contract.examples/layout_dump.rs prints engine frames for a captured render frame
and window safe area.React Native component parity on Android. See docs/react-native-components-parity.md.
React Native component parity on Android. See docs/react-native-components-parity.md.
<Icon font name size color> / Pam\Native\UI\Icon: react-native-vector-icons
glyph maps (assets/fonts/{Font}.ttf + {Font}.json, or Icon::register())
rendered as icon-font text, measured like React Native.stickyHeader="true", Element::stickyHeader()) in
ScrollView and VirtualizedList.VirtualizedList: multi-column rows and horizontal cells are content-sized
too (tallest cell per row); fullSpan="true" (Element::fullSpan(), protocol ListFullSpan` 523) for
header/footer rows in multi-column lists.:active now applies like :pressed; pressed-state transform: translate…
is in points and applies to Pressable (React Native translateY: 1.1).MediaPlayer: on:mediaLoadStart, on:buffering, on:ready with
MediaReadyEvent (natural size, duration); existing handlers keep working.Toast::message() in-app toast with title and message
(react-native-toast-message styling, position, duration, colors, font).
iOS shows the message with the system presentation.<ScrollView keyboardInset>: keyboard-aware bottom content inset.overflow: hidden clips to the padding box with inner radii, so border
rings stay visible around clipped content (avatars), like React Native.fontFamily resolves bundled files by React Native naming
conventions ({Family}-{Weight}.ttf, _bold, …) without synthetic bold
when an exact file exists.Virtual list layout and scroll fixes (chat timelines).
Virtual list layout and scroll fixes (chat timelines).
VirtualizedList cells without an
authored height are now content-sized, like React Native FlatList
cells. rowHeight/estimatedRowHeight remains the prefetch estimate and
the extent of empty cells or cells whose content has no definite intrinsic
height (e.g. percentage-sized media). Previously every such cell was
clamped to the estimate, so variable rows (chat bubbles, comments) were
clipped and overlapped. Multi-column grids keep the previous behaviour.min-width/max-width), so
capped content-sized boxes no longer clip their wrapped lines.initialScrollIndex applied in the same commit no longer
overrides an explicit scroll request, and a list resting at its end stays
there when its cells are re-measured or its viewport shrinks.git worktree (whose .git is a file) is treated as a
source checkout, so its locally built engine is no longer replaced by the
release archive.Render pipeline performance, part 3 (from a Galaxy S10 cold-start trace and the chat benchmark fixture). Chat fixture on an API 36 emulator, 3 runs (1
Render pipeline performance, part 3 (from a Galaxy S10 cold-start trace and the chat benchmark fixture). Chat fixture on an API 36 emulator, 3 runs (1.2.2 → this release): scroll jank 6.0% → 2.9%, scroll frame p90/p99 26/61 → 16/32 ms, typing frame p90 19 → 16 ms; app threads have no frame over 16 ms during scroll in a Perfetto trace (remaining jank is emulator composition).
String.format, which serialised the three image threads on ICU
locale locks and accounted for most of their CPU at cold start on device.Runtime::deferRendering()/flush() (12 ms budget), and
collects cycles only when idle.Gestures and animations on the UI thread (React Native Reanimated + gesture-handler parity for the Zé Chat rewrite). During a drag or an animation PHP
Gestures and animations on the UI thread (React Native Reanimated + gesture-handler parity for the Zé Chat rewrite). During a drag or an animation PHP receives no events; it gets the final semantic event only.
on:press/on:longPress handlers typed PressEvent receive
x/y (RN locationX/Y) and pageX/pageY; untyped handlers keep the
empty payload. Hold-to-record works with delayLongPress + on:longPress
on:pressOut.on:doubleTap (+ doubleTapDelay) defers and cancels the
single press natively; tapEffect (TapEffect) plays an animation
centred on the tap (Reels heart burst) before PHP is notified.Pam\Native\Animation\Drag, :drag on a pan GestureDetector):
bounded one-axis drag with rubber band, per-frame drivers on nativeRef
views (opacity, translate, scale, rotate, borderRadius), snap points with
spring or timing settle, distance/velocity thresholds, haptic tick, groups
and programmatic dragSnap="index@request". GestureEvent gains
snapIndex/thresholdReached; new on:gestureSettle
(GestureSettleEvent).Swipeable (PHP and <Swipeable> tag): left/right action panels, spring
open/close, one open row per group, closeRequest.Pam\Native\Animation\Animation): timing, spring,
set, wait, sequence, with, then, delay, repeat and replay key(), attached
with :animation on any element or <Animated :animation>; presets
heartBurst, likeBounce, backToTop, shimmer, pulse, marquee,
fadeInUp; replayKey; per-keyframe easing.spring(mass stiffness damping);
Element::transition().on:scrollBeginDrag, on:scrollEndDrag (velocity) and
on:momentumScrollEnd with the page index (ScrollPhaseEvent); lists
accept pagingEnabled (one item per page).on:textLayout (TextLayoutEvent: wrapped and visible lines,
truncation, line widths); ellipsizeMode="marquee".animationType="slide-fade" fades the backdrop while the content
slides independently.HttpResponse::date() no longer uses the PHP 8.5-deprecated DateTimeInterface::RFC7231 constant.
React Native typography and layout parity (Android). See docs/typography.md.
Text box with the
Android text stack that draws it (pam_native_engine_set_text_measurer,
PamTextLayout), following React Native: integer (ceil) font pixel sizes,
hinted glyph advances, kerning, emoji/fallback fonts, CustomLineHeightSpan
line heights, ceil widths, last-line heights and real first baselines.
numberOfLines implies a tail ellipsis. iOS keeps the estimator.includeFontPadding now defaults to true like React
Native Android (single-line Roboto 14 sp is 49 px instead of 43 px at
2.625×). Opt out with include-font-padding: false
(-pam-include-font-padding), includeFontPadding="false" or
Text::includeFontPadding(false). Text nodes no longer force linear
(subpixel) advances.Text may contain text and nested Text/Span runs with
their own size, weight, style, family, color, background, decoration,
letter spacing and transform, rendered as one paragraph. on:press on a run
makes it a link/mention (EventKind::SpanPress). PHP: Text::rich().
Whitespace inside Text follows JSX.hairline (= StyleSheet.hairlineWidth) and Ndpx device-pixel units in
CSS; Pam\Native\PixelRatio (get, getFontScale, roundToNearestPixel,
getPixelSizeForLayoutSize, hairlineWidth).align-items/align-self: baseline uses the first
text baseline of container children recursively.on:layout / Element::onLayout() (LayoutEvent x/y/width/height relative
to the parent), on mount and on frame changes, coalesced per frame.SafeAreaView insets are laid out by the engine for the window
edges each view touches, at any nesting level (fixes a bottom-only nested
SafeAreaView under a root without the bottom edge drawing under the
navigation bar). WindowMetrics safe areas are correct from the first
render (PAM_BOOT_METRICS) and Dimensions is re-sent when insets change.
New android.safeAreaBottomFallback (dp) for devices reporting no bottom
inset.appearance.splash (logo, darkLogo, size) for the Android 12+
SplashScreen icon and the legacy starting window.HttpResponse::date() no longer uses the PHP 8.5-deprecated
DateTimeInterface::RFC7231 constant.TextSpans (501) … ScrollKeyboardInset
(508), events SpanPress (66), Layout (67), MediaBuffering (68),
MediaLoadStart (69).Native CSS visual effects on top of the 1.2.0 complete-CSS compiler and the 1.2.x render-pipeline work:
Native CSS visual effects on top of the 1.2.0 complete-CSS compiler and the 1.2.x render-pipeline work:
linear-gradient(), radial-gradient() and their
repeating-* variants (angles, to <side>, magic corners, circle/ellipse,
size keywords, explicit radii, at <position>, multiple and double-position
color stops, hard stops) in background/background-image, layered over
background-color and clipped anti-aliased to border-radius. Stops
interpolate in premultiplied space like browsers (transparent fades never
darken).<LinearGradient colors start end locations> with expo-linear-gradient
semantics for React Native ports.border-image: <gradient> 1 paints a gradient stroke that follows
border-radius (story rings).box-shadow: comma-separated lists, inset shadows and spread; CSS blur
semantics (σ = blur / 2). Outer shadows use cached, downscaled ALPHA_8
nine-slice masks shared across views and now follow the child's full
transform (scale/rotation).filter: blur() plus brightness(), contrast(), saturate(),
grayscale(), sepia(), invert(), opacity() and hue-rotate(),
composed at compile time into one color matrix (RenderEffect on API 31+,
hardware layer paint before). blur() below API 31 is now a logged no-op
instead of a fake elevation.backdrop-filter on containers (Android 12+): GPU blur/color matrix of
the content behind the element, clipped to its radius.<Shimmer baseColor gradientColor duration enabled> skeleton primitive
(Zé Chat shimmer port, shared frame clock, pauses off screen).<Image blurRadius> / filter: blur() on images now blur the bitmap once
with opaque edges on every API level (React Native semantics) instead of
a GPU blur that needed API 31.BackgroundGradient,
BoxShadows, FilterColorMatrix, BackdropBlurRadius,
BackdropColorMatrix, BorderGradient, ShimmerGradientColor,
ShimmerDurationMs, ShimmerEnabled).conic-gradient(), gradient color hints,
url() backgrounds, drop-shadow()/url() filters, non-neutral
background-size/position/clip. iOS paints colors and the first outer
shadow only; the new effects log a one-time debug diagnostic there.background: <color> now also clears gradients and
filter: <function> resets the other filter functions, as in CSS.Your coding agent can read these notes before it upgrades. Set up the MCP server →