From 74902d2af2f7b89a44eb3ab122de758b8cb68a74 Mon Sep 17 00:00:00 2001 From: Dishit Date: Mon, 8 Jun 2026 11:42:25 +0530 Subject: [PATCH 1/6] fix(android): reduce LiteRT native crashes - Add libvndksupport.so to AndroidManifest to fix GPU init on Qualcomm SoCs - Skip GPU backend on Pixel 10 where LiteRT GPU init crashes with uncatchable native abort (libPVROCL OOM/SIGSEGV) - Call cancelProcess() before close() in closeConversationSafely to prevent use-after-free SIGSEGV in nativeSendMessageAsync Co-Authored-By: Dishit Karia --- android/app/src/main/AndroidManifest.xml | 1 + .../ai/offgridmobile/litert/LiteRTModule.kt | 30 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4c8c20b2e..69f1e4beb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ + = when (requested) { - is Backend.NPU -> listOf( - Backend.NPU(nativeLibraryDir = reactContext.applicationInfo.nativeLibraryDir), - Backend.GPU(), - Backend.CPU(), - ) - is Backend.GPU -> listOf(Backend.GPU(), Backend.CPU()) - else -> listOf(Backend.CPU()) + private fun buildBackendChain(requested: Backend): List { + // GPU init crashes on Pixel 10 — open LiteRT SDK bug. + val skipGpu = Build.MODEL?.lowercase()?.contains("pixel 10") == true + return when (requested) { + is Backend.NPU -> listOfNotNull( + Backend.NPU(nativeLibraryDir = reactContext.applicationInfo.nativeLibraryDir), + if (skipGpu) null else Backend.GPU(), + Backend.CPU(), + ) + is Backend.GPU -> if (skipGpu) listOf(Backend.CPU()) else listOf(Backend.GPU(), Backend.CPU()) + else -> listOf(Backend.CPU()) + } } private suspend fun tryInitBackend(modelPath: String, backend: Backend, name: String, visionEnabled: Boolean): Boolean { @@ -427,6 +432,15 @@ class LiteRTModule(private val reactContext: ReactApplicationContext) : } pendingToolCalls.clear() + // Signal the native inference thread to stop before freeing the Conversation object. + // Without this, the native thread can fire onDone after close() frees the native handle, + // causing a use-after-free SIGSEGV in nativeSendMessageAsync. + try { + conversation?.cancelProcess() + } catch (e: Exception) { + Log.w(TAG, "closeConversationSafely — cancelProcess error: ${e.message}") + } + try { conversation?.close() Log.d(TAG, "closeConversationSafely — closed") From c747d7939eeb19fa9210824d3df6f98f5ceb7e2d Mon Sep 17 00:00:00 2001 From: Dishit Date: Mon, 8 Jun 2026 12:19:44 +0530 Subject: [PATCH 2/6] fix(android): fix double-close race in LiteRT conversation teardown Add @Volatile to conversation field so null-first swap in closeConversationSafely is visible across threads; stopGeneration now signals stop synchronously without tearing down the conversation, eliminating the race with unloadModel that caused SIGSEGV Co-Authored-By: Dishit Karia --- .../ai/offgridmobile/litert/LiteRTModule.kt | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/java/ai/offgridmobile/litert/LiteRTModule.kt b/android/app/src/main/java/ai/offgridmobile/litert/LiteRTModule.kt index d337d5bfb..e74969f7e 100644 --- a/android/app/src/main/java/ai/offgridmobile/litert/LiteRTModule.kt +++ b/android/app/src/main/java/ai/offgridmobile/litert/LiteRTModule.kt @@ -69,7 +69,7 @@ class LiteRTModule(private val reactContext: ReactApplicationContext) : private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private var engine: Engine? = null - private var conversation: com.google.ai.edge.litertlm.Conversation? = null + @Volatile private var conversation: com.google.ai.edge.litertlm.Conversation? = null private var activeBackend: String = "cpu" private var supportsVision: Boolean = false private var currentJob: Job? = null @@ -371,18 +371,17 @@ class LiteRTModule(private val reactContext: ReactApplicationContext) : @ReactMethod fun stopGeneration(promise: Promise) { val safe = SafePromise(promise, TAG) - Log.i(TAG, "stopGeneration — tearing down conversation") - - scope.launch { - try { - closeConversationSafely() - Log.i(TAG, "stopGeneration — done") - safe.resolve(null) - } catch (e: Exception) { - Log.w(TAG, "stopGeneration — error during teardown: ${e.message}") - safe.resolve(null) - } + Log.i(TAG, "stopGeneration — signalling stop") + // Cancel the coroutine job — CancellationException propagates into sendMessageAsync + currentJob?.cancel() + // Signal the native inference thread to stop — safe to call from any thread + try { + conversation?.cancelProcess() + } catch (e: Exception) { + Log.w(TAG, "stopGeneration — cancelProcess error: ${e.message}") } + Log.i(TAG, "stopGeneration — done") + safe.resolve(null) } // ------------------------------------------------------------------------- @@ -422,6 +421,7 @@ class LiteRTModule(private val reactContext: ReactApplicationContext) : // ------------------------------------------------------------------------- private suspend fun closeConversationSafely() { + // Cancel and wait for any running inference job to fully stop before closing currentJob?.cancel() currentJob?.join() currentJob = null @@ -432,22 +432,21 @@ class LiteRTModule(private val reactContext: ReactApplicationContext) : } pendingToolCalls.clear() - // Signal the native inference thread to stop before freeing the Conversation object. - // Without this, the native thread can fire onDone after close() frees the native handle, - // causing a use-after-free SIGSEGV in nativeSendMessageAsync. + // Null first — any concurrent caller gets null and returns, preventing double-close + val conv = conversation ?: return + conversation = null + + // Safety net: signal stop in case stopGeneration was not called before close try { - conversation?.cancelProcess() + conv.cancelProcess() } catch (e: Exception) { Log.w(TAG, "closeConversationSafely — cancelProcess error: ${e.message}") } - try { - conversation?.close() + conv.close() Log.d(TAG, "closeConversationSafely — closed") } catch (e: Exception) { Log.w(TAG, "closeConversationSafely — error: ${e.message}") - } finally { - conversation = null } } From 4478a9c5b33075d6ab0892e6aef186436d26c053 Mon Sep 17 00:00:00 2001 From: Dishit Date: Mon, 8 Jun 2026 12:30:33 +0530 Subject: [PATCH 3/6] show ui msg gpu is not supported on pixel 10 Co-Authored-By: Dishit Karia hanmadishit74@gmail.com --- src/screens/ModelsScreen/TextModelsTab.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/screens/ModelsScreen/TextModelsTab.tsx b/src/screens/ModelsScreen/TextModelsTab.tsx index 7523540b0..e0d8df973 100644 --- a/src/screens/ModelsScreen/TextModelsTab.tsx +++ b/src/screens/ModelsScreen/TextModelsTab.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; import { View, Text, FlatList, TextInput, ActivityIndicator, RefreshControl, TouchableOpacity, InteractionManager, Platform } from 'react-native'; +import DeviceInfo from 'react-native-device-info'; import Icon from 'react-native-vector-icons/Feather'; import { AttachStep, useSpotlightTour } from 'react-native-spotlight-tour'; import { Card, ModelCard } from '../../components'; @@ -244,6 +245,15 @@ const ModelDetailView: React.FC = ({ )} + {selectedModel.id === LITERT_PARENT_ID && Platform.OS === 'android' && + DeviceInfo.getModel().toLowerCase().includes('pixel 10') && ( + + + + {'GPU acceleration is not yet supported on Pixel 10. Models will run on CPU.'} + + + )} Available Files {selectedModel.id !== LITERT_PARENT_ID && ( From 6745ea151258defa01994227b6ab3738afc05825 Mon Sep 17 00:00:00 2001 From: Dishit Date: Mon, 8 Jun 2026 13:32:08 +0530 Subject: [PATCH 4/6] reduce number of lines --- src/screens/ModelsScreen/TextModelsTab.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/screens/ModelsScreen/TextModelsTab.tsx b/src/screens/ModelsScreen/TextModelsTab.tsx index e0d8df973..cf52a38cc 100644 --- a/src/screens/ModelsScreen/TextModelsTab.tsx +++ b/src/screens/ModelsScreen/TextModelsTab.tsx @@ -245,13 +245,10 @@ const ModelDetailView: React.FC = ({ )} - {selectedModel.id === LITERT_PARENT_ID && Platform.OS === 'android' && - DeviceInfo.getModel().toLowerCase().includes('pixel 10') && ( + {selectedModel.id === LITERT_PARENT_ID && Platform.OS === 'android' && DeviceInfo.getModel().toLowerCase().includes('pixel 10') && ( - - {'GPU acceleration is not yet supported on Pixel 10. Models will run on CPU.'} - + {'GPU acceleration is not yet supported on Pixel 10. Models will run on CPU.'} )} Available Files @@ -388,7 +385,6 @@ export const TextModelsTab: React.FC = (props) => { setTypeFilter, setSourceFilter, setSizeFilter, setQuantFilter, setSortOption, isModelDownloaded, getDownloadedModel, isRepairingVisionModel, } = props; - const hasNonSortActiveFilters = hasNonSortFilters(filterState); const currentSort = SORT_OPTIONS.find(o => o.key === filterState.sort) ?? SORT_OPTIONS[0]; const isSortActive = filterState.sort !== 'recommended'; From bb37d8dc8df0eb17477e9c109603b2a2a244fa49 Mon Sep 17 00:00:00 2001 From: Dishit Date: Mon, 8 Jun 2026 15:20:22 +0530 Subject: [PATCH 5/6] fix(android): guard null job in whisper realtime finish to prevent SIGSEGV finishRealtimeTranscribeJob dereferenced the result of job_get() without a null check; when the job was already removed (double-finish on the 30s buffer-full path, or an abort/transcribe race) this caused a SIGSEGV in production (0.0.95). Add a null guard in jni.cpp and make the Java finishRealtimeTranscribe a one-shot (synchronized + isRealtimeFinished) so it cannot fire twice for the same job. Co-Authored-By: Dishit Karia --- patches/whisper.rn+0.5.5.patch | 56 ++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/patches/whisper.rn+0.5.5.patch b/patches/whisper.rn+0.5.5.patch index f7488dc62..ebb100cc7 100644 --- a/patches/whisper.rn+0.5.5.patch +++ b/patches/whisper.rn+0.5.5.patch @@ -7,10 +7,44 @@ index 0000000..1675490 +connection.project.dir=../../../android +eclipse.preferences.version=1 diff --git a/node_modules/whisper.rn/android/src/main/java/com/rnwhisper/WhisperContext.java b/node_modules/whisper.rn/android/src/main/java/com/rnwhisper/WhisperContext.java -index a056c22..f4781e9 100644 +index a056c22..346cd72 100644 --- a/node_modules/whisper.rn/android/src/main/java/com/rnwhisper/WhisperContext.java +++ b/node_modules/whisper.rn/android/src/main/java/com/rnwhisper/WhisperContext.java -@@ -465,6 +465,16 @@ public class WhisperContext { +@@ -81,6 +81,11 @@ public class WhisperContext { + private boolean isTdrzEnable = false; + private Thread rootFullHandler = null; + private Thread fullHandler = null; ++ // Guards against finishRealtimeTranscribe running more than once per job. The ++ // buffer-full path can reach finishRealtimeTranscribe at line 185 and again at ++ // the end-of-loop check, which double-removes the native job and crashes in ++ // finishRealtimeTranscribeJob (SIGSEGV). Reset in rewind() per new job. ++ private boolean isRealtimeFinished = false; + + public WhisperContext(int id, ReactApplicationContext reactContext, long context) { + this.id = id; +@@ -102,6 +107,7 @@ public class WhisperContext { + isTdrzEnable = false; + rootFullHandler = null; + fullHandler = null; ++ isRealtimeFinished = false; + } + + public int getId() { +@@ -117,7 +123,12 @@ public class WhisperContext { + return vadSimple(jobId, sliceIndex, nSamples, n); + } + +- private void finishRealtimeTranscribe(WritableMap result) { ++ private synchronized void finishRealtimeTranscribe(WritableMap result) { ++ // Run at most once per job. Multiple code paths in the capture loop can reach ++ // here for the same job; a second call would remove an already-removed native ++ // job and SIGSEGV in finishRealtimeTranscribeJob. ++ if (isRealtimeFinished) return; ++ isRealtimeFinished = true; + emitTranscribeEvent("@RNWhisper_onRealtimeTranscribeEnd", Arguments.createMap()); + finishRealtimeTranscribeJob(jobId, context, sliceNSamples.stream().mapToInt(i -> i).toArray()); + } +@@ -465,6 +476,16 @@ public class WhisperContext { public void release() { stopCurrentTranscribe(); @@ -27,3 +61,21 @@ index a056c22..f4781e9 100644 freeContext(id, context); } +diff --git a/node_modules/whisper.rn/android/src/main/jni.cpp b/node_modules/whisper.rn/android/src/main/jni.cpp +index b1fa548..5f0b7f1 100644 +--- a/node_modules/whisper.rn/android/src/main/jni.cpp ++++ b/node_modules/whisper.rn/android/src/main/jni.cpp +@@ -485,6 +485,13 @@ Java_com_rnwhisper_WhisperContext_finishRealtimeTranscribeJob( + UNUSED(context_ptr); + + rnwhisper::job *job = rnwhisper::job_get(job_id); ++ // The job may already have been removed by fullWithJob completing/aborting, ++ // or by a duplicate finishRealtimeTranscribe call from the realtime thread. ++ // job_get returns nullptr in that case; dereferencing it here is a SIGSEGV. ++ if (job == nullptr) { ++ rnwhisper::job_remove(job_id); ++ return; ++ } + if (job->audio_output_path != nullptr) { + RNWHISPER_LOG_INFO("job->params.language: %s\n", job->params.language); + std::vector slice_n_samples_vec; From 2698cbc49c32675bbdf0364b1efde5d54fdeece8 Mon Sep 17 00:00:00 2001 From: Dishit Date: Mon, 8 Jun 2026 17:01:33 +0530 Subject: [PATCH 6/6] improve pixel 10 warning ui bring in one line with i button and change colour to yellow --- src/screens/ModelsScreen/TextModelsTab.tsx | 4 ++-- src/screens/ModelsScreen/styles.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/screens/ModelsScreen/TextModelsTab.tsx b/src/screens/ModelsScreen/TextModelsTab.tsx index cf52a38cc..9439896a9 100644 --- a/src/screens/ModelsScreen/TextModelsTab.tsx +++ b/src/screens/ModelsScreen/TextModelsTab.tsx @@ -247,8 +247,8 @@ const ModelDetailView: React.FC = ({ {selectedModel.id === LITERT_PARENT_ID && Platform.OS === 'android' && DeviceInfo.getModel().toLowerCase().includes('pixel 10') && ( - - {'GPU acceleration is not yet supported on Pixel 10. Models will run on CPU.'} + + {'GPU acceleration is not yet supported on Pixel 10. Models will run on CPU. Support coming soon.'} )} Available Files diff --git a/src/screens/ModelsScreen/styles.ts b/src/screens/ModelsScreen/styles.ts index d5a846f73..df48f1ab7 100644 --- a/src/screens/ModelsScreen/styles.ts +++ b/src/screens/ModelsScreen/styles.ts @@ -112,15 +112,18 @@ const createBaseStyles = (colors: ThemeColors, shadows: ThemeShadows) => ({ loadingText: { ...TYPOGRAPHY.body, color: colors.textSecondary }, listContent: { paddingHorizontal: 16, paddingBottom: 32 }, deviceBanner: { - backgroundColor: `${colors.primary}12`, + backgroundColor: `${colors.trending}15`, borderRadius: 8, paddingHorizontal: SPACING.md, paddingVertical: SPACING.sm, marginBottom: SPACING.lg, borderWidth: 1, - borderColor: `${colors.primary}30`, + borderColor: `${colors.trending}40`, + flexDirection: 'row' as const, + alignItems: 'center' as const, + gap: SPACING.xs, }, - deviceBannerText: { ...TYPOGRAPHY.meta, color: colors.primary }, + deviceBannerText: { ...TYPOGRAPHY.meta, color: colors.trending, flex: 1 }, deviceBannerWarning: { color: colors.error, marginTop: 2 }, emptyCard: { alignItems: 'center' as const, padding: 32 }, emptyText: { color: colors.textSecondary, textAlign: 'center' as const },