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")]
internal extern static void onFrameEnd(double time);
[DllImport("libryujinxjni")]
internal extern static void setProgressInfo(IntPtr info, float progress);
[DllImport("libryujinxjni")]
internal extern static void setCurrentTransform(long native_window, int transform);

View File

@ -1,10 +1,13 @@
using ARMeilleure.Translation;
using LibHac.Bcat;
using LibRyujinx.Shared;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using Ryujinx.Cpu;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.Gpu.Shader;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan;
using Silk.NET.Vulkan;
@ -153,6 +156,11 @@ namespace LibRyujinx
device.Gpu.Renderer.Initialize(GraphicsDebugLevel.None);
_gpuCancellationTokenSource = new CancellationTokenSource();
device.Gpu.ShaderCacheStateChanged += LoadProgressStateChangedHandler;
device.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += LoadProgressStateChangedHandler;
try
{
device.Gpu.Renderer.RunLoop(() =>
{
_gpuDoneEvent.Reset();
@ -212,6 +220,62 @@ namespace LibRyujinx
_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")]
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::string progressInfo = "";
float progress = -1;
JNIEnv* getEnv(bool isRenderer){
JNIEnv* env;
if(isRenderer){
@ -130,6 +133,14 @@ jstring createString(
return str;
}
jstring createStringFromStdString(
JNIEnv *env,
std::string s) {
auto str = env->NewStringUTF(s.c_str());
return str;
}
}
extern "C"
@ -168,6 +179,11 @@ void onFrameEnd(double time) {
env->CallStaticVoidMethod(cl, _updateFrameTime,
nano);
}
extern "C"
void setProgressInfo(char* info, float progressValue) {
progressInfo = std::string (info);
progress = progressValue;
}
extern "C"
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);
}
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.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -50,7 +52,9 @@ import kotlin.math.abs
import kotlin.math.roundToInt
class GameActivity : ComponentActivity() {
private var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
private var physicalControllerManager: PhysicalControllerManager =
PhysicalControllerManager(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -116,9 +120,11 @@ class GameActivity : ComponentActivity() {
}
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
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) {
@ -139,6 +145,7 @@ class GameActivity : ComponentActivity() {
}
}
}
@Composable
fun GameView(mainViewModel: MainViewModel) {
Box(modifier = Modifier.fillMaxSize()) {
@ -170,6 +177,20 @@ class GameActivity : ComponentActivity() {
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
Surface(color = Color.Transparent, modifier = Modifier
.fillMaxSize()
@ -213,7 +234,9 @@ class GameActivity : ComponentActivity() {
}
}) {
}
if (!showLoading.value) {
GameController.Compose(mainViewModel)
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
@ -230,9 +253,13 @@ class GameActivity : ComponentActivity() {
}
if (showMore.value) {
Popup(alignment = Alignment.BottomCenter, onDismissRequest = {showMore.value = false}) {
Surface(modifier = Modifier.padding(16.dp),
shape = MaterialTheme.shapes.medium) {
Popup(
alignment = Alignment.BottomCenter,
onDismissRequest = { showMore.value = false }) {
Surface(
modifier = Modifier.padding(16.dp),
shape = MaterialTheme.shapes.medium
) {
Row(modifier = Modifier.padding(8.dp)) {
IconButton(modifier = Modifier.padding(4.dp), onClick = {
showMore.value = false
@ -259,6 +286,7 @@ class GameActivity : ComponentActivity() {
}
}
}
}
val showBackNotice = remember {
mutableStateOf(false)
@ -268,6 +296,39 @@ class GameActivity : ComponentActivity() {
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) {
AlertDialog(onDismissRequest = { showBackNotice.value = false }) {
Column {

View File

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

View File

@ -26,4 +26,6 @@ class NativeHelpers {
external fun getMaxSwapInterval(nativeWindow: Long): Int
external fun getMinSwapInterval(nativeWindow: Long): 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) {
private var descriptor: ParcelFileDescriptor? = null
var descriptor: ParcelFileDescriptor? = null
var fileName: String?
var fileSize = 0.0
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.search
import org.ryujinx.android.MainActivity
import kotlin.concurrent.thread
class HomeViewModel(
val activity: MainActivity? = null,
val mainViewModel: MainViewModel? = null
) {
private var isLoading: Boolean = false
private var gameList: SnapshotStateList<GameModel>? = null
private var loadedCache: List<GameModel> = listOf()
private var gameFolderPath: DocumentFile? = null
@ -68,10 +70,17 @@ class HomeViewModel(
fun reloadGameList() {
var storage = activity?.storageHelper ?: return
if(isLoading)
return
val folder = gameFolderPath ?: return
isLoading = true
val files = mutableListOf<GameModel>()
thread {
try {
for (file in folder.search(false, DocumentFileType.FILE)) {
if (file.extension == "xci" || file.extension == "nsp")
activity.let {
@ -81,10 +90,19 @@ class HomeViewModel(
loadedCache = files.toList()
isLoading = false
applyFilter()
}
finally {
isLoading = false
}
}
}
private fun applyFilter() {
if(isLoading)
return
gameList?.clear()
gameList?.addAll(loadedCache)
}

View File

@ -7,6 +7,9 @@ import android.os.Build
import android.os.PerformanceHintManager
import androidx.compose.runtime.MutableState
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.GameController
import org.ryujinx.android.GameHost
@ -25,13 +28,20 @@ import java.io.File
class MainViewModel(val activity: MainActivity) {
var physicalControllerManager: PhysicalControllerManager? = null
var gameModel: GameModel? = null
var gameHost: GameHost? = null
var controller: GameController? = null
var performanceManager: PerformanceManager? = null
var selected: GameModel? = null
private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: 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 homeViewModel: HomeViewModel = HomeViewModel(activity, this)
@ -123,6 +133,11 @@ class MainViewModel(val activity: MainActivity) {
if (!success)
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(
settings.isHostMapped,
settings.useNce,
@ -135,6 +150,13 @@ class MainViewModel(val activity: MainActivity) {
"UTC",
settings.ignoreMissingServices
)
semaphore.release()
}
semaphore.acquire()
semaphore.release()
}
if (!success)
return false
@ -180,4 +202,15 @@ class MainViewModel(val activity: MainActivity) {
val intent = Intent(activity, GameActivity::class.java)
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 coil.compose.AsyncImage
import com.anggrayudi.storage.extension.launchOnUiThread
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.ryujinx.android.MainActivity
import org.ryujinx.android.R
import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel
import java.io.File
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.math.roundToInt
class HomeViews {
@ -380,8 +379,7 @@ class HomeViews {
.combinedClickable(
onClick = {
if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
runBlocking {
launch {
thread {
showLoading.value = true
val success =
viewModel.mainViewModel?.loadGame(gameModel) ?: false
@ -395,7 +393,6 @@ class HomeViews {
showLoading.value = false
}
}
}
},
onLongClick = {
viewModel.mainViewModel?.selected = gameModel