Chapter 5. Hello Cross-platform!!

Android Studio, C++, OpenGL ES, cross-platform


Now we will try to run TheApp spinning triangle sample on Android. For this we'll need:

  • provide /p_android/platform.cpp/h implementation
  • and replace flashing green screen code in main.cpp by theGame.run() call

Didn't find how to add classes/files outside of cpp folder in Android Studio, so let's do it manually.

1. In Windows File Explorer create a new folder

C:/CPP/p_android


2. In a text editor (I use Notepad++) create a file

C:/CPP/p_android/platform.h

Code:

#pragma once
#include <GLES3/gl32.h>

void mylog(const char* _Format, ...);
void mySwapBuffers();
void myPollEvents();


3. In a text editor create a file

C:/CPP/p_android/platform.cpp

#include <android/log.h>
#include "stdio.h"
#include "TheApp.h"
#include <EGL/egl.h>
#include <game-activity/native_app_glue/android_native_app_glue.h>

extern struct android_app* pAndroidApp;
extern EGLDisplay androidDisplay;
extern EGLSurface androidSurface;

extern TheApp theApp;
 
void mylog(const char* _Format, ...) {
    char outStr[1024];
    va_list _ArgList;
    va_start(_ArgList, _Format);
    vsprintf(outStr, _Format, _ArgList);
    __android_log_print(ANDROID_LOG_INFO, "mylog", outStr, NULL);
    va_end(_ArgList);
};
 
void mySwapBuffers() {
    eglSwapBuffers(androidDisplay, androidSurface);
}
void myPollEvents() {
    // Process all pending events before running game logic.
    int events;
    android_poll_source *pSource;
    if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0)
        if (pSource)
            pSource->process(pAndroidApp, pSource);
    //if no display - wait for it
    while (androidDisplay == EGL_NO_DISPLAY)
        if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0)
            if (pSource)
                pSource->process(pAndroidApp, pSource);

    // handle all queued inputs
    for (auto i = 0; i < pAndroidApp->motionEventsCount; i++) {

        // cache the current event
        auto &motionEvent = pAndroidApp->motionEvents[i];

        // cache the current action
        auto action = motionEvent.action;

        // Find the pointer index, mask and bitshift to turn it into a readable value
        auto pointerIndex = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
                >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
        //aout << "Pointer " << pointerIndex << ":";

        // get the x and y position of this event
        auto &pointer = motionEvent.pointers[pointerIndex];
        auto x = GameActivityPointerAxes_getX(&pointer);
        auto y = GameActivityPointerAxes_getY(&pointer);
        //aout << "(" << x << ", " << y << ") ";

        // Only consider touchscreen events, like touches
        auto actionMasked = action & AINPUT_SOURCE_TOUCHSCREEN;

        // determine the kind of event it is
        switch (actionMasked) {
            case AMOTION_EVENT_ACTION_DOWN:
            case AMOTION_EVENT_ACTION_POINTER_DOWN:
                //aout << "Pointer Down";
                break;

            case AMOTION_EVENT_ACTION_UP:
            case AMOTION_EVENT_ACTION_POINTER_UP:
                //aout << "Pointer Up";
                break;

            default:
                ;//aout << "Pointer Move";
        }
    }
    android_app_clear_motion_events(pAndroidApp);

    // handle key inputs
    for (auto i = 0; i < pAndroidApp->keyUpEventsCount; i++) {
        // cache the current event
        auto &keyEvent = pAndroidApp->keyUpEvents[i];
        if (keyEvent.keyCode == AKEYCODE_BACK) {
            // actions on back key
            theApp.bExitApp = true;
        }
    }
    android_app_clear_key_up_events(pAndroidApp);
}

  • Please note: mylog implementation prints in Android Studio's logcat window.

4. Start Android Studio,

open C:\CPP\a999hello\pa project.


5. Replace flashing green screen code by theGame.run() call.

Open main.cpp and replace code by:

#include "platform.h"
#include <jni.h>
#include <EGL/egl.h>

#include <game-activity/GameActivity.cpp>
#include <game-text-input/gametextinput.cpp>
#include <game-activity/native_app_glue/android_native_app_glue.c>

#include "TheApp.cpp"

TheApp theApp;

struct android_app* pAndroidApp = NULL;
EGLDisplay androidDisplay = EGL_NO_DISPLAY;
EGLSurface androidSurface = EGL_NO_SURFACE;
EGLContext androidContext = EGL_NO_CONTEXT;
bool bExitApp = false;
int screenSize[2] = {0,0};

void android_init_display() {
    // Choose your render attributes
    constexpr EGLint attribs[] = {
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_BLUE_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_RED_SIZE, 8,
            EGL_DEPTH_SIZE, 24,
            EGL_NONE
    };
    // The default display is probably what you want on Android
    auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    eglInitialize(display, nullptr, nullptr);

    // figure out how many configs there are
    EGLint numConfigs;
    eglChooseConfig(display, attribs, nullptr, 0, &numConfigs);

    // get the list of configurations
    std::unique_ptr<EGLConfig[]> supportedConfigs(new EGLConfig[numConfigs]);
    eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &numConfigs);

    // Find a config we like.
    // Could likely just grab the first if we don't care about anything else in the config.
    // Otherwise hook in your own heuristic
    auto config = *std::find_if(
            supportedConfigs.get(),
            supportedConfigs.get() + numConfigs,
            [&display](const EGLConfig &config) {
                EGLint red, green, blue, depth;
                if (eglGetConfigAttrib(display, config, EGL_RED_SIZE, &red)
                    && eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &green)
                    && eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &blue)
                    && eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &depth)) {

                    //aout << "Found config with " << red << ", " << green << ", " << blue << ", "
                    //     << depth << std::endl;
                    return red == 8 && green == 8 && blue == 8 && depth == 24;
                }
                return false;
            });
    // create the proper window surface
    EGLint format;
    eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
    EGLSurface surface = eglCreateWindowSurface(display, config, pAndroidApp->window, nullptr);

    // Create a GLES 3 context
    EGLint contextAttribs[] = {
            EGL_CONTEXT_MAJOR_VERSION, 3,
            EGL_CONTEXT_MINOR_VERSION, 2,
            EGL_NONE};
    EGLContext context = eglCreateContext(display, config, nullptr, contextAttribs);

    // get some window metrics
    auto madeCurrent = eglMakeCurrent(display, surface, surface, context);
    if(!madeCurrent) {
        ;
    }
    androidDisplay = display;
    androidSurface = surface;
    androidContext = context;
}
void android_term_display() {
    if (androidDisplay != EGL_NO_DISPLAY) {
        eglMakeCurrent(androidDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        if (androidContext != EGL_NO_CONTEXT) {
            eglDestroyContext(androidDisplay, androidContext);
            androidContext = EGL_NO_CONTEXT;
        }
        if (androidSurface != EGL_NO_SURFACE) {
            eglDestroySurface(androidDisplay, androidSurface);
            androidSurface = EGL_NO_SURFACE;
        }
        eglTerminate(androidDisplay);
        androidDisplay = EGL_NO_DISPLAY;
    }
}


void handle_cmd(android_app *pApp, int32_t cmd) {
    switch (cmd) {
        case APP_CMD_INIT_WINDOW:
            android_init_display();
            //updateRenderArea
            EGLint width,height;
            eglQuerySurface(androidDisplay, androidSurface, EGL_WIDTH, &width);
            eglQuerySurface(androidDisplay, androidSurface, EGL_HEIGHT, &height);
            screenSize[0] = 0;
            screenSize[1] = 0;
            theApp.onScreenResize(width,height);
            break;
        case APP_CMD_TERM_WINDOW:
            android_term_display();
            break;
        default:
            break;
    }
}

/*!
 * This the main entry point for a native activity
 */

void android_main(struct android_app *pApp) {
    pAndroidApp = pApp;

    // register an event handler for Android events
    pApp->onAppCmd = handle_cmd;

    myPollEvents(); //this will wait for display initialization

    theApp.run();

    android_term_display();
    std::terminate();
}


It remains only to add new classes/files to the project. Unlike Visual Studio, here we will do this not in the project properties, but in CMakeLists.txt.

6. Open CMakeLists.txt

and replace code by:

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.22.1)

# Declares and names the project.

project("pa")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

set(rootDir "../../../../../")
set(platform "${rootDir}/../p_android")
set(engine "${rootDir}/../engine")

add_library( # Sets the name of the library.
        pa

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        ${platform}/platform.cpp
        main.cpp
        )
target_include_directories(pa
        PUBLIC
        ${rootDir}
        ${platform}
        ${engine}
        )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Searches for a package provided by the game activity dependency

find_package(game-activity REQUIRED CONFIG)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        pa

        android

        # The game activity
        game-activity::game-activity

        # EGL, required for configuring the display context
        EGL

        # GL ES 3, used for the sample renderer
        GLESv3

        # for AImageDecoder, to load images from resources
        jnigraphics

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

  • Please note, line 17: CMakeLists.txt file is physically located in C:\CPP\a999hello\pa\app\src\main\cpp folder, FIVE folder levels down from project's root in C:\CPP\a999hello.

Just in case, I also added ES 3.2 requirement into the Manifest.

7. Open AndroidManifest.xml

and replace code by:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature android:glEsVersion="0x00030002" android:required="true" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="Hello Android"
        android:supportsRtl="true"
        android:theme="@style/Theme.Pa"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="pa" />
        </activity>
    </application>

</manifest>


8. Turn on your Android, unlock, plug in, allow debugging, wait for it to show up in Android Studio and RUN (green arrow).

Ta-da!!

Runs on BOTH Android AND Windows:

Now we can claim that we DO have a real cross-platform solution!


Again, just in case, both Android and Windows cross-platform solutions are saved on GitHub.

Link: https://github.com/bkantemir/_wg_405


Leave a Reply

Your email address will not be published. Required fields are marked *