mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Android: re-deliver the Surface, so a missed one cannot strand the renderer
Opening a game the instant the app started left a black game area forever, while rotating the device "fixed" it. SurfaceHolder.Callback::surfaceChanged is a one-shot -- Android delivers it when the surface is created or resized and never repeats -- and getNativeWindow() blocks until that single delivery arrives, in a 100 ms sleep loop with no timeout. One missed delivery therefore parks the RSX thread for the rest of the session. A rotation only helped because a configuration change forces a fresh surfaceChanged. EmulationSurface now re-delivers holder.surface on attach and on window visibility changes. It is idempotent: the native side compares the incoming ANativeWindow against the one it holds and no-ops on a match, so this costs nothing when the first delivery already arrived. It has to be post()ed, since onAttachedToWindow runs before layout and a 0x0 report is explicitly ignored. The wait loop also logs now, every three seconds, because the failure was otherwise completely silent: the emulator log stopped dead just after Vulkan device creation, the perf sensor read 0.0% CPU, and nothing said why. Diagnosis took a screenshot and dumpsys SurfaceFlinger to establish the surface existed. Adds GSFrameBase::display_epoch, bumped when the native window is replaced. The swapchain is rebuilt on a size mismatch and nothing else, so a replacement window at identical dimensions was invisible; platforms that cannot swap a window under a live swapchain keep the default and are unaffected.
This commit is contained in:
@@ -57,6 +57,41 @@ class EmulationSurface(context: Context) :
|
||||
super.onAttachedToWindow()
|
||||
hostWindow()?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
displayManager.registerDisplayListener(this, null)
|
||||
redeliverSurface()
|
||||
}
|
||||
|
||||
override fun onWindowVisibilityChanged(visibility: Int) {
|
||||
super.onWindowVisibilityChanged(visibility)
|
||||
if (visibility == VISIBLE) redeliverSurface()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand the current Surface to native again, if we already have a usable one.
|
||||
*
|
||||
* surfaceChanged is a ONE-SHOT: Android delivers it when the surface is created or resized and
|
||||
* never repeats it. The native renderer blocks in getNativeWindow() until that single delivery
|
||||
* arrives — a 100 ms sleep loop with no timeout — so if it is ever missed, the RSX thread parks
|
||||
* forever and the game area stays black at 0% CPU with the boot log stopping dead just after
|
||||
* Vulkan device creation. That is the "launch a game the instant the app opens and get a black
|
||||
* screen" report: the SurfaceView and its compositor layer exist, the touch controls draw over
|
||||
* it, and nothing is wrong except that native was never told. Rotating the device "fixed" it
|
||||
* only because a configuration change forces a fresh surfaceChanged.
|
||||
*
|
||||
* Re-delivering is safe and idempotent: the native side compares the incoming ANativeWindow
|
||||
* against the one it holds and treats an identical pointer as a no-op, so calling this on every
|
||||
* attach and every window-visibility change costs nothing when the surface already arrived.
|
||||
*/
|
||||
fun redeliverSurface() {
|
||||
// post() rather than inline: onAttachedToWindow runs before layout, so width/height are
|
||||
// still 0 here, and a 0x0 report is exactly what the native side is told to ignore.
|
||||
post {
|
||||
val current = holder.surface
|
||||
|
||||
if (current != null && current.isValid && width > 0 && height > 0) {
|
||||
pushDisplayCutoutInset(width, height)
|
||||
NativeApp.onNativeSurfaceChanged(current, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Crypto/unpkg.h"
|
||||
#include "Crypto/unself.h"
|
||||
#include "Emu/Audio/Cubeb/CubebBackend.h"
|
||||
#include "Emu/RSX/VK/VKFrameGen.h"
|
||||
#include "Emu/Audio/Oboe/OboeBackend.h"
|
||||
#include "Emu/Audio/Null/NullAudioBackend.h"
|
||||
#include "Emu/Cell/PPUAnalyser.h"
|
||||
@@ -113,6 +114,12 @@ static std::atomic<u64> g_native_window_size;
|
||||
// Set when losing the surface is what paused the emulator, so getting it back resumes only
|
||||
// the pause we caused.
|
||||
static std::atomic<bool> g_paused_by_surface_loss;
|
||||
|
||||
// Bumped every time g_native_window starts pointing somewhere else. See
|
||||
// GSFrameBase::display_epoch: a new Surface with the same dimensions as the old one is otherwise
|
||||
// undetectable by the renderer, and boot-then-immediately-open-a-game is exactly when the
|
||||
// emulation SurfaceView is replaced under a swapchain that has already been built.
|
||||
static std::atomic<u64> g_native_window_epoch;
|
||||
extern std::string g_android_executable_dir;
|
||||
extern std::string g_android_config_dir;
|
||||
extern std::string g_android_cache_dir;
|
||||
@@ -387,12 +394,26 @@ struct GraphicsFrame : GSFrameBase {
|
||||
|
||||
ANativeWindow *getNativeWindow() const {
|
||||
ANativeWindow *result;
|
||||
|
||||
// Waiting here is normal for a moment at boot -- the RSX thread usually starts before the
|
||||
// SurfaceView has been laid out. Waiting here FOREVER is not, and used to be completely
|
||||
// silent: surfaceChanged is a one-shot, so a single missed delivery parked this loop for the
|
||||
// rest of the session. The symptom was a black game area at 0% CPU with the log stopping dead
|
||||
// just after Vulkan device creation and nothing at all to say why. Say so instead.
|
||||
u32 waited_ms = 0;
|
||||
|
||||
while ((result = g_native_window.load()) == nullptr) [[unlikely]] {
|
||||
if (Emu.IsStopped()) {
|
||||
return activeNativeWindow;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
waited_ms += 100;
|
||||
|
||||
if (waited_ms % 3000 == 0) {
|
||||
rpcsx_android.error("Still waiting for a Surface after %u ms -- the renderer cannot start "
|
||||
"until surfaceChanged reaches native.", waited_ms);
|
||||
}
|
||||
}
|
||||
|
||||
if (result != activeNativeWindow) [[unlikely]] {
|
||||
@@ -529,6 +550,8 @@ struct GraphicsFrame : GSFrameBase {
|
||||
|
||||
display_handle_t handle() const override { return getNativeWindow(); }
|
||||
|
||||
u64 display_epoch() const override { return g_native_window_epoch.load(); }
|
||||
|
||||
bool can_consume_frame() const override { return false; }
|
||||
|
||||
// Upstream takes the buffer by rvalue ref (it moves it); RPCSX's lvalue-ref
|
||||
@@ -3631,6 +3654,7 @@ extern "C" bool _rpcsx_surfaceEvent(JNIEnv *env, jobject surface, jint event) {
|
||||
auto prevWindow = g_native_window.exchange(nullptr);
|
||||
if (prevWindow != nullptr) {
|
||||
ANativeWindow_release(prevWindow);
|
||||
g_native_window_epoch.fetch_add(1);
|
||||
}
|
||||
|
||||
// get_pad_thread() defaults to relaxed=false, which is
|
||||
@@ -3691,6 +3715,15 @@ extern "C" bool _rpcsx_surfaceEvent(JNIEnv *env, jobject surface, jint event) {
|
||||
ANativeWindow_release(prevWindow);
|
||||
}
|
||||
|
||||
// Only when it really is a different window. Rotation keeps the same one (the activity
|
||||
// handles orientation itself), and bumping on every surfaceChanged would rebuild the
|
||||
// swapchain for nothing on every rotation.
|
||||
if (prevWindow != newWindow) {
|
||||
g_native_window_epoch.fetch_add(1);
|
||||
rpcsx_android.notice("native window replaced (%p -> %p), swapchain will be rebuilt",
|
||||
prevWindow, newWindow);
|
||||
}
|
||||
|
||||
if (event == 0 && g_paused_by_surface_loss && Emu.IsPaused()) {
|
||||
g_paused_by_surface_loss = false;
|
||||
Emu.Resume();
|
||||
@@ -4667,6 +4700,26 @@ extern "C" void _rpcsx_settingsEndBatch() {
|
||||
}
|
||||
}
|
||||
|
||||
// Import the Lossless Scaling shaders from a file the user chose.
|
||||
//
|
||||
// The path is a real filesystem path, not a content:// URI -- the Kotlin side copies the picked
|
||||
// file into app storage first, because the extraction walks the PE with ordinary file IO.
|
||||
//
|
||||
// Returns the number of shaders extracted, negative on failure. The message is left where
|
||||
// frameGenShaderError can retrieve it, so the settings screen can say what actually went wrong
|
||||
// rather than "import failed".
|
||||
extern "C" int _rpcsx_frameGenImportShaders(std::string_view path) {
|
||||
return vk::frame_gen::import_shaders(std::string(path));
|
||||
}
|
||||
|
||||
extern "C" int _rpcsx_frameGenShaderCount() {
|
||||
return vk::frame_gen::shader_count();
|
||||
}
|
||||
|
||||
extern "C" const char *_rpcsx_frameGenShaderError() {
|
||||
return vk::frame_gen::last_error();
|
||||
}
|
||||
|
||||
extern "C" bool _rpcsx_settingsSet(std::string_view path,
|
||||
std::string_view valueString) {
|
||||
auto root = find_cfg_node(&g_cfg, path);
|
||||
|
||||
@@ -30,6 +30,15 @@ public:
|
||||
|
||||
virtual display_handle_t handle() const = 0;
|
||||
|
||||
// Bumped whenever handle() starts referring to a DIFFERENT native window.
|
||||
//
|
||||
// The swapchain is rebuilt when client_width()/client_height() stop matching it, and that is
|
||||
// the only trigger there is. A replacement window with the same dimensions is therefore
|
||||
// invisible: the swapchain stays bound to a window that is no longer on screen and the picture
|
||||
// goes black until something happens to change the size -- which is why rotating the device
|
||||
// "fixed" it. Platforms whose window cannot be swapped under a live swapchain keep the default.
|
||||
virtual u64 display_epoch() const { return 0; }
|
||||
|
||||
virtual bool can_consume_frame() const = 0;
|
||||
virtual void present_frame(std::vector<u8>&& data, u32 pitch, u32 width, u32 height, bool is_bgra) const = 0;
|
||||
virtual void take_screenshot(std::vector<u8>&& sshot_data, u32 sshot_width, u32 sshot_height, bool is_bgra) = 0;
|
||||
|
||||
Reference in New Issue
Block a user