diff --git a/CMakeLists.txt b/CMakeLists.txt index 6120c3ee9e..4153c11eff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 0877137a46..cec3d2b2b2 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -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 diff --git a/Core/HLE/sceCtrl.cpp b/Core/HLE/sceCtrl.cpp index c623d2b368..976edcc433 100644 --- a/Core/HLE/sceCtrl.cpp +++ b/Core/HLE/sceCtrl.cpp @@ -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 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 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 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); diff --git a/android/jni/Android.mk b/android/jni/Android.mk index a97bfbed1a..c149e59b19 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -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 \ diff --git a/android/jni/AndroidAudio.cpp b/android/jni/AndroidAudio.cpp index 27f0327054..5eaf930c7e 100644 --- a/android/jni/AndroidAudio.cpp +++ b/android/jni/AndroidAudio.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) { diff --git a/android/jni/OboeContext.cpp b/android/jni/OboeContext.cpp index 15815ce064..b26c8fe730 100644 --- a/android/jni/OboeContext.cpp +++ b/android/jni/OboeContext.cpp @@ -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 { 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 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(*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 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 &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 output_; + // Only touched by the audio callback while output_ is running. + std::unique_ptr outputTuner_; std::shared_ptr 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(cb, framesPerBuffer, sampleRate); + streams_ = std::make_shared(cb, sampleRate); } bool OboeContext::Init() { diff --git a/android/jni/PerformanceHint.cpp b/android/jni/PerformanceHint.cpp new file mode 100644 index 0000000000..4dc3e2d85a --- /dev/null +++ b/android/jni/PerformanceHint.cpp @@ -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 +#include +#include + +#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; +} diff --git a/android/jni/PerformanceHint.h b/android/jni/PerformanceHint.h new file mode 100644 index 0000000000..2bd738fe46 --- /dev/null +++ b/android/jni/PerformanceHint.h @@ -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 + +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; +}; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 4778870450..19b55e9a33 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -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 diff --git a/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java b/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java index 06d518d5b1..8d5cc1a8a9 100644 --- a/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java +++ b/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java @@ -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); + } } } diff --git a/docs/android-device-testing.md b/docs/android-device-testing.md index 73862110cf..811c60247d 100644 --- a/docs/android-device-testing.md +++ b/docs/android-device-testing.md @@ -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