improve async loading. add game load progress

This commit is contained in:
Emmanuel Hansen 2023-08-26 12:02:58 +00:00
parent c0091c838d
commit 28af479e2a
11 changed files with 393 additions and 137 deletions

View File

@ -49,6 +49,9 @@ namespace LibRyujinx
[DllImport("libryujinxjni")] [DllImport("libryujinxjni")]
internal extern static void onFrameEnd(double time); internal extern static void onFrameEnd(double time);
[DllImport("libryujinxjni")]
internal extern static void setProgressInfo(IntPtr info, float progress);
[DllImport("libryujinxjni")] [DllImport("libryujinxjni")]
internal extern static void setCurrentTransform(long native_window, int transform); internal extern static void setCurrentTransform(long native_window, int transform);

View File

@ -1,10 +1,13 @@
using ARMeilleure.Translation; using ARMeilleure.Translation;
using LibHac.Bcat;
using LibRyujinx.Shared; using LibRyujinx.Shared;
using OpenTK.Graphics.OpenGL; using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Cpu;
using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.Gpu.Shader;
using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan; using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
@ -153,6 +156,11 @@ namespace LibRyujinx
device.Gpu.Renderer.Initialize(GraphicsDebugLevel.None); device.Gpu.Renderer.Initialize(GraphicsDebugLevel.None);
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
device.Gpu.ShaderCacheStateChanged += LoadProgressStateChangedHandler;
device.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += LoadProgressStateChangedHandler;
try
{
device.Gpu.Renderer.RunLoop(() => device.Gpu.Renderer.RunLoop(() =>
{ {
_gpuDoneEvent.Reset(); _gpuDoneEvent.Reset();
@ -212,6 +220,62 @@ namespace LibRyujinx
_gpuDoneEvent.Set(); _gpuDoneEvent.Set();
}); });
} }
finally
{
device.Gpu.ShaderCacheStateChanged -= LoadProgressStateChangedHandler;
device.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= LoadProgressStateChangedHandler;
}
}
private static void LoadProgressStateChangedHandler<T>(T state, int current, int total) where T : Enum
{
void SetInfo(string status, float value)
{
var ptr = Marshal.StringToHGlobalAnsi(status);
setProgressInfo(ptr, value);
Marshal.FreeHGlobal(ptr);
}
var status = $"{current} / {total}";
var progress = current / (float)total;
switch (state)
{
case LoadState ptcState:
if (float.IsNaN((progress)))
progress = 0;
switch (ptcState)
{
case LoadState.Unloaded:
case LoadState.Loading:
SetInfo($"Loading PTC {status}", progress);
break;
case LoadState.Loaded:
SetInfo($"PTC Loaded", -1);
break;
}
break;
case ShaderCacheState shaderCacheState:
switch (shaderCacheState)
{
case ShaderCacheState.Start:
case ShaderCacheState.Loading:
SetInfo($"Compiling Shaders {status}", progress);
break;
case ShaderCacheState.Packaging:
SetInfo($"Packaging Shaders {status}", progress);
break;
case ShaderCacheState.Loaded:
SetInfo($"Shaders Loaded", -1);
break;
}
break;
default:
throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}");
}
}
[UnmanagedCallersOnly(EntryPoint = "graphics_renderer_set_swap_buffer_callback")] [UnmanagedCallersOnly(EntryPoint = "graphics_renderer_set_swap_buffer_callback")]
public static void SetSwapBuffersCallbackNative(IntPtr swapBuffersCallback) public static void SetSwapBuffersCallbackNative(IntPtr swapBuffersCallback)

View File

@ -26,6 +26,9 @@ JNIEnv* _rendererEnv = nullptr;
std::chrono::time_point<std::chrono::steady_clock, std::chrono::nanoseconds> _currentTimePoint; std::chrono::time_point<std::chrono::steady_clock, std::chrono::nanoseconds> _currentTimePoint;
std::string progressInfo = "";
float progress = -1;
JNIEnv* getEnv(bool isRenderer){ JNIEnv* getEnv(bool isRenderer){
JNIEnv* env; JNIEnv* env;
if(isRenderer){ if(isRenderer){
@ -130,6 +133,14 @@ jstring createString(
return str; return str;
} }
jstring createStringFromStdString(
JNIEnv *env,
std::string s) {
auto str = env->NewStringUTF(s.c_str());
return str;
}
} }
extern "C" extern "C"
@ -168,6 +179,11 @@ void onFrameEnd(double time) {
env->CallStaticVoidMethod(cl, _updateFrameTime, env->CallStaticVoidMethod(cl, _updateFrameTime,
nano); nano);
} }
extern "C"
void setProgressInfo(char* info, float progressValue) {
progressInfo = std::string (info);
progress = progressValue;
}
extern "C" extern "C"
void setCurrentTransform(long native_window, int transform){ void setCurrentTransform(long native_window, int transform){
@ -283,3 +299,15 @@ Java_org_ryujinx_android_NativeHelpers_setSwapInterval(JNIEnv *env, jobject thiz
return nativeWindow->setSwapInterval(nativeWindow, swap_interval); return nativeWindow->setSwapInterval(nativeWindow, swap_interval);
} }
extern "C"
JNIEXPORT jfloat JNICALL
Java_org_ryujinx_android_NativeHelpers_getProgressValue(JNIEnv *env, jobject thiz) {
return progress;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_org_ryujinx_android_NativeHelpers_getProgressInfo(JNIEnv *env, jobject thiz) {
return createStringFromStdString(env, progressInfo);
}

View File

@ -21,9 +21,11 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -50,7 +52,9 @@ import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
class GameActivity : ComponentActivity() { class GameActivity : ComponentActivity() {
private var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this) private var physicalControllerManager: PhysicalControllerManager =
PhysicalControllerManager(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -116,9 +120,11 @@ class GameActivity : ComponentActivity() {
} }
if (enable) if (enable)
display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) }?.let { window.attributes.preferredDisplayModeId = it.modeId } display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) }
?.let { window.attributes.preferredDisplayModeId = it.modeId }
else else
display?.supportedModes?.maxByOrNull { it.refreshRate }?.let { window.attributes.preferredDisplayModeId = it.modeId } display?.supportedModes?.maxByOrNull { it.refreshRate }
?.let { window.attributes.preferredDisplayModeId = it.modeId }
} }
private fun setFullScreen(fullscreen: Boolean) { private fun setFullScreen(fullscreen: Boolean) {
@ -139,6 +145,7 @@ class GameActivity : ComponentActivity() {
} }
} }
} }
@Composable @Composable
fun GameView(mainViewModel: MainViewModel) { fun GameView(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@ -170,6 +177,20 @@ class GameActivity : ComponentActivity() {
mutableStateOf(false) mutableStateOf(false)
} }
val showLoading = remember {
mutableStateOf(true)
}
val progressValue = remember {
mutableStateOf(0.0f)
}
val progress = remember {
mutableStateOf("Loading")
}
mainViewModel.setProgressStates(showLoading, progressValue, progress)
// touch surface // touch surface
Surface(color = Color.Transparent, modifier = Modifier Surface(color = Color.Transparent, modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -213,7 +234,9 @@ class GameActivity : ComponentActivity() {
} }
}) { }) {
} }
if (!showLoading.value) {
GameController.Compose(mainViewModel) GameController.Compose(mainViewModel)
Row( Row(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
@ -230,9 +253,13 @@ class GameActivity : ComponentActivity() {
} }
if (showMore.value) { if (showMore.value) {
Popup(alignment = Alignment.BottomCenter, onDismissRequest = {showMore.value = false}) { Popup(
Surface(modifier = Modifier.padding(16.dp), alignment = Alignment.BottomCenter,
shape = MaterialTheme.shapes.medium) { onDismissRequest = { showMore.value = false }) {
Surface(
modifier = Modifier.padding(16.dp),
shape = MaterialTheme.shapes.medium
) {
Row(modifier = Modifier.padding(8.dp)) { Row(modifier = Modifier.padding(8.dp)) {
IconButton(modifier = Modifier.padding(4.dp), onClick = { IconButton(modifier = Modifier.padding(4.dp), onClick = {
showMore.value = false showMore.value = false
@ -259,6 +286,7 @@ class GameActivity : ComponentActivity() {
} }
} }
} }
}
val showBackNotice = remember { val showBackNotice = remember {
mutableStateOf(false) mutableStateOf(false)
@ -268,6 +296,39 @@ class GameActivity : ComponentActivity() {
showBackNotice.value = true showBackNotice.value = true
} }
if (showLoading.value) {
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(0.5f)
.align(Alignment.Center),
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(text = progress.value)
if (progressValue.value > -1)
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
progress = progressValue.value
)
else
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
)
}
}
}
if (showBackNotice.value) { if (showBackNotice.value) {
AlertDialog(onDismissRequest = { showBackNotice.value = false }) { AlertDialog(onDismissRequest = { showBackNotice.value = false }) {
Column { Column {

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.os.Build import android.os.Build
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import androidx.compose.runtime.MutableState
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings import org.ryujinx.android.viewmodels.QuickSettings
@ -12,6 +13,10 @@ import kotlin.concurrent.thread
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback { class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
private var isProgressHidden: Boolean = false
private var progress: MutableState<String>? = null
private var progressValue: MutableState<Float>? = null
private var showLoading: MutableState<Boolean>? = null
private var game: GameModel? = null private var game: GameModel? = null
private var _isClosed: Boolean = false private var _isClosed: Boolean = false
private var _renderingThreadWatcher: Thread? = null private var _renderingThreadWatcher: Thread? = null
@ -29,6 +34,8 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
holder.addCallback(this) holder.addCallback(this)
nativeWindow = NativeWindow(this) nativeWindow = NativeWindow(this)
mainViewModel.gameHost = this
} }
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
@ -75,7 +82,6 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
} }
private fun start(surfaceHolder: SurfaceHolder) { private fun start(surfaceHolder: SurfaceHolder) {
mainViewModel.gameHost = this
if(_isStarted) if(_isStarted)
return return
@ -106,11 +112,31 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
_updateThread = thread(start = true) { _updateThread = thread(start = true) {
var c = 0 var c = 0
val helper = NativeHelpers()
while (_isStarted) { while (_isStarted) {
_nativeRyujinx.inputUpdate() _nativeRyujinx.inputUpdate()
Thread.sleep(1) Thread.sleep(1)
showLoading?.apply {
if(value){
var value = helper.getProgressValue()
if(value != -1f)
progress?.apply {
this.value = helper.getProgressInfo()
}
progressValue?.apply {
this.value = value
}
}
}
c++ c++
if (c >= 1000) { if (c >= 1000) {
if(helper.getProgressValue() == -1f)
progress?.apply {
this.value = "Loading ${game!!.titleName}"
}
c = 0 c = 0
mainViewModel.updateStats(_nativeRyujinx.deviceGetGameFifo(), _nativeRyujinx.deviceGetGameFrameRate(), _nativeRyujinx.deviceGetGameFrameTime()) mainViewModel.updateStats(_nativeRyujinx.deviceGetGameFifo(), _nativeRyujinx.deviceGetGameFrameRate(), _nativeRyujinx.deviceGetGameFrameTime())
} }
@ -143,4 +169,26 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
game?.close() game?.close()
} }
fun setProgressStates(
showLoading: MutableState<Boolean>?,
progressValue: MutableState<Float>?,
progress: MutableState<String>?
) {
this.showLoading = showLoading
this.progressValue = progressValue
this.progress = progress
showLoading?.apply {
showLoading.value = !isProgressHidden
}
}
fun hideProgressIndicator() {
isProgressHidden = true
showLoading?.apply {
if (value == isProgressHidden)
value = !isProgressHidden
}
}
} }

View File

@ -34,6 +34,8 @@ class MainActivity : ComponentActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
mainViewModel?.performanceManager?.updateRenderingSessionTime(gameTime) mainViewModel?.performanceManager?.updateRenderingSessionTime(gameTime)
} }
mainViewModel?.gameHost?.hideProgressIndicator()
} }
} }

View File

@ -26,4 +26,6 @@ class NativeHelpers {
external fun getMaxSwapInterval(nativeWindow: Long): Int external fun getMaxSwapInterval(nativeWindow: Long): Int
external fun getMinSwapInterval(nativeWindow: Long): Int external fun getMinSwapInterval(nativeWindow: Long): Int
external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int external fun setSwapInterval(nativeWindow: Long, swapInterval: Int): Int
external fun getProgressInfo() : String
external fun getProgressValue() : Float
} }

View File

@ -8,7 +8,7 @@ import org.ryujinx.android.RyujinxNative
class GameModel(var file: DocumentFile, val context: Context) { class GameModel(var file: DocumentFile, val context: Context) {
private var descriptor: ParcelFileDescriptor? = null var descriptor: ParcelFileDescriptor? = null
var fileName: String? var fileName: String?
var fileSize = 0.0 var fileSize = 0.0
var titleName: String? = null var titleName: String? = null

View File

@ -11,11 +11,13 @@ import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.getAbsolutePath import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.search import com.anggrayudi.storage.file.search
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import kotlin.concurrent.thread
class HomeViewModel( class HomeViewModel(
val activity: MainActivity? = null, val activity: MainActivity? = null,
val mainViewModel: MainViewModel? = null val mainViewModel: MainViewModel? = null
) { ) {
private var isLoading: Boolean = false
private var gameList: SnapshotStateList<GameModel>? = null private var gameList: SnapshotStateList<GameModel>? = null
private var loadedCache: List<GameModel> = listOf() private var loadedCache: List<GameModel> = listOf()
private var gameFolderPath: DocumentFile? = null private var gameFolderPath: DocumentFile? = null
@ -68,10 +70,17 @@ class HomeViewModel(
fun reloadGameList() { fun reloadGameList() {
var storage = activity?.storageHelper ?: return var storage = activity?.storageHelper ?: return
if(isLoading)
return
val folder = gameFolderPath ?: return val folder = gameFolderPath ?: return
isLoading = true
val files = mutableListOf<GameModel>() val files = mutableListOf<GameModel>()
thread {
try {
for (file in folder.search(false, DocumentFileType.FILE)) { for (file in folder.search(false, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp") if (file.extension == "xci" || file.extension == "nsp")
activity.let { activity.let {
@ -81,10 +90,19 @@ class HomeViewModel(
loadedCache = files.toList() loadedCache = files.toList()
isLoading = false
applyFilter() applyFilter()
} }
finally {
isLoading = false
}
}
}
private fun applyFilter() { private fun applyFilter() {
if(isLoading)
return
gameList?.clear() gameList?.clear()
gameList?.addAll(loadedCache) gameList?.addAll(loadedCache)
} }

View File

@ -7,6 +7,9 @@ import android.os.Build
import android.os.PerformanceHintManager import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.anggrayudi.storage.extension.launchOnUiThread
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import org.ryujinx.android.GameActivity import org.ryujinx.android.GameActivity
import org.ryujinx.android.GameController import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
@ -25,13 +28,20 @@ import java.io.File
class MainViewModel(val activity: MainActivity) { class MainViewModel(val activity: MainActivity) {
var physicalControllerManager: PhysicalControllerManager? = null var physicalControllerManager: PhysicalControllerManager? = null
var gameModel: GameModel? = null var gameModel: GameModel? = null
var gameHost: GameHost? = null
var controller: GameController? = null var controller: GameController? = null
var performanceManager: PerformanceManager? = null var performanceManager: PerformanceManager? = null
var selected: GameModel? = null var selected: GameModel? = null
private var gameTimeState: MutableState<Double>? = null private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null private var fifoState: MutableState<Double>? = null
private var progress: MutableState<String>? = null
private var progressValue: MutableState<Float>? = null
private var showLoading: MutableState<Boolean>? = null
var gameHost: GameHost? = null
set(value) {
field = value
field?.setProgressStates(showLoading, progressValue, progress)
}
var navController : NavHostController? = null var navController : NavHostController? = null
var homeViewModel: HomeViewModel = HomeViewModel(activity, this) var homeViewModel: HomeViewModel = HomeViewModel(activity, this)
@ -123,6 +133,11 @@ class MainViewModel(val activity: MainActivity) {
if (!success) if (!success)
return false return false
val semaphore = Semaphore(1, 0)
runBlocking {
semaphore.acquire()
launchOnUiThread {
// We are only able to initialize the emulation context on the main thread
success = nativeRyujinx.deviceInitialize( success = nativeRyujinx.deviceInitialize(
settings.isHostMapped, settings.isHostMapped,
settings.useNce, settings.useNce,
@ -135,6 +150,13 @@ class MainViewModel(val activity: MainActivity) {
"UTC", "UTC",
settings.ignoreMissingServices settings.ignoreMissingServices
) )
semaphore.release()
}
semaphore.acquire()
semaphore.release()
}
if (!success) if (!success)
return false return false
@ -180,4 +202,15 @@ class MainViewModel(val activity: MainActivity) {
val intent = Intent(activity, GameActivity::class.java) val intent = Intent(activity, GameActivity::class.java)
activity.startActivity(intent) activity.startActivity(intent)
} }
fun setProgressStates(
showLoading: MutableState<Boolean>,
progressValue: MutableState<Float>,
progress: MutableState<String>
) {
this.showLoading = showLoading
this.progressValue = progressValue
this.progress = progress
gameHost?.setProgressStates(showLoading, progressValue, progress)
}
} }

View File

@ -61,14 +61,13 @@ import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.anggrayudi.storage.extension.launchOnUiThread import com.anggrayudi.storage.extension.launchOnUiThread
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.R import org.ryujinx.android.R
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel import org.ryujinx.android.viewmodels.HomeViewModel
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread
import kotlin.math.roundToInt import kotlin.math.roundToInt
class HomeViews { class HomeViews {
@ -380,8 +379,7 @@ class HomeViews {
.combinedClickable( .combinedClickable(
onClick = { onClick = {
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
runBlocking { thread {
launch {
showLoading.value = true showLoading.value = true
val success = val success =
viewModel.mainViewModel?.loadGame(gameModel) ?: false viewModel.mainViewModel?.loadGame(gameModel) ?: false
@ -395,7 +393,6 @@ class HomeViews {
showLoading.value = false showLoading.value = false
} }
} }
}
}, },
onLongClick = { onLongClick = {
viewModel.mainViewModel?.selected = gameModel viewModel.mainViewModel?.selected = gameModel