Built-in libretro emulator shows black (SNES) or grey (NES) screen with no audio. Native library reports initialization success but nothing actually renders.
- Native library loads: OK
- GL context created: OpenGL ES 3.2, Adreno 830
- Audio initializes: 48kHz
- Game "starts": 60fps reported
- Screen output: BLACK/GREY
- Audio output: NONE
- No crash or error logs
The issue was RENDERMODE_WHEN_DIRTY vs RENDERMODE_CONTINUOUSLY.
GLSurfaceView has two render modes:
RENDERMODE_CONTINUOUSLY: CallsonDrawFrame()at vsync rate (~60/120fps)RENDERMODE_WHEN_DIRTY: Only callsonDrawFrame()whenrequestRender()is called
Libretro cores expect the host to call retro_run() continuously at the target frame rate. With RENDERMODE_WHEN_DIRTY, onDrawFrame() was only called ONCE (on surface creation), so only ONE frame was ever rendered.
The blackFrameInsertion property had logic that set renderMode = RENDERMODE_WHEN_DIRTY when BFI was disabled (the default). When LibretroActivity set retroView.blackFrameInsertion = false, this triggered the property observer which reset renderMode.
- Added
renderMode = RENDERMODE_CONTINUOUSLYin GLRetroView'sinitblock - Removed the renderMode override from
blackFrameInsertionproperty observer
// In init block:
renderMode = RENDERMODE_CONTINUOUSLY
// In blackFrameInsertion property - removed this line:
// renderMode = if (value) RENDERMODE_CONTINUOUSLY else RENDERMODE_WHEN_DIRTYisEmulationReady flag in GLRetroView stays false, so onDrawFrame() never calls LibretroDroid.step().
Result: Lifecycle observer WAS firing correctly. The issue was renderMode, not lifecycle.
Files: libretrodroid/consumer-rules.pro, app/proguard-rules.pro
Change: Added -keepclassmembers for @OnLifecycleEvent annotated methods
Result: Not the issue - DEBUG builds (no R8) have same problem
File: libretrodroid/src/main/java/com/swordfish/libretrodroid/GLRetroView.kt
Change: Check lifecycle?.currentState?.isAtLeast(Lifecycle.State.RESUMED) and call observer.manualResume()
Result: ANR - was calling LibretroDroid.resume() on UI thread
File: libretrodroid/src/main/java/com/swordfish/libretrodroid/GLRetroView.kt
Change: Wrap observer.manualResume() in queueEvent { }
Result: Caused double-resume (lifecycle event fires synchronously when observer added to already-RESUMED lifecycle)
File: libretrodroid/src/main/java/com/swordfish/libretrodroid/GLRetroView.kt
Change: Removed manual resume check - lifecycle event fires automatically when observer is added
Result: PENDING TEST
File: libretrodroid/src/main/cpp/libretrodroid.cpp
Changes:
resume()now logs with LOGI instead of LOGDstep()now logs with LOGI instead of LOGDcallback_hw_video_refresh()now logs:VIDEO CALLBACK: data=%p width=%u height=%u pitch=%zuResult: SUCCESS - Video callback IS being called with valid data (256x224 NES resolution)
The core IS producing video frames. The VIDEO CALLBACK receives:
- data=0xb40000797d79f380 (valid pointer)
- width=256, height=224 (correct NES resolution)
- pitch=512
The bug is in the rendering pipeline AFTER the callback, not in core execution or callback delivery.
Need to add logging to:
handleVideoRefresh()- does it callvideo->onNewFrame()?video->onNewFrame()- does it process the data?video->renderFrame()- is it being called?- Shader compilation - any errors?
private inner class RenderLifecycleObserver : LifecycleObserver {
fun manualResume() = catchExceptions {
Log.d(TAG_LOG, "RenderLifecycleObserver.manualResume() called")
LibretroDroid.resume()
onResume()
isEmulationReady = true
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun resume() = catchExceptions {
Log.d(TAG_LOG, "RenderLifecycleObserver.resume() lifecycle event")
if (!isEmulationReady) {
LibretroDroid.resume()
onResume()
isEmulationReady = true
}
}
// ... pause unchanged
}KtUtils.runOnUIThread {
val observer = RenderLifecycleObserver()
lifecycle?.addObserver(observer)
if (lifecycle?.currentState?.isAtLeast(Lifecycle.State.RESUMED) == true) {
Log.d(TAG_LOG, "Lifecycle already RESUMED, manually triggering resume")
queueEvent { observer.manualResume() }
}
}# Keep ALL libretrodroid classes - heavily JNI-dependent
-keep class com.swordfish.libretrodroid.** { *; }
-keepclassmembers class com.swordfish.libretrodroid.** { *; }
# Keep lifecycle observer methods (uses reflection to find annotated methods)
-keepclassmembers class * implements androidx.lifecycle.LifecycleObserver {
@androidx.lifecycle.OnLifecycleEvent <methods>;
}
# Keep the method names in inner classes that implement LifecycleObserver
-keepclassmembers class com.swordfish.libretrodroid.GLRetroView$* {
@androidx.lifecycle.OnLifecycleEvent <methods>;
}
"Lifecycle already RESUMED, manually triggering resume"- manual check fired"RenderLifecycleObserver.manualResume() called"- manual resume executed"RenderLifecycleObserver.resume() lifecycle event"- normal lifecycle fired"Performing libretrodroid resume"- native resume called"Stepping into retro_run()"- frames actually running
- User says this was fixed in a previous session but not committed
- User mentioned "imports/refs being pruned" as the original issue
- This affects BOTH debug and release builds
- Device: Odin3 (Adreno 830)
- Verify queueEvent fix works
- If still broken, add more logging to trace exact execution path
- Check if the issue is actually in native code (video callback not firing)
- Compare against original libretrodroid library to see what might have changed