Skip to content

Commit 8485cea

Browse files
committed
Migrate native-activity to Choreographer.
1 parent f541abf commit 8485cea

File tree

3 files changed

+80
-48
lines changed

3 files changed

+80
-48
lines changed

native-activity/app/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ android {
1212

1313
defaultConfig {
1414
applicationId = 'com.example.native_activity'
15-
minSdkVersion 21
15+
// This is the minimum required for using Choreographer directly from the NDK. If you need
16+
// to use a lower minSdkVersion, you must use the Java Choreographer API via JNI.
17+
minSdkVersion 24
1618
targetSdkVersion 34
1719
externalNativeBuild {
1820
cmake {

native-activity/app/src/main/cpp/main.cpp

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// BEGIN_INCLUDE(all)
1919
#include <EGL/egl.h>
2020
#include <GLES/gl.h>
21+
#include <android/choreographer.h>
2122
#include <android/log.h>
2223
#include <android/sensor.h>
2324
#include <android/set_abort_message.h>
@@ -104,18 +105,80 @@ struct Engine {
104105
sensorManager, app->looper, ALOOPER_POLL_CALLBACK, callback, this);
105106
}
106107

107-
bool animating() const { return animating_; }
108+
/// Resumes ticking the application.
109+
void Resume() {
110+
// Checked to make sure we don't double schedule Choreographer.
111+
if (!running_) {
112+
running_ = true;
113+
ScheduleNextTick();
114+
}
115+
}
116+
117+
/// Pauses ticking the application.
118+
///
119+
/// When paused, sensor and input events will still be processed, but the
120+
/// update and render parts of the loop will not run.
121+
void Pause() { running_ = false; }
122+
123+
private:
124+
bool running_;
108125

109-
void StartRendering() {
110-
animating_ = true;
126+
void ScheduleNextTick() {
127+
AChoreographer_postFrameCallback(AChoreographer_getInstance(), Tick, this);
111128
}
112129

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

117-
private:
118-
bool animating_;
147+
void DoTick() {
148+
if (!running_) {
149+
return;
150+
}
151+
152+
// Input and sensor feedback is handled via their own callbacks.
153+
// Choreographer ensures that those callbacks run before this callback does.
154+
155+
// Choreographer does not continuously schedule the callback. We have to re-
156+
// register the callback each time we're ticked.
157+
ScheduleNextTick();
158+
Update();
159+
DrawFrame();
160+
}
161+
162+
void Update() {
163+
state.angle += .01f;
164+
if (state.angle > 1) {
165+
state.angle = 0;
166+
}
167+
}
168+
169+
void DrawFrame() {
170+
if (display == nullptr) {
171+
// No display.
172+
return;
173+
}
174+
175+
// Just fill the screen with a color.
176+
glClearColor(((float)state.x) / width, state.angle,
177+
((float)state.y) / height, 1);
178+
glClear(GL_COLOR_BUFFER_BIT);
179+
180+
eglSwapBuffers(display, surface);
181+
}
119182
};
120183

121184
/**
@@ -218,23 +281,6 @@ static int engine_init_display(Engine* engine) {
218281
return 0;
219282
}
220283

221-
/**
222-
* Just the current frame in the display.
223-
*/
224-
static void engine_draw_frame(Engine* engine) {
225-
if (engine->display == nullptr) {
226-
// No display.
227-
return;
228-
}
229-
230-
// Just fill the screen with a color.
231-
glClearColor(((float)engine->state.x) / engine->width, engine->state.angle,
232-
((float)engine->state.y) / engine->height, 1);
233-
glClear(GL_COLOR_BUFFER_BIT);
234-
235-
eglSwapBuffers(engine->display, engine->surface);
236-
}
237-
238284
/**
239285
* Tear down the EGL context currently associated with the display.
240286
*/
@@ -250,7 +296,7 @@ static void engine_term_display(Engine* engine) {
250296
}
251297
eglTerminate(engine->display);
252298
}
253-
engine->StopRendering();
299+
engine->Pause();
254300
engine->display = EGL_NO_DISPLAY;
255301
engine->context = EGL_NO_CONTEXT;
256302
engine->surface = EGL_NO_SURFACE;
@@ -302,7 +348,7 @@ static void engine_handle_cmd(android_app* app, int32_t cmd) {
302348
engine->accelerometerSensor,
303349
(1000L / 60) * 1000);
304350
}
305-
engine->StartRendering();
351+
engine->Resume();
306352
break;
307353
case APP_CMD_LOST_FOCUS:
308354
// When our app loses focus, we stop monitoring the accelerometer.
@@ -311,7 +357,7 @@ static void engine_handle_cmd(android_app* app, int32_t cmd) {
311357
ASensorEventQueue_disableSensor(engine->sensorEventQueue,
312358
engine->accelerometerSensor);
313359
}
314-
engine->StopRendering();
360+
engine->Pause();
315361
break;
316362
default:
317363
break;
@@ -358,18 +404,14 @@ void android_main(android_app* state) {
358404
engine.state = *(SavedState*)state->savedState;
359405
}
360406

361-
// loop waiting for stuff to do.
362-
363407
while (true) {
364408
// Read all pending events.
365409
int events;
366410
android_poll_source* source;
367411

368-
// If not animating_, we will block forever waiting for events.
369-
// If animating_, we loop until all events are read, then continue
370-
// to draw the next frame of animation.
371-
while ((ALooper_pollAll(engine.animating() ? 0 : -1, nullptr, &events,
372-
(void**)&source)) >= 0) {
412+
// Our input, sensor, and update/render logic is all driven by callbacks, so
413+
// we don't need to use the non-blocking poll.
414+
while ((ALooper_pollAll(-1, nullptr, &events, (void**)&source)) >= 0) {
373415
// Process this event.
374416
if (source != nullptr) {
375417
source->process(state, source);
@@ -381,18 +423,6 @@ void android_main(android_app* state) {
381423
return;
382424
}
383425
}
384-
385-
if (engine.animating()) {
386-
// Done with events; draw next animation frame.
387-
engine.state.angle += .01f;
388-
if (engine.state.angle > 1) {
389-
engine.state.angle = 0;
390-
}
391-
392-
// Drawing is throttled to the screen update rate, so there
393-
// is no need to do timing here.
394-
engine_draw_frame(&engine);
395-
}
396426
}
397427
}
398428
// END_INCLUDE(all)

other-builds/ndkbuild/native-activity/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ android {
1212

1313
defaultConfig {
1414
applicationId = 'com.example.native_activity'
15-
minSdkVersion 21
15+
minSdkVersion 24
1616
targetSdkVersion 33
1717
}
1818
sourceSets {

0 commit comments

Comments
 (0)