1
0
forked from MeloNX/MeloNX

UIThread working

This commit is contained in:
Daniil Vinogradov 2025-03-07 15:46:04 +01:00
parent 877a3b3dd7
commit 552069c8ce
8 changed files with 319 additions and 33 deletions

View File

@ -1,31 +1,31 @@
package melonx.module package melonx.module
import skip.lib.*
import skip.model.*
import skip.foundation.*
import skip.ui.*
import android.Manifest
import android.app.Application import android.app.Application
import androidx.activity.enableEdgeToEdge import android.hardware.Sensor
import android.os.Process
import android.util.Log
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat import com.lenta.presentation.utils.Rebugger
import kotlinx.coroutines.flow.MutableStateFlow
import melo.nxmodel.Ryujinx
import org.libsdl.app.SDLActivity import org.libsdl.app.SDLActivity
import skip.foundation.ProcessInfo
import skip.foundation.SkipLogger
import skip.ui.ColorScheme
import skip.ui.ComposeContext
import skip.ui.PresentationRoot
import skip.ui.UIApplication
internal val logger: SkipLogger = SkipLogger(subsystem = "melonx.module", category = "melonx") internal val logger: SkipLogger = SkipLogger(subsystem = "melonx.module", category = "melonx")
@ -49,6 +49,11 @@ open class MainActivity: SDLActivity {
constructor() { constructor() {
} }
companion object {
}
private val startGameConfigMutableStateFlow = MutableStateFlow<Ryujinx.Configuration?>(null)
override fun onCreate(savedInstanceState: android.os.Bundle?) { override fun onCreate(savedInstanceState: android.os.Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
logger.info("starting activity") logger.info("starting activity")
@ -56,16 +61,15 @@ open class MainActivity: SDLActivity {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
Box {
SDLComposeSurface() val startGameConfig = remember { GameState.shared._startGameConfig.projectedValue }
Box(Modifier.graphicsLayer(alpha = 0.5f)) { if (startGameConfig?.value != null) {
val saveableStateHolder = rememberSaveableStateHolder() runSimulator(startGameConfig.value!!)
saveableStateHolder.SaveableStateProvider(true) { GameState.shared.startGameConfig = null
PresentationRootView(ComposeContext())
SideEffect { saveableStateHolder.removeState(true) }
}
}
} }
val gameIsRunning = remember { GameState.shared._isGameRunning.projectedValue }
RootScreen(gameIsRunning?.value)
} }
// Example of requesting permissions on startup. // Example of requesting permissions on startup.
@ -128,7 +132,27 @@ open class MainActivity: SDLActivity {
logger.info("onRequestPermissionsResult: ${requestCode}") logger.info("onRequestPermissionsResult: ${requestCode}")
} }
companion object { @Composable
fun RootScreen(gameIsRunning: Boolean?) {
Rebugger(
trackMap = mapOf(
"gameIsRunning" to gameIsRunning
)
)
Box {
SDLComposeSurface()
if (gameIsRunning != true) {
// Box(Modifier.graphicsLayer(alpha = 0.2f)) {
val saveableStateHolder = rememberSaveableStateHolder()
saveableStateHolder.SaveableStateProvider(true) {
PresentationRootView(ComposeContext())
SideEffect { saveableStateHolder.removeState(true) }
}
// }
}
}
} }
@Composable @Composable
@ -139,6 +163,27 @@ open class MainActivity: SDLActivity {
} }
) )
} }
fun runSimulator(config: Ryujinx.Configuration) {
class RyujinxMain : Runnable {
override fun run() {
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY)
} catch (e: Exception) {
Log.v("SDL", "modify thread properties failed $e")
}
Ryujinx.shared.start(config)
}
}
// This is the entry point to the C app.
// Start up the C app thread and enable sensor input for the first time
// FIXME: Why aren't we enabling sensor input at start?
mSDLThread = Thread(RyujinxMain(), "SDLThread")
mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true)
mSDLThread.start()
}
} }
@Composable @Composable

View File

@ -0,0 +1,84 @@
package com.lenta.presentation.utils
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
private const val TAG = "Rebugger"
private class Ref<T>(var value: T)
/**
* Fork of Rebugger library, which help catch UI recomposition for composable func
* Used an android Log system instead the project Log, because we would't send on
*
* Example of usage
* ```kotlin
* @Composable
* fun VehicleUi(
* car: Car,
* bike: Bike,
* ) {
* var human by remember { mutableStateOf(Human("John")) }
* // Call Rebugger and pass the states you want to track.
* // It could be a function arg or a state
* Rebugger(
* trackMap = mapOf(
* "car" to car,
* "bike" to bike,
* "human" to human
* ),
* )
* //...
* ```
*
* @see <a href="https://github.com/theapache64/rebugger">
* Rebugger gitHub
* </a>
*/
@Suppress("unused")
@Composable
fun Rebugger(
trackMap: Map<String, Any?>,
composableName: String = Thread.currentThread().stackTrace[3].methodName,
) {
LaunchedEffect(Unit) {
Log.i(TAG, "🐞 Rebugger activated on `$composableName`")
}
val count = remember { Ref(0) }
val flag = remember { Ref(false) }
SideEffect {
count.value++
}
val changeLog = StringBuilder()
for ((key, newArg) in trackMap) {
var recompositionTrigger by remember { mutableStateOf(false) }
val oldArg = remember(recompositionTrigger) { newArg }
if (oldArg != newArg) {
changeLog.append("\n\t `$key` changed from `$oldArg` to `$newArg`, ")
flag.value = true
recompositionTrigger = !recompositionTrigger
}
}
if (changeLog.isNotEmpty()) {
Log.i(TAG, "🐞$composableName recomposed because $changeLog")
} else {
if (count.value >= 1 && !flag.value) {
Log.i(TAG, "🐞$composableName recomposed not because of param change")
} else {
flag.value = false
}
}
}

View File

@ -214,7 +214,7 @@ private extension Ryujinx {
args.append("--enable-debug-logs") args.append("--enable-debug-logs")
} }
args.append("--disable-info-logs") // args.append("--disable-info-logs")
if config.tracelogs { if config.tracelogs {
args.append("--enable-trace-logs") args.append("--enable-trace-logs")

View File

@ -6,6 +6,13 @@ enum ContentTab: String, Hashable {
case games, home, settings case games, home, settings
} }
@Observable public class GameState {
public static var shared = GameState()
public var isGameRunning: Bool = false
public var startGameConfig: Ryujinx.Configuration?
}
struct ContentView: View { struct ContentView: View {
@AppStorage("tab") var tab = ContentTab.games @AppStorage("tab") var tab = ContentTab.games
@State var viewModel = ViewModel() @State var viewModel = ViewModel()
@ -14,6 +21,7 @@ struct ContentView: View {
@State private var config: Ryujinx.Configuration @State private var config: Ryujinx.Configuration
@State private var game: Game? @State private var game: Game?
@State private var isLoading = true @State private var isLoading = true
@State private var startSDL = false
init() { init() {
// let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "") // let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
@ -32,11 +40,9 @@ struct ContentView: View {
} }
} else { } else {
TabView(selection: $tab) { TabView(selection: $tab) {
NavigationStack {
NavigationStack { NavigationStack {
GamesView(startemu: $game) GamesView(startemu: $game)
} }
}
.tabItem { Label("Games", systemImage: "house.fill") } .tabItem { Label("Games", systemImage: "house.fill") }
.tag(ContentTab.games) .tag(ContentTab.games)
@ -70,7 +76,11 @@ private extension ContentView {
var emulationView: some View { var emulationView: some View {
Text("Loading...") Text("Loading...")
.onAppear { .onAppear {
GameState.shared.isGameRunning = true
startSDL = true
setupEmulation() setupEmulation()
// SKIP INSERT: // MainActivity.enableSDLLayer(true)
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if Ryujinx.shared.getCurrentFps() != 0 { if Ryujinx.shared.getCurrentFps() != 0 {
@ -91,9 +101,9 @@ private extension ContentView {
// patchMakeKeyAndVisible() // patchMakeKeyAndVisible()
// isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil) // isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
DispatchQueue.main.async { // DispatchQueue.main.async {
start() start()
} // }
} }
func start() { func start() {
@ -117,7 +127,8 @@ private extension ContentView {
} }
do { do {
try Ryujinx.shared.start(with: config) GameState.shared.startGameConfig = config
// try Ryujinx.shared.start(with: config)
} catch { } catch {
logger.error("Error: \(error.localizedDescription)") logger.error("Error: \(error.localizedDescription)")
} }

View File

@ -315,6 +315,9 @@ namespace Ryujinx.Headless.SDL2
static void Main(string[] args) static void Main(string[] args)
{ {
Logger.Info?.Print(LogClass.Application, $"Start Emu on Thread name: {Thread.CurrentThread.Name}");
Logger.Info?.Print(LogClass.Application, $"Start Emu on Thread id: {Thread.CurrentThread.ManagedThreadId}");
// Make process DPI aware for proper window sizing on high-res screens. // Make process DPI aware for proper window sizing on high-res screens.
ForceDpiAware.Windows(); ForceDpiAware.Windows();

View File

@ -159,6 +159,8 @@ namespace Ryujinx.SDL2.Common
using ManualResetEventSlim waitHandle = new(false); using ManualResetEventSlim waitHandle = new(false);
Logger.Info?.Print(LogClass.Application, $"Start Emu on Thread name 2: {Thread.CurrentThread.Name}");
Logger.Info?.Print(LogClass.Application, $"Start Emu on Thread id 2: {Thread.CurrentThread.ManagedThreadId}");
while (_isRunning) while (_isRunning)
{ {
MainThreadDispatcher?.Invoke(() => MainThreadDispatcher?.Invoke(() =>

141
src/RyujinxAndroid/.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="NONE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b70a156d-88fb-4b09-9b57-c7698aab2d2a" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="device_and_snapshot_combo_box_target[DeviceId(pluginId=LocalEmulator, isTemplate=false, identifier=path=/Users/daniilvinogradov/.android/avd/Pixel_7_Pro_API_34.avd)]" />
<component name="ExternalProjectsData">
<projectState path="$PROJECT_DIR$">
<ProjectState />
</projectState>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/../.." />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 4
}]]></component>
<component name="ProjectId" id="2tjVfEZg0XN8pbpoF5pzqoZcHSH" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Android App.app.executor": "Debug",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"RunOnceActivity.readMode.enableVisualFormatting": "true",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"git-widget-placeholder": "Message-Bridge",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "/Users/daniilvinogradov/Documents/Dev/MeloNX/src/RyujinxAndroid",
"settings.editor.selected.configurable": "AndroidSdkUpdater"
}
}]]></component>
<component name="RunManager">
<configuration name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="RyujinxAndroid.app.main" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="b70a156d-88fb-4b09-9b57-c7698aab2d2a" name="Changes" comment="" />
<created>1740865571599</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1740865571599</updated>
</task>
<servers />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<breakpoint enabled="true" type="OCSymbolicBreakpointType">
<properties>
<option name="moduleName" value="libart.so" />
<option name="symbolPattern" value="art_sigbus_fault" />
</properties>
<option name="timeStamp" value="2" />
</breakpoint>
</breakpoints>
</breakpoint-manager>
</component>
</project>