Files
renderdoc/renderdoccmd/renderdoccmd_android.cpp
Chia-I Wu 8552eec7a1 Fix android compile errors on NDK 27+
ALooper_pollAll was obsoleted since NDK 27 and resulted in

  renderdoccmd_android.cpp:526:8: error: 'ALooper_pollAll' is
  unavailable: obsoleted in Android 1 - ALooper_pollAll may ignore
  wakes. Use ALooper_pollOnce instead. See The API documentation for
  more information

Since we poll from within a loop, we can replace it by ALooper_pollOnce
directly. But to be on the safe side, do this only on NDK 27+.
2026-02-01 03:15:38 +09:00

540 lines
16 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2016-2026 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#include "renderdoccmd.h"
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <dlfcn.h>
#include <locale.h>
#include <math.h>
#include <string.h>
#include <unistd.h>
#include <string>
#include <android_native_app_glue.h>
#include <android/log.h>
#define ANDROID_LOG(...) __android_log_print(ANDROID_LOG_INFO, "renderdoccmd", __VA_ARGS__);
struct android_app *android_state;
pthread_t cmdthread_handle = 0;
struct PThreadLock
{
PThreadLock(const char *n) : name(n)
{
ANDROID_LOG("Creating lock %s", name);
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &mutexattr);
}
~PThreadLock()
{
ANDROID_LOG("Destroying lock %s", name);
pthread_mutex_destroy(&mutex);
pthread_mutexattr_destroy(&mutexattr);
}
bool trylock() { return pthread_mutex_trylock(&mutex) == 0; };
void lock() { pthread_mutex_lock(&mutex); }
void unlock() { pthread_mutex_unlock(&mutex); }
private:
const char *name;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};
PThreadLock m_DrawLock("m_DrawLock"), m_CmdLock("m_CmdLock");
void Daemonise()
{
}
// every 15 minutes we fade briefly to avoid burn-in
const float splashFadePeriod = 45 * 60.0f;
float curtime()
{
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return float(ts.tv_sec) + float(ts.tv_nsec & 0xffffffff) / 1000000000.0f;
}
void DisplayGenericSplash()
{
ANDROID_LOG("Trying to splash");
// if something else is drawing and holding the lock, then bail
if(!m_DrawLock.trylock())
return;
ANDROID_LOG("Doing a splash");
// since we're not pumping this continually and we only draw when we need to, we can just do the
// full initialisation and teardown every time. This means we don't have to pay attention to
// whether something else needs to create a context on the window - we just do it
// opportunistically when we can hold the draw lock.
// This only takes about 30ms anyway, so it's still technically realtime, right!?
// fetch everything dynamically. We can't link against libEGL otherwise it messes with the hooking
// in the main library. Just declare local function pointers with the real names
void *libEGL = dlopen("libEGL.so", RTLD_NOW);
#define DLSYM_GET(name) decltype(&::name) name = (decltype(&::name))dlsym(libEGL, #name);
#define GPA_GET(name) decltype(&::name) name = (decltype(&::name))eglGetProcAddress(#name);
DLSYM_GET(eglBindAPI);
DLSYM_GET(eglGetDisplay);
DLSYM_GET(eglInitialize);
DLSYM_GET(eglGetError);
DLSYM_GET(eglChooseConfig);
DLSYM_GET(eglCreateContext);
DLSYM_GET(eglCreateWindowSurface);
DLSYM_GET(eglMakeCurrent);
DLSYM_GET(eglDestroySurface);
DLSYM_GET(eglDestroyContext);
DLSYM_GET(eglGetProcAddress);
DLSYM_GET(eglSwapBuffers);
DLSYM_GET(eglTerminate);
GPA_GET(glCreateShader);
GPA_GET(glShaderSource);
GPA_GET(glCompileShader);
GPA_GET(glCreateProgram);
GPA_GET(glAttachShader);
GPA_GET(glLinkProgram);
GPA_GET(glGetUniformLocation);
GPA_GET(glUniform2f);
GPA_GET(glUniform1f);
GPA_GET(glUseProgram);
GPA_GET(glVertexAttribPointer);
GPA_GET(glEnableVertexAttribArray);
GPA_GET(glDrawArrays);
GPA_GET(glGetShaderiv);
GPA_GET(glGetShaderInfoLog);
GPA_GET(glGetProgramiv);
GPA_GET(glGetProgramInfoLog);
eglBindAPI(EGL_OPENGL_ES_API);
EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if(eglDisplay && android_state && android_state->window)
{
ANativeWindow *previewWindow = android_state->window;
int major = 0, minor = 0;
EGLBoolean initialised = eglInitialize(eglDisplay, &major, &minor);
if(initialised && major >= 1)
{
const EGLint configAttribs[] = {EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_COLOR_BUFFER_TYPE,
EGL_RGB_BUFFER,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES2_BIT,
EGL_NONE};
EGLint numConfigs;
EGLConfig config;
if(eglChooseConfig(eglDisplay, configAttribs, &config, 1, &numConfigs))
{
// we only need GLES 2 for this
static const EGLint ctxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
EGLContext ctx = eglCreateContext(eglDisplay, config, NULL, ctxAttribs);
if(ctx)
{
EGLSurface surface = eglCreateWindowSurface(eglDisplay, config, previewWindow, NULL);
if(surface)
{
eglMakeCurrent(eglDisplay, surface, surface, ctx);
// simple pass through shader for the fullscreen triangle verts
const char *vertex =
"attribute vec2 pos;\n"
"void main() { gl_Position = vec4(pos, 0.5, 1.0); }";
const char *fragment = R"(
precision highp float;
float circle(in vec2 uv, in vec2 centre, in float radius)
{
return length(uv - centre) - radius;
}
// distance field for RenderDoc logo
float logo(in vec2 uv)
{
// add the outer ring
float ret = circle(uv, vec2(0.5, 0.5), 0.275);
// add the upper arc
ret = min(ret, circle(uv, vec2(0.5, -0.37), 0.71));
// subtract the inner ring
ret = max(ret, -circle(uv, vec2(0.5, 0.5), 0.16));
// subtract the lower arc
ret = max(ret, -circle(uv, vec2(0.5, -1.085), 1.3));
return ret;
}
uniform vec2 iResolution;
uniform float fFade;
void main()
{
vec2 uv = gl_FragCoord.xy / iResolution.xy;
// centre the UVs in a square. This assumes a landscape layout.
uv.x = 0.5 - (uv.x - 0.5) * (iResolution.x / iResolution.y);
// this constant here can be tuned depending on DPI to increase AA
float edgeWidth = 10.0/max(iResolution.x, iResolution.y);
float smoothdist = smoothstep(0.0, edgeWidth, clamp(logo(uv), 0.0, 1.0));
// the green is #3bb779
gl_FragColor = mix(vec4(1.0), vec4(0.2314, 0.7176, 0.4745, 1.0), smoothdist)*fFade;
}
)";
// compile the shaders and link into a program
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &vertex, NULL);
glCompileShader(vs);
GLint status = 0;
char buffer[1025] = {0};
glGetShaderiv(vs, GL_COMPILE_STATUS, &status);
if(status == 0)
{
glGetShaderInfoLog(vs, 1024, NULL, buffer);
ANDROID_LOG("VS error: %s", buffer);
}
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &fragment, NULL);
glCompileShader(fs);
status = 0;
glGetShaderiv(fs, GL_COMPILE_STATUS, &status);
if(status == 0)
{
glGetShaderInfoLog(fs, 1024, NULL, buffer);
ANDROID_LOG("FS error: %s", buffer);
}
GLuint prog = glCreateProgram();
glAttachShader(prog, vs);
glAttachShader(prog, fs);
glLinkProgram(prog);
status = 0;
glGetProgramiv(prog, GL_LINK_STATUS, &status);
if(status == 0)
{
glGetProgramInfoLog(prog, 1024, NULL, buffer);
ANDROID_LOG("Program Error: %s", buffer);
}
glUseProgram(prog);
// set the resolution
GLuint loc = glGetUniformLocation(prog, "iResolution");
glUniform2f(loc, float(ANativeWindow_getWidth(previewWindow)),
float(ANativeWindow_getHeight(previewWindow)));
// loop every 15 minutes, with the fade at the end of the period
float x = splashFadePeriod - fmod(curtime(), splashFadePeriod);
// multiply by 4 so the fade happens over a little more than a second instead of
// 6.28 seconds.
x = float(x) * 4.0f;
// do one cos loop and finish
if(x >= M_PI * 2.0f)
x = M_PI * 2.0f;
float fade = cosf(x);
// set the fade
loc = glGetUniformLocation(prog, "fFade");
glUniform1f(loc, fade);
// fullscreen triangle
float verts[] = {
-1.0f, -1.0f, // vertex 0
3.0f, -1.0f, // vertex 1
-1.0f, 3.0f, // vertex 2
};
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, verts);
glEnableVertexAttribArray(0);
glDrawArrays(GL_TRIANGLES, 0, 3);
eglSwapBuffers(eglDisplay, surface);
}
else
{
ANDROID_LOG("failed making surface: %x", eglGetError());
}
eglMakeCurrent(eglDisplay, 0L, 0L, NULL);
eglDestroyContext(eglDisplay, ctx);
eglDestroySurface(eglDisplay, surface);
}
else
{
ANDROID_LOG("failed making context: %x", eglGetError());
}
}
else
{
ANDROID_LOG("failed choosing config");
}
eglTerminate(eglDisplay);
}
else
{
ANDROID_LOG("failed to initialise EGL");
}
}
m_DrawLock.unlock();
ANDROID_LOG("Done splashing");
}
WindowingData DisplayRemoteServerPreview(bool active, const rdcarray<WindowingSystem> &systems)
{
static bool wasActive = false;
// detect when the preview starts or stops
if(wasActive != active)
{
wasActive = active;
// if we're opening it, aquire the draw lock, otherwise release it.
if(active)
{
ANDROID_LOG("Locking for preview");
m_DrawLock.lock();
}
else
{
m_DrawLock.unlock();
ANDROID_LOG("Unlocking from preview");
// when we release it, re-draw the splash
DisplayGenericSplash();
}
}
WindowingData ret = {WindowingSystem::Unknown};
if(android_state && android_state->window)
ret = CreateAndroidWindowingData(android_state->window);
return ret;
}
void DisplayRendererPreview(IReplayController *renderer, TextureDisplay &displayCfg, uint32_t width,
uint32_t height, uint32_t numLoops)
{
ANativeWindow *connectionScreenWindow = android_state->window;
m_DrawLock.lock();
IReplayOutput *out = renderer->CreateOutput(CreateAndroidWindowingData(connectionScreenWindow),
ReplayOutputType::Texture);
out->SetTextureDisplay(displayCfg);
if(numLoops == 0)
numLoops = 100;
for(uint32_t i = 0; i < numLoops; i++)
{
renderer->SetFrameEvent(10000000, true);
ANDROID_LOG("Frame %u", i);
out->Display();
usleep(100000);
}
m_DrawLock.unlock();
}
// Returns the renderdoccmd arguments passed via am start
// Examples: am start ... -e renderdoccmd "remoteserver"
// -e renderdoccmd "replay /sdcard/capture.rdc"
std::vector<std::string> getRenderdoccmdArgs()
{
JNIEnv *env;
android_state->activity->vm->AttachCurrentThread(&env, 0);
jobject me = android_state->activity->clazz;
jclass acl = env->GetObjectClass(me); // class pointer of NativeActivity
jmethodID giid = env->GetMethodID(acl, "getIntent", "()Landroid/content/Intent;");
jobject intent = env->CallObjectMethod(me, giid); // Got our intent
jclass icl = env->GetObjectClass(intent); // class pointer of Intent
jmethodID gseid =
env->GetMethodID(icl, "getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;");
jstring jsParam1 = (jstring)env->CallObjectMethod(intent, gseid, env->NewStringUTF("renderdoccmd"));
std::vector<std::string> ret;
if(jsParam1) // Check if arg value found
{
ret.push_back("renderdoccmd");
const char *param1 = env->GetStringUTFChars(jsParam1, 0);
std::istringstream iss(param1);
while(iss)
{
std::string sub;
iss >> sub;
ret.push_back(sub);
}
}
android_state->activity->vm->DetachCurrentThread();
return ret;
}
void *cmdthread(void *)
{
std::vector<std::string> args = getRenderdoccmdArgs();
if(args.size())
{
ANDROID_LOG("Entering cmd thread");
m_CmdLock.lock();
GlobalEnvironment env;
renderdoccmd(env, args);
m_CmdLock.unlock();
ANDROID_LOG("Exiting cmd thread");
}
// activity is done and should be closed
ANativeActivity_finish(android_state->activity);
return NULL;
}
void handle_cmd(android_app *app, int32_t cmd)
{
ANDROID_LOG("handle_cmd(%i)", cmd);
switch(cmd)
{
case APP_CMD_INIT_WINDOW:
{
ANDROID_LOG("APP_CMD_INIT_WINDOW");
// if we already have a thread handle, see if it's still running
if(cmdthread_handle != 0)
{
ANDROID_LOG("thread handle exists");
// if the thread isn't running anymore, we can acquire m_CmdLock. If so, we need to join the
// thread and start afresh. If we can't acquire the lock, the thread is still running so we
// leave it alone.
if(m_CmdLock.trylock())
{
ANDROID_LOG("thread is dead, reaping");
m_CmdLock.unlock();
// safe to join here, thread will terminate soon if it hasn't already
pthread_join(cmdthread_handle, NULL);
cmdthread_handle = 0;
}
}
// if we don't have a command thread, start one.
if(cmdthread_handle == 0)
{
ANDROID_LOG("spawning command thread");
pthread_create(&cmdthread_handle, NULL, cmdthread, NULL);
}
DisplayGenericSplash();
break;
}
case APP_CMD_WINDOW_REDRAW_NEEDED:
case APP_CMD_GAINED_FOCUS:
case APP_CMD_LOST_FOCUS:
{
ANDROID_LOG("doing misc splash");
DisplayGenericSplash();
break;
}
}
}
void android_main(struct android_app *state)
{
android_state = state;
android_state->onAppCmd = handle_cmd;
ANDROID_LOG("android_main android_state->window: %p", android_state->window);
// Used to poll the events in the main loop
int events;
android_poll_source *source;
float lastSplash = curtime();
do
{
// if we're near the end of the splash period force splashes at 30Hz
if(splashFadePeriod - fmod(curtime(), splashFadePeriod) < 10.0f)
{
if(curtime() - lastSplash > 1.0f / 30.0f)
{
DisplayGenericSplash();
lastSplash = curtime();
}
}
#if __NDK_MAJOR__ >= 27
if(ALooper_pollOnce(1, nullptr, &events, (void **)&source) >= 0)
#else
if(ALooper_pollAll(1, nullptr, &events, (void **)&source) >= 0)
#endif
{
if(source != NULL)
source->process(android_state, source);
}
} while(android_state->destroyRequested == 0);
ANDROID_LOG("android_main exiting");
android_state = NULL;
}