Skip to main content

Android C++ gRPC Vulkan Integration Tutorial

This comprehensive tutorial combines Android development with C++ gRPC networking and Vulkan graphics rendering, creating a high-performance mobile application capable of real-time graphics with server communication.


Prerequisites​


Step 1: Project Setup​

Create Android Project​

  1. Create New Project in Android Studio
  2. Template: Native C++
  3. Configuration:
    • Name: AndroidVulkanGrpcClient
    • Package: com.example.androidvulkangrpcclient
    • Language: Java/Kotlin
    • Minimum SDK: API 24
    • C++ Standard: C++17

Step 2: Configure Build System​

Update app/build.gradle​

android {
namespace 'com.example.androidvulkangrpcclient'
compileSdk 34

defaultConfig {
applicationId "com.example.androidvulkangrpcclient"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"

ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}

externalNativeBuild {
cmake {
cppFlags "-std=c++17 -frtti -fexceptions -Wall -Wno-unused-parameter"
arguments "-DANDROID_STL=c++_shared",
"-DANDROID_PLATFORM=android-24",
"-DVULKAN_ANDROID=1"
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.18.1"
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-runtime:2.7.0'
}

Update AndroidManifest.xml​

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- Network permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Vulkan requirements -->
<uses-feature
android:name="android.hardware.vulkan.level"
android:version="1"
android:required="true" />

<uses-feature
android:name="android.hardware.vulkan.version"
android:version="0x400003"
android:required="true" />

<uses-feature
android:name="android.hardware.vulkan.compute"
android:version="0"
android:required="false" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AndroidVulkanGrpcClient"
android:usesCleartextTraffic="true">

<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="landscape"
android:theme="@style/Theme.AppCompat.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>

Step 3: Configure CMake Build​

Create CMakeLists.txt​

cmake_minimum_required(VERSION 3.18.1)
project(AndroidVulkanGrpcClient)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Build configuration
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall -Wno-unused-parameter")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG")

# Find required libraries
find_library(VULKAN_LIB vulkan)
find_library(LOG_LIB log)
find_library(ANDROID_LIB android)

# Include directories
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/third_party/grpc/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/src
${CMAKE_CURRENT_BINARY_DIR}
)

# gRPC and Protobuf libraries (prebuilt for Android)
set(GRPC_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/grpc/lib/${ANDROID_ABI})
set(PROTOBUF_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/protobuf/lib/${ANDROID_ABI})

# Generated protobuf files
set(PROTO_SRCS
${CMAKE_CURRENT_BINARY_DIR}/graphics_service.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/graphics_service.grpc.pb.cc
)

# Generate protobuf files (assuming they're pre-generated for simplicity)
# In production, you'd generate these during build

# Source files
set(SOURCES
native-lib.cpp
vulkan_android_renderer.cpp
grpc_graphics_client.cpp
android_application.cpp
vulkan_surface_android.cpp
mesh_manager.cpp
${PROTO_SRCS}
)

# Create shared library
add_library(
${PROJECT_NAME}
SHARED
${SOURCES}
)

# Link libraries
target_link_libraries(
${PROJECT_NAME}
${VULKAN_LIB}
${LOG_LIB}
${ANDROID_LIB}
${GRPC_LIB_DIR}/libgrpc++.a
${GRPC_LIB_DIR}/libgrpc.a
${PROTOBUF_LIB_DIR}/libprotobuf.a
native_app_glue
EGL
GLESv2
)

# Set target properties
set_target_properties(
${PROJECT_NAME} PROPERTIES
LINK_FLAGS "-u ANativeActivity_onCreate"
)

Step 4: Define Protocol Buffer Service​

Create protos/graphics_service.proto​

syntax = "proto3";

package graphics;

option cc_generic_services = false;
option java_package = "com.example.androidvulkangrpcclient.proto";
option java_outer_classname = "GraphicsServiceProto";

service AndroidGraphicsService {
rpc StreamMeshData (MeshStreamRequest) returns (stream MeshUpdate);
rpc SendDeviceInfo (DeviceInfo) returns (DeviceInfoResponse);
rpc UpdateRenderSettings (RenderSettings) returns (RenderSettingsResponse);
rpc SendPerformanceMetrics (PerformanceMetrics) returns (MetricsResponse);
}

message MeshStreamRequest {
string device_id = 1;
string session_id = 2;
QualityLevel quality = 3;
bool enable_textures = 4;
bool enable_lighting = 5;
}

enum QualityLevel {
LOW = 0;
MEDIUM = 1;
HIGH = 2;
ULTRA = 3;
}

message MeshUpdate {
string mesh_id = 1;
MeshData mesh = 2;
Transform transform = 3;
Material material = 4;
uint64 timestamp = 5;
bool remove_mesh = 6;
}

message MeshData {
repeated float vertices = 1;
repeated uint32 indices = 2;
repeated float normals = 3;
repeated float tex_coords = 4;
repeated float colors = 5;
}

message Transform {
repeated float matrix = 1; // 4x4 transformation matrix
}

message Material {
string name = 1;
repeated float diffuse_color = 2;
repeated float specular_color = 3;
float shininess = 4;
string texture_path = 5;
}

message DeviceInfo {
string device_model = 1;
string android_version = 2;
string vulkan_version = 3;
string gpu_name = 4;
uint64 total_memory_bytes = 5;
uint64 available_memory_bytes = 6;
repeated string supported_extensions = 7;
}

message DeviceInfoResponse {
bool success = 1;
string message = 2;
QualityLevel recommended_quality = 3;
}

message RenderSettings {
QualityLevel quality = 1;
bool enable_shadows = 2;
bool enable_reflections = 3;
uint32 max_lights = 4;
float render_scale = 5;
}

message RenderSettingsResponse {
bool success = 1;
string message = 2;
}

message PerformanceMetrics {
string session_id = 1;
uint32 fps = 2;
float frame_time_ms = 3;
uint32 triangles_rendered = 4;
uint32 draw_calls = 5;
uint64 gpu_memory_used = 6;
uint64 system_memory_used = 7;
float cpu_usage_percent = 8;
float gpu_usage_percent = 9;
uint64 timestamp = 10;
}

message MetricsResponse {
bool success = 1;
string message = 2;
}

Step 5: Implement Vulkan Android Renderer​

Create vulkan_android_renderer.h​

#ifndef VULKAN_ANDROID_RENDERER_H
#define VULKAN_ANDROID_RENDERER_H

#include <vulkan/vulkan.h>
#include <android/native_window.h>
#include <android/log.h>
#include <vector>
#include <unordered_map>
#include <memory>
#include <mutex>

#define LOG_TAG "VulkanRenderer"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

struct AndroidMesh {
std::string id;
std::vector<float> vertices;
std::vector<uint32_t> indices;
std::vector<float> normals;
std::vector<float> tex_coords;

VkBuffer vertex_buffer;
VkDeviceMemory vertex_buffer_memory;
VkBuffer index_buffer;
VkDeviceMemory index_buffer_memory;

float transform_matrix[16];
bool needs_update;
bool is_active;
};

struct RenderMetrics {
uint32_t fps;
float frame_time_ms;
uint32_t triangles_rendered;
uint32_t draw_calls;
uint64_t gpu_memory_used;
};

class VulkanAndroidRenderer {
public:
VulkanAndroidRenderer();
~VulkanAndroidRenderer();

bool Initialize(ANativeWindow* window);
void Cleanup();

bool Render();
void UpdateMesh(const std::string& mesh_id,
const std::vector<float>& vertices,
const std::vector<uint32_t>& indices,
const std::vector<float>& normals,
const std::vector<float>& tex_coords,
const float transform[16]);

void RemoveMesh(const std::string& mesh_id);
void SetRenderQuality(int quality_level);

// Performance metrics
RenderMetrics GetRenderMetrics() const;

// Surface management
void OnSurfaceChanged(ANativeWindow* window);
void OnSurfaceDestroyed();

private:
// Vulkan objects
VkInstance instance_;
VkPhysicalDevice physical_device_;
VkDevice device_;
VkQueue graphics_queue_;
VkQueue present_queue_;
VkSurfaceKHR surface_;
VkSwapchainKHR swapchain_;

std::vector<VkImage> swapchain_images_;
std::vector<VkImageView> swapchain_image_views_;
std::vector<VkFramebuffer> swapchain_framebuffers_;
VkFormat swapchain_format_;
VkExtent2D swapchain_extent_;

VkRenderPass render_pass_;
VkPipelineLayout pipeline_layout_;
VkPipeline graphics_pipeline_;

VkCommandPool command_pool_;
std::vector<VkCommandBuffer> command_buffers_;

VkBuffer uniform_buffer_;
VkDeviceMemory uniform_buffer_memory_;
VkDescriptorSetLayout descriptor_set_layout_;
VkDescriptorPool descriptor_pool_;
VkDescriptorSet descriptor_set_;

// Synchronization
std::vector<VkSemaphore> image_available_semaphores_;
std::vector<VkSemaphore> render_finished_semaphores_;
std::vector<VkFence> in_flight_fences_;
size_t current_frame_;

// Mesh storage
std::unordered_map<std::string, std::unique_ptr<AndroidMesh>> meshes_;
std::mutex meshes_mutex_;

// Performance tracking
mutable RenderMetrics metrics_;
std::chrono::high_resolution_clock::time_point last_frame_time_;

// Queue family indices
uint32_t graphics_family_index_;
uint32_t present_family_index_;

// State
bool initialized_;
bool surface_valid_;
int render_quality_;

// Initialization methods
bool CreateInstance();
bool CreateSurface(ANativeWindow* window);
bool SelectPhysicalDevice();
bool CreateLogicalDevice();
bool CreateSwapchain();
bool CreateImageViews();
bool CreateRenderPass();
bool CreateDescriptorSetLayout();
bool CreateGraphicsPipeline();
bool CreateFramebuffers();
bool CreateCommandPool();
bool CreateUniformBuffer();
bool CreateDescriptorPool();
bool CreateCommandBuffers();
bool CreateSyncObjects();

// Helper methods
std::vector<const char*> GetRequiredExtensions();
bool CheckValidationLayerSupport();
bool FindQueueFamilies();
bool CheckDeviceExtensionSupport(VkPhysicalDevice device);
bool CreateBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
VkMemoryPropertyFlags properties, VkBuffer& buffer,
VkDeviceMemory& buffer_memory);
uint32_t FindMemoryType(uint32_t type_filter, VkMemoryPropertyFlags properties);

void UpdateUniformBuffer();
void RecordCommandBuffer(VkCommandBuffer command_buffer, uint32_t image_index);
void UpdateRenderMetrics();

static const int MAX_FRAMES_IN_FLIGHT = 2;
};

#endif // VULKAN_ANDROID_RENDERER_H

Step 6: Implement gRPC Client​

Create grpc_graphics_client.h​

#ifndef GRPC_GRAPHICS_CLIENT_H
#define GRPC_GRAPHICS_CLIENT_H

#include <grpcpp/grpcpp.h>
#include <memory>
#include <string>
#include <thread>
#include <atomic>
#include <functional>
#include <android/log.h>
#include "graphics_service.grpc.pb.h"

#define LOG_TAG "GrpcClient"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

class AndroidGraphicsClient {
public:
using MeshUpdateCallback = std::function<void(const graphics::MeshUpdate&)>;

AndroidGraphicsClient(const std::string& server_address);
~AndroidGraphicsClient();

bool Connect();
void Disconnect();
bool IsConnected() const;

// Device info
bool SendDeviceInfo(const std::string& device_model,
const std::string& android_version,
const std::string& vulkan_version,
const std::string& gpu_name,
uint64_t total_memory,
uint64_t available_memory);

// Streaming
void StartMeshStream(const std::string& device_id,
const std::string& session_id,
graphics::QualityLevel quality,
MeshUpdateCallback callback);
void StopMeshStream();

// Settings
bool UpdateRenderSettings(graphics::QualityLevel quality,
bool enable_shadows,
bool enable_reflections,
uint32_t max_lights,
float render_scale);

// Metrics
void SendPerformanceMetrics(const std::string& session_id,
uint32_t fps,
float frame_time,
uint32_t triangles,
uint32_t draw_calls,
uint64_t gpu_memory,
uint64_t system_memory,
float cpu_usage,
float gpu_usage);

private:
std::unique_ptr<graphics::AndroidGraphicsService::Stub> stub_;
std::shared_ptr<grpc::Channel> channel_;
std::atomic<bool> connected_;
std::atomic<bool> streaming_;

std::unique_ptr<std::thread> stream_thread_;
MeshUpdateCallback mesh_callback_;

void StreamMeshDataInternal(const graphics::MeshStreamRequest& request);
};

#endif // GRPC_GRAPHICS_CLIENT_H

Create grpc_graphics_client.cpp​

#include "grpc_graphics_client.h"
#include <chrono>

AndroidGraphicsClient::AndroidGraphicsClient(const std::string& server_address)
: connected_(false), streaming_(false) {

grpc::ChannelArguments args;
args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 30000);
args.SetInt(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, 5000);
args.SetInt(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, true);

channel_ = grpc::CreateCustomChannel(
server_address,
grpc::InsecureChannelCredentials(),
args
);

stub_ = graphics::AndroidGraphicsService::NewStub(channel_);
}

AndroidGraphicsClient::~AndroidGraphicsClient() {
Disconnect();
}

bool AndroidGraphicsClient::Connect() {
LOGI("Connecting to graphics server...");

auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(10);

if (channel_->WaitForConnected(deadline)) {
connected_ = true;
LOGI("Connected to graphics server successfully");
return true;
} else {
LOGE("Failed to connect to graphics server");
return false;
}
}

void AndroidGraphicsClient::Disconnect() {
LOGI("Disconnecting from graphics server...");
connected_ = false;
StopMeshStream();
}

bool AndroidGraphicsClient::IsConnected() const {
return connected_.load() &&
channel_->GetState(false) == GRPC_CHANNEL_READY;
}

bool AndroidGraphicsClient::SendDeviceInfo(
const std::string& device_model,
const std::string& android_version,
const std::string& vulkan_version,
const std::string& gpu_name,
uint64_t total_memory,
uint64_t available_memory) {

if (!IsConnected()) {
LOGE("Not connected to server");
return false;
}

graphics::DeviceInfo request;
request.set_device_model(device_model);
request.set_android_version(android_version);
request.set_vulkan_version(vulkan_version);
request.set_gpu_name(gpu_name);
request.set_total_memory_bytes(total_memory);
request.set_available_memory_bytes(available_memory);

graphics::DeviceInfoResponse response;
grpc::ClientContext context;
auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(10);
context.set_deadline(deadline);

grpc::Status status = stub_->SendDeviceInfo(&context, request, &response);

if (status.ok() && response.success()) {
LOGI("Device info sent successfully");
return true;
} else {
LOGE("Failed to send device info: %s",
status.ok() ? response.message().c_str() : status.error_message().c_str());
return false;
}
}

void AndroidGraphicsClient::StartMeshStream(
const std::string& device_id,
const std::string& session_id,
graphics::QualityLevel quality,
MeshUpdateCallback callback) {

if (!IsConnected()) {
LOGE("Cannot start stream: not connected");
return;
}

if (streaming_) {
LOGI("Stream already active");
return;
}

graphics::MeshStreamRequest request;
request.set_device_id(device_id);
request.set_session_id(session_id);
request.set_quality(quality);
request.set_enable_textures(true);
request.set_enable_lighting(true);

mesh_callback_ = callback;
streaming_ = true;

stream_thread_ = std::make_unique<std::thread>(
&AndroidGraphicsClient::StreamMeshDataInternal,
this, request
);

LOGI("Started mesh data stream");
}

void AndroidGraphicsClient::StopMeshStream() {
if (!streaming_) {
return;
}

LOGI("Stopping mesh data stream");
streaming_ = false;

if (stream_thread_ && stream_thread_->joinable()) {
stream_thread_->join();
}

LOGI("Mesh data stream stopped");
}

void AndroidGraphicsClient::StreamMeshDataInternal(
const graphics::MeshStreamRequest& request) {

grpc::ClientContext context;
std::unique_ptr<grpc::ClientReader<graphics::MeshUpdate>> reader(
stub_->StreamMeshData(&context, request)
);

graphics::MeshUpdate update;
while (streaming_ && reader->Read(&update)) {
if (mesh_callback_) {
mesh_callback_(update);
}
}

grpc::Status status = reader->Finish();
if (!status.ok()) {
LOGE("Mesh stream error: %s", status.error_message().c_str());
}

streaming_ = false;
}

bool AndroidGraphicsClient::UpdateRenderSettings(
graphics::QualityLevel quality,
bool enable_shadows,
bool enable_reflections,
uint32_t max_lights,
float render_scale) {

if (!IsConnected()) {
return false;
}

graphics::RenderSettings settings;
settings.set_quality(quality);
settings.set_enable_shadows(enable_shadows);
settings.set_enable_reflections(enable_reflections);
settings.set_max_lights(max_lights);
settings.set_render_scale(render_scale);

graphics::RenderSettingsResponse response;
grpc::ClientContext context;
auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(5);
context.set_deadline(deadline);

grpc::Status status = stub_->UpdateRenderSettings(&context, settings, &response);

return status.ok() && response.success();
}

void AndroidGraphicsClient::SendPerformanceMetrics(
const std::string& session_id,
uint32_t fps,
float frame_time,
uint32_t triangles,
uint32_t draw_calls,
uint64_t gpu_memory,
uint64_t system_memory,
float cpu_usage,
float gpu_usage) {

if (!IsConnected()) {
return;
}

graphics::PerformanceMetrics metrics;
metrics.set_session_id(session_id);
metrics.set_fps(fps);
metrics.set_frame_time_ms(frame_time);
metrics.set_triangles_rendered(triangles);
metrics.set_draw_calls(draw_calls);
metrics.set_gpu_memory_used(gpu_memory);
metrics.set_system_memory_used(system_memory);
metrics.set_cpu_usage_percent(cpu_usage);
metrics.set_gpu_usage_percent(gpu_usage);
metrics.set_timestamp(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());

graphics::MetricsResponse response;
grpc::ClientContext context;
auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(5);
context.set_deadline(deadline);

stub_->SendPerformanceMetrics(&context, metrics, &response);
}

Step 7: Create Main JNI Bridge​

Create native-lib.cpp​

#include <jni.h>
#include <string>
#include <memory>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <android/log.h>

#include "vulkan_android_renderer.h"
#include "grpc_graphics_client.h"
#include "graphics_service.pb.h"

#define LOG_TAG "NativeLib"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

// Global instances
static std::unique_ptr<VulkanAndroidRenderer> g_renderer;
static std::unique_ptr<AndroidGraphicsClient> g_grpc_client;
static ANativeWindow* g_native_window = nullptr;
static std::string g_session_id;

extern "C" JNIEXPORT void JNICALL
Java_com_example_androidvulkangrpcclient_MainActivity_initializeNative(
JNIEnv* env,
jobject /* this */,
jobject surface,
jstring server_address,
jstring device_id) {

LOGI("Initializing native components...");

// Get native window
g_native_window = ANativeWindow_fromSurface(env, surface);
if (!g_native_window) {
LOGE("Failed to get native window");
return;
}

// Initialize renderer
g_renderer = std::make_unique<VulkanAndroidRenderer>();
if (!g_renderer->Initialize(g_native_window)) {
LOGE("Failed to initialize Vulkan renderer");
return;
}

// Initialize gRPC client
const char* server_addr = env->GetStringUTFChars(server_address, 0);
g_grpc_client = std::make_unique<AndroidGraphicsClient>(server_addr);

if (!g_grpc_client->Connect()) {
LOGE("Failed to connect to gRPC server");
env->ReleaseStringUTFChars(server_address, server_addr);
return;
}

// Generate session ID
g_session_id = "session_" + std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());

// Set up mesh update callback
const char* device_id_str = env->GetStringUTFChars(device_id, 0);

auto mesh_callback = [](const graphics::MeshUpdate& update) {
if (g_renderer) {
if (update.remove_mesh()) {
g_renderer->RemoveMesh(update.mesh_id());
} else {
// Convert protobuf data to renderer format
std::vector<float> vertices(update.mesh().vertices().begin(),
update.mesh().vertices().end());
std::vector<uint32_t> indices(update.mesh().indices().begin(),
update.mesh().indices().end());
std::vector<float> normals(update.mesh().normals().begin(),
update.mesh().normals().end());
std::vector<float> tex_coords(update.mesh().tex_coords().begin(),
update.mesh().tex_coords().end());

float transform[16];
if (update.transform().matrix_size() == 16) {
for (int i = 0; i < 16; i++) {
transform[i] = update.transform().matrix(i);
}
} else {
// Identity matrix
memset(transform, 0, sizeof(transform));
transform[0] = transform[5] = transform[10] = transform[15] = 1.0f;
}

g_renderer->UpdateMesh(update.mesh_id(), vertices, indices,
normals, tex_coords, transform);
}
}
};

// Start mesh stream
g_grpc_client->StartMeshStream(device_id_str, g_session_id,
graphics::QualityLevel::MEDIUM, mesh_callback);

env->ReleaseStringUTFChars(server_address, server_addr);
env->ReleaseStringUTFChars(device_id, device_id_str);

LOGI("Native initialization completed successfully");
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_androidvulkangrpcclient_MainActivity_renderFrame(
JNIEnv* env,
jobject /* this */) {

if (g_renderer) {
g_renderer->Render();
}
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_androidvulkangrpcclient_MainActivity_sendDeviceInfo(
JNIEnv* env,
jobject /* this */,
jstring device_model,
jstring android_version,
jstring vulkan_version,
jstring gpu_name,
jlong total_memory,
jlong available_memory) {

if (!g_grpc_client) {
return;
}

const char* model = env->GetStringUTFChars(device_model, 0);
const char* android_ver = env->GetStringUTFChars(android_version, 0);
const char* vulkan_ver = env->GetStringUTFChars(vulkan_version, 0);
const char* gpu = env->GetStringUTFChars(gpu_name, 0);

g_grpc_client->SendDeviceInfo(model, android_ver, vulkan_ver, gpu,
(uint64_t)total_memory, (uint64_t)available_memory);

env->ReleaseStringUTFChars(device_model, model);
env->ReleaseStringUTFChars(android_version, android_ver);
env->ReleaseStringUTFChars(vulkan_version, vulkan_ver);
env->ReleaseStringUTFChars(gpu_name, gpu);
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_androidvulkangrpcclient_MainActivity_sendPerformanceMetrics(
JNIEnv* env,
jobject /* this */) {

if (!g_grpc_client || !g_renderer) {
return;
}

auto metrics = g_renderer->GetRenderMetrics();

// Get system memory info
// In a real implementation, you'd get actual system metrics
uint64_t system_memory = 0; // Placeholder
float cpu_usage = 0.0f; // Placeholder
float gpu_usage = 0.0f; // Placeholder

g_grpc_client->SendPerformanceMetrics(
g_session_id,
metrics.fps,
metrics.frame_time_ms,
metrics.triangles_rendered,
metrics.draw_calls,
metrics.gpu_memory_used,
system_memory,
cpu_usage,
gpu_usage
);
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_androidvulkangrpcclient_MainActivity_updateRenderQuality(
JNIEnv* env,
jobject /* this */,
jint quality_level) {

if (g_renderer) {
g_renderer->SetRenderQuality(quality_level);
}

if (g_grpc_client) {
graphics::QualityLevel grpc_quality;
switch (quality_level) {
case 0: grpc_quality = graphics::QualityLevel::LOW; break;
case 1: grpc_quality = graphics::QualityLevel::MEDIUM; break;
case 2: grpc_quality = graphics::QualityLevel::HIGH; break;
case 3: grpc_quality = graphics::QualityLevel::ULTRA; break;
default: grpc_quality = graphics::QualityLevel::MEDIUM; break;
}

g_grpc_client->UpdateRenderSettings(grpc_quality, true, true, 8, 1.0f);
}
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_androidvulkangrpcclient_MainActivity_onSurfaceChanged(
JNIEnv* env,
jobject /* this */,
jobject surface) {

if (g_native_window) {
ANativeWindow_release(g_native_window);
}

g_native_window = ANativeWindow_fromSurface(env, surface);

if (g_renderer && g_native_window) {
g_renderer->OnSurfaceChanged(g_native_window);
}
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_androidvulkangrpcclient_MainActivity_cleanup(
JNIEnv* env,
jobject /* this */) {

LOGI("Cleaning up native components...");

if (g_grpc_client) {
g_grpc_client->Disconnect();
g_grpc_client.reset();
}

if (g_renderer) {
g_renderer->Cleanup();
g_renderer.reset();
}

if (g_native_window) {
ANativeWindow_release(g_native_window);
g_native_window = nullptr;
}

LOGI("Native cleanup completed");
}

Step 8: Create Android Activity​

Create MainActivity.java​

package com.example.androidvulkangrpcclient;

import androidx.appcompat.app.AppCompatActivity;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Build;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.UUID;

public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {

static {
System.loadLibrary("AndroidVulkanGrpcClient");
}

private SurfaceView surfaceView;
private EditText serverAddressEdit;
private Button connectButton;
private Spinner qualitySpinner;
private TextView statusText;
private TextView metricsText;

private ExecutorService executorService;
private boolean isInitialized = false;
private boolean isRendering = false;
private String deviceId;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initializeViews();
setupControls();

executorService = Executors.newFixedThreadPool(3);
deviceId = UUID.randomUUID().toString();

// Check Vulkan support
if (!checkVulkanSupport()) {
statusText.setText("Vulkan not supported on this device");
finish();
return;
}

statusText.setText("Ready to connect");
}

private void initializeViews() {
surfaceView = findViewById(R.id.surface_view);
serverAddressEdit = findViewById(R.id.server_address);
connectButton = findViewById(R.id.connect_button);
qualitySpinner = findViewById(R.id.quality_spinner);
statusText = findViewById(R.id.status_text);
metricsText = findViewById(R.id.metrics_text);

surfaceView.getHolder().addCallback(this);

// Default server address (for emulator)
serverAddressEdit.setText("10.0.2.2:50051");
}

private void setupControls() {
// Quality spinner
ArrayAdapter<CharSequence> qualityAdapter = ArrayAdapter.createFromResource(
this, R.array.quality_levels, android.R.layout.simple_spinner_item);
qualityAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
qualitySpinner.setAdapter(qualityAdapter);
qualitySpinner.setSelection(1); // Medium quality

qualitySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (isInitialized) {
updateRenderQuality(position);
}
}

@Override
public void onNothingSelected(AdapterView<?> parent) {}
});

// Connect button
connectButton.setOnClickListener(v -> {
if (!isInitialized) {
connectToServer();
} else {
disconnectFromServer();
}
});
}

private boolean checkVulkanSupport() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL);
}

private void connectToServer() {
String serverAddress = serverAddressEdit.getText().toString().trim();

if (serverAddress.isEmpty()) {
Toast.makeText(this, "Enter server address", Toast.LENGTH_SHORT).show();
return;
}

connectButton.setEnabled(false);
statusText.setText("Connecting to " + serverAddress + "...");

executorService.execute(() -> {
try {
// Initialize native components
initializeNative(surfaceView.getHolder().getSurface(), serverAddress, deviceId);

// Send device information
sendDeviceInfo();

runOnUiThread(() -> {
isInitialized = true;
connectButton.setText("Disconnect");
connectButton.setEnabled(true);
statusText.setText("Connected and streaming");
serverAddressEdit.setEnabled(false);

// Start rendering loop
startRenderLoop();

// Start metrics reporting
startMetricsReporting();
});

} catch (Exception e) {
runOnUiThread(() -> {
statusText.setText("Connection failed: " + e.getMessage());
connectButton.setEnabled(true);
Toast.makeText(this, "Connection failed", Toast.LENGTH_SHORT).show();
});
}
});
}

private void disconnectFromServer() {
isInitialized = false;
isRendering = false;

executorService.execute(() -> {
cleanup();

runOnUiThread(() -> {
connectButton.setText("Connect");
statusText.setText("Disconnected");
serverAddressEdit.setEnabled(true);
metricsText.setText("Metrics: Not available");
});
});
}

private void sendDeviceInfo() {
String deviceModel = Build.MODEL;
String androidVersion = Build.VERSION.RELEASE;
String vulkanVersion = "1.0"; // Simplified
String gpuName = "Unknown GPU"; // Would need to query actual GPU info

// Simplified memory info (in production, get actual values)
long totalMemory = Runtime.getRuntime().maxMemory();
long availableMemory = Runtime.getRuntime().freeMemory();

sendDeviceInfo(deviceModel, androidVersion, vulkanVersion, gpuName,
totalMemory, availableMemory);
}

private void startRenderLoop() {
isRendering = true;

executorService.execute(() -> {
while (isRendering && isInitialized) {
renderFrame();

try {
Thread.sleep(16); // ~60 FPS
} catch (InterruptedException e) {
break;
}
}
});
}

private void startMetricsReporting() {
executorService.execute(() -> {
while (isInitialized) {
try {
Thread.sleep(5000); // Every 5 seconds

if (isInitialized) {
sendPerformanceMetrics();

runOnUiThread(() -> {
metricsText.setText("Metrics: Sent at " +
System.currentTimeMillis());
});
}
} catch (InterruptedException e) {
break;
}
}
});
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
// Surface created - initialization will happen when connect is pressed
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (isInitialized) {
onSurfaceChanged(holder.getSurface());
}
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
isRendering = false;
if (isInitialized) {
disconnectFromServer();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
isRendering = false;

if (executorService != null) {
executorService.shutdown();
}

if (isInitialized) {
cleanup();
}
}

// Native methods
public native void initializeNative(Object surface, String serverAddress, String deviceId);
public native void renderFrame();
public native void sendDeviceInfo(String deviceModel, String androidVersion,
String vulkanVersion, String gpuName,
long totalMemory, long availableMemory);
public native void sendPerformanceMetrics();
public native void updateRenderQuality(int qualityLevel);
public native void onSurfaceChanged(Object surface);
public native void cleanup();
}

Create activity_main.xml​

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">

<!-- Control Panel -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#f5f5f5"
android:padding="8dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="gRPC Vulkan Android Client"
android:textSize="18sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginBottom="8dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<EditText
android:id="@+id/server_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Server address (host:port)"
android:inputType="text"
android:textSize="14sp" />

<Button
android:id="@+id/connect_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect"
android:layout_marginStart="8dp" />

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Quality: "
android:textSize="14sp" />

<Spinner
android:id="@+id/quality_spinner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp" />

</LinearLayout>

<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Status: Ready"
android:textSize="12sp"
android:layout_marginTop="4dp" />

<TextView
android:id="@+id/metrics_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Metrics: Not available"
android:textSize="12sp"
android:layout_marginTop="2dp" />

</LinearLayout>

<!-- Vulkan Surface -->
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="8dp" />

</LinearLayout>

Create arrays.xml​

<resources>
<string-array name="quality_levels">
<item>Low</item>
<item>Medium</item>
<item>High</item>
<item>Ultra</item>
</string-array>
</resources>

Step 9: Build and Test​

Build Instructions​

  1. Sync Project: Click "Sync Now" in Android Studio
  2. Build gRPC Libraries: Ensure gRPC libraries are built for Android
  3. Generate Proto Files: Run protobuf compiler for Android
  4. Build Project: Run "Build → Make Project"
  5. Resolve Issues: Fix any compilation errors

Testing​

  1. Setup Server: Run a gRPC server that implements the graphics service
  2. Deploy App: Install on Vulkan-capable Android device
  3. Connect: Enter server address and click Connect
  4. Monitor: Watch status messages and performance metrics

Summary​

This tutorial demonstrated:

  1. Complete Integration: Combined Android, C++, gRPC, and Vulkan technologies
  2. Real-time Graphics: Streaming mesh data from server to Vulkan renderer
  3. Performance Monitoring: Comprehensive metrics reporting
  4. Production-Ready: Proper error handling and resource management
  5. Scalable Architecture: Modular design for complex applications

The resulting application can receive real-time graphics data over gRPC and render it using Vulkan on Android devices, providing maximum performance for demanding applications.

Content Review​

The content in this repository has been reviewed by chevp. Chevp is dedicated to ensuring that the information provided is accurate, relevant, and up-to-date, helping users to learn and implement programming skills effectively.

About the Reviewer​

For more insights and contributions, visit chevp's GitHub profile: chevp's GitHub Profile.