fix(vkbridge): Mali memory sharing via dma-heap; debuggable fork builds with a server override

Tablet run 2: dma-heap and AHB imports both land in type 1 (HOST_CACHED, not
COHERENT) and pass the sharing test without any flush (uncached heap pages), so
the self-test now tries each type without flushes first and counts a pass as
coherent; the client reports such a type HOST_COHERENT (and not HOST_CACHED for
dma-heap). On the SM-X826B: memory sharing = dmaheap.

Also: release builds of this fork are debuggable (run-as reaches the session
logs), and a debuggable build loads files/vkbridge/dev/libvkbridge_jni.so when
present, so a rebuilt server can be tried without a new APK. Self-test switches
VKBRIDGE_SELFTEST_NOFLUSH, VKBRIDGE_AHB_RARELY, VKBRIDGE_DMAHEAP_CACHED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ahmad Sayed Alsayed EbrahimandClaude Opus 5.5 committed 2026-10-10 17:58:19 +03:00
1 parent ce34349ffb
commit 6b82334145
5 files changed
+55 -8

No files matched your search

+3
View File
@@ -142,6 +142,9 @@ android {
release {
minifyEnabled false
signingConfig signingConfigs.testkey
// Mali bridge development fork: debuggable so `adb shell run-as` reaches the session
// logs and VkBridgeNative's development override (-Pdroiddeck.notDebuggable to drop).
debuggable !project.hasProperty('droiddeck.notDebuggable')
}
}
+24 -2
View File
@@ -186,6 +186,7 @@ static int dmaheap_fd(void)
pthread_mutex_lock(&lock);
if (fd == -2) {
const char *names[] = {"/dev/dma_heap/system-uncached", "/dev/dma_heap/system"};
if (getenv("VKBRIDGE_DMAHEAP_CACHED")) names[0] = "/dev/dma_heap/system";
fd = -1;
for (int i = 0; i < 2 && fd < 0; i++) {
fd = open(names[i], O_RDONLY | O_CLOEXEC);
@@ -346,8 +347,9 @@ static VkResult alloc_shared(const vkb_dispatch *dt, VkDevice dev, uint32_t s, c
r = VK_ERROR_OUT_OF_DEVICE_MEMORY;
break;
}
vkb_ahb_desc d = {(uint32_t)sz, 1, 1, AHB_FORMAT_BLOB,
AHB_USAGE_CPU_READ_OFTEN | AHB_USAGE_CPU_WRITE_OFTEN | AHB_USAGE_GPU_DATA_BUFFER, 0, 0, 0};
/* CPU usage decides how gralloc caches the pages; VKBRIDGE_AHB_RARELY=1 asks for uncached. */
uint64_t cpu = getenv("VKBRIDGE_AHB_RARELY") ? (2ull | (2ull << 4)) : (AHB_USAGE_CPU_READ_OFTEN | AHB_USAGE_CPU_WRITE_OFTEN);
vkb_ahb_desc d = {(uint32_t)sz, 1, 1, AHB_FORMAT_BLOB, cpu | AHB_USAGE_GPU_DATA_BUFFER, 0, 0, 0};
struct AHardwareBuffer *ahb = NULL;
int ar = p_AHardwareBuffer_allocate(&d, &ahb);
if (ar != 0 || !ahb) {
@@ -549,7 +551,25 @@ static int submit_and_wait(selftest *st, VkCommandBuffer cb)
/* One allocation of `type` with strategy `s`, checked from both sides. 1 = shared and coherent
* (no flush needed), 2 = shared when flushed, 0 = not shared. */
static int test_type_mode(selftest *st, uint32_t s, uint32_t type, int no_flush);
/* Without flushes first: imported pages from an uncached heap are coherent in practice even where
* the driver's type does not say HOST_COHERENT (Mali-G720: dma-heap imports land in type 1,
* HOST_CACHED without COHERENT, and pass with no flush). Only if that fails, with flushes. */
static int test_type(selftest *st, uint32_t s, uint32_t type)
{
int coherent_flag = (st->mp.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
if (!coherent_flag && !getenv("VKBRIDGE_SELFTEST_NOFLUSH")) {
int r = test_type_mode(st, s, type, 1);
if (r) {
VKB_INFO(" %s, type %u: coherent in practice (no flushes needed)", vkb_mem_strategy_name(s), type);
return 1;
}
}
return test_type_mode(st, s, type, 0);
}
static int test_type_mode(selftest *st, uint32_t s, uint32_t type, int no_flush)
{
const vkb_dispatch *dt = &st->dt;
int result = 0;
@@ -603,6 +623,8 @@ static int test_type(selftest *st, uint32_t s, uint32_t type)
goto out;
}
int coherent = (st->mp.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
/* VKBRIDGE_SELFTEST_NOFLUSH=1 forces it for every type. */
if (no_flush || getenv("VKBRIDGE_SELFTEST_NOFLUSH")) coherent = 1;
VkMappedMemoryRange whole = {VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, NULL, m->mem, 0, VK_WHOLE_SIZE};
/* 1: the two CPU mappings are the same pages. */
@@ -34,7 +34,7 @@ class VkBridgeComponent(private val socket: File, private val logFile: File) : S
socket.parentFile?.mkdirs()
// Pipeline cache kept across sessions (Mali recompiles everything in a new process otherwise).
val cacheDir = File(app().cacheDir, "vkbridge-pipelines").apply { mkdirs() }
if (VkBridgeNative.start(socket, logFile, cacheDir)) {
if (VkBridgeNative.start(app(), socket, logFile, cacheDir)) {
Log.i(TAG, "server ready in the app's process at $socket")
return
}
@@ -1,5 +1,7 @@
package com.droiddeck.launcher.gpu
import android.content.Context
import android.content.pm.ApplicationInfo
import android.util.Log
import java.io.File
@@ -15,14 +17,31 @@ import java.io.File
object VkBridgeNative {
private const val TAG = "VkBridge"
private val loaded: Boolean by lazy {
try {
System.loadLibrary("vkbridge_jni")
@Volatile private var loaded: Boolean? = null
/**
* The packaged library, or - in a debuggable build only - files/vkbridge/dev/libvkbridge_jni.so
* when present, so a rebuilt server can be tried with `adb push` + `run-as cp` and an app
* restart instead of a new APK (the app targets SDK 28 and may load code from its files).
*/
private fun load(context: Context): Boolean {
loaded?.let { return it }
val ok = try {
val dev = File(context.filesDir, "vkbridge/dev/libvkbridge_jni.so")
val debuggable = context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
if (debuggable && dev.isFile) {
Log.w(TAG, "in-app server from the development override $dev")
System.load(dev.absolutePath)
} else {
System.loadLibrary("vkbridge_jni")
}
true
} catch (e: Throwable) {
Log.w(TAG, "in-app server unavailable: $e")
false
}
loaded = ok
return ok
}
/** Whether the in-app server is serving (its socket must then be left alone). */
@@ -30,8 +49,8 @@ object VkBridgeNative {
private set
/** Starts the server, or points its log at [log] when it already runs; false when it cannot serve. */
fun start(socket: File, log: File, cacheDir: File): Boolean {
if (!loaded) return false
fun start(context: Context, socket: File, log: File, cacheDir: File): Boolean {
if (!load(context)) return false
val ok = try {
nativeStart(socket.absolutePath, log.absolutePath, cacheDir.absolutePath) == 1
} catch (e: Throwable) {
+3
View File
@@ -71,6 +71,9 @@ static void build_memory_model(vkb_physdev *pd)
if (!(share & (1u << t))) continue; /* host-visible but not shareable: not offered */
VkMemoryPropertyFlags cf = f;
if (!(pd->info.coherent_types & (1u << t))) cf &= ~VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
else cf |= VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; /* coherent in practice (the self-test says so) */
/* dma-heap pages come from the uncached heap: not what an app asking for cached reads wants. */
if (pd->info.strategy == VKB_MEM_DMAHEAP) cf &= ~VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
if (uma) {
cf &= ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
if (host_heap == UINT32_MAX && c->memoryHeapCount < VK_MAX_MEMORY_HEAPS) {