mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-12 17:19:28 +01:00
Merge pull request #22889 from hrydgard/input-latency
Android: Minor input and audio latency improvements
This commit is contained in:
11 files changed
+217
-22
No files matched your search
@@ -896,6 +896,8 @@ if(ANDROID)
|
||||
android/jni/AndroidEGLGraphicsContext.h
|
||||
android/jni/OboeContext.cpp
|
||||
android/jni/OboeContext.h
|
||||
android/jni/PerformanceHint.cpp
|
||||
android/jni/PerformanceHint.h
|
||||
)
|
||||
# No target
|
||||
elseif(IOS AND NOT LIBRETRO)
|
||||
|
||||
@@ -805,7 +805,11 @@ if (WIN32)
|
||||
set(WIN_VERSION_CMD COMMAND ${CMAKE_SOURCE_DIR}/Windows/git-version-gen.cmd PPSSPPWindows)
|
||||
endif()
|
||||
|
||||
# BYPRODUCTS tells Ninja that this writes git-version.cpp, so it checks the file again after the
|
||||
# command has run. Otherwise it only sees the timestamp from before the build, and the object is
|
||||
# recompiled a build late, if at all. The file is only rewritten when the version changes.
|
||||
add_custom_command(OUTPUT something_that_never_exists
|
||||
BYPRODUCTS ${CMAKE_BINARY_DIR}/git-version.cpp
|
||||
COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
|
||||
-DOUTPUT_DIR=${CMAKE_BINARY_DIR}
|
||||
-P ${CMAKE_SOURCE_DIR}/git-version.cmake
|
||||
|
||||
+12
-2
@@ -114,6 +114,10 @@ static u32 emuRapidFireInterval = 5;
|
||||
// no buttons and centered sticks, as impose does on a PSP. Host-side peeks still see the real state.
|
||||
static std::atomic<bool> gameInputBlocked{ false };
|
||||
|
||||
// Buttons pressed since the last sample. A press and release that both land between two samples
|
||||
// would otherwise never be seen by the game. Guarded by ctrlMutex.
|
||||
static u32 ctrlUnsampledPresses = 0;
|
||||
|
||||
// These buttons are not affected by rapid fire (neither is analog.)
|
||||
const u32 CTRL_EMU_RAPIDFIRE_MASK = CTRL_UP | CTRL_DOWN | CTRL_LEFT | CTRL_RIGHT;
|
||||
|
||||
@@ -122,7 +126,8 @@ static void __CtrlUpdateLatch()
|
||||
std::lock_guard<std::mutex> guard(ctrlMutex);
|
||||
u64 t = CoreTiming::GetGlobalTimeUs();
|
||||
|
||||
u32 buttons = ctrlCurrent.buttons;
|
||||
u32 buttons = ctrlCurrent.buttons | ctrlUnsampledPresses;
|
||||
ctrlUnsampledPresses = 0;
|
||||
if (emuRapidFire && emuRapidFireToggle)
|
||||
buttons &= CTRL_EMU_RAPIDFIRE_MASK;
|
||||
|
||||
@@ -284,7 +289,8 @@ void __CtrlUpdateButtons(u32 bitsToSet, u32 bitsToClear)
|
||||
|
||||
std::lock_guard<std::mutex> guard(ctrlMutex);
|
||||
// There's no atomic operation for this, so mutex it is.
|
||||
ctrlCurrent.buttons = (ctrlCurrent.buttons & ~bitsToClear) | bitsToSet;
|
||||
ctrlUnsampledPresses |= bitsToSet & ~ctrlCurrent.buttons;
|
||||
ctrlCurrent.buttons =(ctrlCurrent.buttons & ~bitsToClear) | bitsToSet;
|
||||
}
|
||||
|
||||
void __CtrlSetAnalogXY(int stick, float x, float y)
|
||||
@@ -448,6 +454,7 @@ void __CtrlInit() {
|
||||
latch.btnRelease = 0xffffffff;
|
||||
|
||||
gameInputBlocked = false;
|
||||
ctrlUnsampledPresses = 0;
|
||||
memset(&ctrlCurrent, 0, sizeof(ctrlCurrent));
|
||||
memset(ctrlCurrent.analog, CTRL_ANALOG_CENTER, sizeof(ctrlCurrent.analog));
|
||||
analogEnabled = false;
|
||||
@@ -464,6 +471,9 @@ void __CtrlDoState(PointerWrap &p)
|
||||
if (!s)
|
||||
return;
|
||||
|
||||
// Host input from before the load shouldn't show up in the loaded state.
|
||||
ctrlUnsampledPresses = 0;
|
||||
|
||||
Do(p, analogEnabled);
|
||||
Do(p, ctrlLatchBufs);
|
||||
Do(p, ctrlOldButtons);
|
||||
|
||||
@@ -1049,6 +1049,7 @@ LOCAL_SRC_FILES := \
|
||||
$(SRC)/android/jni/AndroidAudio.cpp \
|
||||
$(SRC)/android/jni/AndroidEGLGraphicsContext.cpp \
|
||||
$(SRC)/android/jni/OboeContext.cpp \
|
||||
$(SRC)/android/jni/PerformanceHint.cpp \
|
||||
$(SRC)/UI/ImDebugger/ImDebugger.cpp \
|
||||
$(SRC)/UI/ImDebugger/ImGe.cpp \
|
||||
$(SRC)/UI/ImDebugger/ImConsole.cpp \
|
||||
|
||||
@@ -25,8 +25,8 @@ AudioContext::AudioContext(AndroidAudioCallback cb, int _FramesPerBuffer, int _S
|
||||
framesPerBuffer = 4096;
|
||||
|
||||
sampleRate = _SampleRate;
|
||||
g_error = "";
|
||||
g_info = "";
|
||||
SetErrorString("");
|
||||
SetInfoString("");
|
||||
}
|
||||
|
||||
void AudioContext::SetErrorString(const std::string &error) {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
// callback on a thread of its own that can outlive the OboeContext.
|
||||
class OboeContext::Streams : public oboe::AudioStreamDataCallback, public oboe::AudioStreamErrorCallback, public std::enable_shared_from_this<OboeContext::Streams> {
|
||||
public:
|
||||
Streams(AndroidAudioCallback cb, int framesPerBuffer, int sampleRate)
|
||||
: audioCallback_(cb), framesPerBuffer_(framesPerBuffer), sampleRate_(sampleRate) {
|
||||
Streams(AndroidAudioCallback cb, int sampleRate)
|
||||
: audioCallback_(cb), sampleRate_(sampleRate) {
|
||||
// The setting isn't offered before Android 8.1, where AAudio isn't stable.
|
||||
if (g_Config.iAndroidAudioApi == (int)AndroidAudioApi::AAUDIO && oboe::AudioStreamBuilder::isAAudioRecommended()) {
|
||||
audioApi_ = oboe::AudioApi::AAudio;
|
||||
@@ -49,12 +49,16 @@ public:
|
||||
void Shutdown() {
|
||||
std::lock_guard<std::mutex> guard(mutex_);
|
||||
shutdown_ = true;
|
||||
CloseStream(output_);
|
||||
CloseOutput();
|
||||
CloseStream(input_);
|
||||
}
|
||||
|
||||
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *stream, void *audioData, int32_t numFrames) override {
|
||||
if (stream->getDirection() == oboe::Direction::Output) {
|
||||
// Grows the buffer a burst at a time when we underrun.
|
||||
if (outputTuner_) {
|
||||
outputTuner_->tune();
|
||||
}
|
||||
audioCallback_((short *)audioData, numFrames, stream->getSampleRate(), nullptr);
|
||||
} else {
|
||||
// This is the audio thread, so just buffer it. The CPU thread picks it up.
|
||||
@@ -77,6 +81,7 @@ public:
|
||||
}
|
||||
WARN_LOG(Log::Audio, "Oboe: %s stream closed with error: %s", isOutput ? "output" : "input", oboe::convertToText(error));
|
||||
if (isOutput) {
|
||||
outputTuner_.reset();
|
||||
output_.reset();
|
||||
} else {
|
||||
input_.reset();
|
||||
@@ -107,7 +112,9 @@ private:
|
||||
->setChannelConversionAllowed(true)
|
||||
->setSampleRate(sampleRate_)
|
||||
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium)
|
||||
->setFramesPerDataCallback(framesPerBuffer_)
|
||||
// No setFramesPerDataCallback: NativeMix takes any size, and forcing one makes Oboe add a
|
||||
// block adapter, which holds back an extra buffer when it doesn't match the burst (MMAP
|
||||
// streams often have a smaller burst than PROPERTY_OUTPUT_FRAMES_PER_BUFFER).
|
||||
->setDataCallback(this)
|
||||
->setErrorCallback(shared_from_this());
|
||||
oboe::Result result = builder.openStream(output_);
|
||||
@@ -115,11 +122,11 @@ private:
|
||||
output_.reset();
|
||||
return Fail("Failed to open output stream", result);
|
||||
}
|
||||
// Double buffering, the same latency as the old OpenSL path.
|
||||
output_->setBufferSizeInFrames(output_->getFramesPerBurst() * 2);
|
||||
// Starts at two bursts.
|
||||
outputTuner_ = std::make_unique<oboe::LatencyTuner>(*output_);
|
||||
result = output_->requestStart();
|
||||
if (result != oboe::Result::OK) {
|
||||
CloseStream(output_);
|
||||
CloseOutput();
|
||||
return Fail("Failed to start output stream", result);
|
||||
}
|
||||
char info[256];
|
||||
@@ -161,6 +168,16 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
void CloseOutput() {
|
||||
if (output_) {
|
||||
oboe::ResultWithValue<int32_t> xruns = output_->getXRunCount();
|
||||
INFO_LOG(Log::Audio, "Oboe: Closing output stream, buffer %d, underruns %d", output_->getBufferSizeInFrames(), xruns ? xruns.value() : -1);
|
||||
}
|
||||
// Stopped first, so the callback is done with the tuner.
|
||||
CloseStream(output_);
|
||||
outputTuner_.reset();
|
||||
}
|
||||
|
||||
static void CloseStream(std::shared_ptr<oboe::AudioStream> &stream) {
|
||||
if (stream) {
|
||||
stream->stop();
|
||||
@@ -176,13 +193,14 @@ private:
|
||||
}
|
||||
|
||||
AndroidAudioCallback audioCallback_;
|
||||
int framesPerBuffer_;
|
||||
int sampleRate_;
|
||||
oboe::AudioApi audioApi_ = oboe::AudioApi::OpenSLES;
|
||||
|
||||
std::mutex mutex_;
|
||||
bool shutdown_ = false;
|
||||
std::shared_ptr<oboe::AudioStream> output_;
|
||||
// Only touched by the audio callback while output_ is running.
|
||||
std::unique_ptr<oboe::LatencyTuner> outputTuner_;
|
||||
std::shared_ptr<oboe::AudioStream> input_;
|
||||
int inputSampleRate_ = 0;
|
||||
};
|
||||
@@ -192,7 +210,7 @@ OboeContext::OboeContext(AndroidAudioCallback cb, int _FramesPerBuffer, int _Sam
|
||||
// Only used by the OpenSL ES fallback, which can't query the device's native values itself.
|
||||
oboe::DefaultStreamValues::SampleRate = sampleRate;
|
||||
oboe::DefaultStreamValues::FramesPerBurst = framesPerBuffer;
|
||||
streams_ = std::make_shared<Streams>(cb, framesPerBuffer, sampleRate);
|
||||
streams_ = std::make_shared<Streams>(cb, sampleRate);
|
||||
}
|
||||
|
||||
bool OboeContext::Init() {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2012- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "Common/Log.h"
|
||||
#include "android/jni/PerformanceHint.h"
|
||||
|
||||
struct APerformanceHintManager;
|
||||
|
||||
namespace {
|
||||
|
||||
// Half a frame. With CPU time as the measure, this leaves the other half as headroom, so the
|
||||
// scheduler keeps us on a core fast enough to finish well before the frame is due.
|
||||
constexpr int64_t TARGET_WORK_NS = 1000000000LL / 60 / 2;
|
||||
|
||||
// Looked up at runtime, since we build against API 21 and these are API 33.
|
||||
struct HintApi {
|
||||
APerformanceHintManager *(*getManager)();
|
||||
APerformanceHintSession *(*createSession)(APerformanceHintManager *, const int32_t *, size_t, int64_t);
|
||||
int (*reportActualWorkDuration)(APerformanceHintSession *, int64_t);
|
||||
void (*closeSession)(APerformanceHintSession *);
|
||||
|
||||
bool Load() {
|
||||
void *lib = dlopen("libandroid.so", RTLD_NOW);
|
||||
if (!lib) {
|
||||
return false;
|
||||
}
|
||||
getManager = (decltype(getManager))dlsym(lib, "APerformanceHint_getManager");
|
||||
createSession = (decltype(createSession))dlsym(lib, "APerformanceHint_createSession");
|
||||
reportActualWorkDuration = (decltype(reportActualWorkDuration))dlsym(lib, "APerformanceHint_reportActualWorkDuration");
|
||||
closeSession = (decltype(closeSession))dlsym(lib, "APerformanceHint_closeSession");
|
||||
return getManager && createSession && reportActualWorkDuration && closeSession;
|
||||
}
|
||||
};
|
||||
|
||||
const HintApi *GetApi() {
|
||||
static HintApi api{};
|
||||
static const bool loaded = api.Load();
|
||||
return loaded ? &api : nullptr;
|
||||
}
|
||||
|
||||
// Time spent running on a CPU, so sleeping in the frame limiter or blocking on vsync or the GPU
|
||||
// doesn't count as work.
|
||||
int64_t ThreadCpuTimeNs() {
|
||||
timespec ts;
|
||||
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PerformanceHintSession::~PerformanceHintSession() {
|
||||
if (session_) {
|
||||
GetApi()->closeSession(session_);
|
||||
}
|
||||
}
|
||||
|
||||
void PerformanceHintSession::ReportFrame() {
|
||||
if (failed_) {
|
||||
return;
|
||||
}
|
||||
if (!session_) {
|
||||
const HintApi *api = GetApi();
|
||||
APerformanceHintManager *manager = api ? api->getManager() : nullptr;
|
||||
const int32_t tid = gettid();
|
||||
session_ = manager ? api->createSession(manager, &tid, 1, TARGET_WORK_NS) : nullptr;
|
||||
if (!session_) {
|
||||
INFO_LOG(Log::System, "Performance hint sessions not available");
|
||||
failed_ = true;
|
||||
return;
|
||||
}
|
||||
INFO_LOG(Log::System, "Performance hint session created for thread %d, target %d us", tid, (int)(TARGET_WORK_NS / 1000));
|
||||
}
|
||||
const int64_t now = ThreadCpuTimeNs();
|
||||
if (lastCpuTimeNs_ >= 0 && now > lastCpuTimeNs_) {
|
||||
GetApi()->reportActualWorkDuration(session_, now - lastCpuTimeNs_);
|
||||
}
|
||||
lastCpuTimeNs_ = now;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2012- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct APerformanceHintSession;
|
||||
|
||||
// An ADPF performance hint session (Android 13+) for the thread that runs the frame loop.
|
||||
// Emulation does a burst of work each frame and then waits, so the scheduler sees a mostly idle
|
||||
// thread and moves it to a slow core at low clocks, where the next burst then runs. Reporting each
|
||||
// frame's work against a target tells it what we actually need. Does nothing where unsupported.
|
||||
class PerformanceHintSession {
|
||||
public:
|
||||
~PerformanceHintSession();
|
||||
|
||||
// Call once per frame, always from the same thread. The session is created on the first call.
|
||||
void ReportFrame();
|
||||
|
||||
private:
|
||||
APerformanceHintSession *session_ = nullptr;
|
||||
bool failed_ = false;
|
||||
int64_t lastCpuTimeNs_ = -1;
|
||||
};
|
||||
@@ -113,6 +113,7 @@ struct JNIEnv {};
|
||||
#include "UI/GameInfoCache.h"
|
||||
|
||||
#include "app-android.h"
|
||||
#include "android/jni/PerformanceHint.h"
|
||||
|
||||
AndroidAudioState *g_audioState;
|
||||
|
||||
@@ -1781,10 +1782,13 @@ static void RenderLoopThread(ANativeWindow *wnd, GraphicsContext *graphicsContex
|
||||
}
|
||||
|
||||
renderer_inited = true;
|
||||
const auto frame = [](GraphicsContext *graphicsContext) {
|
||||
// The frame callback runs on the emu thread, which with OpenGL isn't this one.
|
||||
PerformanceHintSession perfHint;
|
||||
const auto frame = [&perfHint](GraphicsContext *graphicsContext) {
|
||||
ApplyPendingResize();
|
||||
NativeFrame(graphicsContext);
|
||||
ProcessFrameCommands();
|
||||
perfHint.ReportFrame();
|
||||
return !exitRenderLoop;
|
||||
};
|
||||
// Both backends share one lifecycle now, which is the point of all this: RunGraphicsLoop picks
|
||||
|
||||
@@ -7,8 +7,10 @@ package org.ppsspp.ppsspp;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.text.InputType;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceView;
|
||||
@@ -28,6 +30,21 @@ public class NativeSurfaceView extends SurfaceView {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Without this, touch moves and joystick axis events are batched until the next UI vsync,
|
||||
// adding up to a frame of latency before the emulator sees them. Non-pointer sources only
|
||||
// apply while this view is focused, so keep it focusable.
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
setDefaultFocusHighlightEnabled(false);
|
||||
requestFocus();
|
||||
requestUnbufferedDispatch(InputDevice.SOURCE_CLASS_POINTER | InputDevice.SOURCE_CLASS_JOYSTICK);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent ev) {
|
||||
@@ -55,9 +72,12 @@ public class NativeSurfaceView extends SurfaceView {
|
||||
} else {
|
||||
imm.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
imm.restartInput(this);
|
||||
clearFocus();
|
||||
setFocusableInTouchMode(false);
|
||||
setFocusable(false);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
// On R+ we stay focused for unbuffered dispatch, see onAttachedToWindow.
|
||||
clearFocus();
|
||||
setFocusableInTouchMode(false);
|
||||
setFocusable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,10 @@ Verify after every install, not just when something looks odd:
|
||||
A second, independent check is worth it when a result is surprising: grep the log for a symbol that
|
||||
exists only on the branch under test (a renamed class or log tag, say).
|
||||
|
||||
The version string itself can also lie. The Android build regenerates `git-version.cpp` but does not
|
||||
always recompile it, so the APK can carry the previous build's string while containing current code.
|
||||
When the version matters:
|
||||
|
||||
find android/.cxx -name git-version.cpp.o -delete
|
||||
The version string in the app's UI used to lie as well: Ninja regenerated `git-version.cpp` but didn't
|
||||
recompile it, so the APK could carry an older commit's string. That's fixed (the file is now declared
|
||||
as a `BYPRODUCTS` of the generating step in `Core/CMakeLists.txt`), so on a build directory that has
|
||||
been reconfigured since, the string matches the commit the APK was built from.
|
||||
|
||||
## Forcing an activity recreate without touching the device
|
||||
|
||||
|
||||
Reference in new issue
Block a user