Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate native-activity to Choreographer. #1007

Merged
merged 1 commit into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion native-activity/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ android {

defaultConfig {
applicationId = 'com.example.native_activity'
minSdkVersion 21
// This is the minimum required for using Choreographer directly from the NDK. If you need
// to use a lower minSdkVersion, you must use the Java Choreographer API via JNI.
minSdkVersion 24
targetSdkVersion 34
externalNativeBuild {
cmake {
Expand Down
122 changes: 76 additions & 46 deletions native-activity/app/src/main/cpp/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// BEGIN_INCLUDE(all)
#include <EGL/egl.h>
#include <GLES/gl.h>
#include <android/choreographer.h>
#include <android/log.h>
#include <android/sensor.h>
#include <android/set_abort_message.h>
Expand Down Expand Up @@ -104,18 +105,80 @@ struct Engine {
sensorManager, app->looper, ALOOPER_POLL_CALLBACK, callback, this);
}

bool animating() const { return animating_; }
/// Resumes ticking the application.
void Resume() {
// Checked to make sure we don't double schedule Choreographer.
if (!running_) {
running_ = true;
ScheduleNextTick();
}
}

/// Pauses ticking the application.
///
/// When paused, sensor and input events will still be processed, but the
/// update and render parts of the loop will not run.
void Pause() { running_ = false; }

private:
bool running_;

void StartRendering() {
animating_ = true;
void ScheduleNextTick() {
AChoreographer_postFrameCallback(AChoreographer_getInstance(), Tick, this);
}

void StopRendering() {
animating_ = false;
/// Entry point for Choreographer.
///
/// The first argument (the frame time) is not used as it is not needed for
/// this sample. If you copy from this sample and make use of that argument,
/// note that there's an API bug: that time is a signed 32-bit nanosecond
/// counter on 32-bit systems, so it will roll over every ~2 seconds. If your
/// minSdkVersion is 29 or higher, use AChoreographer_postFrameCallback64
/// instead, which is 64-bits for all architectures. Otherwise, bitwise-and
/// the value with the upper bits from CLOCK_MONOTONIC.
///
/// \param data The Engine being ticked.
static void Tick(long, void* data) {
CHECK_NOT_NULL(data);
auto engine = reinterpret_cast<Engine*>(data);
engine->DoTick();
}

private:
bool animating_;
void DoTick() {
if (!running_) {
return;
}

// Input and sensor feedback is handled via their own callbacks.
// Choreographer ensures that those callbacks run before this callback does.

// Choreographer does not continuously schedule the callback. We have to re-
// register the callback each time we're ticked.
ScheduleNextTick();
Update();
DrawFrame();
}

void Update() {
state.angle += .01f;
if (state.angle > 1) {
state.angle = 0;
}
}

void DrawFrame() {
if (display == nullptr) {
// No display.
return;
}

// Just fill the screen with a color.
glClearColor(((float)state.x) / width, state.angle,
((float)state.y) / height, 1);
glClear(GL_COLOR_BUFFER_BIT);

eglSwapBuffers(display, surface);
}
};

/**
Expand Down Expand Up @@ -218,23 +281,6 @@ static int engine_init_display(Engine* engine) {
return 0;
}

/**
* Just the current frame in the display.
*/
static void engine_draw_frame(Engine* engine) {
if (engine->display == nullptr) {
// No display.
return;
}

// Just fill the screen with a color.
glClearColor(((float)engine->state.x) / engine->width, engine->state.angle,
((float)engine->state.y) / engine->height, 1);
glClear(GL_COLOR_BUFFER_BIT);

eglSwapBuffers(engine->display, engine->surface);
}

/**
* Tear down the EGL context currently associated with the display.
*/
Expand All @@ -250,7 +296,7 @@ static void engine_term_display(Engine* engine) {
}
eglTerminate(engine->display);
}
engine->StopRendering();
engine->Pause();
engine->display = EGL_NO_DISPLAY;
engine->context = EGL_NO_CONTEXT;
engine->surface = EGL_NO_SURFACE;
Expand Down Expand Up @@ -302,7 +348,7 @@ static void engine_handle_cmd(android_app* app, int32_t cmd) {
engine->accelerometerSensor,
(1000L / 60) * 1000);
}
engine->StartRendering();
engine->Resume();
break;
case APP_CMD_LOST_FOCUS:
// When our app loses focus, we stop monitoring the accelerometer.
Expand All @@ -311,7 +357,7 @@ static void engine_handle_cmd(android_app* app, int32_t cmd) {
ASensorEventQueue_disableSensor(engine->sensorEventQueue,
engine->accelerometerSensor);
}
engine->StopRendering();
engine->Pause();
break;
default:
break;
Expand Down Expand Up @@ -358,18 +404,14 @@ void android_main(android_app* state) {
engine.state = *(SavedState*)state->savedState;
}

// loop waiting for stuff to do.

while (true) {
// Read all pending events.
int events;
android_poll_source* source;

// If not animating_, we will block forever waiting for events.
// If animating_, we loop until all events are read, then continue
// to draw the next frame of animation.
while ((ALooper_pollAll(engine.animating() ? 0 : -1, nullptr, &events,
(void**)&source)) >= 0) {
// Our input, sensor, and update/render logic is all driven by callbacks, so
// we don't need to use the non-blocking poll.
while ((ALooper_pollAll(-1, nullptr, &events, (void**)&source)) >= 0) {
DanAlbert marked this conversation as resolved.
Show resolved Hide resolved
// Process this event.
if (source != nullptr) {
source->process(state, source);
Expand All @@ -381,18 +423,6 @@ void android_main(android_app* state) {
return;
}
}

if (engine.animating()) {
// Done with events; draw next animation frame.
engine.state.angle += .01f;
if (engine.state.angle > 1) {
engine.state.angle = 0;
}

// Drawing is throttled to the screen update rate, so there
// is no need to do timing here.
engine_draw_frame(&engine);
}
}
}
// END_INCLUDE(all)
2 changes: 1 addition & 1 deletion other-builds/ndkbuild/native-activity/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {

defaultConfig {
applicationId = 'com.example.native_activity'
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 33
}
sourceSets {
Expand Down
Loading