diff --git a/dist/languages/da_DK.ts b/dist/languages/da_DK.ts index 861b59046..3c3125c1d 100644 --- a/dist/languages/da_DK.ts +++ b/dist/languages/da_DK.ts @@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Engelsk + + ConfigureWeb + + + Form + Form + + + + Discord Presence + Discord-presence + + + + Show Current Game in your Discord Status + Vis kørende spil som din Discord-status + + DirectConnect @@ -6861,4 +6879,4 @@ They may have left the room. Afventningstræ - + \ No newline at end of file diff --git a/dist/languages/de.ts b/dist/languages/de.ts index 64f526171..da1f1053d 100644 --- a/dist/languages/de.ts +++ b/dist/languages/de.ts @@ -3585,6 +3585,24 @@ Ziehe Punkte, um ihre Position zu verändern, oder doppelklicke auf Zellen in de Englisch + + ConfigureWeb + + + Form + Form + + + + Discord Presence + Discord Presence + + + + Show Current Game in your Discord Status + Aktuelles Spiel in Ihrem Discordstatus anzeigen + + DirectConnect @@ -6868,4 +6886,4 @@ They may have left the room. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/el.ts b/dist/languages/el.ts index 2731abe21..5aa88a814 100644 --- a/dist/languages/el.ts +++ b/dist/languages/el.ts @@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.Αγγλικά + + ConfigureWeb + + + Form + Φόρμα + + + + Discord Presence + Παρουσία Discord + + + + Show Current Game in your Discord Status + Εμφάνιση τρέχοντος παιχνιδιού στην κατάσταση Discord σας + + DirectConnect @@ -6865,4 +6883,4 @@ They may have left the room. Δένδρο αναμονής - + \ No newline at end of file diff --git a/dist/languages/es_ES.ts b/dist/languages/es_ES.ts index ce5e326d4..090e7a4cc 100644 --- a/dist/languages/es_ES.ts +++ b/dist/languages/es_ES.ts @@ -3585,6 +3585,24 @@ Mueve los puntos para cambiar la posición, o haz doble click en las celdas de l Inglés (English) + + ConfigureWeb + + + Form + Formulario + + + + Discord Presence + Presencia en Discord + + + + Show Current Game in your Discord Status + Mostrar Juego Actual en el Estado de Discord + + DirectConnect @@ -6878,4 +6896,4 @@ Puede que haya dejado la sala. Árbol de Espera - + \ No newline at end of file diff --git a/dist/languages/fi.ts b/dist/languages/fi.ts index b01398ff4..52f9b2b6e 100644 --- a/dist/languages/fi.ts +++ b/dist/languages/fi.ts @@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Englanti + + ConfigureWeb + + + Form + Muot + + + + Discord Presence + Discord läsnäolo + + + + Show Current Game in your Discord Status + Näytä nykyinen peli Discord tilassa + + DirectConnect @@ -6845,4 +6863,4 @@ They may have left the room. - + \ No newline at end of file diff --git a/dist/languages/fr.ts b/dist/languages/fr.ts index 656611455..8dfbf7261 100644 --- a/dist/languages/fr.ts +++ b/dist/languages/fr.ts @@ -3585,6 +3585,24 @@ Glissez les points pour modifier la position, ou double-cliquez les cellules pou Anglais + + ConfigureWeb + + + Form + Forme + + + + Discord Presence + Présence sur Discord + + + + Show Current Game in your Discord Status + Afficher votre jeu en cours dans votre statut Discord + + DirectConnect @@ -6873,4 +6891,4 @@ Il a peut-être quitté la salon. Arbre d'instructions - + \ No newline at end of file diff --git a/dist/languages/hu_HU.ts b/dist/languages/hu_HU.ts index 6abf3b4df..2d5545538 100644 --- a/dist/languages/hu_HU.ts +++ b/dist/languages/hu_HU.ts @@ -3582,6 +3582,72 @@ Drag points to change position, or double-click table cells to edit values.Angol + + ConfigureWeb + + + Form + Forma + + + + Discord Presence + Discord jelenlét + + + + Show Current Game in your Discord Status + Jelenlegi játék megjelenítése a Discord állapotodban + + + + DirectConnect + + + Direct Connect + Közvetlen Kapcsolódás + + + + Server Address + + + + + <html><head/><body><p>Server address of the host</p></body></html> + + + + + Port + Port + + + + <html><head/><body><p>Port number the host is listening on</p></body></html> + <html><head/><body><p>Annak a portnak a száma, amire a gazda figyel</p></body></html> + + + + 24872 + 24872 + + + + Nickname + Becenév + + + + Password + Jelszó + + + + Connect + Kapcsolás + + DirectConnectWindow @@ -6807,4 +6873,4 @@ They may have left the room. Várakozási Fa - + \ No newline at end of file diff --git a/dist/languages/id.ts b/dist/languages/id.ts index 4bb737e4f..a6c6cff68 100644 --- a/dist/languages/id.ts +++ b/dist/languages/id.ts @@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.Inggris + + ConfigureWeb + + + Form + Formulir + + + + Discord Presence + Status Discord + + + + Show Current Game in your Discord Status + Tampilkan Game Saat Ini ke Status Discord Anda + + DirectConnect @@ -6861,4 +6879,4 @@ They may have left the room. Tunggu Tree - + \ No newline at end of file diff --git a/dist/languages/it.ts b/dist/languages/it.ts index d4bd4ccb8..bde755430 100644 --- a/dist/languages/it.ts +++ b/dist/languages/it.ts @@ -3585,6 +3585,24 @@ Trascina i punti per cambiarne la posizione, o fai doppio clic sulla tabella per Inglese + + ConfigureWeb + + + Form + Modulo + + + + Discord Presence + Discord Presence + + + + Show Current Game in your Discord Status + Mostra il gioco attuale nel tuo stato di Discord + + DirectConnect @@ -6874,4 +6892,4 @@ Potrebbe aver lasciato la stanza. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/ja_JP.ts b/dist/languages/ja_JP.ts index 95ec541b4..3cc021d2a 100644 --- a/dist/languages/ja_JP.ts +++ b/dist/languages/ja_JP.ts @@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.英語 + + ConfigureWeb + + + Form + フォーム + + + + Discord Presence + Discord Presence + + + + Show Current Game in your Discord Status + プレイ中のゲームをDiscordに表示 + + DirectConnect @@ -6866,4 +6884,4 @@ They may have left the room. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/ko_KR.ts b/dist/languages/ko_KR.ts index fba684f70..351783d05 100644 --- a/dist/languages/ko_KR.ts +++ b/dist/languages/ko_KR.ts @@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.English + + ConfigureWeb + + + Form + 종류 + + + + Discord Presence + 디스코드 있음 + + + + Show Current Game in your Discord Status + 사용자의 디스코드 상태에 현재 게임 표시하기 + + DirectConnect @@ -6868,4 +6886,4 @@ They may have left the room. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/lt_LT.ts b/dist/languages/lt_LT.ts index 176e6cc01..1c7b45213 100644 --- a/dist/languages/lt_LT.ts +++ b/dist/languages/lt_LT.ts @@ -3581,6 +3581,24 @@ Drag points to change position, or double-click table cells to edit values.Anglų k. + + ConfigureWeb + + + Form + Forma + + + + Discord Presence + Discord nustatymai + + + + Show Current Game in your Discord Status + Rodyti jūsų žaidžiamą žaidimą Discord'e + + DirectConnect @@ -6853,4 +6871,4 @@ They may have left the room. Laukimo gijų medis - + \ No newline at end of file diff --git a/dist/languages/nb.ts b/dist/languages/nb.ts index a3c44b557..30ee9deac 100644 --- a/dist/languages/nb.ts +++ b/dist/languages/nb.ts @@ -3584,6 +3584,24 @@ Dra punkter for å endre posisjon, eller dobbeltklikk på tabellceller for å re Engelsk + + ConfigureWeb + + + Form + Form + + + + Discord Presence + Discord tilstedeværelse + + + + Show Current Game in your Discord Status + Vis Gjeldende Spill i Discord Statusen din. + + DirectConnect @@ -6865,4 +6883,4 @@ They may have left the room. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/nl.ts b/dist/languages/nl.ts index a5b218d06..9bedcf20a 100644 --- a/dist/languages/nl.ts +++ b/dist/languages/nl.ts @@ -3585,6 +3585,24 @@ Sleep punten om de positie te wijzigen of dubbelklik op tabelcellen om waarden t Engels + + ConfigureWeb + + + Form + Formulier + + + + Discord Presence + Discord Presence + + + + Show Current Game in your Discord Status + Toon Huidige Spel in je Discord Status + + DirectConnect @@ -6876,4 +6894,4 @@ Misschien hebben ze de kamer verlaten. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/pl_PL.ts b/dist/languages/pl_PL.ts index 64be5be5d..ad25cfd1f 100644 --- a/dist/languages/pl_PL.ts +++ b/dist/languages/pl_PL.ts @@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Angielski (English) + + ConfigureWeb + + + Form + Formularz + + + + Discord Presence + Widoczność na Discordzie + + + + Show Current Game in your Discord Status + Pokaż obecnie włączoną grę w statusie na Discrodzie + + DirectConnect @@ -6856,4 +6874,4 @@ They may have left the room. Kolejka Oczekiwania - + \ No newline at end of file diff --git a/dist/languages/pt_BR.ts b/dist/languages/pt_BR.ts index 8c7da57f5..de10a88a8 100644 --- a/dist/languages/pt_BR.ts +++ b/dist/languages/pt_BR.ts @@ -3585,6 +3585,24 @@ Arraste os pontos para alterar a posição ou clique duas vezes nas células da Inglês (English) + + ConfigureWeb + + + Form + Formulário + + + + Discord Presence + Presença no Discord + + + + Show Current Game in your Discord Status + Mostrar o jogo atual no seu perfil do Discord + + DirectConnect @@ -6869,4 +6887,4 @@ They may have left the room. Árvore de espera - + \ No newline at end of file diff --git a/dist/languages/ro_RO.ts b/dist/languages/ro_RO.ts index 141b171a6..ca8f8091e 100644 --- a/dist/languages/ro_RO.ts +++ b/dist/languages/ro_RO.ts @@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.Engleză + + ConfigureWeb + + + Form + Model + + + + Discord Presence + Prezență pe Discord + + + + Show Current Game in your Discord Status + Afișează Jocul Prezent pe Statusul Discord + + DirectConnect @@ -6863,4 +6881,4 @@ They may have left the room. Copac de Așteptare - + \ No newline at end of file diff --git a/dist/languages/ru_RU.ts b/dist/languages/ru_RU.ts index 992939f49..1e7304d26 100644 --- a/dist/languages/ru_RU.ts +++ b/dist/languages/ru_RU.ts @@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.Английский + + ConfigureWeb + + + Form + Форма + + + + Discord Presence + Интеграция с Discord + + + + Show Current Game in your Discord Status + Показывать текущую игру в статусе Discord + + DirectConnect @@ -6868,4 +6886,4 @@ They may have left the room. Дерево цепочки ожидания - + \ No newline at end of file diff --git a/dist/languages/tr_TR.ts b/dist/languages/tr_TR.ts index 025759bdf..1a7a2e17c 100644 --- a/dist/languages/tr_TR.ts +++ b/dist/languages/tr_TR.ts @@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.İngilizce + + ConfigureWeb + + + Form + Form + + + + Discord Presence + Discord Görünümü + + + + Show Current Game in your Discord Status + Şu Anki Oyunu Discord Durumunda Göster + + DirectConnect @@ -6865,4 +6883,4 @@ They may have left the room. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/vi_VN.ts b/dist/languages/vi_VN.ts index 617efbbe2..0b8866d55 100644 --- a/dist/languages/vi_VN.ts +++ b/dist/languages/vi_VN.ts @@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.Tiếng Anh + + ConfigureWeb + + + Form + Định dạng + + + + Discord Presence + + + + + Show Current Game in your Discord Status + Hiển thị game đang chơi trên trạng thái Discord + + DirectConnect @@ -6861,4 +6879,4 @@ They may have left the room. Wait Tree - + \ No newline at end of file diff --git a/dist/languages/zh_CN.ts b/dist/languages/zh_CN.ts index 402eafcb4..fbc6b80f8 100644 --- a/dist/languages/zh_CN.ts +++ b/dist/languages/zh_CN.ts @@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.英语 + + ConfigureWeb + + + Form + 格式 + + + + Discord Presence + Discord 状态 + + + + Show Current Game in your Discord Status + 在您的 Discord 状态中显示当前游戏 + + DirectConnect @@ -6873,4 +6891,4 @@ They may have left the room. 等待树 - + \ No newline at end of file diff --git a/dist/languages/zh_TW.ts b/dist/languages/zh_TW.ts index 55eb19ad9..220cdf95c 100644 --- a/dist/languages/zh_TW.ts +++ b/dist/languages/zh_TW.ts @@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.English + + ConfigureWeb + + + Form + Form + + + + Discord Presence + Discord 狀態 + + + + Show Current Game in your Discord Status + 在 Discord 狀態中顯示正在玩的遊戲 + + DirectConnect @@ -6865,4 +6883,4 @@ They may have left the room. 樹狀等待 - + \ No newline at end of file diff --git a/externals/dynarmic b/externals/dynarmic index 30f1a3c62..a41c38024 160000 --- a/externals/dynarmic +++ b/externals/dynarmic @@ -1 +1 @@ -Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c +Subproject commit a41c380246d3d9f9874f0f792d234dc0cc17c180 diff --git a/externals/fmt b/externals/fmt index 2dd4fa874..fcd3e1e19 160000 --- a/externals/fmt +++ b/externals/fmt @@ -1 +1 @@ -Subproject commit 2dd4fa8742fdac36468f8d8ea3e06e78215551f8 +Subproject commit fcd3e1e19c8d2df94bb6cb40d7f1c97a9872cf2b diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index b1289ec3d..9ffd5055d 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -29,7 +29,7 @@ android { namespace = "org.citra.citra_emu" compileSdkVersion = "android-34" - ndkVersion = "26.1.10909125" + ndkVersion = "26.3.11579264" compileOptions { sourceCompatibility = JavaVersion.VERSION_17 diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index bfbe658f8..135794d65 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -183,13 +183,13 @@ object NativeLibrary { private var coreErrorAlertResult = false private val coreErrorAlertLock = Object() - private fun onCoreErrorImpl(title: String, message: String) { + private fun onCoreErrorImpl(title: String, message: String, canContinue: Boolean) { val emulationActivity = sEmulationActivity.get() if (emulationActivity == null) { Log.error("[NativeLibrary] EmulationActivity not present") return } - val fragment = CoreErrorDialogFragment.newInstance(title, message) + val fragment = CoreErrorDialogFragment.newInstance(title, message, canContinue) fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG) } @@ -207,6 +207,7 @@ object NativeLibrary { } val title: String val message: String + val canContinue: Boolean when (error) { CoreError.ErrorSystemFiles -> { title = emulationActivity.getString(R.string.system_archive_not_found) @@ -214,16 +215,25 @@ object NativeLibrary { R.string.system_archive_not_found_message, details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } ) + canContinue = true } CoreError.ErrorSavestate -> { title = emulationActivity.getString(R.string.save_load_error) message = details + canContinue = true + } + + CoreError.ErrorArticDisconnected -> { + title = emulationActivity.getString(R.string.artic_base) + message = emulationActivity.getString(R.string.artic_server_comm_error) + canContinue = false } CoreError.ErrorUnknown -> { title = emulationActivity.getString(R.string.fatal_error) message = emulationActivity.getString(R.string.fatal_error_message) + canContinue = true } else -> { @@ -232,7 +242,7 @@ object NativeLibrary { } // Show the AlertDialog on the main thread. - emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) }) + emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message, canContinue) }) // Wait for the lock to notify that it is complete. synchronized(coreErrorAlertLock) { @@ -346,6 +356,11 @@ object NativeLibrary { return } + if (resultCode == EmulationErrorDialogFragment.ShutdownRequested) { + emulationActivity.finish() + return + } + emulationActivity.runOnUiThread { EmulationErrorDialogFragment.newInstance(resultCode).showNow( emulationActivity.supportFragmentManager, @@ -361,16 +376,23 @@ object NativeLibrary { emulationActivity = requireActivity() as EmulationActivity var captionId = R.string.loader_error_invalid_format - if (requireArguments().getInt(RESULT_CODE) == ErrorLoader_ErrorEncrypted) { + val result = requireArguments().getInt(RESULT_CODE) + if (result == ErrorLoader_ErrorEncrypted) { captionId = R.string.loader_error_encrypted } + if (result == ErrorArticDisconnected) { + captionId = R.string.artic_base + } val alert = MaterialAlertDialogBuilder(requireContext()) .setTitle(captionId) .setMessage( Html.fromHtml( - CitraApplication.appContext.resources.getString(R.string.redump_games), - Html.FROM_HTML_MODE_LEGACY + if (result == ErrorArticDisconnected) + CitraApplication.appContext.resources.getString(R.string.artic_server_comm_error) + else + CitraApplication.appContext.resources.getString(R.string.redump_games), + Html.FROM_HTML_MODE_LEGACY ) ) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> @@ -398,7 +420,10 @@ object NativeLibrary { const val ErrorLoader = 4 const val ErrorLoader_ErrorEncrypted = 5 const val ErrorLoader_ErrorInvalidFormat = 6 - const val ErrorSystemFiles = 7 + const val ErrorLoader_ErrorGBATitle = 7 + const val ErrorSystemFiles = 8 + const val ErrorSavestate = 9 + const val ErrorArticDisconnected = 10 const val ShutdownRequested = 11 const val ErrorUnknown = 12 @@ -502,12 +527,28 @@ object NativeLibrary { external fun removeAmiibo() - const val SAVESTATE_SLOT_COUNT = 10 + const val SAVESTATE_SLOT_COUNT = 11 + const val QUICKSAVE_SLOT = 0 external fun getSavestateInfo(): Array? external fun saveState(slot: Int) + fun loadStateIfAvailable(slot: Int): Boolean { + var available = false + getSavestateInfo()?.forEach { + if (it.slot == slot){ + available = true + return@forEach + } + } + if (available) { + loadState(slot) + return true + } + return false + } + external fun loadState(slot: Int) /** @@ -619,6 +660,7 @@ object NativeLibrary { enum class CoreError { ErrorSystemFiles, ErrorSavestate, + ErrorArticDisconnected, ErrorUnknown } @@ -633,23 +675,33 @@ object NativeLibrary { } class CoreErrorDialogFragment : DialogFragment() { + private var userChosen = false override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val title = requireArguments().getString(TITLE) val message = requireArguments().getString(MESSAGE) - return MaterialAlertDialogBuilder(requireContext()) + val canContinue = requireArguments().getBoolean(CAN_CONTINUE) + val dialog = MaterialAlertDialogBuilder(requireContext()) .setTitle(title) .setMessage(message) - .setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int -> + if (canContinue) { + dialog.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int -> coreErrorAlertResult = true + userChosen = true } - .setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> - coreErrorAlertResult = false - }.show() + } + dialog.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> + coreErrorAlertResult = false + userChosen = true + } + return dialog.show() } override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) - coreErrorAlertResult = true + val canContinue = requireArguments().getBoolean(CAN_CONTINUE) + if (!userChosen) { + coreErrorAlertResult = canContinue + } synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } } @@ -658,12 +710,14 @@ object NativeLibrary { const val TITLE = "title" const val MESSAGE = "message" + const val CAN_CONTINUE = "canContinue" - fun newInstance(title: String, message: String): CoreErrorDialogFragment { + fun newInstance(title: String, message: String, canContinue: Boolean): CoreErrorDialogFragment { val frag = CoreErrorDialogFragment() val args = Bundle() args.putString(TITLE, title) args.putString(MESSAGE, message) + args.putBoolean(CAN_CONTINUE, canContinue) frag.arguments = args return frag } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index a911b0c31..4681bd319 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -38,7 +38,6 @@ import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.utils.ControllerMappingHelper import org.citra.citra_emu.utils.FileBrowserHelper -import org.citra.citra_emu.utils.ForegroundService import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.EmulationMenuSettings import org.citra.citra_emu.utils.ThemeUtil @@ -47,7 +46,6 @@ import org.citra.citra_emu.viewmodel.EmulationViewModel class EmulationActivity : AppCompatActivity() { private val preferences: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - private var foregroundService: Intent? = null var isActivityRecreated = false private val settingsViewModel: SettingsViewModel by viewModels() @@ -66,7 +64,7 @@ class EmulationActivity : AppCompatActivity() { binding = ActivityEmulationBinding.inflate(layoutInflater) screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings) - hotkeyUtility = HotkeyUtility(screenAdjustmentUtil) + hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this) setContentView(binding.root) val navHostFragment = @@ -85,10 +83,6 @@ class EmulationActivity : AppCompatActivity() { windowManager.defaultDisplay.rotation ) - // Start a foreground service to prevent the app from getting killed in the background - foregroundService = Intent(this, ForegroundService::class.java) - startForegroundService(foregroundService) - EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() }) } @@ -112,7 +106,6 @@ class EmulationActivity : AppCompatActivity() { override fun onDestroy() { EmulationLifecycleUtil.clear() - stopForegroundService(this) super.onDestroy() } @@ -186,8 +179,7 @@ class EmulationActivity : AppCompatActivity() { return false } - val button = - preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode) + val button = preferences.getInt(InputBindingSetting.getInputButtonKey(event), event.scanCode) val action: Int = when (event.action) { KeyEvent.ACTION_DOWN -> { // On some devices, the back gesture / button press is not intercepted by androidx @@ -453,12 +445,4 @@ class EmulationActivity : AppCompatActivity() { OnFilePickerResult(result.toString()) } - - companion object { - fun stopForegroundService(activity: Activity) { - val startIntent = Intent(activity, ForegroundService::class.java) - startIntent.action = ForegroundService.ACTION_STOP - activity.startForegroundService(startIntent) - } - } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt index b19cb03da..db99abf67 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt @@ -8,5 +8,7 @@ enum class Hotkey(val button: Int) { SWAP_SCREEN(10001), CYCLE_LAYOUT(10002), CLOSE_GAME(10003), - PAUSE_OR_RESUME(10004); + PAUSE_OR_RESUME(10004), + QUICKSAVE(10005), + QUICKLOAD(10006); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt index 830b57b29..25f6a493b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt @@ -4,10 +4,14 @@ package org.citra.citra_emu.features.hotkeys +import android.content.Context +import android.widget.Toast +import org.citra.citra_emu.NativeLibrary +import org.citra.citra_emu.R import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.display.ScreenAdjustmentUtil -class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) { +class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil, private val context: Context) { val hotkeyButtons = Hotkey.entries.map { it.button } @@ -18,6 +22,23 @@ class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) { Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() + Hotkey.QUICKSAVE.button -> { + NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT) + Toast.makeText(context, + context.getString(R.string.quicksave_saving), + Toast.LENGTH_SHORT).show() + } + Hotkey.QUICKLOAD.button -> { + val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT) + val stringRes = if(wasLoaded) { + R.string.quickload_loading + } else { + R.string.quickload_not_found + } + Toast.makeText(context, + context.getString(stringRes), + Toast.LENGTH_SHORT).show() + } else -> {} } return true diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index b2c378397..f0726f665 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -40,7 +40,9 @@ enum class IntSetting( VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1), DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0), TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0), - USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1); + USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1), + DELAY_RENDER_THREAD_US("delay_game_render_thread_us", Settings.SECTION_RENDERER, 0), + USE_ARTIC_BASE_CONTROLLER("use_artic_base_controller", Settings.SECTION_CONTROLS, 0); override var int: Int = defaultValue @@ -68,7 +70,8 @@ enum class IntSetting( DEBUG_RENDERER, CPU_JIT, ASYNC_CUSTOM_LOADING, - AUDIO_INPUT_TYPE + AUDIO_INPUT_TYPE, + USE_ARTIC_BASE_CONTROLLER ) fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index d94489021..0e92138cb 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -136,6 +136,8 @@ class Settings { const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout" const val HOTKEY_CLOSE_GAME = "hotkey_close_game" const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game" + const val HOTKEY_QUICKSAVE = "hotkey_quickload" + const val HOTKEY_QUICKlOAD = "hotkey_quickpause" val buttonKeys = listOf( KEY_BUTTON_A, @@ -187,13 +189,17 @@ class Settings { HOTKEY_SCREEN_SWAP, HOTKEY_CYCLE_LAYOUT, HOTKEY_CLOSE_GAME, - HOTKEY_PAUSE_OR_RESUME + HOTKEY_PAUSE_OR_RESUME, + HOTKEY_QUICKSAVE, + HOTKEY_QUICKlOAD ) val hotkeyTitles = listOf( R.string.emulation_swap_screens, R.string.emulation_cycle_landscape_layouts, R.string.emulation_close_game, - R.string.emulation_toggle_pause + R.string.emulation_toggle_pause, + R.string.emulation_quicksave, + R.string.emulation_quickload, ) const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 55b454093..c3756abf7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -133,6 +133,8 @@ class InputBindingSetting( Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button + Settings.HOTKEY_QUICKSAVE -> Hotkey.QUICKSAVE.button + Settings.HOTKEY_QUICKlOAD -> Hotkey.QUICKLOAD.button else -> -1 } @@ -222,8 +224,10 @@ class InputBindingSetting( Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show() return } - writeButtonMapping(getInputButtonKey(keyEvent.keyCode)) - val uiString = "${keyEvent.device.name}: Button ${keyEvent.keyCode}" + + val code = translateEventToKeyId(keyEvent) + writeButtonMapping(getInputButtonKey(code)) + val uiString = "${keyEvent.device.name}: Button $code" value = uiString } @@ -283,9 +287,17 @@ class InputBindingSetting( /** * Helper function to get the settings key for an gamepad button. + * */ + @Deprecated("Use the new getInputButtonKey(keyEvent) method to handle unknown keys") fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}" + /** + * Helper function to get the settings key for an gamepad button. + * + */ + fun getInputButtonKey(event: KeyEvent): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${translateEventToKeyId(event)}" + /** * Helper function to get the settings key for an gamepad axis. */ @@ -301,5 +313,23 @@ class InputBindingSetting( */ fun getInputAxisOrientationKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestOrientation" + + + /** + * This function translates a keyEvent into an "keyid" + * This key id is either the keyCode from the event, or + * the raw scanCode. + * Only when the keyCode itself is 0, (so it is an unknown key) + * we fall back to the raw scan code. + * This handles keys like the media-keys on google statia-controllers + * that don't have a conventional "mapping" and report as "unknown" + */ + fun translateEventToKeyId(event: KeyEvent): Int { + return if (event.keyCode == 0) { + event.scanCode + } else { + event.keyCode + } + } } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 9f504e603..8f104f0b5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -626,6 +626,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) val button = getInputObject(key) add(InputBindingSetting(button, Settings.hotkeyTitles[i])) } + add(HeaderSetting(R.string.miscellaneous)) + add( + SwitchSetting( + IntSetting.USE_ARTIC_BASE_CONTROLLER, + R.string.use_artic_base_controller, + R.string.use_artic_base_controller_desc, + IntSetting.USE_ARTIC_BASE_CONTROLLER.key, + IntSetting.USE_ARTIC_BASE_CONTROLLER.defaultValue + ) + ) } } @@ -729,6 +739,18 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.TEXTURE_FILTER.defaultValue ) ) + add( + SliderSetting( + IntSetting.DELAY_RENDER_THREAD_US, + R.string.delay_render_thread, + R.string.delay_render_thread_description, + 0, + 16000, + " μs", + IntSetting.DELAY_RENDER_THREAD_US.key, + IntSetting.DELAY_RENDER_THREAD_US.defaultValue.toFloat() + ) + ) add(HeaderSetting(R.string.stereoscopy)) add( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index eeff4ff1b..7aa8ce7b7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -481,12 +481,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.setOnMenuItemClickListener { when (it.itemId) { R.id.menu_emulation_save_state -> { - showSaveStateSubmenu() + showStateSubmenu(true) true } R.id.menu_emulation_load_state -> { - showLoadStateSubmenu() + showStateSubmenu(false) true } @@ -497,7 +497,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.show() } - private fun showSaveStateSubmenu() { + private fun showStateSubmenu(isSaving: Boolean) { + val savestates = NativeLibrary.getSavestateInfo() val popupMenu = PopupMenu( @@ -507,19 +508,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram popupMenu.menu.apply { for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) { - val slot = i + 1 - val text = getString(R.string.emulation_empty_state_slot, slot) - add(text).setEnabled(true).setOnMenuItemClickListener { - displaySavestateWarning() - NativeLibrary.saveState(slot) + val slot = i + var enableClick = isSaving + val text = if (slot == NativeLibrary.QUICKSAVE_SLOT) { + enableClick = false + getString(R.string.emulation_quicksave_slot) + } else { + getString(R.string.emulation_empty_state_slot, slot) + } + + add(text).setEnabled(enableClick).setOnMenuItemClickListener { + if(isSaving) { + NativeLibrary.saveState(slot) + } else { + NativeLibrary.loadState(slot) + binding.drawerLayout.close() + Toast.makeText(context, + getString(R.string.quickload_loading), + Toast.LENGTH_SHORT).show() + } true } } } savestates?.forEach { - val text = getString(R.string.emulation_occupied_state_slot, it.slot, it.time) - popupMenu.menu.getItem(it.slot - 1).setTitle(text) + var enableClick = true + val text = if(it.slot == NativeLibrary.QUICKSAVE_SLOT) { + // do not allow saving in quicksave slot + enableClick = !isSaving + getString(R.string.emulation_occupied_quicksave_slot, it.time) + } else{ + getString(R.string.emulation_occupied_state_slot, it.slot, it.time) + } + popupMenu.menu.getItem(it.slot).setTitle(text).setEnabled(enableClick) } popupMenu.show() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt index b19fdd7b9..fd026a32b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt @@ -16,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -23,14 +24,19 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialSharedAxis import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.HomeNavigationDirections import org.citra.citra_emu.R import org.citra.citra_emu.adapters.HomeSettingAdapter +import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile +import org.citra.citra_emu.model.Game import org.citra.citra_emu.model.HomeSetting import org.citra.citra_emu.ui.main.MainActivity import org.citra.citra_emu.utils.GameHelper @@ -76,6 +82,44 @@ class HomeSettingsFragment : Fragment() { R.drawable.ic_settings, { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } ), + HomeSetting( + R.string.artic_base_connect, + R.string.artic_base_connect_description, + R.drawable.ic_network, + { + val inflater = LayoutInflater.from(context) + val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater) + var textInputValue: String = preferences.getString("last_artic_base_addr", "")!! + + inputBinding.editTextInput.setText(textInputValue) + inputBinding.editTextInput.doOnTextChanged { text, _, _, _ -> + textInputValue = text.toString() + } + + val dialog = context?.let { + MaterialAlertDialogBuilder(it) + .setView(inputBinding.root) + .setTitle(getString(R.string.artic_base_enter_address)) + .setPositiveButton(android.R.string.ok) { _, _ -> + if (textInputValue.isNotEmpty()) { + preferences.edit() + .putString("last_artic_base_addr", textInputValue) + .apply() + val menu = Game( + title = getString(R.string.artic_base), + path = "articbase://$textInputValue", + filename = "" + ) + val action = + HomeNavigationDirections.actionGlobalEmulationActivity(menu) + binding.root.findNavController().navigate(action) + } + } + .setNegativeButton(android.R.string.cancel) {_, _ -> } + .show() + } + } + ), HomeSetting( R.string.system_files, R.string.system_files_description, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt index c2aa87de4..3e9b48f7c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt @@ -156,9 +156,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } } - // Dismiss previous notifications (should not happen unless a crash occurred) - EmulationActivity.stopForegroundService(this) - setInsets() } @@ -170,7 +167,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } override fun onDestroy() { - EmulationActivity.stopForegroundService(this) super.onDestroy() } diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 4e862f3a0..e565c8ec8 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -128,6 +128,8 @@ void Config::ReadValues() { static_cast(sdl2_config->GetInteger("Controls", "udp_input_port", InputCommon::CemuhookUDP::DEFAULT_PORT)); + ReadSetting("Controls", Settings::values.use_artic_base_controller); + // Core ReadSetting("Core", Settings::values.use_cpu_jit); ReadSetting("Core", Settings::values.cpu_clock_percentage); @@ -169,6 +171,7 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.bg_red); ReadSetting("Renderer", Settings::values.bg_green); ReadSetting("Renderer", Settings::values.bg_blue); + ReadSetting("Renderer", Settings::values.delay_game_render_thread_us); // Layout Settings::values.layout_option = static_cast(sdl2_config->GetInteger( diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index c46395fea..acf42a73f 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -86,6 +86,9 @@ udp_input_port= # The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0) udp_pad_index= +# Use Artic Controller when connected to Artic Base Server. (Default 0) +use_artic_base_controller= + [Core] # Whether to use the Just-In-Time (JIT) compiler for CPU emulation # 0: Interpreter (slow), 1 (default): JIT (fast) @@ -175,6 +178,10 @@ anaglyph_shader_name = # 0: Nearest, 1 (default): Linear filter_mode = +# Delays the game render thread by the specified amount of microseconds +# Set to 0 for no delay, only useful in dynamic-fps games to simulate GPU delay. +delay_game_render_thread_us = + [Layout] # Layout for the screen inside the render window. # 0 (default): Default Top Bottom Screen, 1: Single Screen Only, 2: Large Screen Small Screen, 3: Side by Side diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index 436442acd..8e1c40cd7 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -27,14 +27,18 @@ static void UpdateLandscapeScreenLayout() { IDCache::GetNativeLibraryClass(), IDCache::GetLandscapeScreenLayout())); } -void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { - render_window = surface; +bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { + if (render_window == surface) { + return false; + } + render_window = surface; window_info.type = Frontend::WindowSystemType::Android; window_info.render_surface = surface; StopPresenting(); OnFramebufferSizeChanged(); + return true; } bool EmuWindow_Android::OnTouchEvent(int x, int y, bool pressed) { diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h index 64ead5c7f..4266fd1bb 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.h +++ b/src/android/app/src/main/jni/emu_window/emu_window.h @@ -17,7 +17,7 @@ public: ~EmuWindow_Android(); /// Called by the onSurfaceChanges() method to change the surface - void OnSurfaceChanged(ANativeWindow* surface); + bool OnSurfaceChanged(ANativeWindow* surface); /// Handles touch event that occur.(Touched or released) bool OnTouchEvent(int x, int y, bool pressed); diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 13dbc0f2c..393ce906d 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -82,6 +82,7 @@ static jobject ToJavaCoreError(Core::System::ResultStatus result) { static const std::map CoreErrorNameMap{ {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, + {Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"}, {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, }; @@ -178,6 +179,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { auto app_loader = Loader::GetLoader(filepath); if (app_loader) { app_loader->ReadProgramId(program_id); + system.RegisterAppLoaderEarly(app_loader); GameSettings::LoadOverrides(program_id); } system.ApplySettings(); @@ -231,6 +233,10 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { InputManager::NDKMotionHandler()->DisableSensors(); if (!HandleCoreError(result, system.GetStatusDetails())) { // Frontend requests us to abort + // If the error was an Artic disconnect, return shutdown request. + if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + return Core::System::ResultStatus::ShutdownRequested; + } return result; } InputManager::NDKMotionHandler()->EnableSensors(); @@ -288,12 +294,13 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jobject surf) { s_surf = ANativeWindow_fromSurface(env, surf); + bool notify = false; if (window) { - window->OnSurfaceChanged(s_surf); + notify = window->OnSurfaceChanged(s_surf); } auto& system = Core::System::GetInstance(); - if (system.IsPoweredOn()) { + if (notify && system.IsPoweredOn()) { system.GPU().Renderer().NotifySurfaceChanged(); } @@ -314,7 +321,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* en if (stop_run || pause_emulation) { return; } - window->TryPresenting(); + if (window) { + window->TryPresenting(); + } } void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver( diff --git a/src/android/app/src/main/res/drawable/ic_network.xml b/src/android/app/src/main/res/drawable/ic_network.xml new file mode 100644 index 000000000..91559b988 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_network.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml index 7755c81cc..2b83bc36d 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -657,4 +657,16 @@ Se esperan fallos gráficos temporales cuando ésta esté activado. Noviembre Diciembre + + Fallo de comunicación con el servidor Artic Base. La emulación se detendrá. + Artic Base + Conectar con Artic Base + Conectar con una consola real que esté ejecutando un servidor Artic Base + Introduce la dirección del servidor Artic Base + Retrasa el hilo de dibujado del juego + Retrasa el hilo de dibujado del juego cuando envía datos a la GPU. Ayuda con problemas de rendimiento en los (muy pocos) juegos de fps dinámicos. + Misceláneo + Usar Artic Controller cuando se está conectado a Artic Base Server + Usa los controles proporcionados por Artic Base Server cuando esté conectado a él en lugar del dispositivo de entrada configurado. + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 781e78da1..aad33db15 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -683,4 +683,25 @@ November December + + Failed to communicate with the Artic Base server. Emulation will stop. + Artic Base + Connect to a real console that is running an Artic Base server + Connect to Artic Base + Enter Artic Base server address + Delay game render thread + Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) dynamic-fps games. + + + Quicksave + Quicksave + Quickload + Quicksave - %1$tF %1$tR + Saving… + Loading… + No Quicksave available. + Miscellaneous + Use Artic Controller when connected to Artic Base Server + Use the controls provided by Artic Base Server when connected to it instead of the configured input device. + diff --git a/src/android/gradle/wrapper/gradle-wrapper.properties b/src/android/gradle/wrapper/gradle-wrapper.properties index 10cba3572..03ef85ab3 100644 --- a/src/android/gradle/wrapper/gradle-wrapper.properties +++ b/src/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip diff --git a/src/citra/config.cpp b/src/citra/config.cpp index e47421102..bd60d4764 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -147,6 +147,7 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.use_vsync_new); ReadSetting("Renderer", Settings::values.texture_filter); ReadSetting("Renderer", Settings::values.texture_sampling); + ReadSetting("Renderer", Settings::values.delay_game_render_thread_us); ReadSetting("Renderer", Settings::values.mono_render_option); ReadSetting("Renderer", Settings::values.render_3d); diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 4e67d84c4..1cb3cb4d5 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -81,6 +81,9 @@ add_executable(citra-qt configuration/configure_ui.cpp configuration/configure_ui.h configuration/configure_ui.ui + configuration/configure_web.cpp + configuration/configure_web.h + configuration/configure_web.ui configuration/configure_cheats.cpp configuration/configure_cheats.h configuration/configure_cheats.ui diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 7ddbc7eb9..886b66aa2 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -327,6 +327,8 @@ void Config::ReadCameraValues() { void Config::ReadControlValues() { qt_config->beginGroup(QStringLiteral("Controls")); + ReadBasicSetting(Settings::values.use_artic_base_controller); + int num_touch_from_button_maps = qt_config->beginReadArray(QStringLiteral("touch_from_button_maps")); @@ -636,6 +638,8 @@ void Config::ReadPathValues() { UISettings::values.game_dirs.append(game_dir); } } + UISettings::values.last_artic_base_addr = + ReadSetting(QStringLiteral("last_artic_base_addr"), QString{}).toString(); UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); UISettings::values.language = ReadSetting(QStringLiteral("language"), QString{}).toString(); } @@ -665,6 +669,8 @@ void Config::ReadRendererValues() { ReadGlobalSetting(Settings::values.texture_filter); ReadGlobalSetting(Settings::values.texture_sampling); + ReadGlobalSetting(Settings::values.delay_game_render_thread_us); + if (global) { ReadBasicSetting(Settings::values.use_shader_jit); } @@ -920,6 +926,8 @@ void Config::SaveCameraValues() { void Config::SaveControlValues() { qt_config->beginGroup(QStringLiteral("Controls")); + WriteBasicSetting(Settings::values.use_artic_base_controller); + WriteSetting(QStringLiteral("profile"), Settings::values.current_input_profile_index, 0); qt_config->beginWriteArray(QStringLiteral("profiles")); for (std::size_t p = 0; p < Settings::values.input_profiles.size(); ++p) { @@ -1135,6 +1143,8 @@ void Config::SavePathValues() { WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true); } qt_config->endArray(); + WriteSetting(QStringLiteral("last_artic_base_addr"), + UISettings::values.last_artic_base_addr, QString{}); WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); WriteSetting(QStringLiteral("language"), UISettings::values.language, QString{}); } @@ -1164,6 +1174,8 @@ void Config::SaveRendererValues() { WriteGlobalSetting(Settings::values.texture_filter); WriteGlobalSetting(Settings::values.texture_sampling); + WriteGlobalSetting(Settings::values.delay_game_render_thread_us); + if (global) { WriteSetting(QStringLiteral("use_shader_jit"), Settings::values.use_shader_jit.GetValue(), true); diff --git a/src/citra_qt/configuration/configure.ui b/src/citra_qt/configuration/configure.ui index e94cbbc9c..f6f5a517a 100644 --- a/src/citra_qt/configuration/configure.ui +++ b/src/citra_qt/configuration/configure.ui @@ -97,6 +97,12 @@
configuration/configure_enhancements.h
1 + + ConfigureWeb + QWidget +
configuration/configure_web.h
+ 1 +
ConfigureUi QWidget diff --git a/src/citra_qt/configuration/configure_debug.ui b/src/citra_qt/configuration/configure_debug.ui index eacf85be9..860df0ffe 100644 --- a/src/citra_qt/configuration/configure_debug.ui +++ b/src/citra_qt/configuration/configure_debug.ui @@ -86,7 +86,7 @@ - + @@ -100,7 +100,7 @@ - + @@ -125,7 +125,7 @@ CPU - + diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp index 4dd5635c5..dd00e932b 100644 --- a/src/citra_qt/configuration/configure_dialog.cpp +++ b/src/citra_qt/configuration/configure_dialog.cpp @@ -16,6 +16,7 @@ #include "citra_qt/configuration/configure_storage.h" #include "citra_qt/configuration/configure_system.h" #include "citra_qt/configuration/configure_ui.h" +#include "citra_qt/configuration/configure_web.h" #include "citra_qt/hotkeys.h" #include "common/settings.h" #include "core/core.h" @@ -28,7 +29,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor system{system_}, is_powered_on{system.IsPoweredOn()}, general_tab{std::make_unique(this)}, system_tab{std::make_unique(system, this)}, - input_tab{std::make_unique(this)}, + input_tab{std::make_unique(system, this)}, hotkeys_tab{std::make_unique(this)}, graphics_tab{ std::make_unique(gl_renderer, physical_devices, is_powered_on, this)}, @@ -37,7 +38,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor camera_tab{std::make_unique(this)}, debug_tab{std::make_unique(is_powered_on, this)}, storage_tab{std::make_unique(is_powered_on, this)}, - ui_tab{std::make_unique(this)} { + web_tab{std::make_unique(this)}, ui_tab{std::make_unique(this)} { Settings::SetConfiguringGlobal(true); ui->setupUi(this); @@ -52,6 +53,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor ui->tabWidget->addTab(camera_tab.get(), tr("Camera")); ui->tabWidget->addTab(debug_tab.get(), tr("Debug")); ui->tabWidget->addTab(storage_tab.get(), tr("Storage")); + ui->tabWidget->addTab(web_tab.get(), tr("Web")); ui->tabWidget->addTab(ui_tab.get(), tr("UI")); hotkeys_tab->Populate(registry); @@ -87,6 +89,7 @@ void ConfigureDialog::SetConfiguration() { audio_tab->SetConfiguration(); camera_tab->SetConfiguration(); debug_tab->SetConfiguration(); + web_tab->SetConfiguration(); ui_tab->SetConfiguration(); storage_tab->SetConfiguration(); } @@ -102,6 +105,7 @@ void ConfigureDialog::ApplyConfiguration() { audio_tab->ApplyConfiguration(); camera_tab->ApplyConfiguration(); debug_tab->ApplyConfiguration(); + web_tab->ApplyConfiguration(); ui_tab->ApplyConfiguration(); storage_tab->ApplyConfiguration(); system.ApplySettings(); @@ -114,7 +118,7 @@ void ConfigureDialog::PopulateSelectionList() { ui->selectorList->clear(); const std::array>, 5> items{ - {{tr("General"), {general_tab.get(), debug_tab.get(), ui_tab.get()}}, + {{tr("General"), {general_tab.get(), web_tab.get(), debug_tab.get(), ui_tab.get()}}, {tr("System"), {system_tab.get(), camera_tab.get(), storage_tab.get()}}, {tr("Graphics"), {enhancements_tab.get(), graphics_tab.get()}}, {tr("Audio"), {audio_tab.get()}}, @@ -154,6 +158,7 @@ void ConfigureDialog::RetranslateUI() { audio_tab->RetranslateUI(); camera_tab->RetranslateUI(); debug_tab->RetranslateUI(); + web_tab->RetranslateUI(); ui_tab->RetranslateUI(); storage_tab->RetranslateUI(); } @@ -173,6 +178,7 @@ void ConfigureDialog::UpdateVisibleTabs() { {camera_tab.get(), tr("Camera")}, {debug_tab.get(), tr("Debug")}, {storage_tab.get(), tr("Storage")}, + {web_tab.get(), tr("Web")}, {ui_tab.get(), tr("UI")}}; ui->tabWidget->clear(); diff --git a/src/citra_qt/configuration/configure_dialog.h b/src/citra_qt/configuration/configure_dialog.h index ac500224e..db2549fcb 100644 --- a/src/citra_qt/configuration/configure_dialog.h +++ b/src/citra_qt/configuration/configure_dialog.h @@ -29,6 +29,7 @@ class ConfigureAudio; class ConfigureCamera; class ConfigureDebug; class ConfigureStorage; +class ConfigureWeb; class ConfigureUi; class ConfigureDialog : public QDialog { @@ -69,5 +70,6 @@ private: std::unique_ptr camera_tab; std::unique_ptr debug_tab; std::unique_ptr storage_tab; + std::unique_ptr web_tab; std::unique_ptr ui_tab; }; diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp index 80c2d138d..2e244b33a 100644 --- a/src/citra_qt/configuration/configure_graphics.cpp +++ b/src/citra_qt/configuration/configure_graphics.cpp @@ -26,6 +26,10 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::spangraphics_api_combo->setCurrentIndex(-1); + const auto width = static_cast(QString::fromStdString("000000000").size() * 6); + ui->delay_render_display_label->setMinimumWidth(width); + ui->delay_render_combo->setVisible(!Settings::IsConfiguringGlobal()); + auto graphics_api_combo_model = qobject_cast(ui->graphics_api_combo->model()); #ifndef ENABLE_SOFTWARE_RENDERER @@ -82,12 +86,25 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::spangraphics_api_combo, qOverload(&QComboBox::currentIndexChanged), this, &ConfigureGraphics::SetPhysicalDeviceComboVisibility); + connect(ui->delay_render_slider, &QSlider::valueChanged, this, [&](int value) { + ui->delay_render_display_label->setText( + QStringLiteral("%1 ms") + .arg(((double)value) / 1000.f, 0, 'f', 3) + .rightJustified(QString::fromStdString("000000000").size())); + }); + SetConfiguration(); } ConfigureGraphics::~ConfigureGraphics() = default; void ConfigureGraphics::SetConfiguration() { + ui->delay_render_slider->setValue(Settings::values.delay_game_render_thread_us.GetValue()); + ui->delay_render_display_label->setText( + QStringLiteral("%1 ms") + .arg(((double)ui->delay_render_slider->value()) / 1000, 0, 'f', 3) + .rightJustified(QString::fromStdString("000000000").size())); + if (!Settings::IsConfiguringGlobal()) { ConfigurationShared::SetHighlight(ui->graphics_api_group, !Settings::values.graphics_api.UsingGlobal()); @@ -101,6 +118,16 @@ void ConfigureGraphics::SetConfiguration() { &Settings::values.texture_sampling); ConfigurationShared::SetHighlight(ui->widget_texture_sampling, !Settings::values.texture_sampling.UsingGlobal()); + ConfigurationShared::SetHighlight( + ui->delay_render_layout, !Settings::values.delay_game_render_thread_us.UsingGlobal()); + + if (Settings::values.delay_game_render_thread_us.UsingGlobal()) { + ui->delay_render_combo->setCurrentIndex(0); + ui->delay_render_slider->setEnabled(false); + } else { + ui->delay_render_combo->setCurrentIndex(1); + ui->delay_render_slider->setEnabled(true); + } } else { ui->graphics_api_combo->setCurrentIndex( static_cast(Settings::values.graphics_api.GetValue())); @@ -144,6 +171,9 @@ void ConfigureGraphics::ApplyConfiguration() { ui->toggle_disk_shader_cache, use_disk_shader_cache); ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync_new, ui->toggle_vsync_new, use_vsync_new); + ConfigurationShared::ApplyPerGameSetting( + &Settings::values.delay_game_render_thread_us, ui->delay_render_combo, + [this](s32) { return ui->delay_render_slider->value(); }); if (Settings::IsConfiguringGlobal()) { Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked(); @@ -170,9 +200,16 @@ void ConfigureGraphics::SetupPerGameUI() { ui->toggle_async_present->setEnabled(Settings::values.async_presentation.UsingGlobal()); ui->graphics_api_combo->setEnabled(Settings::values.graphics_api.UsingGlobal()); ui->physical_device_combo->setEnabled(Settings::values.physical_device.UsingGlobal()); + ui->delay_render_combo->setEnabled( + Settings::values.delay_game_render_thread_us.UsingGlobal()); return; } + connect(ui->delay_render_combo, qOverload(&QComboBox::activated), this, [this](int index) { + ui->delay_render_slider->setEnabled(index == 1); + ConfigurationShared::SetHighlight(ui->delay_render_layout, index == 1); + }); + ui->toggle_shader_jit->setVisible(false); ConfigurationShared::SetColoredComboBox( diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui index a052186cd..122fdddcd 100644 --- a/src/citra_qt/configuration/configure_graphics.ui +++ b/src/citra_qt/configuration/configure_graphics.ui @@ -307,6 +307,83 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Use global + + + + + Use per-game + + + + + + + + Delay game render thread: + + + <html><head/><body><p>Delays the emulated game render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic-fps games to fix performance issues.</p></body></html> + + + + + + + 0 + + + 16000 + + + 100 + + + 250 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp index da98da0d0..158885ca5 100644 --- a/src/citra_qt/configuration/configure_input.cpp +++ b/src/citra_qt/configuration/configure_input.cpp @@ -16,6 +16,7 @@ #include "citra_qt/configuration/configure_input.h" #include "citra_qt/configuration/configure_motion_touch.h" #include "common/param_package.h" +#include "core/core.h" #include "ui_configure_input.h" const std::array @@ -145,8 +146,8 @@ static QString AnalogToText(const Common::ParamPackage& param, const std::string return QObject::tr("[unknown]"); } -ConfigureInput::ConfigureInput(QWidget* parent) - : QWidget(parent), ui(std::make_unique()), +ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) + : QWidget(parent), system(_system), ui(std::make_unique()), timeout_timer(std::make_unique()), poll_timer(std::make_unique()) { ui->setupUi(this); setFocusPolicy(Qt::ClickFocus); @@ -400,6 +401,9 @@ ConfigureInput::ConfigureInput(QWidget* parent) ConfigureInput::~ConfigureInput() = default; void ConfigureInput::ApplyConfiguration() { + + Settings::values.use_artic_base_controller = ui->use_artic_controller->isChecked(); + std::transform(buttons_param.begin(), buttons_param.end(), Settings::values.current_input_profile.buttons.begin(), [](const Common::ParamPackage& param) { return param.Serialize(); }); @@ -444,6 +448,10 @@ QList ConfigureInput::GetUsedKeyboardKeys() { } void ConfigureInput::LoadConfiguration() { + + ui->use_artic_controller->setChecked(Settings::values.use_artic_base_controller.GetValue()); + ui->use_artic_controller->setEnabled(!system.IsPoweredOn()); + std::transform(Settings::values.current_input_profile.buttons.begin(), Settings::values.current_input_profile.buttons.end(), buttons_param.begin(), [](const std::string& str) { return Common::ParamPackage(str); }); diff --git a/src/citra_qt/configuration/configure_input.h b/src/citra_qt/configuration/configure_input.h index fb00444c6..45a7a8329 100644 --- a/src/citra_qt/configuration/configure_input.h +++ b/src/citra_qt/configuration/configure_input.h @@ -30,7 +30,7 @@ class ConfigureInput : public QWidget { Q_OBJECT public: - explicit ConfigureInput(QWidget* parent = nullptr); + explicit ConfigureInput(Core::System& system, QWidget* parent = nullptr); ~ConfigureInput() override; /// Save all button configurations to settings file @@ -50,6 +50,7 @@ signals: void InputKeysChanged(QList new_key_list); private: + Core::System& system; std::unique_ptr ui; std::unique_ptr timeout_timer; diff --git a/src/citra_qt/configuration/configure_input.ui b/src/citra_qt/configuration/configure_input.ui index 2d199e667..7168b7190 100644 --- a/src/citra_qt/configuration/configure_input.ui +++ b/src/citra_qt/configuration/configure_input.ui @@ -841,6 +841,13 @@ + + + + Use Artic Controller when connected to Artic Base Server + + + diff --git a/src/citra_qt/configuration/configure_per_game.cpp b/src/citra_qt/configuration/configure_per_game.cpp index c11847a0d..f9c249911 100644 --- a/src/citra_qt/configuration/configure_per_game.cpp +++ b/src/citra_qt/configuration/configure_per_game.cpp @@ -151,7 +151,14 @@ void ConfigurePerGame::LoadConfiguration() { ui->display_title_id->setText( QStringLiteral("%1").arg(title_id, 16, 16, QLatin1Char{'0'}).toUpper()); - const auto loader = Loader::GetLoader(filename); + std::unique_ptr loader_ptr; + Loader::AppLoader* loader; + if (system.IsPoweredOn()) { + loader = &system.GetAppLoader(); + } else { + loader_ptr = Loader::GetLoader(filename); + loader = loader_ptr.get(); + } std::string title; if (loader->ReadTitle(title) == Loader::ResultStatus::Success) diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui index 20f585637..71819029e 100644 --- a/src/citra_qt/configuration/configure_system.ui +++ b/src/citra_qt/configuration/configure_system.ui @@ -13,640 +13,671 @@ Form - + - - - - - System Settings - - - - - - Enable New 3DS mode - - - - - - - Use LLE applets (if installed) - - - - - - - - 0 - 0 - - - - 10 - - - - - - - Username - - - - - - - Birthday - - - - - - - - - - January - - - - - February - - - - - March - - - - - April - - - - - May - - - - - June - - - - - July - - - - - August - - - - - September - - - - - October - - - - - November - - - - - December - - - - - - - - - - - - - Language - - - - - - - Note: this can be overridden when region setting is auto-select - - - - Japanese (日本語) + + + + 0 + 480 + + + + QFrame::NoFrame + + + 1 + + + true + + + + + 0 + 0 + 422 + 500 + + + + + + + + + System Settings - - - - English - - - - - French (français) - - - - - German (Deutsch) - - - - - Italian (italiano) - - - - - Spanish (español) - - - - - Simplified Chinese (简体中文) - - - - - Korean (한국어) - - - - - Dutch (Nederlands) - - - - - Portuguese (português) - - - - - Russian (Русский) - - - - - Traditional Chinese (正體中文) - - - - - - - - Sound output mode - - - - - - - - Mono - - - - - Stereo - - - - - Surround - - - - - - - - Country - - - - - - - - - - Clock - - - - - - - - System Clock - - - - - Fixed Time - - - - - - - - Startup time - - - - - - - yyyy-MM-ddTHH:mm:ss - - - - - - - Offset time - - - - - - - - - days - - - -2147483648 - - - 2147483647 - - - - - - - HH:mm:ss - - - - - - - - - Initial System Ticks - - - - - - - - Random - - - - - Fixed - - - - - - - - Initial System Ticks Override - - - - - - - - 0 - 0 - - - - 20 - - - - - - - Play Coins: - - - - - - - 300 - - - - - - - Run System Setup when Home Menu is launched - - - - - - - Console ID: - - - - - - - - 0 - 0 - - - - Qt::RightToLeft - - - Regenerate - - - - - - - 3GX Plugin Loader: - - - - - - - Enable 3GX plugin loader - - - - - - - Allow games to change plugin loader state - - - - - - - Download System Files from Nitendo servers - - - - - - - - - - - Minimal - - - - - Old 3DS - - - - - New 3DS - - - - - - - - - JPN - - - - - USA - - - - - EUR - - - - - AUS - - - - - CHN - - - - - KOR - - - - - TWN - - - - - - - - - 0 - 0 - - - - Qt::RightToLeft - - - Download - - - - - - - - - - - - - Real Console Unique Data - - + - - - SecureInfo_A/B - - - - - - - - - - - - - - - - - - 0 - 0 - - - - Qt::RightToLeft - - - Choose - - - - - + + + Enable New 3DS mode + + - - - LocalFriendCodeSeed_A/B - - - - - - - - - - - - - - - - - - 0 - 0 - - - - Qt::RightToLeft - - - Choose - - - - - - - - - - CTCert - - + + + Use LLE applets (if installed) + + - - - - - - - - - - - - - - 0 - 0 - - - - Qt::RightToLeft - - - Choose - - - - - + + + + 0 + 0 + + + + 10 + + - - - - - - - System settings are available only when game is not running. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + + Username + + + + + + + Birthday + + + + + + + + + + January + + + + + February + + + + + March + + + + + April + + + + + May + + + + + June + + + + + July + + + + + August + + + + + September + + + + + October + + + + + November + + + + + December + + + + + + + + + + + + + Language + + + + + + + Note: this can be overridden when region setting is auto-select + + + + Japanese (日本語) + + + + + English + + + + + French (français) + + + + + German (Deutsch) + + + + + Italian (italiano) + + + + + Spanish (español) + + + + + Simplified Chinese (简体中文) + + + + + Korean (한국어) + + + + + Dutch (Nederlands) + + + + + Portuguese (português) + + + + + Russian (Русский) + + + + + Traditional Chinese (正體中文) + + + + + + + + Sound output mode + + + + + + + + Mono + + + + + Stereo + + + + + Surround + + + + + + + + Country + + + + + + + + + + Clock + + + + + + + + System Clock + + + + + Fixed Time + + + + + + + + Startup time + + + + + + + yyyy-MM-ddTHH:mm:ss + + + + + + + Offset time + + + + + + + + + days + + + -2147483648 + + + 2147483647 + + + + + + + HH:mm:ss + + + + + + + + + Initial System Ticks + + + + + + + + Random + + + + + Fixed + + + + + + + + Initial System Ticks Override + + + + + + + + 0 + 0 + + + + 20 + + + + + + + Play Coins: + + + + + + + 300 + + + + + + + Run System Setup when Home Menu is launched + + + + + + + Console ID: + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + Regenerate + + + + + + + 3GX Plugin Loader: + + + + + + + Enable 3GX plugin loader + + + + + + + Allow games to change plugin loader state + + + + + + + Download System Files from Nitendo servers + + + + + + + + + + + Minimal + + + + + Old 3DS + + + + + New 3DS + + + + + + + + + JPN + + + + + USA + + + + + EUR + + + + + AUS + + + + + CHN + + + + + KOR + + + + + TWN + + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + Download + + + + + + + + + + + + + Real Console Unique Data + + + + + + SecureInfo_A/B + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + Choose + + + + + + + + + + LocalFriendCodeSeed_A/B + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + Choose + + + + + + + + + + CTCert + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Qt::RightToLeft + + + Choose + + + + + + + + + + + + + System settings are available only when game is not running. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/citra_qt/configuration/configure_web.cpp b/src/citra_qt/configuration/configure_web.cpp new file mode 100644 index 000000000..2c61b02dc --- /dev/null +++ b/src/citra_qt/configuration/configure_web.cpp @@ -0,0 +1,36 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/configuration/configure_web.h" +#include "citra_qt/uisettings.h" +#include "network/network_settings.h" +#include "ui_configure_web.h" + +ConfigureWeb::ConfigureWeb(QWidget* parent) + : QWidget(parent), ui(std::make_unique()) { + ui->setupUi(this); + +#ifndef USE_DISCORD_PRESENCE + ui->discord_group->setVisible(false); +#endif + SetConfiguration(); +} + +ConfigureWeb::~ConfigureWeb() = default; + +void ConfigureWeb::SetConfiguration() { + + ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence.GetValue()); +} + +void ConfigureWeb::ApplyConfiguration() { + UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked(); +} + +void ConfigureWeb::RetranslateUI() { + ui->retranslateUi(this); +} diff --git a/src/citra_qt/configuration/configure_web.h b/src/citra_qt/configuration/configure_web.h new file mode 100644 index 000000000..53f7f2b18 --- /dev/null +++ b/src/citra_qt/configuration/configure_web.h @@ -0,0 +1,28 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +namespace Ui { +class ConfigureWeb; +} + +class ConfigureWeb : public QWidget { + Q_OBJECT + +public: + explicit ConfigureWeb(QWidget* parent = nullptr); + ~ConfigureWeb() override; + + void ApplyConfiguration(); + void RetranslateUI(); + void SetConfiguration(); + +private: + std::unique_ptr ui; +}; diff --git a/src/citra_qt/configuration/configure_web.ui b/src/citra_qt/configuration/configure_web.ui new file mode 100644 index 000000000..a68936c59 --- /dev/null +++ b/src/citra_qt/configuration/configure_web.ui @@ -0,0 +1,53 @@ + + + ConfigureWeb + + + + 0 + 0 + 996 + 561 + + + + Form + + + + + + Discord Presence + + + + + + Show Current Game in your Discord Status + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + toggle_discordrpc + + + + diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index e724ea37b..0b5b16695 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -381,6 +381,10 @@ void GMainWindow::InitializeWidgets() { progress_bar->hide(); statusBar()->addPermanentWidget(progress_bar); + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + emu_speed_label = new QLabel(); emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " "indicate emulation is running faster or slower than a 3DS.")); @@ -392,7 +396,8 @@ void GMainWindow::InitializeWidgets() { tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " "full-speed emulation this should be at most 16.67 ms.")); - for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) { + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { label->setVisible(false); label->setFrameStyle(QFrame::NoFrame); label->setContentsMargins(4, 0, 4, 0); @@ -866,6 +871,7 @@ void GMainWindow::ConnectMenuEvents() { // File connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), [this, region] { OnMenuBootHomeMenu(region); }); @@ -935,6 +941,10 @@ void GMainWindow::ConnectMenuEvents() { // Help connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder); + connect_menu(ui->action_Open_Log_Folder, []() { + QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); connect_menu(ui->action_FAQ, []() { QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); }); @@ -964,7 +974,7 @@ void GMainWindow::UpdateMenuState() { action->setEnabled(emulation_running); } - ui->action_Capture_Screenshot->setEnabled(emulation_running && !is_paused); + ui->action_Capture_Screenshot->setEnabled(emulation_running); if (emulation_running && is_paused) { ui->action_Pause->setText(tr("&Continue")); @@ -1203,6 +1213,14 @@ bool GMainWindow::LoadROM(const QString& filename) { tr("GBA Virtual Console ROMs are not supported by Citra.")); break; + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr(fmt::format( + "An error has occurred whilst communicating with the Artic Base Server.\n{}", + system.GetStatusDetails()) + .c_str())); + break; default: QMessageBox::critical( this, tr("Error while loading ROM!"), @@ -1223,7 +1241,13 @@ bool GMainWindow::LoadROM(const QString& filename) { } void GMainWindow::BootGame(const QString& filename) { - if (filename.endsWith(QStringLiteral(".cia"))) { + if (emu_thread) { + ShutdownGame(); + } + + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { const auto answer = QMessageBox::question( this, tr("CIA must be installed before usage"), tr("Before using this CIA, you must install it. Do you want to install it now?"), @@ -1235,8 +1259,12 @@ void GMainWindow::BootGame(const QString& filename) { return; } + show_artic_label = is_artic; + LOG_INFO(Frontend, "Citra starting..."); - StoreRecentFile(filename); // Put the filename on top of the list + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } if (movie_record_on_start) { movie.PrepareForRecording(); @@ -1246,16 +1274,26 @@ void GMainWindow::BootGame(const QString& filename) { } const std::string path = filename.toStdString(); - const auto loader = Loader::GetLoader(path); + auto loader = Loader::GetLoader(path); u64 title_id{0}; - loader->ReadProgramId(title_id); + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } - // Load per game settings - const std::string name{FileUtil::GetFilename(filename.toStdString())}; - const std::string config_file_name = title_id == 0 ? name : fmt::format("{:016X}", title_id); - LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); - Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); system.ApplySettings(); Settings::LogSettings(); @@ -1265,8 +1303,11 @@ void GMainWindow::BootGame(const QString& filename) { game_list->SaveInterfaceLayout(); config->Save(); - if (!LoadROM(filename)) + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); return; + } // Set everything up if (movie_record_on_start) { @@ -1420,6 +1461,8 @@ void GMainWindow::ShutdownGame() { // Disable status bar updates status_bar_update_timer.stop(); message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); emu_speed_label->setVisible(false); game_fps_label->setVisible(false); emu_frametime_label->setVisible(false); @@ -1759,6 +1802,17 @@ void GMainWindow::OnMenuInstallCIA() { InstallCIA(filepaths); } +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + void GMainWindow::OnMenuBootHomeMenu(u32 region) { BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); } @@ -2365,33 +2419,47 @@ void GMainWindow::OnSaveMovie() { } void GMainWindow::OnCaptureScreenshot() { - if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { + if (!emu_thread) [[unlikely]] { return; } - OnPauseGame(); - std::string path = UISettings::values.screenshot_path.GetValue(); - if (!FileUtil::IsDirectory(path)) { - if (!FileUtil::CreateFullPath(path)) { - QMessageBox::information(this, tr("Invalid Screenshot Directory"), - tr("Cannot create specified screenshot directory. Screenshot " - "path is set back to its default value.")); - path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); - path.append("screenshots/"); - UISettings::values.screenshot_path = path; - }; + const bool was_running = emu_thread->IsRunning(); + + if (was_running || + (QMessageBox::question( + this, tr("Game will unpause"), + tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running) { + OnPauseGame(); + } + std::string path = UISettings::values.screenshot_path.GetValue(); + if (!FileUtil::IsDirectory(path)) { + if (!FileUtil::CreateFullPath(path)) { + QMessageBox::information( + this, tr("Invalid Screenshot Directory"), + tr("Cannot create specified screenshot directory. Screenshot " + "path is set back to its default value.")); + path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + path.append("screenshots/"); + UISettings::values.screenshot_path = path; + }; + } + + static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); + const std::string filename = game_title.remove(expr).toStdString(); + const std::string timestamp = QDateTime::currentDateTime() + .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) + .toStdString(); + path.append(fmt::format("/{}_{}.png", filename, timestamp)); + + auto* const screenshot_window = + secondary_window->HasFocus() ? secondary_window : render_window; + screenshot_window->CaptureScreenshot( + UISettings::values.screenshot_resolution_factor.GetValue(), + QString::fromStdString(path)); + OnStartGame(); } - - static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); - const std::string filename = game_title.remove(expr).toStdString(); - const std::string timestamp = - QDateTime::currentDateTime().toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")).toStdString(); - path.append(fmt::format("/{}_{}.png", filename, timestamp)); - - auto* const screenshot_window = secondary_window->HasFocus() ? secondary_window : render_window; - screenshot_window->CaptureScreenshot(UISettings::values.screenshot_resolution_factor.GetValue(), - QString::fromStdString(path)); - OnStartGame(); } void GMainWindow::OnDumpVideo() { @@ -2575,6 +2643,53 @@ void GMainWindow::UpdateStatusBar() { auto results = system.GetAndResetPerfStats(); + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 5> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + tr("(Accessing SystemSaveData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + if (Settings::values.frame_limit.GetValue() == 0) { emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); } else { @@ -2585,6 +2700,9 @@ void GMainWindow::UpdateStatusBar() { game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } emu_speed_label->setVisible(true); game_fps_label->setVisible(true); emu_frametime_label->setVisible(true); @@ -2736,6 +2854,7 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det QString title, message; QMessageBox::Icon error_severity_icon; + bool can_continue = true; if (result == Core::System::ResultStatus::ErrorSystemFiles) { const QString common_message = tr("%1 is missing. Please + @@ -202,6 +203,7 @@ + @@ -222,6 +224,11 @@ Install CIA... + + + Connect to Artic Base... + + JPN @@ -473,6 +480,14 @@ Fullscreen + + + Open Log Folder + + + Opens the Citra Log folder + + Modify Citra Install diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index 6dedff0f0..5ac61e204 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -116,6 +116,7 @@ struct Values { bool game_dir_deprecated_deepscan; QVector game_dirs; QStringList recent_files; + QString last_artic_base_addr; QString language; QString theme; diff --git a/src/common/logging/formatter.h b/src/common/logging/formatter.h index ad6adb143..1bfd534b5 100644 --- a/src/common/logging/formatter.h +++ b/src/common/logging/formatter.h @@ -12,9 +12,9 @@ #if FMT_VERSION >= 80100 template struct fmt::formatter, char>> - : formatter> { + : fmt::formatter> { template - auto format(const T& value, FormatContext& ctx) -> decltype(ctx.out()) { + auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) { return fmt::formatter>::format( static_cast>(value), ctx); } diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 657747b61..69cc6f4ef 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -83,6 +83,7 @@ void LogSettings() { LOG_INFO(Config, "Citra Configuration:"); log_setting("Core_UseCpuJit", values.use_cpu_jit.GetValue()); log_setting("Core_CPUClockPercentage", values.cpu_clock_percentage.GetValue()); + log_setting("Controller_UseArticController", values.use_artic_base_controller.GetValue()); log_setting("Renderer_UseGLES", values.use_gles.GetValue()); log_setting("Renderer_GraphicsAPI", GetGraphicsAPIName(values.graphics_api.GetValue())); log_setting("Renderer_AsyncShaders", values.async_shader_compilation.GetValue()); @@ -100,6 +101,7 @@ void LogSettings() { log_setting("Renderer_TextureFilter", GetTextureFilterName(values.texture_filter.GetValue())); log_setting("Renderer_TextureSampling", GetTextureSamplingName(values.texture_sampling.GetValue())); + log_setting("Renderer_DelayGameRenderThreasUs", values.delay_game_render_thread_us.GetValue()); log_setting("Stereoscopy_Render3d", values.render_3d.GetValue()); log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue()); log_setting("Stereoscopy_MonoRenderOption", values.mono_render_option.GetValue()); @@ -192,6 +194,7 @@ void RestoreGlobalState(bool is_powered_on) { values.frame_limit.SetGlobal(true); values.texture_filter.SetGlobal(true); values.texture_sampling.SetGlobal(true); + values.delay_game_render_thread_us.SetGlobal(true); values.layout_option.SetGlobal(true); values.swap_screen.SetGlobal(true); values.upright_screen.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index 64bba90ee..b61957a6a 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -425,6 +425,7 @@ struct Values { int current_input_profile_index; ///< The current input profile index std::vector input_profiles; ///< The list of input profiles std::vector touch_from_button_maps; + Setting use_artic_base_controller{false, "use_artic_base_controller"}; SwitchableSetting enable_gamemode{true, "enable_gamemode"}; @@ -479,6 +480,8 @@ struct Values { SwitchableSetting texture_filter{TextureFilter::None, "texture_filter"}; SwitchableSetting texture_sampling{TextureSampling::GameControlled, "texture_sampling"}; + SwitchableSetting delay_game_render_thread_us{0, 0, 16000, + "delay_game_render_thread_us"}; SwitchableSetting layout_option{LayoutOption::Default, "layout_option"}; SwitchableSetting swap_screen{false, "swap_screen"}; diff --git a/src/common/static_lru_cache.h b/src/common/static_lru_cache.h index b91f046a0..bd692e94e 100644 --- a/src/common/static_lru_cache.h +++ b/src/common/static_lru_cache.h @@ -14,6 +14,7 @@ //---------------------------------------------------------------------------// #pragma once +#include #include #include #include diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 62a9cde64..de4439ece 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -40,6 +40,8 @@ add_library(citra_core STATIC dumping/backend.h dumping/ffmpeg_backend.cpp dumping/ffmpeg_backend.h + file_sys/archive_artic.cpp + file_sys/archive_artic.h file_sys/archive_backend.cpp file_sys/archive_backend.h file_sys/archive_extsavedata.cpp @@ -60,6 +62,8 @@ add_library(citra_core STATIC file_sys/archive_source_sd_savedata.h file_sys/archive_systemsavedata.cpp file_sys/archive_systemsavedata.h + file_sys/artic_cache.cpp + file_sys/artic_cache.h file_sys/cia_common.h file_sys/cia_container.cpp file_sys/cia_container.h @@ -87,6 +91,10 @@ add_library(citra_core STATIC file_sys/romfs_reader.h file_sys/savedata_archive.cpp file_sys/savedata_archive.h + file_sys/secure_value_backend_artic.cpp + file_sys/secure_value_backend_artic.h + file_sys/secure_value_backend.cpp + file_sys/secure_value_backend.h file_sys/seed_db.cpp file_sys/seed_db.h file_sys/ticket.cpp @@ -445,6 +453,8 @@ add_library(citra_core STATIC hw/y2r.h loader/3dsx.cpp loader/3dsx.h + loader/artic.cpp + loader/artic.h loader/elf.cpp loader/elf.h loader/loader.cpp @@ -470,7 +480,7 @@ add_library(citra_core STATIC tracer/citrace.h tracer/recorder.cpp tracer/recorder.h -) + ) create_target_directory_groups(citra_core) diff --git a/src/core/core.cpp b/src/core/core.cpp index 3db61dfcd..9f4fcf406 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -256,7 +256,11 @@ System::ResultStatus System::SingleStep() { System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath, Frontend::EmuWindow* secondary_window) { FileUtil::SetCurrentRomPath(filepath); - app_loader = Loader::GetLoader(filepath); + if (early_app_loader) { + app_loader = std::move(early_app_loader); + } else { + app_loader = Loader::GetLoader(filepath); + } if (!app_loader) { LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath); return ResultStatus::ErrorGetLoader; @@ -286,6 +290,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st return ResultStatus::ErrorLoader_ErrorInvalidFormat; case Loader::ResultStatus::ErrorGbaTitle: return ResultStatus::ErrorLoader_ErrorGbaTitle; + case Loader::ResultStatus::ErrorArtic: + return ResultStatus::ErrorArticDisconnected; default: return ResultStatus::ErrorSystemMode; } @@ -334,6 +340,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st return ResultStatus::ErrorLoader_ErrorInvalidFormat; case Loader::ResultStatus::ErrorGbaTitle: return ResultStatus::ErrorLoader_ErrorGbaTitle; + case Loader::ResultStatus::ErrorArtic: + return ResultStatus::ErrorArticDisconnected; default: return ResultStatus::ErrorLoader; } @@ -691,6 +699,10 @@ void System::ApplySettings() { } } +void System::RegisterAppLoaderEarly(std::unique_ptr& loader) { + early_app_loader = std::move(loader); +} + template void System::serialize(Archive& ar, const unsigned int file_version) { diff --git a/src/core/core.h b/src/core/core.h index 50d31ff74..a395360ac 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -99,6 +99,7 @@ public: ///< Console ErrorSystemFiles, ///< Error in finding system files ErrorSavestate, ///< Error saving or loading + ErrorArticDisconnected, ///< Error when artic base disconnects ShutdownRequested, ///< Emulated program requested a system shutdown ErrorUnknown ///< Any other error }; @@ -169,6 +170,18 @@ public: [[nodiscard]] PerfStats::Results GetAndResetPerfStats(); + void ReportArticTraffic(u32 bytes) { + if (perf_stats) { + perf_stats->AddArticBaseTraffic(bytes); + } + } + + void ReportPerfArticEvent(PerfStats::PerfArticEventBits event, bool set) { + if (perf_stats) { + perf_stats->ReportPerfArticEvent(event, set); + } + } + [[nodiscard]] PerfStats::Results GetLastPerfStats(); /** @@ -346,6 +359,8 @@ public: /// Applies any changes to settings to this core instance. void ApplySettings(); + void RegisterAppLoaderEarly(std::unique_ptr& loader); + private: /** * Initialize the emulated system. @@ -366,6 +381,9 @@ private: /// AppLoader used to load the current executing application std::unique_ptr app_loader; + // Temporary app loader passed from frontend + std::unique_ptr early_app_loader; + /// ARM11 CPU core std::vector> cpu_cores; ARM_Interface* running_core = nullptr; diff --git a/src/core/file_sys/archive_artic.cpp b/src/core/file_sys/archive_artic.cpp new file mode 100644 index 000000000..a4eb4a599 --- /dev/null +++ b/src/core/file_sys/archive_artic.cpp @@ -0,0 +1,557 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "archive_artic.h" + +namespace FileSys { + +std::vector ArticArchive::BuildFSPath(const Path& path) { + std::vector ret(sizeof(u32) * 2); + u32* raw_data = reinterpret_cast(ret.data()); + auto path_type = path.GetType(); + auto binary = path.AsBinary(); + raw_data[0] = static_cast(path_type); + raw_data[1] = static_cast(binary.size()); + if (!binary.empty()) { + ret.insert(ret.end(), binary.begin(), binary.end()); + } + + // The insert may have invalidated the pointer + raw_data = reinterpret_cast(ret.data()); + if (path_type != LowPathType::Binary && path_type != LowPathType::Invalid) { + if (path_type == LowPathType::Wchar) { + raw_data[1] += 2; + ret.push_back(0); + ret.push_back(0); + } else { + raw_data[1] += 1; + ret.push_back(0); + } + } + + return ret; +} + +Result ArticArchive::RespResult(const std::optional& resp) { + if (!resp.has_value() || !resp->Succeeded()) { + return ResultUnknown; + } + return Result(static_cast(resp->GetMethodResult())); +} + +ArticArchive::~ArticArchive() { + if (clear_cache_on_close) { + cache_provider->ClearAllCache(); + } + if (archive_handle != -1) { + auto req = client->NewRequest("FSUSER_CloseArchive"); + req.AddParameterS64(archive_handle); + client->Send(req); + if (report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } + } +} + +ResultVal> ArticArchive::Open( + std::shared_ptr& client, Service::FS::ArchiveIdCode archive_id, + const Path& path, Core::PerfStats::PerfArticEventBits report_artic_event, + ArticCacheProvider& cache_provider, bool clear_cache_on_close) { + + auto req = client->NewRequest("FSUSER_OpenArchive"); + + req.AddParameterS32(static_cast(archive_id)); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) { + return ResultUnknown; + } + Result res(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto handle_opt = resp->GetResponseS64(0); + if (!handle_opt.has_value()) { + return ResultUnknown; + } + + return std::make_unique(client, *handle_opt, report_artic_event, cache_provider, + path, clear_cache_on_close); +} + +void ArticArchive::Close() { + if (clear_cache_on_close) { + cache_provider->ClearAllCache(); + } + + auto req = client->NewRequest("FSUSER_CloseArchive"); + req.AddParameterS64(archive_handle); + if (RespResult(client->Send(req)).IsSuccess()) { + archive_handle = -1; + if (report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } + } +} + +std::string ArticArchive::GetName() const { + return "ArticArchive"; +} + +ResultVal> ArticArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { + if (mode.create_flag) { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), false); + if (cache != nullptr) { + cache->Clear(); + } + } + auto req = client->NewRequest("FSUSER_OpenFile"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + req.AddParameterU32(mode.hex); + req.AddParameterU32(attributes); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + auto handle_opt = resp->GetResponseS32(0); + if (!handle_opt.has_value()) + return ResultUnknown; + + auto size_opt = resp->GetResponseU64(1); + if (size_opt.has_value()) { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), true); + if (cache != nullptr) { + cache->ForceSetSize(static_cast(*size_opt)); + } + } + + if (open_reporter->open_files++ == 0 && + report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event) | (1ULL << 32)); + } + + return std::make_unique(client, *handle_opt, open_reporter, archive_path, + *cache_provider, path); +} + +Result ArticArchive::DeleteFile(const Path& path) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), false); + if (cache != nullptr) { + cache->Clear(); + } + + auto req = client->NewRequest("FSUSER_DeleteFile"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::RenameFile(const Path& src_path, const Path& dest_path) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, src_path), false); + if (cache != nullptr) { + cache->Clear(); + } + cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, dest_path), false); + if (cache != nullptr) { + cache->Clear(); + } + + auto req = client->NewRequest("FSUSER_RenameFile"); + + req.AddParameterS64(archive_handle); + auto src_path_buf = BuildFSPath(src_path); + req.AddParameterBuffer(src_path_buf.data(), src_path_buf.size()); + req.AddParameterS64(archive_handle); + auto dest_path_buf = BuildFSPath(dest_path); + req.AddParameterBuffer(dest_path_buf.data(), dest_path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::DeleteDirectory(const Path& path) const { + cache_provider->ClearAllCache(); + + auto req = client->NewRequest("FSUSER_DeleteDirectory"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::DeleteDirectoryRecursively(const Path& path) const { + cache_provider->ClearAllCache(); + + auto req = client->NewRequest("FSUSER_DeleteDirectoryRec"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::CreateFile(const Path& path, u64 size, u32 attributes) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, path), false); + if (cache != nullptr) { + cache->Clear(); + } + + auto req = client->NewRequest("FSUSER_CreateFile"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + req.AddParameterU32(attributes); + req.AddParameterU64(size); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::CreateDirectory(const Path& path, u32 attributes) const { + auto req = client->NewRequest("FSUSER_CreateDirectory"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + req.AddParameterU32(attributes); + + return RespResult(client->Send(req)); +} + +Result ArticArchive::RenameDirectory(const Path& src_path, const Path& dest_path) const { + cache_provider->ClearAllCache(); + + auto req = client->NewRequest("FSUSER_RenameDirectory"); + + req.AddParameterS64(archive_handle); + auto src_path_buf = BuildFSPath(src_path); + req.AddParameterBuffer(src_path_buf.data(), src_path_buf.size()); + req.AddParameterS64(archive_handle); + auto dest_path_buf = BuildFSPath(dest_path); + req.AddParameterBuffer(dest_path_buf.data(), dest_path_buf.size()); + + return RespResult(client->Send(req)); +} + +ResultVal> ArticArchive::OpenDirectory(const Path& path) { + auto req = client->NewRequest("FSUSER_OpenDirectory"); + + req.AddParameterS64(archive_handle); + auto path_buf = BuildFSPath(path); + req.AddParameterBuffer(path_buf.data(), path_buf.size()); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + auto handle_opt = resp->GetResponseS32(0); + if (!handle_opt.has_value()) + return ResultUnknown; + + if (open_reporter->open_files++ == 0 && + report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event) | (1ULL << 32)); + } + + return std::make_unique(client, *handle_opt, archive_path, + open_reporter); +} + +u64 ArticArchive::GetFreeBytes() const { + auto req = client->NewRequest("FSUSER_GetFreeBytes"); + + req.AddParameterS64(archive_handle); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) // TODO(PabloMK7): Return error code and not u64 + return 0; + + auto free_bytes_opt = resp->GetResponseS64(0); + return free_bytes_opt.has_value() ? static_cast(*free_bytes_opt) : 0; +} + +Result ArticArchive::Control(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) { + auto req = client->NewRequest("FSUSER_ControlArchive"); + + req.AddParameterS64(archive_handle); + req.AddParameterU32(action); + req.AddParameterBuffer(input, input_size); + req.AddParameterU32(static_cast(output_size)); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != output_size) + return ResultUnknown; + + memcpy(output, output_buf->first, output_buf->second); + return res; +} + +Result ArticArchive::SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) { + auto req = client->NewRequest("FSUSER_SetSaveDataSecureValue"); + + req.AddParameterS64(archive_handle); + req.AddParameterU32(secure_value_slot); + req.AddParameterU64(secure_value); + req.AddParameterS8(flush != 0); + + return RespResult(client->Send(req)); +} + +ResultVal> ArticArchive::GetSaveDataSecureValue(u32 secure_value_slot) { + auto req = client->NewRequest("FSUSER_GetSaveDataSecureValue"); + + req.AddParameterS64(archive_handle); + req.AddParameterU32(secure_value_slot); + + auto resp = client->Send(req); + auto res = RespResult(resp); + if (res.IsError()) + return res; + + struct { + bool exists; + bool isGamecard; + u64 secure_value; + } secure_value_result; + static_assert(sizeof(secure_value_result) == 0x10); + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != sizeof(secure_value_result)) + return ResultUnknown; + + memcpy(&secure_value_result, output_buf->first, output_buf->second); + return std::make_tuple(secure_value_result.exists, secure_value_result.isGamecard, + secure_value_result.secure_value); +} + +void ArticArchive::OpenFileReporter::OnFileClosed() { + if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } +} + +void ArticArchive::OpenFileReporter::OnDirectoryClosed() { + if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) { + client->ReportArticEvent(static_cast(report_artic_event)); + } +} + +ArticFileBackend::~ArticFileBackend() { + if (file_handle != -1) { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(file_handle); + client->Send(req); + open_reporter->OnFileClosed(); + } +} + +ResultVal ArticFileBackend::Read(u64 offset, std::size_t length, u8* buffer) const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + + if (cache != nullptr && (offset + static_cast(length)) < GetSize()) { + return cache->Read(file_handle, offset, length, buffer); + } + + size_t read_amount = 0; + while (read_amount != length) { + size_t to_read = + std::min(client->GetServerRequestMaxSize() - 0x100, length - read_amount); + + auto req = client->NewRequest("FSFILE_Read"); + req.AddParameterS32(file_handle); + req.AddParameterS64(static_cast(offset + read_amount)); + req.AddParameterS32(static_cast(to_read)); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto read_buff = resp->GetResponseBuffer(0); + size_t actually_read = 0; + if (read_buff.has_value()) { + actually_read = read_buff->second; + memcpy(buffer + read_amount, read_buff->first, actually_read); + } + + read_amount += actually_read; + if (actually_read != to_read) + break; + } + return read_amount; +} + +ResultVal ArticFileBackend::Write(u64 offset, std::size_t length, bool flush, + bool update_timestamp, const u8* buffer) { + u32 flags = (flush ? 1 : 0) | (update_timestamp ? (1 << 8) : 0); + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + if (cache != nullptr) { + return cache->Write(file_handle, offset, length, buffer, flags); + } else { + size_t written_amount = 0; + while (written_amount != length) { + size_t to_write = std::min(client->GetServerRequestMaxSize() - 0x100, + length - written_amount); + + auto req = client->NewRequest("FSFILE_Write"); + req.AddParameterS32(file_handle); + req.AddParameterS64(static_cast(offset + written_amount)); + req.AddParameterS32(static_cast(to_write)); + req.AddParameterS32(static_cast(flags)); + req.AddParameterBuffer(buffer + written_amount, to_write); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto actually_written_opt = resp->GetResponseS32(0); + if (!actually_written_opt.has_value()) + return Result(-1); + + size_t actually_written = static_cast(actually_written_opt.value()); + + written_amount += actually_written; + if (actually_written != to_write) + break; + } + return written_amount; + } +} + +u64 ArticFileBackend::GetSize() const { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + if (cache != nullptr) { + auto res = cache->GetSize(file_handle); + if (res.Failed()) + return 0; + return res.Unwrap(); + } else { + + auto req = client->NewRequest("FSFILE_GetSize"); + + req.AddParameterS32(file_handle); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return 0; + + auto size_buf = resp->GetResponseS64(0); + if (!size_buf) { + return 0; + } + return *size_buf; + } +} + +bool ArticFileBackend::SetSize(u64 size) const { + auto req = client->NewRequest("FSFILE_SetSize"); + + req.AddParameterS32(file_handle); + req.AddParameterU64(size); + + return ArticArchive::RespResult(client->Send(req)).IsSuccess(); +} + +bool ArticFileBackend::Close() { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(file_handle); + bool ret = ArticArchive::RespResult(client->Send(req)).IsSuccess(); + if (ret) { + file_handle = -1; + open_reporter->OnFileClosed(); + } + return ret; +} + +void ArticFileBackend::Flush() const { + auto req = client->NewRequest("FSFILE_Flush"); + + req.AddParameterS32(file_handle); + + client->Send(req); +} + +ArticDirectoryBackend::~ArticDirectoryBackend() { + if (dir_handle != -1) { + auto req = client->NewRequest("FSDIR_Close"); + req.AddParameterS32(dir_handle); + client->Send(req); + open_reporter->OnDirectoryClosed(); + } +} + +u32 ArticDirectoryBackend::Read(const u32 count, Entry* entries) { + auto req = client->NewRequest("FSDIR_Read"); + + req.AddParameterS32(dir_handle); + req.AddParameterU32(count); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return 0; + + auto entry_buf = resp->GetResponseBuffer(0); + if (!entry_buf) { + return 0; + } + u32 ret_count = static_cast(entry_buf->second / sizeof(Entry)); + + memcpy(entries, entry_buf->first, ret_count * sizeof(Entry)); + return ret_count; +} + +bool ArticDirectoryBackend::Close() { + auto req = client->NewRequest("FSDIR_Close"); + req.AddParameterS32(dir_handle); + bool ret = ArticArchive::RespResult(client->Send(req)).IsSuccess(); + if (ret) { + dir_handle = -1; + open_reporter->OnDirectoryClosed(); + } + return ret; +} +} // namespace FileSys diff --git a/src/core/file_sys/archive_artic.h b/src/core/file_sys/archive_artic.h new file mode 100644 index 000000000..beb0f63b2 --- /dev/null +++ b/src/core/file_sys/archive_artic.h @@ -0,0 +1,268 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "atomic" + +#include +#include "common/common_types.h" +#include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" +#include "core/file_sys/directory_backend.h" +#include "core/file_sys/file_backend.h" +#include "core/hle/service/fs/archive.h" +#include "core/perf_stats.h" +#include "network/artic_base/artic_base_client.h" + +namespace FileSys { + +class ArticArchive : public ArchiveBackend { +public: + static std::vector BuildFSPath(const Path& path); + static Result RespResult(const std::optional& resp); + + explicit ArticArchive(std::shared_ptr& _client, s64 _archive_handle, + Core::PerfStats::PerfArticEventBits _report_artic_event, + ArticCacheProvider& _cache_provider, const Path& _archive_path, + bool _clear_cache_on_close) + : client(_client), archive_handle(_archive_handle), report_artic_event(_report_artic_event), + cache_provider(&_cache_provider), archive_path(_archive_path), + clear_cache_on_close(_clear_cache_on_close) { + open_reporter = std::make_shared(_client, _report_artic_event); + } + ~ArticArchive() override; + + static ResultVal> Open( + std::shared_ptr& client, Service::FS::ArchiveIdCode archive_id, + const Path& path, Core::PerfStats::PerfArticEventBits report_artic_event, + ArticCacheProvider& cache_provider, bool clear_cache_on_close); + + void Close() override; + + /** + * Get a descriptive name for the archive (e.g. "RomFS", "SaveData", etc.) + */ + std::string GetName() const override; + + /** + * Open a file specified by its path, using the specified mode + * @param path Path relative to the archive + * @param mode Mode to open the file with + * @return Opened file, or error code + */ + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; + + /** + * Delete a file specified by its path + * @param path Path relative to the archive + * @return Result of the operation + */ + Result DeleteFile(const Path& path) const override; + + /** + * Rename a File specified by its path + * @param src_path Source path relative to the archive + * @param dest_path Destination path relative to the archive + * @return Result of the operation + */ + Result RenameFile(const Path& src_path, const Path& dest_path) const override; + + /** + * Delete a directory specified by its path + * @param path Path relative to the archive + * @return Result of the operation + */ + Result DeleteDirectory(const Path& path) const override; + + /** + * Delete a directory specified by its path and anything under it + * @param path Path relative to the archive + * @return Result of the operation + */ + Result DeleteDirectoryRecursively(const Path& path) const override; + + /** + * Create a file specified by its path + * @param path Path relative to the Archive + * @param size The size of the new file, filled with zeroes + * @return Result of the operation + */ + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + + /** + * Create a directory specified by its path + * @param path Path relative to the archive + * @return Result of the operation + */ + Result CreateDirectory(const Path& path, u32 attributes) const override; + + /** + * Rename a Directory specified by its path + * @param src_path Source path relative to the archive + * @param dest_path Destination path relative to the archive + * @return Result of the operation + */ + Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; + + /** + * Open a directory specified by its path + * @param path Path relative to the archive + * @return Opened directory, or error code + */ + ResultVal> OpenDirectory(const Path& path) override; + + /** + * Get the free space + * @return The number of free bytes in the archive + */ + u64 GetFreeBytes() const override; + + Result Control(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) override; + + Result SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) override; + + ResultVal> GetSaveDataSecureValue(u32 secure_value_slot) override; + + bool IsSlow() override { + return true; + } + + const Path& GetArchivePath() { + return archive_path; + } + +protected: + ArticArchive() = default; + +private: + friend class ArticFileBackend; + friend class ArticDirectoryBackend; + class OpenFileReporter { + public: + OpenFileReporter(const std::shared_ptr& cli, + Core::PerfStats::PerfArticEventBits _report_artic_event) + : client(cli), report_artic_event(_report_artic_event) {} + + void OnFileClosed(); + + void OnDirectoryClosed(); + + std::shared_ptr client; + Core::PerfStats::PerfArticEventBits report_artic_event = + Core::PerfStats::PerfArticEventBits::NONE; + std::atomic open_files = 0; + }; + + std::shared_ptr client; + s64 archive_handle; + std::shared_ptr open_reporter; + Core::PerfStats::PerfArticEventBits report_artic_event = + Core::PerfStats::PerfArticEventBits::NONE; + ArticCacheProvider* cache_provider = nullptr; + Path archive_path; + bool clear_cache_on_close; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& archive_handle; + } + friend class boost::serialization::access; +}; + +class ArticFileBackend : public FileBackend { +public: + explicit ArticFileBackend(std::shared_ptr& _client, + s32 _file_handle, + const std::shared_ptr& _open_reporter, + const Path& _archive_path, ArticCacheProvider& _cache_provider, + const Path& _file_path) + : client(_client), file_handle(_file_handle), open_reporter(_open_reporter), + archive_path(_archive_path), cache_provider(&_cache_provider), file_path(_file_path) {} + ~ArticFileBackend() override; + + ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; + + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, + const u8* buffer) override; + + u64 GetSize() const override; + + bool SetSize(u64 size) const override; + + bool Close() override; + + void Flush() const override; + + bool AllowsCachedReads() const override { + return true; + } + + bool CacheReady(std::size_t file_offset, std::size_t length) override { + auto cache = cache_provider->ProvideCache( + client, cache_provider->PathsToVector(archive_path, file_path), true); + if (cache == nullptr) { + return false; + } + return cache->CacheReady(file_offset, length); + } + +protected: + ArticFileBackend() = default; + +private: + std::shared_ptr client; + s32 file_handle; + std::shared_ptr open_reporter; + Path archive_path; + ArticCacheProvider* cache_provider = nullptr; + Path file_path; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& file_handle; + } + friend class boost::serialization::access; +}; + +class ArticDirectoryBackend : public DirectoryBackend { +public: + explicit ArticDirectoryBackend( + std::shared_ptr& _client, s32 _dir_handle, + const Path& _archive_path, + const std::shared_ptr& _open_reporter) + : client(_client), dir_handle(_dir_handle), archive_path(_archive_path), + open_reporter(_open_reporter) {} + ~ArticDirectoryBackend() override; + + u32 Read(const u32 count, Entry* entries) override; + bool Close() override; + + bool IsSlow() override { + return true; + } + +protected: + ArticDirectoryBackend() = default; + +private: + std::shared_ptr client; + s32 dir_handle; + Path archive_path; + std::shared_ptr open_reporter; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& dir_handle; + } + friend class boost::serialization::access; +}; +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::ArticArchive) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticFileBackend) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticDirectoryBackend) \ No newline at end of file diff --git a/src/core/file_sys/archive_backend.cpp b/src/core/file_sys/archive_backend.cpp index bc4df30d0..1600171fa 100644 --- a/src/core/file_sys/archive_backend.cpp +++ b/src/core/file_sys/archive_backend.cpp @@ -105,8 +105,7 @@ std::vector Path::AsBinary() const { std::vector to_return(u16str.size() * 2); for (std::size_t i = 0; i < u16str.size(); ++i) { u16 tmp_char = u16str.at(i); - to_return[i * 2] = (tmp_char & 0xFF00) >> 8; - to_return[i * 2 + 1] = (tmp_char & 0x00FF); + *reinterpret_cast(to_return.data() + i * 2) = tmp_char; } return to_return; } diff --git a/src/core/file_sys/archive_backend.h b/src/core/file_sys/archive_backend.h index 7eb3893ba..7997c72ab 100644 --- a/src/core/file_sys/archive_backend.h +++ b/src/core/file_sys/archive_backend.h @@ -103,6 +103,7 @@ struct ArchiveFormatInfo { u8 duplicate_data; ///< Whether the archive should duplicate the data. }; static_assert(std::is_trivial_v, "ArchiveFormatInfo is not POD"); +static_assert(sizeof(ArchiveFormatInfo) == 16, "Invalid ArchiveFormatInfo size"); class ArchiveBackend : NonCopyable { public: @@ -119,8 +120,8 @@ public: * @param mode Mode to open the file with * @return Opened file, or error code */ - virtual ResultVal> OpenFile(const Path& path, - const Mode& mode) const = 0; + virtual ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes = 0) = 0; /** * Delete a file specified by its path @@ -157,14 +158,14 @@ public: * @param size The size of the new file, filled with zeroes * @return Result of the operation */ - virtual Result CreateFile(const Path& path, u64 size) const = 0; + virtual Result CreateFile(const Path& path, u64 size, u32 attributes = 0) const = 0; /** * Create a directory specified by its path * @param path Path relative to the archive * @return Result of the operation */ - virtual Result CreateDirectory(const Path& path) const = 0; + virtual Result CreateDirectory(const Path& path, u32 attributes = 0) const = 0; /** * Rename a Directory specified by its path @@ -179,7 +180,7 @@ public: * @param path Path relative to the archive * @return Opened directory, or error code */ - virtual ResultVal> OpenDirectory(const Path& path) const = 0; + virtual ResultVal> OpenDirectory(const Path& path) = 0; /** * Get the free space @@ -187,6 +188,20 @@ public: */ virtual u64 GetFreeBytes() const = 0; + /** + * Close the archive + */ + virtual void Close() {} + + virtual Result Control(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) { + LOG_WARNING(Service_FS, + "(STUBBED) called, archive={}, action={:08X}, input_size={:08X}, " + "output_size={:08X}", + GetName(), action, input_size, output_size); + return ResultSuccess; + } + u64 GetOpenDelayNs() { if (delay_generator != nullptr) { return delay_generator->GetOpenDelayNs(); @@ -196,6 +211,31 @@ public: return delay_generator->GetOpenDelayNs(); } + virtual Result SetSaveDataSecureValue(u32 secure_value_slot, u64 secure_value, bool flush) { + + // TODO: Generate and Save the Secure Value + + LOG_WARNING(Service_FS, + "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:04X} " + "flush={}", + secure_value, secure_value_slot, flush); + + return ResultSuccess; + } + + virtual ResultVal> GetSaveDataSecureValue(u32 secure_value_slot) { + + // TODO: Implement Secure Value Lookup & Generation + + LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X}", secure_value_slot); + + return std::make_tuple(false, true, 0); + } + + virtual bool IsSlow() { + return false; + } + protected: std::unique_ptr delay_generator; @@ -232,7 +272,7 @@ public: * @return Result of the operation, 0 on success */ virtual Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) = 0; + u64 program_id, u32 directory_buckets, u32 file_buckets) = 0; /** * Retrieves the format info about the archive with the specified path @@ -242,6 +282,10 @@ public: */ virtual ResultVal GetFormatInfo(const Path& path, u64 program_id) const = 0; + virtual bool IsSlow() { + return false; + } + template void serialize(Archive& ar, const unsigned int) {} friend class boost::serialization::access; diff --git a/src/core/file_sys/archive_extsavedata.cpp b/src/core/file_sys/archive_extsavedata.cpp index cfa97fe7f..4e9ea6ef8 100644 --- a/src/core/file_sys/archive_extsavedata.cpp +++ b/src/core/file_sys/archive_extsavedata.cpp @@ -10,6 +10,7 @@ #include "common/common_types.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "core/file_sys/archive_artic.h" #include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/disk_archive.h" #include "core/file_sys/errors.h" @@ -37,7 +38,7 @@ public: return false; } - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override { if (offset > size) { return ResultWriteBeyondEnd; @@ -49,7 +50,7 @@ public: length = size - offset; } - return DiskFile::Write(offset, length, flush, buffer); + return DiskFile::Write(offset, length, flush, update_timestamp, buffer); } private: @@ -100,8 +101,8 @@ public: return "ExtSaveDataArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override { + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override { LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex); const PathParser path_parser(path); @@ -234,69 +235,193 @@ Path ArchiveFactory_ExtSaveData::GetCorrectedPath(const Path& path) { return {binary_data}; } -ResultVal> ArchiveFactory_ExtSaveData::Open(const Path& path, - u64 program_id) { - const auto directory = type == ExtSaveDataType::Boss ? "boss/" : "user/"; - const auto fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + directory; - if (!FileUtil::Exists(fullpath)) { - // TODO(Subv): Verify the archive behavior of SharedExtSaveData compared to ExtSaveData. - // ExtSaveData seems to return FS_NotFound (120) when the archive doesn't exist. - if (type != ExtSaveDataType::Shared) { - return ResultNotFoundInvalidState; - } else { - return ResultNotFormatted; - } +static Service::FS::ArchiveIdCode ExtSaveDataTypeToArchiveID(ExtSaveDataType type) { + switch (type) { + case FileSys::ExtSaveDataType::Normal: + return Service::FS::ArchiveIdCode::ExtSaveData; + case FileSys::ExtSaveDataType::Shared: + return Service::FS::ArchiveIdCode::SharedExtSaveData; + case FileSys::ExtSaveDataType::Boss: + return Service::FS::ArchiveIdCode::BossExtSaveData; + default: + return Service::FS::ArchiveIdCode::ExtSaveData; } - std::unique_ptr delay_generator = std::make_unique(); - return std::make_unique(fullpath, std::move(delay_generator)); } -Result ArchiveFactory_ExtSaveData::Format(const Path& path, - const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { - auto corrected_path = GetCorrectedPath(path); - - // These folders are always created with the ExtSaveData - std::string user_path = GetExtSaveDataPath(mount_point, corrected_path) + "user/"; - std::string boss_path = GetExtSaveDataPath(mount_point, corrected_path) + "boss/"; - FileUtil::CreateFullPath(user_path); - FileUtil::CreateFullPath(boss_path); - - // Write the format metadata - std::string metadata_path = GetExtSaveDataPath(mount_point, corrected_path) + "metadata"; - FileUtil::IOFile file(metadata_path, "wb"); - - if (!file.IsOpen()) { - // TODO(Subv): Find the correct error code - return ResultUnknown; +static Core::PerfStats::PerfArticEventBits ExtSaveDataTypeToPerfArtic(ExtSaveDataType type) { + switch (type) { + case FileSys::ExtSaveDataType::Normal: + return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA; + case FileSys::ExtSaveDataType::Shared: + return Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA; + case FileSys::ExtSaveDataType::Boss: + return Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA; + default: + return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA; } +} - file.WriteBytes(&format_info, sizeof(format_info)); - return ResultSuccess; +ResultVal> ArchiveFactory_ExtSaveData::Open(const Path& path, + u64 program_id) { + if (IsUsingArtic()) { + EnsureCacheCreated(); + return ArticArchive::Open(artic_client, ExtSaveDataTypeToArchiveID(type), path, + ExtSaveDataTypeToPerfArtic(type), *this, + type != FileSys::ExtSaveDataType::Normal); + } else { + const auto directory = type == ExtSaveDataType::Boss ? "boss/" : "user/"; + const auto fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + directory; + if (!FileUtil::Exists(fullpath)) { + // TODO(Subv): Verify the archive behavior of SharedExtSaveData compared to ExtSaveData. + // ExtSaveData seems to return FS_NotFound (120) when the archive doesn't exist. + if (type != ExtSaveDataType::Shared) { + return ResultNotFoundInvalidState; + } else { + return ResultNotFormatted; + } + } + std::unique_ptr delay_generator = + std::make_unique(); + return std::make_unique(fullpath, std::move(delay_generator)); + } +} + +Result ArchiveFactory_ExtSaveData::FormatAsExtData(const Path& path, + const FileSys::ArchiveFormatInfo& format_info, + u8 unknown, u64 program_id, u64 total_size, + std::optional> icon) { + if (IsUsingArtic()) { + if (!icon.has_value()) { + LOG_ERROR(Service_FS, "No icon provided while using Artic Base"); + return ResultUnknown; + } + + ExtSaveDataArchivePath path_data; + std::memcpy(&path_data, path.AsBinary().data(), sizeof(path_data)); + + Service::FS::ExtSaveDataInfo artic_extdata_path; + + artic_extdata_path.media_type = static_cast(path_data.media_type); + artic_extdata_path.unknown = unknown; + artic_extdata_path.save_id_low = path_data.save_low; + artic_extdata_path.save_id_high = path_data.save_high; + + auto req = artic_client->NewRequest("FSUSER_CreateExtSaveData"); + + req.AddParameterBuffer(&artic_extdata_path, sizeof(artic_extdata_path)); + req.AddParameterU32(format_info.number_directories); + req.AddParameterU32(format_info.number_files); + req.AddParameterU64(total_size); + req.AddParameterBuffer(icon->data(), icon->size()); + + return ArticArchive::RespResult(artic_client->Send(req)); + } else { + auto corrected_path = GetCorrectedPath(path); + + // These folders are always created with the ExtSaveData + std::string user_path = GetExtSaveDataPath(mount_point, corrected_path) + "user/"; + std::string boss_path = GetExtSaveDataPath(mount_point, corrected_path) + "boss/"; + FileUtil::CreateFullPath(user_path); + FileUtil::CreateFullPath(boss_path); + + // Write the format metadata + std::string metadata_path = GetExtSaveDataPath(mount_point, corrected_path) + "metadata"; + FileUtil::IOFile file(metadata_path, "wb"); + + if (!file.IsOpen()) { + // TODO(Subv): Find the correct error code + return ResultUnknown; + } + + file.WriteBytes(&format_info, sizeof(format_info)); + + if (icon.has_value()) { + FileUtil::IOFile icon_file(FileSys::GetExtSaveDataPath(GetMountPoint(), path) + "icon", + "wb"); + icon_file.WriteBytes(icon->data(), icon->size()); + } + return ResultSuccess; + } +} + +Result ArchiveFactory_ExtSaveData::DeleteExtData(Service::FS::MediaType media_type, u8 unknown, + u32 high, u32 low) { + if (IsUsingArtic()) { + Service::FS::ExtSaveDataInfo artic_extdata_path; + + artic_extdata_path.media_type = static_cast(media_type); + artic_extdata_path.unknown = unknown; + artic_extdata_path.save_id_low = low; + artic_extdata_path.save_id_high = high; + + auto req = artic_client->NewRequest("FSUSER_DeleteExtSaveData"); + + req.AddParameterBuffer(&artic_extdata_path, sizeof(artic_extdata_path)); + + return ArticArchive::RespResult(artic_client->Send(req)); + } else { + // Construct the binary path to the archive first + FileSys::Path path = + FileSys::ConstructExtDataBinaryPath(static_cast(media_type), high, low); + + std::string media_type_directory; + if (media_type == Service::FS::MediaType::NAND) { + media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); + } else if (media_type == Service::FS::MediaType::SDMC) { + media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + } else { + LOG_ERROR(Service_FS, "Unsupported media type {}", media_type); + return ResultUnknown; // TODO(Subv): Find the right error code + } + + // Delete all directories (/user, /boss) and the icon file. + std::string base_path = FileSys::GetExtDataContainerPath( + media_type_directory, media_type == Service::FS::MediaType::NAND); + std::string extsavedata_path = FileSys::GetExtSaveDataPath(base_path, path); + if (FileUtil::Exists(extsavedata_path) && !FileUtil::DeleteDirRecursively(extsavedata_path)) + return ResultUnknown; // TODO(Subv): Find the right error code + return ResultSuccess; + } } ResultVal ArchiveFactory_ExtSaveData::GetFormatInfo(const Path& path, u64 program_id) const { - std::string metadata_path = GetExtSaveDataPath(mount_point, path) + "metadata"; - FileUtil::IOFile file(metadata_path, "rb"); + if (IsUsingArtic()) { + auto req = artic_client->NewRequest("FSUSER_GetFormatInfo"); - if (!file.IsOpen()) { - LOG_ERROR(Service_FS, "Could not open metadata information for archive"); - // TODO(Subv): Verify error code - return ResultNotFormatted; + req.AddParameterS32(static_cast(ExtSaveDataTypeToArchiveID(type))); + auto path_artic = ArticArchive::BuildFSPath(path); + req.AddParameterBuffer(path_artic.data(), path_artic.size()); + + auto resp = artic_client->Send(req); + Result res = ArticArchive::RespResult(resp); + if (R_FAILED(res)) { + return res; + } + + auto info_buf = resp->GetResponseBuffer(0); + if (!info_buf.has_value() || info_buf->second != sizeof(ArchiveFormatInfo)) { + return ResultUnknown; + } + + ArchiveFormatInfo info; + memcpy(&info, info_buf->first, sizeof(info)); + return info; + } else { + std::string metadata_path = GetExtSaveDataPath(mount_point, path) + "metadata"; + FileUtil::IOFile file(metadata_path, "rb"); + + if (!file.IsOpen()) { + LOG_ERROR(Service_FS, "Could not open metadata information for archive"); + // TODO(Subv): Verify error code + return ResultNotFormatted; + } + + ArchiveFormatInfo info = {}; + file.ReadBytes(&info, sizeof(info)); + return info; } - - ArchiveFormatInfo info = {}; - file.ReadBytes(&info, sizeof(info)); - return info; } - -void ArchiveFactory_ExtSaveData::WriteIcon(const Path& path, std::span icon) { - std::string game_path = FileSys::GetExtSaveDataPath(GetMountPoint(), path); - FileUtil::IOFile icon_file(game_path + "icon", "wb"); - icon_file.WriteBytes(icon.data(), icon.size()); -} - } // namespace FileSys SERIALIZE_EXPORT_IMPL(FileSys::ExtSaveDataDelayGenerator) diff --git a/src/core/file_sys/archive_extsavedata.h b/src/core/file_sys/archive_extsavedata.h index 5093ecdc1..29589d418 100644 --- a/src/core/file_sys/archive_extsavedata.h +++ b/src/core/file_sys/archive_extsavedata.h @@ -5,13 +5,17 @@ #pragma once #include +#include #include #include #include #include #include "common/common_types.h" #include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" #include "core/hle/result.h" +#include "core/hle/service/fs/archive.h" +#include "network/artic_base/artic_base_client.h" namespace FileSys { @@ -22,7 +26,7 @@ enum class ExtSaveDataType { }; /// File system interface to the ExtSaveData archive -class ArchiveFactory_ExtSaveData final : public ArchiveFactory { +class ArchiveFactory_ExtSaveData final : public ArchiveFactory, public ArticCacheProvider { public: ArchiveFactory_ExtSaveData(const std::string& mount_point, ExtSaveDataType type_); @@ -31,21 +35,35 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return IsUsingArtic(); + } + const std::string& GetMountPoint() const { return mount_point; } - /** - * Writes the SMDH icon of the ExtSaveData to file - * @param path Path of this ExtSaveData - * @param icon_data Binary data of the icon - * @param icon_size Size of the icon data - */ - void WriteIcon(const Path& path, std::span icon); + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override { + return UnimplementedFunction(ErrorModule::FS); + }; + + Result FormatAsExtData(const Path& path, const FileSys::ArchiveFormatInfo& format_info, + u8 unknown, u64 program_id, u64 total_size, + std::optional> icon); + + Result DeleteExtData(Service::FS::MediaType media_type, u8 unknown, u32 high, u32 low); + + void RegisterArtic(std::shared_ptr& client) { + artic_client = client; + } + + bool IsUsingArtic() const { + return artic_client.get() != nullptr; + } private: /// Type of ext save data archive being accessed. @@ -61,10 +79,13 @@ private: /// Returns a path with the correct SaveIdHigh value for Shared extdata paths. Path GetCorrectedPath(const Path& path); + std::shared_ptr artic_client = nullptr; + ArchiveFactory_ExtSaveData() = default; template void serialize(Archive& ar, const unsigned int) { ar& boost::serialization::base_object(*this); + ar& boost::serialization::base_object(*this); ar& type; ar& mount_point; } diff --git a/src/core/file_sys/archive_ncch.cpp b/src/core/file_sys/archive_ncch.cpp index 4b3a478e2..54d4639c5 100644 --- a/src/core/file_sys/archive_ncch.cpp +++ b/src/core/file_sys/archive_ncch.cpp @@ -15,6 +15,7 @@ #include "common/string_util.h" #include "common/swap.h" #include "core/core.h" +#include "core/file_sys/archive_artic.h" #include "core/file_sys/archive_ncch.h" #include "core/file_sys/errors.h" #include "core/file_sys/ivfc_archive.h" @@ -69,8 +70,9 @@ Path MakeNCCHFilePath(NCCHFileOpenType open_type, u32 content_index, NCCHFilePat return FileSys::Path(std::move(file)); } -ResultVal> NCCHArchive::OpenFile(const Path& path, - const Mode& mode) const { +ResultVal> NCCHArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { + if (path.GetType() != LowPathType::Binary) { LOG_ERROR(Service_FS, "Path need to be Binary"); return ResultInvalidPath; @@ -207,14 +209,14 @@ Result NCCHArchive::DeleteDirectoryRecursively(const Path& path) const { return ResultUnknown; } -Result NCCHArchive::CreateFile(const Path& path, u64 size) const { +Result NCCHArchive::CreateFile(const Path& path, u64 size, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a file in an NCCH archive ({}).", GetName()); // TODO: Verify error code return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, ErrorLevel::Permanent); } -Result NCCHArchive::CreateDirectory(const Path& path) const { +Result NCCHArchive::CreateDirectory(const Path& path, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a directory in an NCCH archive ({}).", GetName()); // TODO(wwylele): Use correct error code return ResultUnknown; @@ -226,7 +228,7 @@ Result NCCHArchive::RenameDirectory(const Path& src_path, const Path& dest_path) return ResultUnknown; } -ResultVal> NCCHArchive::OpenDirectory(const Path& path) const { +ResultVal> NCCHArchive::OpenDirectory(const Path& path) { LOG_CRITICAL(Service_FS, "Attempted to open a directory within an NCCH archive ({}).", GetName().c_str()); // TODO(shinyquagsire23): Use correct error code @@ -255,7 +257,7 @@ ResultVal NCCHFile::Read(const u64 offset, const std::size_t length } ResultVal NCCHFile::Write(const u64 offset, const std::size_t length, const bool flush, - const u8* buffer) { + const bool update_timestamp, const u8* buffer) { LOG_ERROR(Service_FS, "Attempted to write to NCCH file"); // TODO(shinyquagsire23): Find error code return 0ULL; @@ -274,6 +276,13 @@ ArchiveFactory_NCCH::ArchiveFactory_NCCH() {} ResultVal> ArchiveFactory_NCCH::Open(const Path& path, u64 program_id) { + + if (IsUsingArtic()) { + EnsureCacheCreated(); + return ArticArchive::Open(artic_client, Service::FS::ArchiveIdCode::NCCH, path, + Core::PerfStats::PerfArticEventBits::NONE, *this, false); + } + if (path.GetType() != LowPathType::Binary) { LOG_ERROR(Service_FS, "Path need to be Binary"); return ResultInvalidPath; @@ -293,7 +302,7 @@ ResultVal> ArchiveFactory_NCCH::Open(const Path& } Result ArchiveFactory_NCCH::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, u32 file_buckets) { LOG_ERROR(Service_FS, "Attempted to format a NCCH archive."); // TODO: Verify error code return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, diff --git a/src/core/file_sys/archive_ncch.h b/src/core/file_sys/archive_ncch.h index 3e22ef02a..725752f19 100644 --- a/src/core/file_sys/archive_ncch.h +++ b/src/core/file_sys/archive_ncch.h @@ -11,8 +11,10 @@ #include #include #include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" #include "core/file_sys/file_backend.h" #include "core/hle/result.h" +#include "network/artic_base/artic_base_client.h" namespace Service::FS { enum class MediaType : u32; @@ -48,16 +50,16 @@ public: return "NCCHArchive"; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: @@ -82,11 +84,11 @@ public: explicit NCCHFile(std::vector buffer, std::unique_ptr delay_generator_); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} @@ -105,7 +107,7 @@ private: }; /// File system interface to the NCCH archive -class ArchiveFactory_NCCH final : public ArchiveFactory { +class ArchiveFactory_NCCH final : public ArchiveFactory, public ArticCacheProvider { public: explicit ArchiveFactory_NCCH(); @@ -114,14 +116,29 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return IsUsingArtic(); + } + + void RegisterArtic(std::shared_ptr& client) { + artic_client = client; + } + + bool IsUsingArtic() const { + return artic_client.get() != nullptr; + } + private: + std::shared_ptr artic_client = nullptr; + template void serialize(Archive& ar, const unsigned int) { ar& boost::serialization::base_object(*this); + ar& boost::serialization::base_object(*this); } friend class boost::serialization::access; }; diff --git a/src/core/file_sys/archive_other_savedata.cpp b/src/core/file_sys/archive_other_savedata.cpp index 3944fce6f..c6aaaff0c 100644 --- a/src/core/file_sys/archive_other_savedata.cpp +++ b/src/core/file_sys/archive_other_savedata.cpp @@ -75,12 +75,14 @@ ResultVal> ArchiveFactory_OtherSaveDataPermitted return ResultGamecardNotInserted; } - return sd_savedata_source->Open(program_id); + return sd_savedata_source->Open(Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path, + program_id); } Result ArchiveFactory_OtherSaveDataPermitted::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, + u32 file_buckets) { LOG_ERROR(Service_FS, "Attempted to format a OtherSaveDataPermitted archive."); return ResultInvalidPath; } @@ -96,7 +98,8 @@ ResultVal ArchiveFactory_OtherSaveDataPermitted::GetFormatInf return ResultGamecardNotInserted; } - return sd_savedata_source->GetFormatInfo(program_id); + return sd_savedata_source->GetFormatInfo( + program_id, Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path); } ArchiveFactory_OtherSaveDataGeneral::ArchiveFactory_OtherSaveDataGeneral( @@ -114,12 +117,14 @@ ResultVal> ArchiveFactory_OtherSaveDataGeneral:: return ResultGamecardNotInserted; } - return sd_savedata_source->Open(program_id); + return sd_savedata_source->Open(Service::FS::ArchiveIdCode::OtherSaveDataGeneral, path, + program_id); } Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 /*client_program_id*/) { + u64 /*client_program_id*/, u32 directory_buckets, + u32 file_buckets) { MediaType media_type; u64 program_id; CASCADE_RESULT(std::tie(media_type, program_id), ParsePathGeneral(path)); @@ -129,7 +134,9 @@ Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path, return ResultGamecardNotInserted; } - return sd_savedata_source->Format(program_id, format_info); + return sd_savedata_source->Format(program_id, format_info, + Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path, + directory_buckets, file_buckets); } ResultVal ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo( @@ -143,7 +150,8 @@ ResultVal ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo( return ResultGamecardNotInserted; } - return sd_savedata_source->GetFormatInfo(program_id); + return sd_savedata_source->GetFormatInfo( + program_id, Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path); } } // namespace FileSys diff --git a/src/core/file_sys/archive_other_savedata.h b/src/core/file_sys/archive_other_savedata.h index c9e720a8d..fffcb5481 100644 --- a/src/core/file_sys/archive_other_savedata.h +++ b/src/core/file_sys/archive_other_savedata.h @@ -22,10 +22,14 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return sd_savedata_source->IsUsingArtic(); + } + private: std::shared_ptr sd_savedata_source; @@ -49,8 +53,8 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_savedata.cpp b/src/core/file_sys/archive_savedata.cpp index a5e767b14..aa06f9544 100644 --- a/src/core/file_sys/archive_savedata.cpp +++ b/src/core/file_sys/archive_savedata.cpp @@ -18,18 +18,20 @@ ArchiveFactory_SaveData::ArchiveFactory_SaveData( ResultVal> ArchiveFactory_SaveData::Open(const Path& path, u64 program_id) { - return sd_savedata_source->Open(program_id); + return sd_savedata_source->Open(Service::FS::ArchiveIdCode::SaveData, path, program_id); } Result ArchiveFactory_SaveData::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { - return sd_savedata_source->Format(program_id, format_info); + u64 program_id, u32 directory_buckets, u32 file_buckets) { + return sd_savedata_source->Format(program_id, format_info, Service::FS::ArchiveIdCode::SaveData, + path, directory_buckets, file_buckets); } ResultVal ArchiveFactory_SaveData::GetFormatInfo(const Path& path, u64 program_id) const { - return sd_savedata_source->GetFormatInfo(program_id); + return sd_savedata_source->GetFormatInfo(program_id, Service::FS::ArchiveIdCode::SaveData, + path); } } // namespace FileSys diff --git a/src/core/file_sys/archive_savedata.h b/src/core/file_sys/archive_savedata.h index dba6d5cdb..4b018f214 100644 --- a/src/core/file_sys/archive_savedata.h +++ b/src/core/file_sys/archive_savedata.h @@ -20,11 +20,15 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + bool IsSlow() override { + return sd_savedata_source->IsUsingArtic(); + } + private: std::shared_ptr sd_savedata_source; diff --git a/src/core/file_sys/archive_sdmc.cpp b/src/core/file_sys/archive_sdmc.cpp index 8ba01e7b8..91b1fa680 100644 --- a/src/core/file_sys/archive_sdmc.cpp +++ b/src/core/file_sys/archive_sdmc.cpp @@ -43,8 +43,8 @@ public: SERIALIZE_DELAY_GENERATOR }; -ResultVal> SDMCArchive::OpenFile(const Path& path, - const Mode& mode) const { +ResultVal> SDMCArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { Mode modified_mode; modified_mode.hex = mode.hex; @@ -222,7 +222,7 @@ Result SDMCArchive::DeleteDirectoryRecursively(const Path& path) const { path, mount_point, [](const std::string& p) { return FileUtil::DeleteDirRecursively(p); }); } -Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size) const { +Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -267,7 +267,7 @@ Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size) const { ErrorLevel::Info); } -Result SDMCArchive::CreateDirectory(const Path& path) const { +Result SDMCArchive::CreateDirectory(const Path& path, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -331,7 +331,7 @@ Result SDMCArchive::RenameDirectory(const Path& src_path, const Path& dest_path) ErrorSummary::NothingHappened, ErrorLevel::Status); } -ResultVal> SDMCArchive::OpenDirectory(const Path& path) const { +ResultVal> SDMCArchive::OpenDirectory(const Path& path) { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -392,7 +392,7 @@ ResultVal> ArchiveFactory_SDMC::Open(const Path& } Result ArchiveFactory_SDMC::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, u32 file_buckets) { // This is kind of an undesirable operation, so let's just ignore it. :) return ResultSuccess; } diff --git a/src/core/file_sys/archive_sdmc.h b/src/core/file_sys/archive_sdmc.h index 267b1dfdf..2da56cd2e 100644 --- a/src/core/file_sys/archive_sdmc.h +++ b/src/core/file_sys/archive_sdmc.h @@ -27,16 +27,16 @@ public: return "SDMCArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: @@ -68,8 +68,8 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_sdmcwriteonly.cpp b/src/core/file_sys/archive_sdmcwriteonly.cpp index 31c27c2d2..fc8191964 100644 --- a/src/core/file_sys/archive_sdmcwriteonly.cpp +++ b/src/core/file_sys/archive_sdmcwriteonly.cpp @@ -41,7 +41,8 @@ public: }; ResultVal> SDMCWriteOnlyArchive::OpenFile(const Path& path, - const Mode& mode) const { + const Mode& mode, + u32 attributes) { if (mode.read_flag) { LOG_ERROR(Service_FS, "Read flag is not supported"); return ResultInvalidReadFlag; @@ -49,8 +50,7 @@ ResultVal> SDMCWriteOnlyArchive::OpenFile(const Pat return SDMCArchive::OpenFileBase(path, mode); } -ResultVal> SDMCWriteOnlyArchive::OpenDirectory( - const Path& path) const { +ResultVal> SDMCWriteOnlyArchive::OpenDirectory(const Path& path) { LOG_ERROR(Service_FS, "Not supported"); return ResultUnsupportedOpenFlags; } @@ -83,7 +83,8 @@ ResultVal> ArchiveFactory_SDMCWriteOnly::Open(co Result ArchiveFactory_SDMCWriteOnly::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u32 directory_buckets, + u32 file_buckets) { // TODO(wwylele): hwtest this LOG_ERROR(Service_FS, "Attempted to format a SDMC write-only archive."); return ResultUnknown; diff --git a/src/core/file_sys/archive_sdmcwriteonly.h b/src/core/file_sys/archive_sdmcwriteonly.h index c05f408d9..c60723a04 100644 --- a/src/core/file_sys/archive_sdmcwriteonly.h +++ b/src/core/file_sys/archive_sdmcwriteonly.h @@ -24,10 +24,10 @@ public: return "SDMCWriteOnlyArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; private: SDMCWriteOnlyArchive() = default; @@ -54,8 +54,8 @@ public: } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_selfncch.cpp b/src/core/file_sys/archive_selfncch.cpp index 60454d674..5472eda01 100644 --- a/src/core/file_sys/archive_selfncch.cpp +++ b/src/core/file_sys/archive_selfncch.cpp @@ -51,7 +51,7 @@ public: return data->size(); } - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override { LOG_ERROR(Service_FS, "The file is read-only!"); return ResultUnsupportedOpenFlags; @@ -65,7 +65,7 @@ public: return false; } - bool Close() const override { + bool Close() override { return true; } @@ -94,7 +94,8 @@ public: return "SelfNCCHArchive"; } - ResultVal> OpenFile(const Path& path, const Mode&) const override { + ResultVal> OpenFile(const Path& path, const Mode&, + u32 attributes) override { // Note: SelfNCCHArchive doesn't check the open mode. if (path.GetType() != LowPathType::Binary) { @@ -154,12 +155,12 @@ public: return ResultUnsupportedOpenFlags; } - Result CreateFile(const Path& path, u64 size) const override { + Result CreateFile(const Path& path, u64 size, u32 attributes) const override { LOG_ERROR(Service_FS, "Unsupported"); return ResultUnsupportedOpenFlags; } - Result CreateDirectory(const Path& path) const override { + Result CreateDirectory(const Path& path, u32 attributes) const override { LOG_ERROR(Service_FS, "Unsupported"); return ResultUnsupportedOpenFlags; } @@ -169,7 +170,7 @@ public: return ResultUnsupportedOpenFlags; } - ResultVal> OpenDirectory(const Path& path) const override { + ResultVal> OpenDirectory(const Path& path) override { LOG_ERROR(Service_FS, "Unsupported"); return ResultUnsupportedOpenFlags; } @@ -297,7 +298,7 @@ ResultVal> ArchiveFactory_SelfNCCH::Open(const P } Result ArchiveFactory_SelfNCCH::Format(const Path&, const FileSys::ArchiveFormatInfo&, - u64 program_id) { + u64 program_id, u32 directory_buckets, u32 file_buckets) { LOG_ERROR(Service_FS, "Attempted to format a SelfNCCH archive."); return ResultInvalidPath; } diff --git a/src/core/file_sys/archive_selfncch.h b/src/core/file_sys/archive_selfncch.h index 0643faf63..e25526105 100644 --- a/src/core/file_sys/archive_selfncch.h +++ b/src/core/file_sys/archive_selfncch.h @@ -50,8 +50,8 @@ public: return "SelfNCCH"; } ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; private: diff --git a/src/core/file_sys/archive_source_sd_savedata.cpp b/src/core/file_sys/archive_source_sd_savedata.cpp index 2f4cdcb54..9ab16be0a 100644 --- a/src/core/file_sys/archive_source_sd_savedata.cpp +++ b/src/core/file_sys/archive_source_sd_savedata.cpp @@ -6,6 +6,7 @@ #include "common/archives.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "core/file_sys/archive_artic.h" #include "core/file_sys/archive_source_sd_savedata.h" #include "core/file_sys/errors.h" #include "core/file_sys/savedata_archive.h" @@ -40,49 +41,101 @@ ArchiveSource_SDSaveData::ArchiveSource_SDSaveData(const std::string& sdmc_direc LOG_DEBUG(Service_FS, "Directory {} set as SaveData.", mount_point); } -ResultVal> ArchiveSource_SDSaveData::Open(u64 program_id) { - std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); - if (!FileUtil::Exists(concrete_mount_point)) { - // When a SaveData archive is created for the first time, it is not yet formatted and the - // save file/directory structure expected by the game has not yet been initialized. - // Returning the NotFormatted error code will signal the game to provision the SaveData - // archive with the files and folders that it expects. - return ResultNotFormatted; - } +ResultVal> ArchiveSource_SDSaveData::Open( + Service::FS::ArchiveIdCode archive_id, const Path& path, u64 program_id) { + if (IsUsingArtic()) { + EnsureCacheCreated(); + return ArticArchive::Open(artic_client, archive_id, path, + Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, *this, + archive_id != Service::FS::ArchiveIdCode::SaveData); + } else { + std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); + if (!FileUtil::Exists(concrete_mount_point)) { + // When a SaveData archive is created for the first time, it is not yet formatted and + // the save file/directory structure expected by the game has not yet been initialized. + // Returning the NotFormatted error code will signal the game to provision the SaveData + // archive with the files and folders that it expects. + return ResultNotFormatted; + } - return std::make_unique(std::move(concrete_mount_point)); + return std::make_unique(std::move(concrete_mount_point)); + } } Result ArchiveSource_SDSaveData::Format(u64 program_id, - const FileSys::ArchiveFormatInfo& format_info) { - std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); - FileUtil::DeleteDirRecursively(concrete_mount_point); - FileUtil::CreateFullPath(concrete_mount_point); + const FileSys::ArchiveFormatInfo& format_info, + Service::FS::ArchiveIdCode archive_id, const Path& path, + u32 directory_buckets, u32 file_buckets) { + if (IsUsingArtic()) { + ClearAllCache(); + auto req = artic_client->NewRequest("FSUSER_FormatSaveData"); - // Write the format metadata - std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); - FileUtil::IOFile file(metadata_path, "wb"); + req.AddParameterS32(static_cast(archive_id)); + auto artic_path = ArticArchive::BuildFSPath(path); + req.AddParameterBuffer(artic_path.data(), artic_path.size()); + req.AddParameterU32(format_info.total_size / 512); + req.AddParameterU32(format_info.number_directories); + req.AddParameterU32(format_info.number_files); + req.AddParameterU32(directory_buckets); + req.AddParameterU32(file_buckets); + req.AddParameterU8(format_info.duplicate_data); - if (file.IsOpen()) { - file.WriteBytes(&format_info, sizeof(format_info)); + auto resp = artic_client->Send(req); + return ArticArchive::RespResult(resp); + } else { + std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); + FileUtil::DeleteDirRecursively(concrete_mount_point); + FileUtil::CreateFullPath(concrete_mount_point); + + // Write the format metadata + std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); + FileUtil::IOFile file(metadata_path, "wb"); + + if (file.IsOpen()) { + file.WriteBytes(&format_info, sizeof(format_info)); + return ResultSuccess; + } return ResultSuccess; } - return ResultSuccess; } -ResultVal ArchiveSource_SDSaveData::GetFormatInfo(u64 program_id) const { - std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); - FileUtil::IOFile file(metadata_path, "rb"); +ResultVal ArchiveSource_SDSaveData::GetFormatInfo( + u64 program_id, Service::FS::ArchiveIdCode archive_id, const Path& path) const { + if (IsUsingArtic()) { + auto req = artic_client->NewRequest("FSUSER_GetFormatInfo"); - if (!file.IsOpen()) { - LOG_ERROR(Service_FS, "Could not open metadata information for archive"); - // TODO(Subv): Verify error code - return ResultNotFormatted; + req.AddParameterS32(static_cast(archive_id)); + auto path_artic = ArticArchive::BuildFSPath(path); + req.AddParameterBuffer(path_artic.data(), path_artic.size()); + + auto resp = artic_client->Send(req); + Result res = ArticArchive::RespResult(resp); + if (R_FAILED(res)) { + return res; + } + + auto info_buf = resp->GetResponseBuffer(0); + if (!info_buf.has_value() || info_buf->second != sizeof(ArchiveFormatInfo)) { + return ResultUnknown; + } + + ArchiveFormatInfo info; + memcpy(&info, info_buf->first, sizeof(info)); + return info; + } else { + std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); + FileUtil::IOFile file(metadata_path, "rb"); + + if (!file.IsOpen()) { + LOG_ERROR(Service_FS, "Could not open metadata information for archive"); + // TODO(Subv): Verify error code + return ResultNotFormatted; + } + + ArchiveFormatInfo info = {}; + file.ReadBytes(&info, sizeof(info)); + return info; } - - ArchiveFormatInfo info = {}; - file.ReadBytes(&info, sizeof(info)); - return info; } std::string ArchiveSource_SDSaveData::GetSaveDataPathFor(const std::string& mount_point, diff --git a/src/core/file_sys/archive_source_sd_savedata.h b/src/core/file_sys/archive_source_sd_savedata.h index 07832c3ae..56fdb2c5b 100644 --- a/src/core/file_sys/archive_source_sd_savedata.h +++ b/src/core/file_sys/archive_source_sd_savedata.h @@ -9,27 +9,48 @@ #include #include #include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" #include "core/hle/result.h" +#include "network/artic_base/artic_base_client.h" + +namespace Service::FS { +enum class ArchiveIdCode : u32; +} // namespace Service::FS namespace FileSys { /// A common source of SD save data archive -class ArchiveSource_SDSaveData { +class ArchiveSource_SDSaveData : public ArticCacheProvider { public: explicit ArchiveSource_SDSaveData(const std::string& mount_point); - ResultVal> Open(u64 program_id); - Result Format(u64 program_id, const FileSys::ArchiveFormatInfo& format_info); - ResultVal GetFormatInfo(u64 program_id) const; + ResultVal> Open(Service::FS::ArchiveIdCode archive_id, + const Path& path, u64 program_id); + Result Format(u64 program_id, const FileSys::ArchiveFormatInfo& format_info, + Service::FS::ArchiveIdCode archive_id, const Path& path, u32 directory_buckets, + u32 file_buckets); + ResultVal GetFormatInfo(u64 program_id, + Service::FS::ArchiveIdCode archive_id, + const Path& path) const; static std::string GetSaveDataPathFor(const std::string& mount_point, u64 program_id); + void RegisterArtic(std::shared_ptr& client) { + artic_client = client; + } + + bool IsUsingArtic() const { + return artic_client.get() != nullptr; + } + private: std::string mount_point; + std::shared_ptr artic_client = nullptr; ArchiveSource_SDSaveData() = default; template void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); ar& mount_point; } friend class boost::serialization::access; diff --git a/src/core/file_sys/archive_systemsavedata.cpp b/src/core/file_sys/archive_systemsavedata.cpp index 62734fca6..9020666b1 100644 --- a/src/core/file_sys/archive_systemsavedata.cpp +++ b/src/core/file_sys/archive_systemsavedata.cpp @@ -10,6 +10,7 @@ #include "common/archives.h" #include "common/common_types.h" #include "common/file_util.h" +#include "core/file_sys/archive_artic.h" #include "core/file_sys/archive_systemsavedata.h" #include "core/file_sys/errors.h" #include "core/file_sys/savedata_archive.h" @@ -52,23 +53,45 @@ Path ConstructSystemSaveDataBinaryPath(u32 high, u32 low) { ArchiveFactory_SystemSaveData::ArchiveFactory_SystemSaveData(const std::string& nand_path) : base_path(GetSystemSaveDataContainerPath(nand_path)) {} +static bool AllowArticSystemSaveData(const Path& path) { + constexpr u32 APP_SYSTEM_SAVE_DATA_MASK = 0x00020000; + if (path.GetType() == FileSys::LowPathType::Binary) { + std::vector path_data = path.AsBinary(); + return path_data.size() == 8 && + (*reinterpret_cast(path_data.data() + 4) & APP_SYSTEM_SAVE_DATA_MASK) != 0; + } + return false; +} + ResultVal> ArchiveFactory_SystemSaveData::Open(const Path& path, u64 program_id) { - std::string fullpath = GetSystemSaveDataPath(base_path, path); - if (!FileUtil::Exists(fullpath)) { - // TODO(Subv): Check error code, this one is probably wrong - return ResultNotFound; + if (IsUsingArtic() && AllowArticSystemSaveData(path)) { + EnsureCacheCreated(); + return ArticArchive::Open(artic_client, Service::FS::ArchiveIdCode::SystemSaveData, path, + Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + *this, false); + } else { + std::string fullpath = GetSystemSaveDataPath(base_path, path); + if (!FileUtil::Exists(fullpath)) { + // TODO(Subv): Check error code, this one is probably wrong + return ResultNotFound; + } + return std::make_unique(fullpath); } - return std::make_unique(fullpath); } Result ArchiveFactory_SystemSaveData::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { - std::string fullpath = GetSystemSaveDataPath(base_path, path); - FileUtil::DeleteDirRecursively(fullpath); - FileUtil::CreateFullPath(fullpath); - return ResultSuccess; + u64 program_id, u32 directory_buckets, + u32 file_buckets) { + const std::vector vec_data = path.AsBinary(); + u32 save_low; + u32 save_high; + std::memcpy(&save_low, &vec_data[4], sizeof(u32)); + std::memcpy(&save_high, &vec_data[0], sizeof(u32)); + return FormatAsSysData(save_high, save_low, format_info.total_size, 0x1000, + format_info.number_directories, format_info.number_files, + directory_buckets, file_buckets, format_info.duplicate_data); } ResultVal ArchiveFactory_SystemSaveData::GetFormatInfo(const Path& path, @@ -78,4 +101,45 @@ ResultVal ArchiveFactory_SystemSaveData::GetFormatInfo(const return ResultUnknown; } +Result ArchiveFactory_SystemSaveData::FormatAsSysData(u32 high, u32 low, u32 total_size, + u32 block_size, u32 number_directories, + u32 number_files, + u32 number_directory_buckets, + u32 number_file_buckets, u8 duplicate_data) { + if (IsUsingArtic() && + AllowArticSystemSaveData(FileSys::ConstructSystemSaveDataBinaryPath(high, low))) { + auto req = artic_client->NewRequest("FSUSER_CreateSysSaveData"); + + req.AddParameterU32(high); + req.AddParameterU32(low); + req.AddParameterU32(total_size); + req.AddParameterU32(block_size); + req.AddParameterU32(number_directories); + req.AddParameterU32(number_files); + req.AddParameterU32(number_directory_buckets); + req.AddParameterU32(number_file_buckets); + req.AddParameterU8(duplicate_data); + + auto resp = artic_client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) { + return ResultUnknown; + } + + Result res(static_cast(resp->GetMethodResult())); + return res; + + } else { + // Construct the binary path to the archive first + const FileSys::Path path = FileSys::ConstructSystemSaveDataBinaryPath(high, low); + + const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); + const std::string base_path = FileSys::GetSystemSaveDataContainerPath(nand_directory); + const std::string systemsavedata_path = FileSys::GetSystemSaveDataPath(base_path, path); + if (!FileUtil::CreateFullPath(systemsavedata_path)) { + return ResultUnknown; // TODO(Subv): Find the right error code + } + return ResultSuccess; + } +} + } // namespace FileSys diff --git a/src/core/file_sys/archive_systemsavedata.h b/src/core/file_sys/archive_systemsavedata.h index af7c341e2..2c86f51e6 100644 --- a/src/core/file_sys/archive_systemsavedata.h +++ b/src/core/file_sys/archive_systemsavedata.h @@ -10,27 +10,48 @@ #include #include "common/common_types.h" #include "core/file_sys/archive_backend.h" +#include "core/file_sys/artic_cache.h" #include "core/hle/result.h" +#include "core/hle/service/fs/archive.h" +#include "network/artic_base/artic_base_client.h" namespace FileSys { /// File system interface to the SystemSaveData archive -class ArchiveFactory_SystemSaveData final : public ArchiveFactory { +class ArchiveFactory_SystemSaveData final : public ArchiveFactory, public ArticCacheProvider { public: explicit ArchiveFactory_SystemSaveData(const std::string& mount_point); ResultVal> Open(const Path& path, u64 program_id) override; - Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) override; + Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u32 directory_buckets, u32 file_buckets) override; ResultVal GetFormatInfo(const Path& path, u64 program_id) const override; + Result FormatAsSysData(u32 high, u32 low, u32 total_size, u32 block_size, + u32 number_directories, u32 number_files, u32 number_directory_buckets, + u32 number_file_buckets, u8 duplicate_data); + std::string GetName() const override { return "SystemSaveData"; } + bool IsSlow() override { + return IsUsingArtic(); + } + + void RegisterArtic(std::shared_ptr& client) { + artic_client = client; + } + + bool IsUsingArtic() const { + return artic_client.get() != nullptr; + } + private: std::string base_path; + std::shared_ptr artic_client = nullptr; + ArchiveFactory_SystemSaveData() = default; template void serialize(Archive& ar, const unsigned int) { diff --git a/src/core/file_sys/artic_cache.cpp b/src/core/file_sys/artic_cache.cpp new file mode 100644 index 000000000..1aa770e93 --- /dev/null +++ b/src/core/file_sys/artic_cache.cpp @@ -0,0 +1,236 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "artic_cache.h" + +namespace FileSys { +ResultVal ArticCache::Read(s32 file_handle, std::size_t offset, std::size_t length, + u8* buffer) { + if (length == 0) + return size_t(); + + const auto segments = BreakupRead(offset, length); + std::size_t read_progress = 0; + + // Skip cache if the read is too big + if (segments.size() == 1 && segments[0].second > cache_line_size) { + if (segments[0].second < big_cache_skip) { + std::unique_lock big_read_guard(big_cache_mutex); + auto big_cache_entry = big_cache.request(std::make_pair(offset, length)); + if (!big_cache_entry.first) { + LOG_TRACE(Service_FS, "ArticCache BMISS: offset={}, length={}", offset, length); + big_cache_entry.second.clear(); + big_cache_entry.second.resize(length); + auto res = + ReadFromArtic(file_handle, reinterpret_cast(big_cache_entry.second.data()), + length, offset); + if (res.Failed()) + return res; + length = res.Unwrap(); + } else { + LOG_TRACE(Service_FS, "ArticCache BHIT: offset={}, length={}", offset, length); + } + memcpy(buffer, big_cache_entry.second.data(), length); + } else { + if (segments[0].second < very_big_cache_skip) { + std::unique_lock very_big_read_guard(very_big_cache_mutex); + auto very_big_cache_entry = very_big_cache.request(std::make_pair(offset, length)); + if (!very_big_cache_entry.first) { + LOG_TRACE(Service_FS, "ArticCache VBMISS: offset={}, length={}", offset, + length); + very_big_cache_entry.second.clear(); + very_big_cache_entry.second.resize(length); + auto res = ReadFromArtic( + file_handle, reinterpret_cast(very_big_cache_entry.second.data()), + length, offset); + if (res.Failed()) + return res; + length = res.Unwrap(); + } else { + LOG_TRACE(Service_FS, "ArticCache VBHIT: offset={}, length={}", offset, length); + } + memcpy(buffer, very_big_cache_entry.second.data(), length); + } else { + LOG_TRACE(Service_FS, "ArticCache SKIP: offset={}, length={}", offset, length); + + auto res = ReadFromArtic(file_handle, buffer, length, offset); + if (res.Failed()) + return res; + length = res.Unwrap(); + } + } + return length; + } + + // TODO(PabloMK7): Make cache thread safe, read the comment in CacheReady function. + std::unique_lock read_guard(cache_mutex); + for (const auto& seg : segments) { + std::size_t read_size = cache_line_size; + std::size_t page = OffsetToPage(seg.first); + // Check if segment is in cache + auto cache_entry = cache.request(page); + if (!cache_entry.first) { + // If not found, read from artic and cache the data + auto res = ReadFromArtic(file_handle, cache_entry.second.data(), read_size, page); + if (res.Failed()) + return res; + read_size = res.Unwrap(); + LOG_TRACE(Service_FS, "ArticCache MISS: page={}, length={}, into={}", page, seg.second, + (seg.first - page)); + } else { + LOG_TRACE(Service_FS, "ArticCache HIT: page={}, length={}, into={}", page, seg.second, + (seg.first - page)); + } + std::size_t copy_amount = + (read_size > (seg.first - page)) + ? std::min((seg.first - page) + seg.second, read_size) - (seg.first - page) + : 0; + std::memcpy(buffer + read_progress, cache_entry.second.data() + (seg.first - page), + copy_amount); + read_progress += copy_amount; + } + return read_progress; +} + +bool ArticCache::CacheReady(std::size_t file_offset, std::size_t length) { + auto segments = BreakupRead(file_offset, length); + if (segments.size() == 1 && segments[0].second > cache_line_size) { + return false; + } else { + std::shared_lock read_guard(cache_mutex); + for (auto it = segments.begin(); it != segments.end(); it++) { + if (!cache.contains(OffsetToPage(it->first))) + return false; + } + return true; + } +} + +void ArticCache::Clear() { + std::unique_lock l1(cache_mutex), l2(big_cache_mutex), l3(very_big_cache_mutex); + cache.clear(); + big_cache.clear(); + very_big_cache.clear(); + data_size = std::nullopt; +} + +ResultVal ArticCache::Write(s32 file_handle, std::size_t offset, std::size_t length, + const u8* buffer, u32 flags) { + // Can probably do better, but write operations are usually done at the end, so it doesn't + // matter much + Clear(); + + size_t written_amount = 0; + while (written_amount != length) { + size_t to_write = + std::min(client->GetServerRequestMaxSize() - 0x100, length - written_amount); + + auto req = client->NewRequest("FSFILE_Write"); + req.AddParameterS32(file_handle); + req.AddParameterS64(static_cast(offset + written_amount)); + req.AddParameterS32(static_cast(to_write)); + req.AddParameterS32(static_cast(flags)); + req.AddParameterBuffer(buffer + written_amount, to_write); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto actually_written_opt = resp->GetResponseS32(0); + if (!actually_written_opt.has_value()) + return Result(-1); + + size_t actually_written = static_cast(actually_written_opt.value()); + + written_amount += actually_written; + if (actually_written != to_write) + break; + } + return written_amount; +} + +ResultVal ArticCache::GetSize(s32 file_handle) { + std::unique_lock l1(cache_mutex); + + if (data_size.has_value()) + return data_size.value(); + + auto req = client->NewRequest("FSFILE_GetSize"); + + req.AddParameterS32(file_handle); + + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto size_buf = resp->GetResponseS64(0); + if (!size_buf) { + return Result(-1); + } + + data_size = static_cast(*size_buf); + return data_size.value(); +} + +ResultVal ArticCache::ReadFromArtic(s32 file_handle, u8* buffer, size_t len, + size_t offset) { + size_t read_amount = 0; + while (read_amount != len) { + size_t to_read = + std::min(client->GetServerRequestMaxSize() - 0x100, len - read_amount); + + auto req = client->NewRequest("FSFILE_Read"); + req.AddParameterS32(file_handle); + req.AddParameterS64(static_cast(offset + read_amount)); + req.AddParameterS32(static_cast(to_read)); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto read_buff = resp->GetResponseBuffer(0); + size_t actually_read = 0; + if (read_buff.has_value()) { + actually_read = read_buff->second; + memcpy(buffer + read_amount, read_buff->first, actually_read); + } + + read_amount += actually_read; + if (actually_read != to_read) + break; + } + return read_amount; +} + +std::vector> ArticCache::BreakupRead(std::size_t offset, + std::size_t length) { + std::vector> ret; + + // Reads bigger than the cache line size will probably never hit again + if (length > max_breakup_size) { + ret.push_back(std::make_pair(offset, length)); + return ret; + } + + std::size_t curr_offset = offset; + while (length) { + std::size_t next_page = OffsetToPage(curr_offset + cache_line_size); + std::size_t curr_page_len = std::min(length, next_page - curr_offset); + ret.push_back(std::make_pair(curr_offset, curr_page_len)); + curr_offset = next_page; + length -= curr_page_len; + } + return ret; +} +} // namespace FileSys diff --git a/src/core/file_sys/artic_cache.h b/src/core/file_sys/artic_cache.h new file mode 100644 index 000000000..7e8ed591c --- /dev/null +++ b/src/core/file_sys/artic_cache.h @@ -0,0 +1,154 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "vector" + +#include +#include +#include +#include "common/alignment.h" +#include "common/common_types.h" +#include "common/static_lru_cache.h" +#include "core/file_sys/archive_backend.h" +#include "core/hle/result.h" +#include "network/artic_base/artic_base_client.h" + +namespace FileSys { +class ArticCache { +public: + ArticCache() = default; + + ArticCache(const std::shared_ptr& cli) : client(cli) {} + + ResultVal Read(s32 file_handle, std::size_t offset, std::size_t length, + u8* buffer); + + bool CacheReady(std::size_t file_offset, std::size_t length); + + void Clear(); + + ResultVal Write(s32 file_handle, std::size_t offset, std::size_t length, + const u8* buffer, u32 flags); + + ResultVal GetSize(s32 file_handle); + + void ForceSetSize(const std::optional& size) { + data_size = size; + } + +private: + std::shared_ptr client; + std::optional data_size; + + // Total cache size: 32MB small, 512MB big (worst case), 160MB very big (worst case). + // The worst case values are unrealistic, they will never happen in any real game. + static constexpr std::size_t cache_line_size = 4 * 1024; + static constexpr std::size_t cache_line_count = 256; + static constexpr std::size_t max_breakup_size = 8 * 1024; + + static constexpr std::size_t big_cache_skip = 1 * 1024 * 1024; + static constexpr std::size_t big_cache_lines = 1024; + + static constexpr std::size_t very_big_cache_skip = 10 * 1024 * 1024; + static constexpr std::size_t very_big_cache_lines = 24; + + Common::StaticLRUCache, cache_line_count> cache; + std::shared_mutex cache_mutex; + + struct NoInitChar { + u8 value; + NoInitChar() noexcept { + // do nothing + static_assert(sizeof *this == sizeof value, "invalid size"); + } + }; + Common::StaticLRUCache, std::vector, + big_cache_lines> + big_cache; + std::shared_mutex big_cache_mutex; + Common::StaticLRUCache, std::vector, + very_big_cache_lines> + very_big_cache; + std::shared_mutex very_big_cache_mutex; + + ResultVal ReadFromArtic(s32 file_handle, u8* buffer, size_t len, size_t offset); + + std::size_t OffsetToPage(std::size_t offset) { + return Common::AlignDown(offset, cache_line_size); + } + + std::vector> BreakupRead(std::size_t offset, + std::size_t length); + +protected: + template + void serialize(Archive& ar, const unsigned int) {} + friend class boost::serialization::access; +}; + +class ArticCacheProvider { +public: + virtual ~ArticCacheProvider() {} + + std::vector PathsToVector(const Path& archive_path, const Path& file_path) { + auto archive_path_binary = archive_path.AsBinary(); + auto file_path_binary = file_path.AsBinary(); + + std::vector ret; + ret.push_back(static_cast(file_path.GetType())); + ret.insert(ret.end(), archive_path_binary.begin(), archive_path_binary.end()); + ret.push_back(static_cast(archive_path.GetType())); + ret.insert(ret.end(), file_path_binary.begin(), file_path_binary.end()); + return ret; + } + + virtual std::shared_ptr ProvideCache( + const std::shared_ptr& cli, const std::vector& path, + bool create) { + if (file_caches == nullptr) + return nullptr; + + auto it = file_caches->find(path); + if (it == file_caches->end()) { + if (!create) { + return nullptr; + } + auto res = std::make_shared(cli); + file_caches->insert({path, res}); + return res; + } + return it->second; + } + + virtual void ClearAllCache() { + if (file_caches != nullptr) { + file_caches->clear(); + } + } + + virtual void EnsureCacheCreated() { + if (file_caches == nullptr) { + file_caches = + std::make_unique, std::shared_ptr>>(); + } + } + +protected: + template + void serialize(Archive& ar, const unsigned int) {} + friend class boost::serialization::access; + +private: + std::unique_ptr, std::shared_ptr>> file_caches = nullptr; + std::shared_ptr client; +}; + +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::ArticCache) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticCacheProvider) \ No newline at end of file diff --git a/src/core/file_sys/directory_backend.h b/src/core/file_sys/directory_backend.h index b5a2617bb..e88363ae6 100644 --- a/src/core/file_sys/directory_backend.h +++ b/src/core/file_sys/directory_backend.h @@ -49,7 +49,11 @@ public: * Close the directory * @return true if the directory closed correctly */ - virtual bool Close() const = 0; + virtual bool Close() = 0; + + virtual bool IsSlow() { + return false; + } private: template diff --git a/src/core/file_sys/disk_archive.cpp b/src/core/file_sys/disk_archive.cpp index 12ea8932a..a7ae5e92e 100644 --- a/src/core/file_sys/disk_archive.cpp +++ b/src/core/file_sys/disk_archive.cpp @@ -26,7 +26,7 @@ ResultVal DiskFile::Read(const u64 offset, const std::size_t length } ResultVal DiskFile::Write(const u64 offset, const std::size_t length, const bool flush, - const u8* buffer) { + const bool update_timestamp, const u8* buffer) { if (!mode.write_flag) return ResultInvalidOpenFlags; @@ -47,7 +47,7 @@ bool DiskFile::SetSize(const u64 size) const { return true; } -bool DiskFile::Close() const { +bool DiskFile::Close() { return file->Close(); } diff --git a/src/core/file_sys/disk_archive.h b/src/core/file_sys/disk_archive.h index 5843a37d5..0af741be7 100644 --- a/src/core/file_sys/disk_archive.h +++ b/src/core/file_sys/disk_archive.h @@ -30,11 +30,11 @@ public: } ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override; + bool Close() override; void Flush() const override { file->Flush(); @@ -66,7 +66,7 @@ public: u32 Read(u32 count, Entry* entries) override; - bool Close() const override { + bool Close() override { return true; } diff --git a/src/core/file_sys/file_backend.h b/src/core/file_sys/file_backend.h index bf0a1b493..e491ae0e8 100644 --- a/src/core/file_sys/file_backend.h +++ b/src/core/file_sys/file_backend.h @@ -37,7 +37,7 @@ public: * @return Number of bytes written, or error code */ virtual ResultVal Write(u64 offset, std::size_t length, bool flush, - const u8* buffer) = 0; + bool update_timestamp, const u8* buffer) = 0; /** * Get the amount of time a 3ds needs to read those data @@ -79,7 +79,7 @@ public: * Close the file * @return true if the file closed correctly */ - virtual bool Close() const = 0; + virtual bool Close() = 0; /** * Flushes the file diff --git a/src/core/file_sys/ivfc_archive.cpp b/src/core/file_sys/ivfc_archive.cpp index de71883a7..e58c78d22 100644 --- a/src/core/file_sys/ivfc_archive.cpp +++ b/src/core/file_sys/ivfc_archive.cpp @@ -28,8 +28,8 @@ std::string IVFCArchive::GetName() const { return "IVFC"; } -ResultVal> IVFCArchive::OpenFile(const Path& path, - const Mode& mode) const { +ResultVal> IVFCArchive::OpenFile(const Path& path, const Mode& mode, + u32 attributes) { std::unique_ptr delay_generator = std::make_unique(); return std::make_unique(romfs_file, std::move(delay_generator)); } @@ -61,14 +61,14 @@ Result IVFCArchive::DeleteDirectoryRecursively(const Path& path) const { return ResultUnknown; } -Result IVFCArchive::CreateFile(const Path& path, u64 size) const { +Result IVFCArchive::CreateFile(const Path& path, u64 size, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a file in an IVFC archive ({}).", GetName()); // TODO: Verify error code return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, ErrorLevel::Permanent); } -Result IVFCArchive::CreateDirectory(const Path& path) const { +Result IVFCArchive::CreateDirectory(const Path& path, u32 attributes) const { LOG_CRITICAL(Service_FS, "Attempted to create a directory in an IVFC archive ({}).", GetName()); // TODO(wwylele): Use correct error code return ResultUnknown; @@ -80,7 +80,7 @@ Result IVFCArchive::RenameDirectory(const Path& src_path, const Path& dest_path) return ResultUnknown; } -ResultVal> IVFCArchive::OpenDirectory(const Path& path) const { +ResultVal> IVFCArchive::OpenDirectory(const Path& path) { return std::make_unique(); } @@ -102,7 +102,7 @@ ResultVal IVFCFile::Read(const u64 offset, const std::size_t length } ResultVal IVFCFile::Write(const u64 offset, const std::size_t length, const bool flush, - const u8* buffer) { + const bool update_timestamp, const u8* buffer) { LOG_ERROR(Service_FS, "Attempted to write to IVFC file"); // TODO(Subv): Find error code return 0ULL; @@ -133,7 +133,8 @@ ResultVal IVFCFileInMemory::Read(const u64 offset, const std::size_ } ResultVal IVFCFileInMemory::Write(const u64 offset, const std::size_t length, - const bool flush, const u8* buffer) { + const bool flush, const bool update_timestamp, + const u8* buffer) { LOG_ERROR(Service_FS, "Attempted to write to IVFC file"); // TODO(Subv): Find error code return 0ULL; diff --git a/src/core/file_sys/ivfc_archive.h b/src/core/file_sys/ivfc_archive.h index 765e41444..632d643ec 100644 --- a/src/core/file_sys/ivfc_archive.h +++ b/src/core/file_sys/ivfc_archive.h @@ -101,16 +101,16 @@ public: std::string GetName() const override; - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: @@ -122,11 +122,11 @@ public: IVFCFile(std::shared_ptr file, std::unique_ptr delay_generator_); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} @@ -157,7 +157,7 @@ public: u32 Read(const u32 count, Entry* entries) override { return 0; } - bool Close() const override { + bool Close() override { return false; } }; @@ -168,11 +168,11 @@ public: std::unique_ptr delay_generator_); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} diff --git a/src/core/file_sys/romfs_reader.cpp b/src/core/file_sys/romfs_reader.cpp index 40823d833..2cff2825b 100644 --- a/src/core/file_sys/romfs_reader.cpp +++ b/src/core/file_sys/romfs_reader.cpp @@ -4,7 +4,11 @@ #include #include "common/archives.h" #include "common/logging/log.h" +#include "core/file_sys/archive_artic.h" +#include "core/file_sys/archive_backend.h" #include "core/file_sys/romfs_reader.h" +#include "core/hle/service/fs/fs_user.h" +#include "core/loader/loader.h" SERIALIZE_EXPORT_IMPL(FileSys::DirectRomFSReader) @@ -109,4 +113,102 @@ std::vector> DirectRomFSReader::BreakupRead( return ret; } +ArticRomFSReader::ArticRomFSReader(std::shared_ptr& cli, + bool is_update_romfs) + : client(cli), cache(cli) { + auto req = client->NewRequest("FSUSER_OpenFileDirectly"); + + FileSys::Path archive(FileSys::LowPathType::Empty, {}); + std::vector fileVec(0xC); + fileVec[0] = static_cast(is_update_romfs ? 5 : 0); + FileSys::Path file(FileSys::LowPathType::Binary, fileVec); + + req.AddParameterS32(static_cast(Service::FS::ArchiveIdCode::SelfNCCH)); + + auto archive_buf = ArticArchive::BuildFSPath(archive); + req.AddParameterBuffer(archive_buf.data(), archive_buf.size()); + auto file_buf = ArticArchive::BuildFSPath(file); + req.AddParameterBuffer(file_buf.data(), file_buf.size()); + + req.AddParameterS32(1); + req.AddParameterS32(0); + + auto resp = client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + load_status = Loader::ResultStatus::Error; + return; + } + if (resp->GetMethodResult() != 0) { + load_status = Loader::ResultStatus::ErrorNotUsed; + return; + } + + auto handle_buf = resp->GetResponseBuffer(0); + if (!handle_buf.has_value() || handle_buf->second != sizeof(s32)) { + load_status = Loader::ResultStatus::Error; + return; + } + + romfs_handle = *reinterpret_cast(handle_buf->first); + + req = client->NewRequest("FSFILE_GetSize"); + + req.AddParameterS32(romfs_handle); + + resp = client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + load_status = Loader::ResultStatus::Error; + return; + } + if (resp->GetMethodResult() != 0) { + load_status = Loader::ResultStatus::ErrorNotUsed; + return; + } + + auto size_buf = resp->GetResponseBuffer(0); + if (!size_buf.has_value() || size_buf->second != sizeof(u64)) { + load_status = Loader::ResultStatus::Error; + return; + } + + data_size = static_cast(*reinterpret_cast(size_buf->first)); + load_status = Loader::ResultStatus::Success; +} + +ArticRomFSReader::~ArticRomFSReader() { + if (romfs_handle != -1) { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(romfs_handle); + client->Send(req); + romfs_handle = -1; + } +} + +std::size_t ArticRomFSReader::ReadFile(std::size_t offset, std::size_t length, u8* buffer) { + length = std::min(length, static_cast(data_size) - offset); + auto res = cache.Read(romfs_handle, offset, length, buffer); + if (res.Failed()) + return 0; + return res.Unwrap(); +} + +bool ArticRomFSReader::AllowsCachedReads() const { + return true; +} + +bool ArticRomFSReader::CacheReady(std::size_t file_offset, std::size_t length) { + return cache.CacheReady(file_offset, length); +} + +void ArticRomFSReader::CloseFile() { + if (romfs_handle != -1) { + auto req = client->NewRequest("FSFILE_Close"); + req.AddParameterS32(romfs_handle); + client->Send(req); + romfs_handle = -1; + } +} + } // namespace FileSys diff --git a/src/core/file_sys/romfs_reader.h b/src/core/file_sys/romfs_reader.h index 128d10dbd..63b88e262 100644 --- a/src/core/file_sys/romfs_reader.h +++ b/src/core/file_sys/romfs_reader.h @@ -9,6 +9,12 @@ #include "common/common_types.h" #include "common/file_util.h" #include "common/static_lru_cache.h" +#include "core/file_sys/artic_cache.h" +#include "network/artic_base/artic_base_client.h" + +namespace Loader { +enum class ResultStatus; +} namespace FileSys { @@ -97,6 +103,53 @@ private: friend class boost::serialization::access; }; +/** + * A RomFS reader that reads from an artic base server. + */ +class ArticRomFSReader : public RomFSReader { +public: + ArticRomFSReader() = default; + ArticRomFSReader(std::shared_ptr& cli, bool is_update_romfs); + + ~ArticRomFSReader() override; + + std::size_t GetSize() const override { + return data_size; + } + + std::size_t ReadFile(std::size_t offset, std::size_t length, u8* buffer) override; + + bool AllowsCachedReads() const override; + + bool CacheReady(std::size_t file_offset, std::size_t length) override; + + Loader::ResultStatus OpenStatus() { + return load_status; + } + + void ClearCache() { + cache.Clear(); + } + + void CloseFile(); + +private: + std::shared_ptr client; + size_t data_size = 0; + s32 romfs_handle = -1; + Loader::ResultStatus load_status; + + ArticCache cache; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& data_size; + } + friend class boost::serialization::access; +}; + } // namespace FileSys BOOST_CLASS_EXPORT_KEY(FileSys::DirectRomFSReader) +BOOST_CLASS_EXPORT_KEY(FileSys::ArticRomFSReader) diff --git a/src/core/file_sys/savedata_archive.cpp b/src/core/file_sys/savedata_archive.cpp index 17380e6ac..00ee84d8f 100644 --- a/src/core/file_sys/savedata_archive.cpp +++ b/src/core/file_sys/savedata_archive.cpp @@ -36,7 +36,8 @@ public: }; ResultVal> SaveDataArchive::OpenFile(const Path& path, - const Mode& mode) const { + const Mode& mode, + u32 attributes) { LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex); const PathParser path_parser(path); @@ -203,7 +204,7 @@ Result SaveDataArchive::DeleteDirectoryRecursively(const Path& path) const { path, mount_point, [](const std::string& p) { return FileUtil::DeleteDirRecursively(p); }); } -Result SaveDataArchive::CreateFile(const FileSys::Path& path, u64 size) const { +Result SaveDataArchive::CreateFile(const FileSys::Path& path, u64 size, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -253,7 +254,7 @@ Result SaveDataArchive::CreateFile(const FileSys::Path& path, u64 size) const { ErrorLevel::Info); } -Result SaveDataArchive::CreateDirectory(const Path& path) const { +Result SaveDataArchive::CreateDirectory(const Path& path, u32 attributes) const { const PathParser path_parser(path); if (!path_parser.IsValid()) { @@ -319,8 +320,7 @@ Result SaveDataArchive::RenameDirectory(const Path& src_path, const Path& dest_p ErrorSummary::NothingHappened, ErrorLevel::Status); } -ResultVal> SaveDataArchive::OpenDirectory( - const Path& path) const { +ResultVal> SaveDataArchive::OpenDirectory(const Path& path) { const PathParser path_parser(path); if (!path_parser.IsValid()) { diff --git a/src/core/file_sys/savedata_archive.h b/src/core/file_sys/savedata_archive.h index f72f924f1..0169d918e 100644 --- a/src/core/file_sys/savedata_archive.h +++ b/src/core/file_sys/savedata_archive.h @@ -22,16 +22,16 @@ public: return "SaveDataArchive: " + mount_point; } - ResultVal> OpenFile(const Path& path, - const Mode& mode) const override; + ResultVal> OpenFile(const Path& path, const Mode& mode, + u32 attributes) override; Result DeleteFile(const Path& path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result DeleteDirectory(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override; - Result CreateFile(const Path& path, u64 size) const override; - Result CreateDirectory(const Path& path) const override; + Result CreateFile(const Path& path, u64 size, u32 attributes) const override; + Result CreateDirectory(const Path& path, u32 attributes) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; - ResultVal> OpenDirectory(const Path& path) const override; + ResultVal> OpenDirectory(const Path& path) override; u64 GetFreeBytes() const override; protected: diff --git a/src/core/file_sys/secure_value_backend.cpp b/src/core/file_sys/secure_value_backend.cpp new file mode 100644 index 000000000..c0b0bcae6 --- /dev/null +++ b/src/core/file_sys/secure_value_backend.cpp @@ -0,0 +1,74 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/archives.h" +#include "secure_value_backend.h" + +SERIALIZE_EXPORT_IMPL(FileSys::DefaultSecureValueBackend) + +namespace FileSys { + +Result DefaultSecureValueBackend::ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, + u32 secure_value_slot, + u64 secure_value) { + + // TODO: Generate and Save the Secure Value + + LOG_WARNING(Service_FS, + "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:08X} " + "unqiue_id=0x{:08X} title_variation=0x{:02X}", + secure_value, secure_value_slot, unique_id, title_variation); + + return ResultSuccess; +} + +ResultVal> DefaultSecureValueBackend::ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) { + + // TODO: Implement Secure Value Lookup & Generation + + LOG_WARNING(Service_FS, + "(STUBBED) called, secure_value_slot=0x{:08X} " + "unqiue_id=0x{:08X} title_variation=0x{:02X}", + secure_value_slot, unique_id, title_variation); + + return std::make_tuple(false, 0); +} + +Result DefaultSecureValueBackend::ControlSecureSave(u32 action, u8* input, size_t input_size, + u8* output, size_t output_size) { + + LOG_WARNING(Service_FS, + "(STUBBED) called, action=0x{:08X} " + "input_size=0x{:016X} output_size=0x{:016X}", + action, input_size, output_size); + + return ResultSuccess; +} + +Result DefaultSecureValueBackend::SetThisSaveDataSecureValue(u32 secure_value_slot, + u64 secure_value) { + // TODO: Generate and Save the Secure Value + + LOG_WARNING(Service_FS, "(STUBBED) called, secure_value=0x{:016x} secure_value_slot=0x{:08X}", + secure_value, secure_value_slot); + + return ResultSuccess; +} + +ResultVal> DefaultSecureValueBackend::GetThisSaveDataSecureValue( + u32 secure_value_slot) { + + // TODO: Implement Secure Value Lookup & Generation + + LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X}", secure_value_slot); + + return std::make_tuple(false, true, 0); +} + +template +void FileSys::DefaultSecureValueBackend::serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); +} +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/secure_value_backend.h b/src/core/file_sys/secure_value_backend.h new file mode 100644 index 000000000..e9d3d3da2 --- /dev/null +++ b/src/core/file_sys/secure_value_backend.h @@ -0,0 +1,65 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "tuple" + +#include "common/common_types.h" +#include "core/hle/result.h" +#include "core/hle/service/fs/archive.h" + +namespace FileSys { +class SecureValueBackend : NonCopyable { +public: + virtual ~SecureValueBackend(){}; + + virtual Result ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, + u32 secure_value_slot, u64 secure_value) = 0; + + virtual ResultVal> ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) = 0; + + virtual Result ControlSecureSave(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) = 0; + + virtual Result SetThisSaveDataSecureValue(u32 secure_value_slot, u64 secure_value) = 0; + + virtual ResultVal> GetThisSaveDataSecureValue( + u32 secure_value_slot) = 0; + + virtual bool BackendIsSlow() { + return false; + } + +protected: + template + void serialize(Archive& ar, const unsigned int) {} + friend class boost::serialization::access; +}; + +class DefaultSecureValueBackend : public SecureValueBackend { +public: + Result ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, u32 secure_value_slot, + u64 secure_value) override; + + ResultVal> ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) override; + + Result ControlSecureSave(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) override; + + Result SetThisSaveDataSecureValue(u32 secure_value_slot, u64 secure_value) override; + + ResultVal> GetThisSaveDataSecureValue( + u32 secure_value_slot) override; + +protected: + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::DefaultSecureValueBackend) \ No newline at end of file diff --git a/src/core/file_sys/secure_value_backend_artic.cpp b/src/core/file_sys/secure_value_backend_artic.cpp new file mode 100644 index 000000000..11f0449ab --- /dev/null +++ b/src/core/file_sys/secure_value_backend_artic.cpp @@ -0,0 +1,119 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/archives.h" +#include "core/file_sys/archive_artic.h" +#include "core/file_sys/secure_value_backend_artic.h" + +SERIALIZE_EXPORT_IMPL(FileSys::ArticSecureValueBackend) + +namespace FileSys { +Result ArticSecureValueBackend::ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, + u32 secure_value_slot, + u64 secure_value) { + auto req = client->NewRequest("FSUSER_ObsSetSaveDataSecureVal"); + + req.AddParameterU64(secure_value); + req.AddParameterU32(secure_value_slot); + req.AddParameterU32(unique_id); + req.AddParameterU8(title_variation); + + return ArticArchive::RespResult(client->Send(req)); +} + +ResultVal> ArticSecureValueBackend::ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) { + + auto req = client->NewRequest("FSUSER_ObsGetSaveDataSecureVal"); + + req.AddParameterU32(secure_value_slot); + req.AddParameterU32(unique_id); + req.AddParameterU8(title_variation); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + struct { + bool exists; + u64 secure_value; + } secure_value_result; + static_assert(sizeof(secure_value_result) == 0x10); + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != sizeof(secure_value_result)) + return ResultUnknown; + + memcpy(&secure_value_result, output_buf->first, output_buf->second); + return std::make_tuple(secure_value_result.exists, secure_value_result.secure_value); +} + +Result ArticSecureValueBackend::ControlSecureSave(u32 action, u8* input, size_t input_size, + u8* output, size_t output_size) { + auto req = client->NewRequest("FSUSER_ControlSecureSave"); + + req.AddParameterU32(action); + req.AddParameterBuffer(input, input_size); + req.AddParameterU32(static_cast(output_size)); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != output_size) + return ResultUnknown; + + memcpy(output, output_buf->first, output_buf->second); + return res; +} + +Result ArticSecureValueBackend::SetThisSaveDataSecureValue(u32 secure_value_slot, + u64 secure_value) { + auto req = client->NewRequest("FSUSER_SetThisSaveDataSecVal"); + + req.AddParameterU32(secure_value_slot); + req.AddParameterU64(secure_value); + + return ArticArchive::RespResult(client->Send(req)); +} + +ResultVal> ArticSecureValueBackend::GetThisSaveDataSecureValue( + u32 secure_value_slot) { + auto req = client->NewRequest("FSUSER_GetThisSaveDataSecVal"); + + req.AddParameterU32(secure_value_slot); + + auto resp = client->Send(req); + auto res = ArticArchive::RespResult(resp); + if (res.IsError()) + return res; + + struct { + bool exists; + bool isGamecard; + u64 secure_value; + } secure_value_result; + static_assert(sizeof(secure_value_result) == 0x10); + + auto output_buf = resp->GetResponseBuffer(0); + if (!output_buf.has_value()) + return res; + + if (output_buf->second != sizeof(secure_value_result)) + return ResultUnknown; + + memcpy(&secure_value_result, output_buf->first, output_buf->second); + return std::make_tuple(secure_value_result.exists, secure_value_result.isGamecard, + secure_value_result.secure_value); +} +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/secure_value_backend_artic.h b/src/core/file_sys/secure_value_backend_artic.h new file mode 100644 index 000000000..23d6fae1b --- /dev/null +++ b/src/core/file_sys/secure_value_backend_artic.h @@ -0,0 +1,53 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "tuple" + +#include "common/common_types.h" +#include "core/file_sys/secure_value_backend.h" +#include "core/hle/result.h" +#include "core/hle/service/fs/archive.h" +#include "network/artic_base/artic_base_client.h" + +namespace FileSys { +class ArticSecureValueBackend : public SecureValueBackend { +public: + ArticSecureValueBackend(const std::shared_ptr& _client) + : client(_client) {} + + Result ObsoletedSetSaveDataSecureValue(u32 unique_id, u8 title_variation, u32 secure_value_slot, + u64 secure_value) override; + + ResultVal> ObsoletedGetSaveDataSecureValue( + u32 unique_id, u8 title_variation, u32 secure_value_slot) override; + + Result ControlSecureSave(u32 action, u8* input, size_t input_size, u8* output, + size_t output_size) override; + + Result SetThisSaveDataSecureValue(u32 secure_value_slot, u64 secure_value) override; + + ResultVal> GetThisSaveDataSecureValue( + u32 secure_value_slot) override; + + bool BackendIsSlow() override { + return true; + } + +protected: + ArticSecureValueBackend() = default; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + } + friend class boost::serialization::access; + +private: + std::shared_ptr client; +}; +} // namespace FileSys + +BOOST_CLASS_EXPORT_KEY(FileSys::ArticSecureValueBackend) \ No newline at end of file diff --git a/src/core/hle/ipc_helpers.h b/src/core/hle/ipc_helpers.h index 9b7dbe73a..46c87514e 100644 --- a/src/core/hle/ipc_helpers.h +++ b/src/core/hle/ipc_helpers.h @@ -58,6 +58,11 @@ public: : RequestBuilder( context, Header{MakeHeader(command_id, normal_params_size, translate_params_size)}) {} + RequestBuilder(Kernel::HLERequestContext& context, unsigned normal_params_size, + unsigned translate_params_size) + : RequestBuilder(context, Header{MakeHeader(context.CommandID(), normal_params_size, + translate_params_size)}) {} + // Validate on destruction, as there shouldn't be any case where we don't want it ~RequestBuilder() { ValidateHeader(); diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index 5b6ab88b5..64a6ed949 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -206,6 +206,11 @@ public: return {cmd_buf[0]}; } + /// Returns the Command ID from the IPC command buffer. + u16 CommandID() const { + return static_cast(CommandHeader().command_id.Value()); + } + /** * Returns the session through which this request was made. This can be used as a map key to * access per-client data on services. diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 1807fc185..aee587734 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -266,7 +266,7 @@ ResultVal CIAFile::WriteContentData(u64 offset, std::size_t length, } ResultVal CIAFile::Write(u64 offset, std::size_t length, bool flush, - const u8* buffer) { + bool update_timestamp, const u8* buffer) { written += length; // TODO(shinyquagsire23): Can we assume that things will only be written in sequence? @@ -347,7 +347,7 @@ bool CIAFile::SetSize(u64 size) const { return false; } -bool CIAFile::Close() const { +bool CIAFile::Close() { bool complete = install_state >= CIAInstallState::TMDLoaded && content_written.size() == container.GetTitleMetadata().GetContentCount() && @@ -419,7 +419,7 @@ ResultVal TicketFile::Read(u64 offset, std::size_t length, u8* buff } ResultVal TicketFile::Write(u64 offset, std::size_t length, bool flush, - const u8* buffer) { + bool update_timestamp, const u8* buffer) { written += length; data.resize(written); std::memcpy(data.data() + offset, buffer, length); @@ -434,7 +434,7 @@ bool TicketFile::SetSize(u64 size) const { return false; } -bool TicketFile::Close() const { +bool TicketFile::Close() { FileSys::Ticket ticket; if (ticket.Load(data, 0) == Loader::ResultStatus::Success) { LOG_WARNING(Service_AM, "Discarding ticket for {:#016X}.", ticket.GetTitleID()); @@ -480,7 +480,7 @@ InstallStatus InstallCIA(const std::string& path, while (total_bytes_read != file_size) { std::size_t bytes_read = file.ReadBytes(buffer.data(), buffer.size()); auto result = installFile.Write(static_cast(total_bytes_read), bytes_read, true, - static_cast(buffer.data())); + false, static_cast(buffer.data())); if (update_callback) { update_callback(total_bytes_read, file_size); @@ -590,7 +590,8 @@ InstallStatus InstallFromNus(u64 title_id, int version) { const u64 offset = Common::AlignUp(current_offset + data.size(), FileSys::CIA_SECTION_ALIGNMENT); data.resize(offset - current_offset, 0); - const auto result = install_file.Write(current_offset, data.size(), true, data.data()); + const auto result = + install_file.Write(current_offset, data.size(), true, false, data.data()); if (result.Failed()) { LOG_ERROR(Service_AM, "CIA file installation aborted with error code {:08x}", result.Code().raw); @@ -803,76 +804,181 @@ Module::Interface::~Interface() = default; void Module::Interface::GetNumPrograms(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - u32 media_type = rp.Pop(); + u8 media_type = rp.Pop(); - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(ResultSuccess); - rb.Push(static_cast(am->am_title_list[media_type].size())); + if (artic_client.get()) { + struct AsyncData { + u8 media_type; + + ResultVal res; + }; + auto async_data = std::make_shared(); + async_data->media_type = media_type; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AM_GetTitleCount"); + + req.AddParameterU8(async_data->media_type); + + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + + auto count = resp->GetResponseS32(0); + if (!count.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->res = *count; + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 0); + + rb.Push(async_data->res.Code()); + rb.Push( + static_cast(async_data->res.Succeeded() ? async_data->res.Unwrap() : 0)); + }, + true); + } else { + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(static_cast(am->am_title_list[media_type].size())); + } } void Module::Interface::FindDLCContentInfos(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); + IPC::RequestParser rp(ctx); auto media_type = static_cast(rp.Pop()); u64 title_id = rp.Pop(); u32 content_count = rp.Pop(); auto& content_requested_in = rp.PopMappedBuffer(); - auto& content_info_out = rp.PopMappedBuffer(); + if (artic_client.get()) { + struct AsyncData { + u8 media_type; + u64 title_id; + std::vector content_requested; - // Validate that only DLC TIDs are passed in - u32 tid_high = static_cast(title_id >> 32); - if (tid_high != TID_HIGH_DLC) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); - rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, ErrorSummary::InvalidArgument, - ErrorLevel::Usage)); - rb.PushMappedBuffer(content_requested_in); - rb.PushMappedBuffer(content_info_out); - return; - } + Result res{0}; + std::vector out; + Kernel::MappedBuffer* content_info_out; + }; + auto async_data = std::make_shared(); + async_data->media_type = static_cast(media_type); + async_data->title_id = title_id; + async_data->content_requested.resize(content_count); + content_requested_in.Read(async_data->content_requested.data(), 0, + content_count * sizeof(u16)); + async_data->content_info_out = &rp.PopMappedBuffer(); - std::vector content_requested(content_count); - content_requested_in.Read(content_requested.data(), 0, content_count * sizeof(u16)); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AMAPP_FindDLCContentInfos"); - std::string tmd_path = GetTitleMetadataPath(media_type, title_id); + req.AddParameterU8(async_data->media_type); + req.AddParameterU64(async_data->title_id); + req.AddParameterBuffer(async_data->content_requested.data(), + async_data->content_requested.size() * sizeof(u16)); - FileSys::TitleMetadata tmd; - if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { - std::size_t write_offset = 0; - // Get info for each content index requested - for (std::size_t i = 0; i < content_count; i++) { - if (content_requested[i] >= tmd.GetContentCount()) { - LOG_ERROR(Service_AM, - "Attempted to get info for non-existent content index {:04x}.", - content_requested[i]); + auto resp = artic_client->Send(req); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); - rb.Push(-1); // TODO(Steveice10): Find the right error code - rb.PushMappedBuffer(content_requested_in); - rb.PushMappedBuffer(content_info_out); - return; - } + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } - ContentInfo content_info = {}; - content_info.index = content_requested[i]; - content_info.type = tmd.GetContentTypeByIndex(content_requested[i]); - content_info.content_id = tmd.GetContentIDByIndex(content_requested[i]); - content_info.size = tmd.GetContentSizeByIndex(content_requested[i]); - content_info.ownership = - OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket. + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } - if (FileUtil::Exists(GetTitleContentPath(media_type, title_id, content_requested[i]))) { - content_info.ownership |= OWNERSHIP_DOWNLOADED; - } + auto content_info = resp->GetResponseBuffer(0); + if (!content_info.has_value()) { + async_data->res = Result(-1); + return 0; + } - content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo)); - write_offset += sizeof(ContentInfo); + async_data->out.resize(content_info->second); + memcpy(async_data->out.data(), content_info->first, content_info->second); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.IsSuccess()) { + async_data->content_info_out->Write(async_data->out.data(), 0, + async_data->out.size()); + } + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); + } else { + + auto& content_info_out = rp.PopMappedBuffer(); + + // Validate that only DLC TIDs are passed in + u32 tid_high = static_cast(title_id >> 32); + if (tid_high != TID_HIGH_DLC) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, + ErrorSummary::InvalidArgument, ErrorLevel::Usage)); + return; } - } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); - rb.Push(ResultSuccess); - rb.PushMappedBuffer(content_requested_in); - rb.PushMappedBuffer(content_info_out); + std::vector content_requested(content_count); + content_requested_in.Read(content_requested.data(), 0, content_count * sizeof(u16)); + + std::string tmd_path = GetTitleMetadataPath(media_type, title_id); + + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { + std::size_t write_offset = 0; + // Get info for each content index requested + for (std::size_t i = 0; i < content_count; i++) { + if (content_requested[i] >= tmd.GetContentCount()) { + LOG_ERROR(Service_AM, + "Attempted to get info for non-existent content index {:04x}.", + content_requested[i]); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(-1); // TODO(Steveice10): Find the right error code + return; + } + + ContentInfo content_info = {}; + content_info.index = content_requested[i]; + content_info.type = tmd.GetContentTypeByIndex(content_requested[i]); + content_info.content_id = tmd.GetContentIDByIndex(content_requested[i]); + content_info.size = tmd.GetContentSizeByIndex(content_requested[i]); + content_info.ownership = + OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket. + + if (FileUtil::Exists( + GetTitleContentPath(media_type, title_id, content_requested[i]))) { + content_info.ownership |= OWNERSHIP_DOWNLOADED; + } + + content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo)); + write_offset += sizeof(ContentInfo); + } + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + } } void Module::Interface::ListDLCContentInfos(Kernel::HLERequestContext& ctx) { @@ -882,50 +988,112 @@ void Module::Interface::ListDLCContentInfos(Kernel::HLERequestContext& ctx) { auto media_type = static_cast(rp.Pop()); u64 title_id = rp.Pop(); u32 start_index = rp.Pop(); - auto& content_info_out = rp.PopMappedBuffer(); - // Validate that only DLC TIDs are passed in - u32 tid_high = static_cast(title_id >> 32); - if (tid_high != TID_HIGH_DLC) { - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, ErrorSummary::InvalidArgument, - ErrorLevel::Usage)); - rb.Push(0); - rb.PushMappedBuffer(content_info_out); - return; - } + if (artic_client.get()) { + struct AsyncData { + u8 media_type; + u64 title_id; + u32 content_count; + u32 start_index; - std::string tmd_path = GetTitleMetadataPath(media_type, title_id); + Result res{0}; + std::vector out; + Kernel::MappedBuffer* content_info_out; + }; + auto async_data = std::make_shared(); + async_data->media_type = static_cast(media_type); + async_data->title_id = title_id; + async_data->content_count = content_count; + async_data->start_index = start_index; + async_data->content_info_out = &rp.PopMappedBuffer(); - u32 copied = 0; - FileSys::TitleMetadata tmd; - if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { - u32 end_index = - std::min(start_index + content_count, static_cast(tmd.GetContentCount())); - std::size_t write_offset = 0; - for (u32 i = start_index; i < end_index; i++) { - ContentInfo content_info = {}; - content_info.index = static_cast(i); - content_info.type = tmd.GetContentTypeByIndex(i); - content_info.content_id = tmd.GetContentIDByIndex(i); - content_info.size = tmd.GetContentSizeByIndex(i); - content_info.ownership = - OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket. + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AMAPP_ListDLCContentInfos"); - if (FileUtil::Exists(GetTitleContentPath(media_type, title_id, i))) { - content_info.ownership |= OWNERSHIP_DOWNLOADED; - } + req.AddParameterU32(async_data->content_count); + req.AddParameterU8(async_data->media_type); + req.AddParameterU64(async_data->title_id); + req.AddParameterU32(async_data->start_index); - content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo)); - write_offset += sizeof(ContentInfo); - copied++; + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + + auto content_info = resp->GetResponseBuffer(0); + if (!content_info.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->out.resize(content_info->second); + memcpy(async_data->out.data(), content_info->first, content_info->second); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.IsSuccess()) { + async_data->content_info_out->Write(async_data->out.data(), 0, + async_data->out.size()); + } + IPC::RequestBuilder rb(ctx, 2, 0); + rb.Push(async_data->res); + rb.Push(static_cast(async_data->out.size() / sizeof(ContentInfo))); + }, + true); + } else { + + auto& content_info_out = rp.PopMappedBuffer(); + + // Validate that only DLC TIDs are passed in + u32 tid_high = static_cast(title_id >> 32); + if (tid_high != TID_HIGH_DLC) { + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, + ErrorSummary::InvalidArgument, ErrorLevel::Usage)); + rb.Push(0); + return; } - } - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(ResultSuccess); - rb.Push(copied); - rb.PushMappedBuffer(content_info_out); + std::string tmd_path = GetTitleMetadataPath(media_type, title_id); + + u32 copied = 0; + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { + u32 end_index = + std::min(start_index + content_count, static_cast(tmd.GetContentCount())); + std::size_t write_offset = 0; + for (u32 i = start_index; i < end_index; i++) { + ContentInfo content_info = {}; + content_info.index = static_cast(i); + content_info.type = tmd.GetContentTypeByIndex(i); + content_info.content_id = tmd.GetContentIDByIndex(i); + content_info.size = tmd.GetContentSizeByIndex(i); + content_info.ownership = + OWNERSHIP_OWNED; // TODO(Steveice10): Pull this from the ticket. + + if (FileUtil::Exists(GetTitleContentPath(media_type, title_id, i))) { + content_info.ownership |= OWNERSHIP_DOWNLOADED; + } + + content_info_out.Write(&content_info, write_offset, sizeof(ContentInfo)); + write_offset += sizeof(ContentInfo); + copied++; + } + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(copied); + } } void Module::Interface::DeleteContents(Kernel::HLERequestContext& ctx) { @@ -944,28 +1112,89 @@ void Module::Interface::DeleteContents(Kernel::HLERequestContext& ctx) { void Module::Interface::GetProgramList(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - u32 count = rp.Pop(); u8 media_type = rp.Pop(); - auto& title_ids_output = rp.PopMappedBuffer(); - if (media_type > 2) { - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(-1); // TODO(shinyquagsire23): Find the right error code - rb.Push(0); - rb.PushMappedBuffer(title_ids_output); - return; + if (artic_client.get()) { + struct AsyncData { + u32 count; + u8 media_type; + + Result res{0}; + std::vector out; + Kernel::MappedBuffer* title_ids_output; + }; + auto async_data = std::make_shared(); + async_data->count = count; + async_data->media_type = media_type; + async_data->title_ids_output = &rp.PopMappedBuffer(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AM_GetTitleList"); + + req.AddParameterU32(async_data->count); + req.AddParameterU8(async_data->media_type); + + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + async_data->res = res; + + auto title_ids = resp->GetResponseBuffer(0); + if (!title_ids.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->out.resize(title_ids->second); + memcpy(async_data->out.data(), title_ids->first, title_ids->second); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (!async_data->res.IsSuccess()) { + IPC::RequestBuilder rb(ctx, 2, 0); + rb.Push(async_data->res); + rb.Push(0); + } else { + async_data->title_ids_output->Write(async_data->out.data(), 0, + async_data->out.size()); + + IPC::RequestBuilder rb(ctx, 2, 0); + rb.Push(async_data->res); + rb.Push(static_cast(async_data->out.size() / sizeof(u64))); + } + }, + true); + + } else { + auto& title_ids_output = rp.PopMappedBuffer(); + + if (media_type > 2) { + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(-1); // TODO(shinyquagsire23): Find the right error code + rb.Push(0); + return; + } + + u32 media_count = static_cast(am->am_title_list[media_type].size()); + u32 copied = std::min(media_count, count); + + title_ids_output.Write(am->am_title_list[media_type].data(), 0, copied * sizeof(u64)); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(copied); } - - u32 media_count = static_cast(am->am_title_list[media_type].size()); - u32 copied = std::min(media_count, count); - - title_ids_output.Write(am->am_title_list[media_type].data(), 0, copied * sizeof(u64)); - - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(ResultSuccess); - rb.Push(copied); - rb.PushMappedBuffer(title_ids_output); } Result GetTitleInfoFromList(std::span title_id_list, Service::FS::MediaType media_type, @@ -995,27 +1224,111 @@ Result GetTitleInfoFromList(std::span title_id_list, Service::FS::Med return ResultSuccess; } -void Module::Interface::GetProgramInfos(Kernel::HLERequestContext& ctx) { +void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool ignore_platform) { IPC::RequestParser rp(ctx); auto media_type = static_cast(rp.Pop()); u32 title_count = rp.Pop(); - auto& title_id_list_buffer = rp.PopMappedBuffer(); - auto& title_info_out = rp.PopMappedBuffer(); - std::vector title_id_list(title_count); - title_id_list_buffer.Read(title_id_list.data(), 0, title_count * sizeof(u64)); + if (artic_client.get()) { + struct AsyncData { + u8 media_type; + bool ignore_platform; + std::vector title_id_list; - Result result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); + Result res{0}; + std::vector out; + Kernel::MappedBuffer* title_id_list_buffer; + Kernel::MappedBuffer* title_info_out; + }; + auto async_data = std::make_shared(); + async_data->media_type = static_cast(media_type); + async_data->ignore_platform = ignore_platform; + async_data->title_id_list.resize(title_count); + async_data->title_id_list_buffer = &rp.PopMappedBuffer(); + async_data->title_id_list_buffer->Read(async_data->title_id_list.data(), 0, + title_count * sizeof(u64)); + async_data->title_info_out = &rp.PopMappedBuffer(); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); - rb.Push(result); - rb.PushMappedBuffer(title_id_list_buffer); - rb.PushMappedBuffer(title_info_out); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AM_GetTitleInfo"); + + req.AddParameterU8(async_data->media_type); + req.AddParameterBuffer(async_data->title_id_list.data(), + async_data->title_id_list.size() * sizeof(u64)); + req.AddParameterU8(async_data->ignore_platform ? 1 : 0); + + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + async_data->res = res; + + auto title_infos = resp->GetResponseBuffer(0); + if (!title_infos.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->out.resize(title_infos->second); + memcpy(async_data->out.data(), title_infos->first, title_infos->second); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (!async_data->res.IsSuccess()) { + IPC::RequestBuilder rb(ctx, 1, async_data->ignore_platform ? 0 : 4); + rb.Push(async_data->res); + if (!async_data->ignore_platform) { + rb.PushMappedBuffer(*async_data->title_id_list_buffer); + rb.PushMappedBuffer(*async_data->title_info_out); + } + } else { + async_data->title_info_out->Write(async_data->out.data(), 0, + async_data->out.size()); + + IPC::RequestBuilder rb(ctx, 1, async_data->ignore_platform ? 0 : 4); + rb.Push(async_data->res); + if (!async_data->ignore_platform) { + rb.PushMappedBuffer(*async_data->title_id_list_buffer); + rb.PushMappedBuffer(*async_data->title_info_out); + } + } + }, + true); + + } else { + auto& title_id_list_buffer = rp.PopMappedBuffer(); + auto& title_info_out = rp.PopMappedBuffer(); + + std::vector title_id_list(title_count); + title_id_list_buffer.Read(title_id_list.data(), 0, title_count * sizeof(u64)); + + Result result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, ignore_platform ? 0 : 4); + rb.Push(result); + if (!ignore_platform) { + rb.PushMappedBuffer(title_id_list_buffer); + rb.PushMappedBuffer(title_info_out); + } + } +} + +void Module::Interface::GetProgramInfos(Kernel::HLERequestContext& ctx) { + GetProgramInfosImpl(ctx, false); } void Module::Interface::GetProgramInfosIgnorePlatform(Kernel::HLERequestContext& ctx) { - GetProgramInfos(ctx); + GetProgramInfosImpl(ctx, true); } void Module::Interface::DeleteUserProgram(Kernel::HLERequestContext& ctx) { @@ -1077,32 +1390,102 @@ void Module::Interface::GetDLCTitleInfos(Kernel::HLERequestContext& ctx) { auto media_type = static_cast(rp.Pop()); u32 title_count = rp.Pop(); - auto& title_id_list_buffer = rp.PopMappedBuffer(); - auto& title_info_out = rp.PopMappedBuffer(); - std::vector title_id_list(title_count); - title_id_list_buffer.Read(title_id_list.data(), 0, title_count * sizeof(u64)); + if (artic_client.get()) { + struct AsyncData { + u8 media_type; + std::vector title_id_list; - Result result = ResultSuccess; + Result res{0}; + std::vector out; + Kernel::MappedBuffer* title_id_list_buffer; + Kernel::MappedBuffer* title_info_out; + }; + auto async_data = std::make_shared(); + async_data->media_type = static_cast(media_type); + async_data->title_id_list.resize(title_count); + async_data->title_id_list_buffer = &rp.PopMappedBuffer(); + async_data->title_id_list_buffer->Read(async_data->title_id_list.data(), 0, + title_count * sizeof(u64)); + async_data->title_info_out = &rp.PopMappedBuffer(); - // Validate that DLC TIDs were passed in - for (u32 i = 0; i < title_count; i++) { - u32 tid_high = static_cast(title_id_list[i] >> 32); - if (tid_high != TID_HIGH_DLC) { - result = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, - ErrorSummary::InvalidArgument, ErrorLevel::Usage); - break; + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AMAPP_GetDLCTitleInfos"); + + req.AddParameterU8(async_data->media_type); + req.AddParameterBuffer(async_data->title_id_list.data(), + async_data->title_id_list.size() * sizeof(u64)); + + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + async_data->res = res; + + auto title_infos = resp->GetResponseBuffer(0); + if (!title_infos.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->out.resize(title_infos->second); + memcpy(async_data->out.data(), title_infos->first, title_infos->second); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (!async_data->res.IsSuccess()) { + IPC::RequestBuilder rb(ctx, 1, 4); + rb.Push(async_data->res); + rb.PushMappedBuffer(*async_data->title_id_list_buffer); + rb.PushMappedBuffer(*async_data->title_info_out); + } else { + async_data->title_info_out->Write(async_data->out.data(), 0, + async_data->out.size()); + + IPC::RequestBuilder rb(ctx, 1, 4); + rb.Push(async_data->res); + rb.PushMappedBuffer(*async_data->title_id_list_buffer); + rb.PushMappedBuffer(*async_data->title_info_out); + } + }, + true); + } else { + auto& title_id_list_buffer = rp.PopMappedBuffer(); + auto& title_info_out = rp.PopMappedBuffer(); + + std::vector title_id_list(title_count); + title_id_list_buffer.Read(title_id_list.data(), 0, title_count * sizeof(u64)); + + Result result = ResultSuccess; + + // Validate that DLC TIDs were passed in + for (u32 i = 0; i < title_count; i++) { + u32 tid_high = static_cast(title_id_list[i] >> 32); + if (tid_high != TID_HIGH_DLC) { + result = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, + ErrorSummary::InvalidArgument, ErrorLevel::Usage); + break; + } } - } - if (result.IsSuccess()) { - result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); - } + if (result.IsSuccess()) { + result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); + } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); - rb.Push(result); - rb.PushMappedBuffer(title_id_list_buffer); - rb.PushMappedBuffer(title_info_out); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); + rb.Push(result); + rb.PushMappedBuffer(title_id_list_buffer); + rb.PushMappedBuffer(title_info_out); + } } void Module::Interface::GetPatchTitleInfos(Kernel::HLERequestContext& ctx) { @@ -1110,32 +1493,102 @@ void Module::Interface::GetPatchTitleInfos(Kernel::HLERequestContext& ctx) { auto media_type = static_cast(rp.Pop()); u32 title_count = rp.Pop(); - auto& title_id_list_buffer = rp.PopMappedBuffer(); - auto& title_info_out = rp.PopMappedBuffer(); - std::vector title_id_list(title_count); - title_id_list_buffer.Read(title_id_list.data(), 0, title_count * sizeof(u64)); + if (artic_client.get()) { + struct AsyncData { + u8 media_type; + std::vector title_id_list; - Result result = ResultSuccess; + Result res{0}; + std::vector out; + Kernel::MappedBuffer* title_id_list_buffer; + Kernel::MappedBuffer* title_info_out; + }; + auto async_data = std::make_shared(); + async_data->media_type = static_cast(media_type); + async_data->title_id_list.resize(title_count); + async_data->title_id_list_buffer = &rp.PopMappedBuffer(); + async_data->title_id_list_buffer->Read(async_data->title_id_list.data(), 0, + title_count * sizeof(u64)); + async_data->title_info_out = &rp.PopMappedBuffer(); - // Validate that update TIDs were passed in - for (u32 i = 0; i < title_count; i++) { - u32 tid_high = static_cast(title_id_list[i] >> 32); - if (tid_high != TID_HIGH_UPDATE) { - result = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, - ErrorSummary::InvalidArgument, ErrorLevel::Usage); - break; + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AMAPP_GetPatchTitleInfos"); + + req.AddParameterU8(async_data->media_type); + req.AddParameterBuffer(async_data->title_id_list.data(), + async_data->title_id_list.size() * sizeof(u64)); + + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + async_data->res = res; + + auto title_infos = resp->GetResponseBuffer(0); + if (!title_infos.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->out.resize(title_infos->second); + memcpy(async_data->out.data(), title_infos->first, title_infos->second); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (!async_data->res.IsSuccess()) { + IPC::RequestBuilder rb(ctx, 1, 4); + rb.Push(async_data->res); + rb.PushMappedBuffer(*async_data->title_id_list_buffer); + rb.PushMappedBuffer(*async_data->title_info_out); + } else { + async_data->title_info_out->Write(async_data->out.data(), 0, + async_data->out.size()); + + IPC::RequestBuilder rb(ctx, 1, 4); + rb.Push(async_data->res); + rb.PushMappedBuffer(*async_data->title_id_list_buffer); + rb.PushMappedBuffer(*async_data->title_info_out); + } + }, + true); + } else { + auto& title_id_list_buffer = rp.PopMappedBuffer(); + auto& title_info_out = rp.PopMappedBuffer(); + + std::vector title_id_list(title_count); + title_id_list_buffer.Read(title_id_list.data(), 0, title_count * sizeof(u64)); + + Result result = ResultSuccess; + + // Validate that update TIDs were passed in + for (u32 i = 0; i < title_count; i++) { + u32 tid_high = static_cast(title_id_list[i] >> 32); + if (tid_high != TID_HIGH_UPDATE) { + result = Result(ErrCodes::InvalidTIDInList, ErrorModule::AM, + ErrorSummary::InvalidArgument, ErrorLevel::Usage); + break; + } } - } - if (result.IsSuccess()) { - result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); - } + if (result.IsSuccess()) { + result = GetTitleInfoFromList(title_id_list, media_type, title_info_out); + } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); - rb.Push(result); - rb.PushMappedBuffer(title_id_list_buffer); - rb.PushMappedBuffer(title_info_out); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 4); + rb.Push(result); + rb.PushMappedBuffer(title_id_list_buffer); + rb.PushMappedBuffer(title_info_out); + } } void Module::Interface::ListDataTitleTicketInfos(Kernel::HLERequestContext& ctx) { @@ -1143,56 +1596,165 @@ void Module::Interface::ListDataTitleTicketInfos(Kernel::HLERequestContext& ctx) u32 ticket_count = rp.Pop(); u64 title_id = rp.Pop(); u32 start_index = rp.Pop(); - auto& ticket_info_out = rp.PopMappedBuffer(); + if (artic_client.get()) { + struct AsyncData { + u64 title_id; + u32 ticket_count; + u32 start_index; - std::size_t write_offset = 0; - for (u32 i = 0; i < ticket_count; i++) { - TicketInfo ticket_info = {}; - ticket_info.title_id = title_id; - ticket_info.version = 0; // TODO - ticket_info.size = 0; // TODO + Result res{0}; + std::vector out; + Kernel::MappedBuffer* ticket_info_out; + }; + auto async_data = std::make_shared(); + async_data->title_id = title_id; + async_data->ticket_count = ticket_count; + async_data->start_index = start_index; + async_data->ticket_info_out = &rp.PopMappedBuffer(); - ticket_info_out.Write(&ticket_info, write_offset, sizeof(TicketInfo)); - write_offset += sizeof(TicketInfo); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AMAPP_ListDataTitleTicketInfos"); + + req.AddParameterU32(async_data->ticket_count); + req.AddParameterU64(async_data->title_id); + req.AddParameterU32(async_data->start_index); + + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + + auto content_info = resp->GetResponseBuffer(0); + if (!content_info.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->out.resize(content_info->second); + memcpy(async_data->out.data(), content_info->first, content_info->second); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.IsSuccess()) { + async_data->ticket_info_out->Write(async_data->out.data(), 0, + async_data->out.size()); + } + IPC::RequestBuilder rb(ctx, 2, 0); + rb.Push(async_data->res); + rb.Push(static_cast(async_data->out.size() / sizeof(TicketInfo))); + }, + true); + } else { + auto& ticket_info_out = rp.PopMappedBuffer(); + + std::size_t write_offset = 0; + for (u32 i = 0; i < ticket_count; i++) { + TicketInfo ticket_info = {}; + ticket_info.title_id = title_id; + ticket_info.version = 0; // TODO + ticket_info.size = 0; // TODO + + ticket_info_out.Write(&ticket_info, write_offset, sizeof(TicketInfo)); + write_offset += sizeof(TicketInfo); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + rb.Push(ResultSuccess); + rb.Push(ticket_count); + rb.PushMappedBuffer(ticket_info_out); + + LOG_WARNING(Service_AM, + "(STUBBED) ticket_count=0x{:08X}, title_id=0x{:016x}, start_index=0x{:08X}", + ticket_count, title_id, start_index); } - - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(ResultSuccess); - rb.Push(ticket_count); - rb.PushMappedBuffer(ticket_info_out); - - LOG_WARNING(Service_AM, - "(STUBBED) ticket_count=0x{:08X}, title_id=0x{:016x}, start_index=0x{:08X}", - ticket_count, title_id, start_index); } void Module::Interface::GetDLCContentInfoCount(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); auto media_type = static_cast(rp.Pop()); u64 title_id = rp.Pop(); + if (artic_client.get()) { + struct AsyncData { + u8 media_type; + u64 title_id; - // Validate that only DLC TIDs are passed in - u32 tid_high = static_cast(title_id >> 32); - if (tid_high != TID_HIGH_DLC) { - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(Result(ErrCodes::InvalidTID, ErrorModule::AM, ErrorSummary::InvalidArgument, - ErrorLevel::Usage)); - rb.Push(0); - return; - } + ResultVal res; + }; + auto async_data = std::make_shared(); + async_data->media_type = static_cast(media_type); + async_data->title_id = title_id; - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(ResultSuccess); // No error + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + auto req = artic_client->NewRequest("AMAPP_GetDLCContentInfoCount"); - std::string tmd_path = GetTitleMetadataPath(media_type, title_id); + req.AddParameterU8(async_data->media_type); + req.AddParameterU64(async_data->title_id); - FileSys::TitleMetadata tmd; - if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { - rb.Push(static_cast(tmd.GetContentCount())); + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) { + async_data->res = Result(-1); + return 0; + } + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) { + async_data->res = res; + return 0; + } + + auto count = resp->GetResponseS32(0); + if (!count.has_value()) { + async_data->res = Result(-1); + return 0; + } + + async_data->res = *count; + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 0); + + rb.Push(async_data->res.Code()); + rb.Push( + static_cast(async_data->res.Succeeded() ? async_data->res.Unwrap() : 0)); + }, + true); } else { - rb.Push(1); // Number of content infos plus one - LOG_WARNING(Service_AM, "(STUBBED) called media_type={}, title_id=0x{:016x}", media_type, - title_id); + + // Validate that only DLC TIDs are passed in + u32 tid_high = static_cast(title_id >> 32); + if (tid_high != TID_HIGH_DLC) { + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(Result(ErrCodes::InvalidTID, ErrorModule::AM, ErrorSummary::InvalidArgument, + ErrorLevel::Usage)); + rb.Push(0); + return; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); // No error + + std::string tmd_path = GetTitleMetadataPath(media_type, title_id); + + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { + rb.Push(static_cast(tmd.GetContentCount())); + } else { + rb.Push(1); // Number of content infos plus one + LOG_WARNING(Service_AM, "(STUBBED) called media_type={}, title_id=0x{:016x}", + media_type, title_id); + } } } @@ -1464,9 +2026,9 @@ public: return file->backend->Read(offset + file_offset, length, buffer); } - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override { - return file->backend->Write(offset + file_offset, length, flush, buffer); + return file->backend->Write(offset + file_offset, length, flush, update_timestamp, buffer); } u64 GetSize() const override { @@ -1475,7 +2037,7 @@ public: bool SetSize(u64 size) const override { return false; } - bool Close() const override { + bool Close() override { return false; } void Flush() const override {} diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 26b9b1056..c56fa0888 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -20,6 +20,7 @@ #include "core/hle/kernel/mutex.h" #include "core/hle/result.h" #include "core/hle/service/service.h" +#include "network/artic_base/artic_base_client.h" namespace Core { class System; @@ -111,11 +112,11 @@ public: Result WriteTicket(); Result WriteTitleMetadata(); ResultVal WriteContentData(u64 offset, std::size_t length, const u8* buffer); - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override; + bool Close() override; void Flush() const override; private: @@ -146,11 +147,11 @@ public: ~TicketFile(); ResultVal Read(u64 offset, std::size_t length, u8* buffer) const override; - ResultVal Write(u64 offset, std::size_t length, bool flush, + ResultVal Write(u64 offset, std::size_t length, bool flush, bool update_timestamp, const u8* buffer) override; u64 GetSize() const override; bool SetSize(u64 size) const override; - bool Close() const override; + bool Close() override; void Flush() const override; private: @@ -245,7 +246,13 @@ public: return am; } + void UseArticClient(std::shared_ptr& client) { + artic_client = client; + } + protected: + void GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool ignore_platform); + /** * AM::GetNumPrograms service function * Gets the number of installed titles in the requested media type @@ -753,6 +760,9 @@ public: protected: std::shared_ptr am; + + // Placed on the interface level so that only am:net and am:app have it. + std::shared_ptr artic_client = nullptr; }; /** diff --git a/src/core/hle/service/apt/apt.cpp b/src/core/hle/service/apt/apt.cpp index 3e552dfc4..58cac93e3 100644 --- a/src/core/hle/service/apt/apt.cpp +++ b/src/core/hle/service/apt/apt.cpp @@ -217,7 +217,7 @@ bool Module::LoadSharedFont() { const FileSys::Path file_path(std::vector(20, 0)); FileSys::Mode open_mode = {}; open_mode.read_flag.Assign(1); - auto file_result = archive.OpenFile(file_path, open_mode); + auto file_result = archive.OpenFile(file_path, open_mode, 0); if (file_result.Failed()) return false; diff --git a/src/core/hle/service/boss/online_service.cpp b/src/core/hle/service/boss/online_service.cpp index d289c910b..cd587aa51 100644 --- a/src/core/hle/service/boss/online_service.cpp +++ b/src/core/hle/service/boss/online_service.cpp @@ -75,7 +75,7 @@ Result OnlineService::InitializeSession(u64 init_program_id) { boss_system_save_data_archive = std::move(archive_result).Unwrap(); } else if (archive_result.Code() == FileSys::ResultNotFound) { // If the archive didn't exist, create the files inside - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists auto create_archive_result = systemsavedata_factory.Open(archive_path, 0); diff --git a/src/core/hle/service/cam/y2r_u.cpp b/src/core/hle/service/cam/y2r_u.cpp index 10194f853..0c6f97a14 100644 --- a/src/core/hle/service/cam/y2r_u.cpp +++ b/src/core/hle/service/cam/y2r_u.cpp @@ -544,6 +544,7 @@ void Y2R_U::StopConversion(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); if (is_busy_conversion) { + is_busy_conversion = false; system.CoreTiming().RemoveEvent(completion_signal_event); } diff --git a/src/core/hle/service/cecd/cecd.cpp b/src/core/hle/service/cecd/cecd.cpp index bbd4cc0a2..112e766af 100644 --- a/src/core/hle/service/cecd/cecd.cpp +++ b/src/core/hle/service/cecd/cecd.cpp @@ -116,7 +116,7 @@ void Module::Interface::Open(Kernel::HLERequestContext& ctx) { std::vector program_id(8); u64_le le_program_id = cecd->system.Kernel().GetCurrentProcess()->codeset->program_id; std::memcpy(program_id.data(), &le_program_id, sizeof(u64)); - session_data->file->Write(0, sizeof(u64), true, program_id.data()); + session_data->file->Write(0, sizeof(u64), true, false, program_id.data()); session_data->file->Close(); } } @@ -373,7 +373,7 @@ void Module::Interface::Write(Kernel::HLERequestContext& ctx) { } [[maybe_unused]] const u32 bytes_written = static_cast( - session_data->file->Write(0, buffer.size(), true, buffer.data()).Unwrap()); + session_data->file->Write(0, buffer.size(), true, false, buffer.data()).Unwrap()); session_data->file->Close(); rb.Push(ResultSuccess); @@ -435,7 +435,7 @@ void Module::Interface::WriteMessage(Kernel::HLERequestContext& ctx) { msg_header.forward_count, msg_header.user_data); [[maybe_unused]] const u32 bytes_written = - static_cast(message->Write(0, buffer_size, true, buffer.data()).Unwrap()); + static_cast(message->Write(0, buffer_size, true, false, buffer.data()).Unwrap()); message->Close(); rb.Push(ResultSuccess); @@ -522,7 +522,7 @@ void Module::Interface::WriteMessageWithHMAC(Kernel::HLERequestContext& ctx) { std::memcpy(buffer.data() + hmac_offset, hmac_digest.data(), hmac_size); [[maybe_unused]] const u32 bytes_written = - static_cast(message->Write(0, buffer_size, true, buffer.data()).Unwrap()); + static_cast(message->Write(0, buffer_size, true, false, buffer.data()).Unwrap()); message->Close(); rb.Push(ResultSuccess); @@ -607,7 +607,7 @@ void Module::Interface::SetData(Kernel::HLERequestContext& ctx) { cecd->CheckAndUpdateFile(CecDataPathType::OutboxIndex, ncch_program_id, buffer); - file->Write(0, buffer.size(), true, buffer.data()); + file->Write(0, buffer.size(), true, false, buffer.data()); file->Close(); } } @@ -764,8 +764,8 @@ void Module::Interface::OpenAndWrite(Kernel::HLERequestContext& ctx) { cecd->CheckAndUpdateFile(path_type, ncch_program_id, buffer); } - [[maybe_unused]] const u32 bytes_written = - static_cast(file->Write(0, buffer.size(), true, buffer.data()).Unwrap()); + [[maybe_unused]] const u32 bytes_written = static_cast( + file->Write(0, buffer.size(), true, false, buffer.data()).Unwrap()); file->Close(); rb.Push(ResultSuccess); @@ -1409,7 +1409,7 @@ Module::Module(Core::System& system) : system(system) { cecd_system_save_data_archive = std::move(archive_result).Unwrap(); } else { // Format the archive to create the directories - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists cecd_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); @@ -1442,7 +1442,7 @@ Module::Module(Core::System& system) : system(system) { eventlog_buffer[1] = 0x41; eventlog_buffer[2] = 0x12; - eventlog->Write(0, eventlog_size, true, eventlog_buffer.data()); + eventlog->Write(0, eventlog_size, true, false, eventlog_buffer.data()); eventlog->Close(); /// MBoxList____ resides within the root CEC/ directory. @@ -1464,7 +1464,7 @@ Module::Module(Core::System& system) : system(system) { // mboxlist_buffer[2-3] are already zeroed mboxlist_buffer[4] = 0x01; - mboxlist->Write(0, mboxlist_size, true, mboxlist_buffer.data()); + mboxlist->Write(0, mboxlist_size, true, false, mboxlist_buffer.data()); mboxlist->Close(); } } diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index d118250ba..0eef32aca 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include "common/archives.h" #include "common/file_util.h" #include "common/logging/log.h" @@ -278,7 +278,7 @@ void Module::Interface::GetTransferableId(Kernel::HLERequestContext& ctx) { std::array buffer; const Result result = - cfg->GetConfigBlock(ConsoleUniqueID2BlockID, 8, AccessFlag::SystemRead, buffer.data()); + cfg->GetConfigBlock(ConsoleUniqueID2BlockID, 8, AccessFlag::Global, buffer.data()); rb.Push(result); if (result.IsSuccess()) { std::memcpy(&buffer[8], &app_id_salt, sizeof(u32)); @@ -502,11 +502,42 @@ ResultVal Module::GetConfigBlockPointer(u32 block_id, u32 size, AccessFla } Result Module::GetConfigBlock(u32 block_id, u32 size, AccessFlag accesss_flag, void* output) { - void* pointer = nullptr; - CASCADE_RESULT(pointer, GetConfigBlockPointer(block_id, size, accesss_flag)); - std::memcpy(output, pointer, size); + bool get_from_artic = + block_id == ConsoleUniqueID2BlockID && + (static_cast(accesss_flag) & static_cast(AccessFlag::UserRead)) != 0; - return ResultSuccess; + if (get_from_artic && artic_client.get()) { + auto req = artic_client->NewRequest("CFGU_GetConfigInfoBlk2"); + + req.AddParameterS32(block_id); + req.AddParameterU32(size); + + auto resp = artic_client->Send(req); + + if (!resp.has_value() || !resp->Succeeded()) + return Result(-1); + + auto res = Result(static_cast(resp->GetMethodResult())); + if (res.IsError()) + return res; + + auto buff = resp->GetResponseBuffer(0); + if (!buff.has_value()) + return Result(-1); + size_t actually_read = buff->second; + if (actually_read > size) + return Result(-1); + + memcpy(output, buff->first, actually_read); + return ResultSuccess; + + } else { + void* pointer = nullptr; + CASCADE_RESULT(pointer, GetConfigBlockPointer(block_id, size, accesss_flag)); + std::memcpy(output, pointer, size); + + return ResultSuccess; + } } Result Module::SetConfigBlock(u32 block_id, u32 size, AccessFlag accesss_flag, const void* input) { @@ -565,7 +596,7 @@ Result Module::UpdateConfigNANDSavegame() { ASSERT_MSG(config_result.Succeeded(), "could not open file"); auto config = std::move(config_result).Unwrap(); - config->Write(0, CONFIG_SAVEFILE_SIZE, 1, cfg_config_file_buffer.data()); + config->Write(0, CONFIG_SAVEFILE_SIZE, true, false, cfg_config_file_buffer.data()); return ResultSuccess; } @@ -625,7 +656,7 @@ Result Module::LoadConfigNANDSaveFile() { // If the archive didn't exist, create the files inside if (archive_result.Code() == FileSys::ResultNotFound) { // Format the archive to create the directories - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists cfg_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); diff --git a/src/core/hle/service/cfg/cfg.h b/src/core/hle/service/cfg/cfg.h index 7bd6a1310..263cd51e6 100644 --- a/src/core/hle/service/cfg/cfg.h +++ b/src/core/hle/service/cfg/cfg.h @@ -11,6 +11,7 @@ #include #include "common/common_types.h" #include "core/hle/service/service.h" +#include "network/artic_base/artic_base_client.h" namespace FileSys { class ArchiveBackend; @@ -210,6 +211,10 @@ public: std::shared_ptr GetModule() const; + void UseArticClient(std::shared_ptr& client) { + GetModule()->artic_client = client; + } + /** * CFG::GetCountryCodeString service function * Inputs: @@ -680,6 +685,8 @@ private: bool preferred_region_chosen = false; MCUData mcu_data{}; + std::shared_ptr artic_client = nullptr; + template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; diff --git a/src/core/hle/service/fs/archive.cpp b/src/core/hle/service/fs/archive.cpp index 44e303129..2a48a741a 100644 --- a/src/core/hle/service/fs/archive.cpp +++ b/src/core/hle/service/fs/archive.cpp @@ -67,10 +67,24 @@ ResultVal ArchiveManager::OpenArchive(ArchiveIdCode id_code, } Result ArchiveManager::CloseArchive(ArchiveHandle handle) { - if (handle_map.erase(handle) == 0) + auto itr = handle_map.find(handle); + if (itr != handle_map.end()) { + itr->second->Close(); + } else { return FileSys::ResultInvalidArchiveHandle; - else - return ResultSuccess; + } + handle_map.erase(itr); + return ResultSuccess; +} + +Result ArchiveManager::ControlArchive(ArchiveHandle handle, u32 action, u8* input, + size_t input_size, u8* output, size_t output_size) { + auto itr = handle_map.find(handle); + if (itr != handle_map.end()) { + return itr->second->Control(action, input, input_size, output, output_size); + } else { + return FileSys::ResultInvalidArchiveHandle; + } } // TODO(yuriks): This might be what the fs:REG service is for. See the Register/Unregister calls in @@ -90,14 +104,14 @@ Result ArchiveManager::RegisterArchiveType(std::unique_ptr>, std::chrono::nanoseconds> ArchiveManager::OpenFileFromArchive(ArchiveHandle archive_handle, const FileSys::Path& path, - const FileSys::Mode mode) { + const FileSys::Mode mode, u32 attributes) { ArchiveBackend* archive = GetArchive(archive_handle); if (archive == nullptr) { return std::make_pair(FileSys::ResultInvalidArchiveHandle, std::chrono::nanoseconds{0}); } const std::chrono::nanoseconds open_timeout_ns{archive->GetOpenDelayNs()}; - auto backend = archive->OpenFile(path, mode); + auto backend = archive->OpenFile(path, mode, attributes); if (backend.Failed()) { return std::make_pair(backend.Code(), open_timeout_ns); } @@ -151,21 +165,21 @@ Result ArchiveManager::DeleteDirectoryRecursivelyFromArchive(ArchiveHandle archi } Result ArchiveManager::CreateFileInArchive(ArchiveHandle archive_handle, const FileSys::Path& path, - u64 file_size) { + u64 file_size, u32 attributes) { ArchiveBackend* archive = GetArchive(archive_handle); if (archive == nullptr) return FileSys::ResultInvalidArchiveHandle; - return archive->CreateFile(path, file_size); + return archive->CreateFile(path, file_size, attributes); } Result ArchiveManager::CreateDirectoryFromArchive(ArchiveHandle archive_handle, - const FileSys::Path& path) { + const FileSys::Path& path, u32 attributes) { ArchiveBackend* archive = GetArchive(archive_handle); if (archive == nullptr) return FileSys::ResultInvalidArchiveHandle; - return archive->CreateDirectory(path); + return archive->CreateDirectory(path, attributes); } Result ArchiveManager::RenameDirectoryBetweenArchives(ArchiveHandle src_archive_handle, @@ -210,13 +224,15 @@ ResultVal ArchiveManager::GetFreeBytesInArchive(ArchiveHandle archive_handl Result ArchiveManager::FormatArchive(ArchiveIdCode id_code, const FileSys::ArchiveFormatInfo& format_info, - const FileSys::Path& path, u64 program_id) { + const FileSys::Path& path, u64 program_id, + u32 directory_buckets, u32 file_buckets) { auto archive_itr = id_code_map.find(id_code); if (archive_itr == id_code_map.end()) { return UnimplementedFunction(ErrorModule::FS); // TODO(Subv): Find the right error } - return archive_itr->second->Format(path, format_info, program_id); + return archive_itr->second->Format(path, format_info, program_id, directory_buckets, + file_buckets); } ResultVal ArchiveManager::GetArchiveFormatInfo( @@ -229,10 +245,10 @@ ResultVal ArchiveManager::GetArchiveFormatInfo( return archive->second->GetFormatInfo(archive_path, program_id); } -Result ArchiveManager::CreateExtSaveData(MediaType media_type, u32 high, u32 low, +Result ArchiveManager::CreateExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low, std::span smdh_icon, const FileSys::ArchiveFormatInfo& format_info, - u64 program_id) { + u64 program_id, u64 total_size) { // Construct the binary path to the archive first FileSys::Path path = FileSys::ConstructExtDataBinaryPath(static_cast(media_type), high, low); @@ -246,37 +262,26 @@ Result ArchiveManager::CreateExtSaveData(MediaType media_type, u32 high, u32 low auto ext_savedata = static_cast(archive->second.get()); - Result result = ext_savedata->Format(path, format_info, program_id); + Result result = ext_savedata->FormatAsExtData(path, format_info, unknown, program_id, + total_size, smdh_icon); if (result.IsError()) { return result; } - ext_savedata->WriteIcon(path, smdh_icon); return ResultSuccess; } -Result ArchiveManager::DeleteExtSaveData(MediaType media_type, u32 high, u32 low) { - // Construct the binary path to the archive first - FileSys::Path path = - FileSys::ConstructExtDataBinaryPath(static_cast(media_type), high, low); +Result ArchiveManager::DeleteExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low) { + auto archive = id_code_map.find(media_type == MediaType::NAND ? ArchiveIdCode::SharedExtSaveData + : ArchiveIdCode::ExtSaveData); - std::string media_type_directory; - if (media_type == MediaType::NAND) { - media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); - } else if (media_type == MediaType::SDMC) { - media_type_directory = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); - } else { - LOG_ERROR(Service_FS, "Unsupported media type {}", media_type); - return ResultUnknown; // TODO(Subv): Find the right error code + if (archive == id_code_map.end()) { + return UnimplementedFunction(ErrorModule::FS); // TODO(Subv): Find the right error } - // Delete all directories (/user, /boss) and the icon file. - std::string base_path = - FileSys::GetExtDataContainerPath(media_type_directory, media_type == MediaType::NAND); - std::string extsavedata_path = FileSys::GetExtSaveDataPath(base_path, path); - if (FileUtil::Exists(extsavedata_path) && !FileUtil::DeleteDirRecursively(extsavedata_path)) - return ResultUnknown; // TODO(Subv): Find the right error code - return ResultSuccess; + auto ext_savedata = static_cast(archive->second.get()); + + return ext_savedata->DeleteExtData(media_type, unknown, high, low); } Result ArchiveManager::DeleteSystemSaveData(u32 high, u32 low) { @@ -293,18 +298,22 @@ Result ArchiveManager::DeleteSystemSaveData(u32 high, u32 low) { return ResultSuccess; } -Result ArchiveManager::CreateSystemSaveData(u32 high, u32 low) { - // Construct the binary path to the archive first - const FileSys::Path path = FileSys::ConstructSystemSaveDataBinaryPath(high, low); +Result ArchiveManager::CreateSystemSaveData(u32 high, u32 low, u32 total_size, u32 block_size, + u32 number_directories, u32 number_files, + u32 number_directory_buckets, u32 number_file_buckets, + u8 duplicate_data) { - const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); - const std::string base_path = FileSys::GetSystemSaveDataContainerPath(nand_directory); - const std::string systemsavedata_path = FileSys::GetSystemSaveDataPath(base_path, path); - if (!FileUtil::CreateFullPath(systemsavedata_path)) { - return ResultUnknown; // TODO(Subv): Find the right error code + auto archive = id_code_map.find(ArchiveIdCode::SystemSaveData); + + if (archive == id_code_map.end()) { + return UnimplementedFunction(ErrorModule::FS); // TODO(Subv): Find the right error } - return ResultSuccess; + auto sys_savedata = static_cast(archive->second.get()); + + return sys_savedata->FormatAsSysData(high, low, total_size, block_size, number_directories, + number_files, number_directory_buckets, + number_file_buckets, duplicate_data); } ResultVal ArchiveManager::GetArchiveResource(MediaType media_type) const { @@ -317,6 +326,24 @@ ResultVal ArchiveManager::GetArchiveResource(MediaType media_ty return resource; } +Result ArchiveManager::SetSaveDataSecureValue(ArchiveHandle archive_handle, u32 secure_value_slot, + u64 secure_value, bool flush) { + ArchiveBackend* archive = GetArchive(archive_handle); + if (archive == nullptr) { + return FileSys::ResultInvalidArchiveHandle; + } + return archive->SetSaveDataSecureValue(secure_value_slot, secure_value, flush); +} + +ResultVal> ArchiveManager::GetSaveDataSecureValue( + ArchiveHandle archive_handle, u32 secure_value_slot) { + ArchiveBackend* archive = GetArchive(archive_handle); + if (archive == nullptr) { + return FileSys::ResultInvalidArchiveHandle; + } + return archive->GetSaveDataSecureValue(secure_value_slot); +} + void ArchiveManager::RegisterArchiveTypes() { // TODO(Subv): Add the other archive types (see here for the known types: // http://3dbrew.org/wiki/FS:OpenArchive#Archive_idcodes). @@ -337,7 +364,7 @@ void ArchiveManager::RegisterArchiveTypes() { sdmc_directory); // Create the SaveData archive - auto sd_savedata_source = std::make_shared(sdmc_directory); + sd_savedata_source = std::make_shared(sdmc_directory); auto savedata_factory = std::make_unique(sd_savedata_source); RegisterArchiveType(std::move(savedata_factory), ArchiveIdCode::SaveData); auto other_savedata_permitted_factory = @@ -373,6 +400,23 @@ void ArchiveManager::RegisterArchiveTypes() { RegisterArchiveType(std::move(selfncch_factory), ArchiveIdCode::SelfNCCH); } +bool ArchiveManager::ArchiveIsSlow(ArchiveIdCode archive_id) { + auto itr = id_code_map.find(archive_id); + if (itr == id_code_map.end() || itr->second.get() == nullptr) { + return false; + } + + return itr->second->IsSlow(); +} + +bool ArchiveManager::ArchiveIsSlow(ArchiveHandle archive_handle) { + ArchiveBackend* archive = GetArchive(archive_handle); + if (archive == nullptr) { + return false; + } + return archive->IsSlow(); +} + void ArchiveManager::RegisterSelfNCCH(Loader::AppLoader& app_loader) { auto itr = id_code_map.find(ArchiveIdCode::SelfNCCH); if (itr == id_code_map.end()) { @@ -385,6 +429,45 @@ void ArchiveManager::RegisterSelfNCCH(Loader::AppLoader& app_loader) { factory->Register(app_loader); } +void ArchiveManager::RegisterArticSaveDataSource( + std::shared_ptr& client) { + if (!sd_savedata_source.get()) { + LOG_ERROR(Service_FS, "Could not register artic save data source."); + return; + } + sd_savedata_source->RegisterArtic(client); +} + +void ArchiveManager::RegisterArticExtData(std::shared_ptr& client) { + for (auto it : {ArchiveIdCode::ExtSaveData, ArchiveIdCode::SharedExtSaveData, + ArchiveIdCode::BossExtSaveData}) { + auto itr = id_code_map.find(it); + if (itr == id_code_map.end() || itr->second.get() == nullptr) { + continue; + } + reinterpret_cast(itr->second.get()) + ->RegisterArtic(client); + } +} + +void ArchiveManager::RegisterArticNCCH(std::shared_ptr& client) { + auto itr = id_code_map.find(ArchiveIdCode::NCCH); + if (itr == id_code_map.end() || itr->second.get() == nullptr) { + return; + } + reinterpret_cast(itr->second.get())->RegisterArtic(client); +} + +void ArchiveManager::RegisterArticSystemSaveData( + std::shared_ptr& client) { + auto itr = id_code_map.find(ArchiveIdCode::SystemSaveData); + if (itr == id_code_map.end() || itr->second.get() == nullptr) { + return; + } + reinterpret_cast(itr->second.get()) + ->RegisterArtic(client); +} + ArchiveManager::ArchiveManager(Core::System& system) : system(system) { RegisterArchiveTypes(); } diff --git a/src/core/hle/service/fs/archive.h b/src/core/hle/service/fs/archive.h index 245e929f9..c9d5df87c 100644 --- a/src/core/hle/service/fs/archive.h +++ b/src/core/hle/service/fs/archive.h @@ -12,9 +12,11 @@ #include #include "common/common_types.h" #include "core/file_sys/archive_backend.h" +#include "core/file_sys/archive_source_sd_savedata.h" #include "core/hle/result.h" #include "core/hle/service/fs/directory.h" #include "core/hle/service/fs/file.h" +#include "network/artic_base/artic_base_client.h" /// The unique system identifier hash, also known as ID0 static constexpr char SYSTEM_ID[]{"00000000000000000000000000000000"}; @@ -67,6 +69,19 @@ struct ArchiveResource { }; static_assert(sizeof(ArchiveResource) == 0x10, "ArchiveResource has incorrect size"); +struct ExtSaveDataInfo { + u8 media_type; + u8 unknown; + u16 reserved1; + u32 save_id_low; + u32 save_id_high; + u32 reserved2; +}; +static_assert(sizeof(ExtSaveDataInfo) == 0x10, "ExtSaveDataInfo struct has incorrect size"); +static_assert(std::is_trivial(), "ExtSaveDataInfo should be trivial"); +static_assert(std::is_trivially_copyable(), + "ExtSaveDataInfo should be trivially copyable"); + using FileSys::ArchiveBackend; using FileSys::ArchiveFactory; @@ -90,6 +105,9 @@ public: */ Result CloseArchive(ArchiveHandle handle); + Result ControlArchive(ArchiveHandle handle, u32 action, u8* input, size_t input_size, + u8* output, size_t output_size); + /** * Open a File from an Archive * @param archive_handle Handle to an open Archive object @@ -98,7 +116,8 @@ public: * @return Pair containing the opened File object and the open delay */ std::pair>, std::chrono::nanoseconds> OpenFileFromArchive( - ArchiveHandle archive_handle, const FileSys::Path& path, FileSys::Mode mode); + ArchiveHandle archive_handle, const FileSys::Path& path, FileSys::Mode mode, + u32 attributes); /** * Delete a File from an Archive @@ -146,7 +165,7 @@ public: * @return File creation result code */ Result CreateFileInArchive(ArchiveHandle archive_handle, const FileSys::Path& path, - u64 file_size); + u64 file_size, u32 attributes); /** * Create a Directory from an Archive @@ -154,7 +173,8 @@ public: * @param path Path to the Directory inside of the Archive * @return Whether creation of directory succeeded */ - Result CreateDirectoryFromArchive(ArchiveHandle archive_handle, const FileSys::Path& path); + Result CreateDirectoryFromArchive(ArchiveHandle archive_handle, const FileSys::Path& path, + u32 attributes); /** * Rename a Directory between two Archives @@ -195,7 +215,8 @@ public: * @return Result 0 on success or the corresponding code on error */ Result FormatArchive(ArchiveIdCode id_code, const FileSys::ArchiveFormatInfo& format_info, - const FileSys::Path& path, u64 program_id); + const FileSys::Path& path, u64 program_id, u32 directory_buckets, + u32 file_buckets); /** * Retrieves the format info about the archive of the specified type and path. @@ -219,8 +240,10 @@ public: * @param program_id the program ID of the client that requests the operation * @return Result 0 on success or the corresponding code on error */ - Result CreateExtSaveData(MediaType media_type, u32 high, u32 low, std::span smdh_icon, - const FileSys::ArchiveFormatInfo& format_info, u64 program_id); + Result CreateExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low, + std::span smdh_icon, + const FileSys::ArchiveFormatInfo& format_info, u64 program_id, + u64 total_size); /** * Deletes the SharedExtSaveData archive for the specified extdata ID @@ -229,7 +252,7 @@ public: * @param low The low word of the extdata id to delete * @return Result 0 on success or the corresponding code on error */ - Result DeleteExtSaveData(MediaType media_type, u32 high, u32 low); + Result DeleteExtSaveData(MediaType media_type, u8 unknown, u32 high, u32 low); /** * Deletes the SystemSaveData archive folder for the specified save data id @@ -241,11 +264,12 @@ public: /** * Creates the SystemSaveData archive folder for the specified save data id - * @param high The high word of the SystemSaveData archive to create - * @param low The low word of the SystemSaveData archive to create * @return Result 0 on success or the corresponding code on error */ - Result CreateSystemSaveData(u32 high, u32 low); + Result CreateSystemSaveData(u32 high, u32 low, u32 total_size, u32 block_size, + u32 number_directories, u32 number_files, + u32 number_directory_buckets, u32 number_file_buckets, + u8 duplicate_data); /** * Returns capacity and free space information about the given media type. @@ -254,9 +278,27 @@ public: */ ResultVal GetArchiveResource(MediaType media_type) const; + Result SetSaveDataSecureValue(ArchiveHandle archive_handle, u32 secure_value_slot, + u64 secure_value, bool flush); + + ResultVal> GetSaveDataSecureValue(ArchiveHandle archive_handle, + u32 secure_value_slot); + + bool ArchiveIsSlow(ArchiveIdCode archive_id); + + bool ArchiveIsSlow(ArchiveHandle archive_handle); + /// Registers a new NCCH file with the SelfNCCH archive factory void RegisterSelfNCCH(Loader::AppLoader& app_loader); + void RegisterArticSaveDataSource(std::shared_ptr& client); + + void RegisterArticExtData(std::shared_ptr& client); + + void RegisterArticNCCH(std::shared_ptr& client); + + void RegisterArticSystemSaveData(std::shared_ptr& client); + private: Core::System& system; @@ -285,11 +327,17 @@ private: std::unordered_map> handle_map; ArchiveHandle next_handle = 1; + /** + * Savedata source + */ + std::shared_ptr sd_savedata_source; + template void serialize(Archive& ar, const unsigned int) { ar& id_code_map; ar& handle_map; ar& next_handle; + ar& sd_savedata_source; } friend class boost::serialization::access; }; diff --git a/src/core/hle/service/fs/file.cpp b/src/core/hle/service/fs/file.cpp index f9134a848..347aac3ae 100644 --- a/src/core/hle/service/fs/file.cpp +++ b/src/core/hle/service/fs/file.cpp @@ -169,10 +169,9 @@ void File::Write(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u64 offset = rp.Pop(); u32 length = rp.Pop(); - u32 flush = rp.Pop(); - auto& buffer = rp.PopMappedBuffer(); - LOG_TRACE(Service_FS, "Write {}: offset=0x{:x} length={}, flush=0x{:x}", GetName(), offset, - length, flush); + u32 flags = rp.Pop(); + LOG_TRACE(Service_FS, "Write {}: offset=0x{:x} length={}, flags=0x{:x}", GetName(), offset, + length, flags); IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); @@ -182,25 +181,75 @@ void File::Write(Kernel::HLERequestContext& ctx) { if (file->subfile) { rb.Push(FileSys::ResultUnsupportedOpenFlags); rb.Push(0); + rb.PushMappedBuffer(rp.PopMappedBuffer()); + return; + } + bool flush = (flags & 0xFF) != 0, update_timestamp = (flags & 0xFF00) != 0; + + if (!backend->AllowsCachedReads()) { + std::vector data(length); + auto& buffer = rp.PopMappedBuffer(); + buffer.Read(data.data(), 0, data.size()); + ResultVal written = + backend->Write(offset, data.size(), flush, update_timestamp, data.data()); + + // Update file size + file->size = backend->GetSize(); + + if (written.Failed()) { + rb.Push(written.Code()); + rb.Push(0); + } else { + rb.Push(ResultSuccess); + rb.Push(static_cast(*written)); + } rb.PushMappedBuffer(buffer); return; } - std::vector data(length); - buffer.Read(data.data(), 0, data.size()); - ResultVal written = backend->Write(offset, data.size(), flush != 0, data.data()); + struct AsyncData { + // Input + u32 length; + u64 offset; + bool flush; + bool update_timestamp; + Kernel::MappedBuffer* buffer; + FileSessionSlot* file; - // Update file size - file->size = backend->GetSize(); + // Output + ResultVal written; + }; + auto async_data = std::make_shared(); + async_data->length = length; + async_data->offset = offset; + async_data->flush = flush; + async_data->update_timestamp = update_timestamp; + async_data->buffer = &rp.PopMappedBuffer(); + async_data->file = file; - if (written.Failed()) { - rb.Push(written.Code()); - rb.Push(0); - } else { - rb.Push(ResultSuccess); - rb.Push(static_cast(*written)); - } - rb.PushMappedBuffer(buffer); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + std::vector data(async_data->length); + async_data->buffer->Read(data.data(), 0, data.size()); + async_data->written = backend->Write(async_data->offset, data.size(), async_data->flush, + async_data->update_timestamp, data.data()); + + // Update file size + async_data->file->size = backend->GetSize(); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 2); + if (async_data->written.Failed()) { + rb.Push(async_data->written.Code()); + rb.Push(0); + } else { + rb.Push(ResultSuccess); + rb.Push(static_cast(*async_data->written)); + } + rb.PushMappedBuffer(*async_data->buffer); + }, + true); } void File::GetSize(Kernel::HLERequestContext& ctx) { @@ -219,17 +268,32 @@ void File::SetSize(Kernel::HLERequestContext& ctx) { FileSessionSlot* file = GetSessionData(ctx.Session()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - // SetSize can not be called on subfiles. if (file->subfile) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(FileSys::ResultUnsupportedOpenFlags); return; } - file->size = size; - backend->SetSize(size); - rb.Push(ResultSuccess); + if (!backend->AllowsCachedReads()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + file->size = size; + backend->SetSize(size); + rb.Push(ResultSuccess); + return; + } + + ctx.RunAsync( + [file, size, this](Kernel::HLERequestContext& ctx) { + file->size = size; + backend->SetSize(size); + return 0; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + true); } void File::Close(Kernel::HLERequestContext& ctx) { @@ -240,26 +304,53 @@ void File::Close(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_FS, "Closing File backend but {} clients still connected", connected_sessions.size()); - backend->Close(); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + if (!backend->AllowsCachedReads()) { + backend->Close(); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + return; + } + + ctx.RunAsync( + [this](Kernel::HLERequestContext& ctx) { + backend->Close(); + return 0; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + true); } void File::Flush(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - const FileSessionSlot* file = GetSessionData(ctx.Session()); // Subfiles can not be flushed. if (file->subfile) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(FileSys::ResultUnsupportedOpenFlags); return; } - backend->Flush(); - rb.Push(ResultSuccess); + if (!backend->AllowsCachedReads()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + backend->Flush(); + rb.Push(ResultSuccess); + } + + ctx.RunAsync( + [this](Kernel::HLERequestContext& ctx) { + backend->Flush(); + return 0; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + true); } void File::SetPriority(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index f893c8df3..ac1c70b2d 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -54,26 +54,72 @@ void FS_USER::OpenFile(Kernel::HLERequestContext& ctx) { const auto filename_type = rp.PopEnum(); const auto filename_size = rp.Pop(); const FileSys::Mode mode{rp.Pop()}; - const auto attributes = rp.Pop(); // TODO(Link Mauve): do something with those attributes. + const auto attributes = rp.Pop(); std::vector filename = rp.PopStaticBuffer(); ASSERT(filename.size() == filename_size); const FileSys::Path file_path(filename_type, std::move(filename)); LOG_DEBUG(Service_FS, "path={}, mode={} attrs={}", file_path.DebugStr(), mode.hex, attributes); - const auto [file_res, open_timeout_ns] = - archives.OpenFileFromArchive(archive_handle, file_path, mode); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(file_res.Code()); - if (file_res.Succeeded()) { - std::shared_ptr file = *file_res; - rb.PushMoveObjects(file->Connect()); - } else { - rb.PushMoveObjects(nullptr); - LOG_DEBUG(Service_FS, "failed to get a handle for file {}", file_path.DebugStr()); + if (!archives.ArchiveIsSlow(archive_handle)) { + const auto [file_res, open_timeout_ns] = + archives.OpenFileFromArchive(archive_handle, file_path, mode, attributes); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(file_res.Code()); + if (file_res.Succeeded()) { + std::shared_ptr file = *file_res; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {}", file_path.DebugStr()); + } + + ctx.SleepClientThread("fs_user::open", open_timeout_ns, nullptr); + return; } - ctx.SleepClientThread("fs_user::open", open_timeout_ns, nullptr); + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path file_path; + FileSys::Mode mode; + u32 attributes; + std::chrono::steady_clock::time_point pre_timer; + + std::pair>, std::chrono::nanoseconds> file; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->file_path = file_path; + async_data->mode = mode; + async_data->attributes = attributes; + async_data->pre_timer = std::chrono::steady_clock::now(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->file = + archives.OpenFileFromArchive(async_data->archive_handle, async_data->file_path, + async_data->mode, async_data->attributes); + const auto time_took = std::chrono::duration_cast( + std::chrono::steady_clock::now() - async_data->pre_timer); + return static_cast(((async_data->file.second > time_took) + ? (async_data->file.second - time_took) + : std::chrono::nanoseconds()) + .count()); + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 2); + + rb.Push(async_data->file.first.Code()); + if (async_data->file.first.Succeeded()) { + std::shared_ptr file = *async_data->file.first; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {}", + async_data->file_path.DebugStr()); + } + }, + true); } void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) { @@ -97,35 +143,100 @@ void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "archive_id=0x{:08X} archive_path={} file_path={}, mode={} attributes={}", archive_id, archive_path.DebugStr(), file_path.DebugStr(), mode.hex, attributes); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + u64 program_id = GetSessionData(ctx.Session())->program_id; - ClientSlot* slot = GetSessionData(ctx.Session()); + if (!archives.ArchiveIsSlow(archive_id)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - ResultVal archive_handle = - archives.OpenArchive(archive_id, archive_path, slot->program_id); - if (archive_handle.Failed()) { - LOG_ERROR(Service_FS, - "Failed to get a handle for archive archive_id=0x{:08X} archive_path={}", - archive_id, archive_path.DebugStr()); - rb.Push(archive_handle.Code()); - rb.PushMoveObjects(nullptr); + ResultVal archive_handle = + archives.OpenArchive(archive_id, archive_path, program_id); + if (archive_handle.Failed()) { + LOG_ERROR(Service_FS, + "Failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + archive_id, archive_path.DebugStr()); + rb.Push(archive_handle.Code()); + rb.PushMoveObjects(nullptr); + return; + } + SCOPE_EXIT({ archives.CloseArchive(*archive_handle); }); + + const auto [file_res, open_timeout_ns] = + archives.OpenFileFromArchive(*archive_handle, file_path, mode, attributes); + rb.Push(file_res.Code()); + if (file_res.Succeeded()) { + std::shared_ptr file = *file_res; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {} mode={} attributes={}", + file_path.DebugStr(), mode.hex, attributes); + } + + ctx.SleepClientThread("fs_user::open_directly", open_timeout_ns, nullptr); return; } - SCOPE_EXIT({ archives.CloseArchive(*archive_handle); }); - const auto [file_res, open_timeout_ns] = - archives.OpenFileFromArchive(*archive_handle, file_path, mode); - rb.Push(file_res.Code()); - if (file_res.Succeeded()) { - std::shared_ptr file = *file_res; - rb.PushMoveObjects(file->Connect()); - } else { - rb.PushMoveObjects(nullptr); - LOG_DEBUG(Service_FS, "failed to get a handle for file {} mode={} attributes={}", - file_path.DebugStr(), mode.hex, attributes); - } + struct AsyncData { + ArchiveIdCode archive_id; + FileSys::Path archive_path; + FileSys::Path file_path; + u64 program_id; + FileSys::Mode mode; + u32 attributes; + std::chrono::steady_clock::time_point pre_timer; - ctx.SleepClientThread("fs_user::open_directly", open_timeout_ns, nullptr); + ResultVal archive_handle; + std::pair>, std::chrono::nanoseconds> file; + }; + auto async_data = std::make_shared(); + async_data->archive_id = archive_id; + async_data->archive_path = archive_path; + async_data->file_path = file_path; + async_data->program_id = program_id; + async_data->mode = mode; + async_data->attributes = attributes; + async_data->pre_timer = std::chrono::steady_clock::now(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->archive_handle = archives.OpenArchive( + async_data->archive_id, async_data->archive_path, async_data->program_id); + if (async_data->archive_handle.Failed()) { + LOG_ERROR(Service_FS, + "Failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + async_data->archive_id, async_data->archive_path.DebugStr()); + return s64(); + } + async_data->file = + archives.OpenFileFromArchive(*async_data->archive_handle, async_data->file_path, + async_data->mode, async_data->attributes); + archives.CloseArchive(*async_data->archive_handle); + const auto time_took = std::chrono::duration_cast( + std::chrono::steady_clock::now() - async_data->pre_timer); + return static_cast(((async_data->file.second > time_took) + ? (async_data->file.second - time_took) + : std::chrono::nanoseconds()) + .count()); + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 2); + + if (async_data->archive_handle.Failed()) { + rb.Push(async_data->archive_handle.Code()); + rb.PushMoveObjects(nullptr); + } + + rb.Push(async_data->file.first.Code()); + if (async_data->file.first.Succeeded()) { + std::shared_ptr file = *async_data->file.first; + rb.PushMoveObjects(file->Connect()); + } else { + rb.PushMoveObjects(nullptr); + LOG_DEBUG(Service_FS, "failed to get a handle for file {}", + async_data->file_path.DebugStr()); + } + }, + true); } void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) { @@ -142,8 +253,33 @@ void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", filename_type, filename_size, file_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteFileFromArchive(archive_handle, file_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.DeleteFileFromArchive(archive_handle, file_path)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path file_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->file_path = file_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = + archives.DeleteFileFromArchive(async_data->archive_handle, async_data->file_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) { @@ -169,9 +305,40 @@ void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) { src_filename_type, src_filename_size, src_file_path.DebugStr(), dest_filename_type, dest_filename_size, dest_file_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.RenameFileBetweenArchives(src_archive_handle, src_file_path, - dest_archive_handle, dest_file_path)); + if (!archives.ArchiveIsSlow(src_archive_handle) && + !archives.ArchiveIsSlow(dest_archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.RenameFileBetweenArchives(src_archive_handle, src_file_path, + dest_archive_handle, dest_file_path)); + return; + } + + struct AsyncData { + ArchiveHandle src_archive_handle; + FileSys::Path src_file_path; + ArchiveHandle dest_archive_handle; + FileSys::Path dest_file_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->src_archive_handle = src_archive_handle; + async_data->src_file_path = src_file_path; + async_data->dest_archive_handle = dest_archive_handle; + async_data->dest_file_path = dest_file_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.RenameFileBetweenArchives( + async_data->src_archive_handle, async_data->src_file_path, + async_data->dest_archive_handle, async_data->dest_file_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) { @@ -189,8 +356,33 @@ void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteDirectoryFromArchive(archive_handle, dir_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.DeleteDirectoryFromArchive(archive_handle, dir_path)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.DeleteDirectoryFromArchive(async_data->archive_handle, + async_data->dir_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) { @@ -208,8 +400,33 @@ void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteDirectoryRecursivelyFromArchive(archive_handle, dir_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.DeleteDirectoryRecursivelyFromArchive(archive_handle, dir_path)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.DeleteDirectoryRecursivelyFromArchive( + async_data->archive_handle, async_data->dir_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) { @@ -229,8 +446,38 @@ void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} attributes={} size={:x} data={}", filename_type, attributes, file_size, file_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.CreateFileInArchive(archive_handle, file_path, file_size)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.CreateFileInArchive(archive_handle, file_path, file_size, attributes)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path file_path; + u64 file_size; + u32 attributes; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->file_path = file_path; + async_data->file_size = file_size; + async_data->attributes = attributes; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = + archives.CreateFileInArchive(async_data->archive_handle, async_data->file_path, + async_data->file_size, async_data->attributes); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { @@ -239,7 +486,7 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { const auto archive_handle = rp.PopRaw(); const auto dirname_type = rp.PopEnum(); const auto dirname_size = rp.Pop(); - [[maybe_unused]] const auto attributes = rp.Pop(); + const auto attributes = rp.Pop(); std::vector dirname = rp.PopStaticBuffer(); ASSERT(dirname.size() == dirname_size); const FileSys::Path dir_path(dirname_type, std::move(dirname)); @@ -247,8 +494,35 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.CreateDirectoryFromArchive(archive_handle, dir_path)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.CreateDirectoryFromArchive(archive_handle, dir_path, attributes)); + return; + } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + u32 attributes; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + async_data->attributes = attributes; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.CreateDirectoryFromArchive( + async_data->archive_handle, async_data->dir_path, async_data->attributes); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) { @@ -273,9 +547,40 @@ void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) { src_dirname_type, src_dirname_size, src_dir_path.DebugStr(), dest_dirname_type, dest_dirname_size, dest_dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.RenameDirectoryBetweenArchives(src_archive_handle, src_dir_path, - dest_archive_handle, dest_dir_path)); + if (!archives.ArchiveIsSlow(src_archive_handle) && + !archives.ArchiveIsSlow(dest_archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.RenameDirectoryBetweenArchives(src_archive_handle, src_dir_path, + dest_archive_handle, dest_dir_path)); + return; + } + + struct AsyncData { + ArchiveHandle src_archive_handle; + FileSys::Path src_dir_path; + ArchiveHandle dest_archive_handle; + FileSys::Path dest_dir_path; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->src_archive_handle = src_archive_handle; + async_data->src_dir_path = src_dir_path; + async_data->dest_archive_handle = dest_archive_handle; + async_data->dest_dir_path = dest_dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.RenameDirectoryBetweenArchives( + async_data->src_archive_handle, async_data->src_dir_path, + async_data->dest_archive_handle, async_data->dest_dir_path); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) { @@ -291,20 +596,56 @@ void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - ResultVal> dir_res = - archives.OpenDirectoryFromArchive(archive_handle, dir_path); - rb.Push(dir_res.Code()); - if (dir_res.Succeeded()) { - std::shared_ptr directory = *dir_res; - auto [server, client] = system.Kernel().CreateSessionPair(directory->GetName()); - directory->ClientConnected(server); - rb.PushMoveObjects(client); - } else { - LOG_DEBUG(Service_FS, "failed to get a handle for directory type={} size={} data={}", - dirname_type, dirname_size, dir_path.DebugStr()); - rb.PushMoveObjects(nullptr); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + ResultVal> dir_res = + archives.OpenDirectoryFromArchive(archive_handle, dir_path); + rb.Push(dir_res.Code()); + if (dir_res.Succeeded()) { + std::shared_ptr directory = *dir_res; + auto [server, client] = system.Kernel().CreateSessionPair(directory->GetName()); + directory->ClientConnected(server); + rb.PushMoveObjects(client); + } else { + LOG_DEBUG(Service_FS, "failed to get a handle for directory type={} size={} data={}", + dirname_type, dirname_size, dir_path.DebugStr()); + rb.PushMoveObjects(nullptr); + } + return; } + + struct AsyncData { + ArchiveHandle archive_handle; + FileSys::Path dir_path; + + ResultVal> dir_res; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->dir_path = dir_path; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->dir_res = + archives.OpenDirectoryFromArchive(async_data->archive_handle, async_data->dir_path); + return 0; + }, + [this, async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 2); + + rb.Push(async_data->dir_res.Code()); + if (async_data->dir_res.Succeeded()) { + std::shared_ptr directory = *async_data->dir_res; + auto [server, client] = system.Kernel().CreateSessionPair(directory->GetName()); + directory->ClientConnected(server); + rb.PushMoveObjects(client); + } else { + LOG_DEBUG(Service_FS, "failed to get a handle for directory path={}", + async_data->dir_path.DebugStr()); + rb.PushMoveObjects(nullptr); + } + }, + true); } void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) { @@ -318,20 +659,59 @@ void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "archive_id=0x{:08X} archive_path={}", archive_id, archive_path.DebugStr()); - - IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); ClientSlot* slot = GetSessionData(ctx.Session()); - const ResultVal handle = - archives.OpenArchive(archive_id, archive_path, slot->program_id); - rb.Push(handle.Code()); - if (handle.Succeeded()) { - rb.PushRaw(*handle); - } else { - rb.Push(0); - LOG_ERROR(Service_FS, - "failed to get a handle for archive archive_id=0x{:08X} archive_path={}", - archive_id, archive_path.DebugStr()); + u64 program_id = slot->program_id; + + // Conventional opening + if (!archives.ArchiveIsSlow(archive_id)) { + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + const ResultVal handle = + archives.OpenArchive(archive_id, archive_path, program_id); + rb.Push(handle.Code()); + if (handle.Succeeded()) { + rb.PushRaw(*handle); + } else { + rb.Push(0); + LOG_ERROR(Service_FS, + "failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + archive_id, archive_path.DebugStr()); + } + return; } + + struct AsyncData { + // Input + ArchiveIdCode archive_id; + FileSys::Path archive_path; + u64 program_id; + + // Output + ResultVal handle; + }; + auto async_data = std::make_shared(); + async_data->archive_id = archive_id; + async_data->archive_path = archive_path; + async_data->program_id = program_id; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->handle = archives.OpenArchive( + async_data->archive_id, async_data->archive_path, async_data->program_id); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 3, 0); + rb.Push(async_data->handle.Code()); + if (async_data->handle.Succeeded()) { + rb.PushRaw(*async_data->handle); + } else { + rb.Push(0); + LOG_ERROR(Service_FS, + "failed to get a handle for archive archive_id=0x{:08X} archive_path={}", + async_data->archive_id, async_data->archive_path.DebugStr()); + } + }, + true); } void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) { @@ -340,24 +720,96 @@ void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) { const auto action = rp.Pop(); const auto input_size = rp.Pop(); const auto output_size = rp.Pop(); - [[maybe_unused]] const auto input = rp.PopMappedBuffer(); - [[maybe_unused]] const auto output = rp.PopMappedBuffer(); - LOG_WARNING(Service_FS, - "(STUBBED) called, archive_handle={:016X}, action={:08X}, input_size={:08X}, " - "output_size={:08X}", - archive_handle, action, input_size, output_size); + if (!archives.ArchiveIsSlow(archive_handle)) { + auto input = rp.PopMappedBuffer(); + auto output = rp.PopMappedBuffer(); + std::vector in_data(input_size); + input.Read(in_data.data(), 0, in_data.size()); + std::vector out_data(output_size); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + const Result res = + archives.ControlArchive(archive_handle, action, in_data.data(), in_data.size(), + out_data.data(), out_data.size()); + + if (res.IsSuccess() && output_size != 0) { + output.Write(out_data.data(), 0, out_data.size()); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res); + return; + } + + struct AsyncData { + ArchiveHandle handle; + u32 action; + Kernel::MappedBuffer* in_buffer; + u32 in_size; + u32 out_size; + + Result res{0}; + std::vector out_data; + Kernel::MappedBuffer* out_buffer; + }; + + auto async_data = std::make_shared(); + async_data->handle = archive_handle; + async_data->action = action; + async_data->in_size = input_size; + async_data->out_size = output_size; + async_data->in_buffer = &rp.PopMappedBuffer(); + async_data->out_buffer = &rp.PopMappedBuffer(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + std::vector in_data(async_data->in_size); + async_data->in_buffer->Read(in_data.data(), 0, in_data.size()); + async_data->out_data.resize(async_data->out_size); + + async_data->res = archives.ControlArchive( + async_data->handle, async_data->action, in_data.data(), in_data.size(), + async_data->out_data.data(), async_data->out_data.size()); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + if (async_data->res.IsSuccess() && async_data->out_size != 0) { + async_data->out_buffer->Write(async_data->out_data.data(), 0, + async_data->out_data.size()); + } + rb.Push(async_data->res); + }, + true); } void FS_USER::CloseArchive(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto archive_handle = rp.PopRaw(); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.CloseArchive(archive_handle)); + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(archives.CloseArchive(archive_handle)); + return; + } + + struct AsyncData { + ArchiveHandle handle; + + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->handle = archive_handle; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.CloseArchive(async_data->handle); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::IsSdmcDetected(Kernel::HLERequestContext& ctx) { @@ -377,8 +829,6 @@ void FS_USER::IsSdmcWriteable(Kernel::HLERequestContext& ctx) { } void FS_USER::FormatSaveData(Kernel::HLERequestContext& ctx) { - LOG_WARNING(Service_FS, "(STUBBED)"); - IPC::RequestParser rp(ctx); const auto archive_id = rp.PopEnum(); const auto archivename_type = rp.PopEnum(); @@ -386,8 +836,8 @@ void FS_USER::FormatSaveData(Kernel::HLERequestContext& ctx) { const auto block_size = rp.Pop(); const auto number_directories = rp.Pop(); const auto number_files = rp.Pop(); - [[maybe_unused]] const auto directory_buckets = rp.Pop(); - [[maybe_unused]] const auto file_buckets = rp.Pop(); + const auto directory_buckets = rp.Pop(); + const auto file_buckets = rp.Pop(); const bool duplicate_data = rp.Pop(); std::vector archivename = rp.PopStaticBuffer(); ASSERT(archivename.size() == archivename_size); @@ -417,7 +867,7 @@ void FS_USER::FormatSaveData(Kernel::HLERequestContext& ctx) { ClientSlot* slot = GetSessionData(ctx.Session()); rb.Push(archives.FormatArchive(ArchiveIdCode::SaveData, format_info, archive_path, - slot->program_id)); + slot->program_id, directory_buckets, file_buckets)); } void FS_USER::FormatThisUserSaveData(Kernel::HLERequestContext& ctx) { @@ -425,8 +875,8 @@ void FS_USER::FormatThisUserSaveData(Kernel::HLERequestContext& ctx) { const auto block_size = rp.Pop(); const auto number_directories = rp.Pop(); const auto number_files = rp.Pop(); - [[maybe_unused]] const auto directory_buckets = rp.Pop(); - [[maybe_unused]] const auto file_buckets = rp.Pop(); + const auto directory_buckets = rp.Pop(); + const auto file_buckets = rp.Pop(); const auto duplicate_data = rp.Pop(); FileSys::ArchiveFormatInfo format_info; @@ -438,7 +888,7 @@ void FS_USER::FormatThisUserSaveData(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); ClientSlot* slot = GetSessionData(ctx.Session()); rb.Push(archives.FormatArchive(ArchiveIdCode::SaveData, format_info, FileSys::Path(), - slot->program_id)); + slot->program_id, directory_buckets, file_buckets)); LOG_TRACE(Service_FS, "called"); } @@ -495,10 +945,7 @@ void FS_USER::GetNandArchiveResource(Kernel::HLERequestContext& ctx) { void FS_USER::CreateExtSaveData(Kernel::HLERequestContext& ctx) { // TODO(Subv): Figure out the other parameters. IPC::RequestParser rp(ctx); - MediaType media_type = static_cast(rp.Pop()); // the other bytes are unknown - u32 save_low = rp.Pop(); - u32 save_high = rp.Pop(); - u32 unknown = rp.Pop(); + auto ext_data_info = rp.PopRaw(); u32 directories = rp.Pop(); u32 files = rp.Pop(); u64 size_limit = rp.Pop(); @@ -512,33 +959,34 @@ void FS_USER::CreateExtSaveData(Kernel::HLERequestContext& ctx) { format_info.number_directories = directories; format_info.number_files = files; format_info.duplicate_data = false; - format_info.total_size = 0; + format_info.total_size = static_cast(size_limit); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); ClientSlot* slot = GetSessionData(ctx.Session()); - rb.Push(archives.CreateExtSaveData(media_type, save_high, save_low, icon, format_info, - slot->program_id)); + rb.Push(archives.CreateExtSaveData(static_cast(ext_data_info.media_type), + ext_data_info.unknown, ext_data_info.save_id_high, + ext_data_info.save_id_low, icon, format_info, + slot->program_id, size_limit)); rb.PushMappedBuffer(icon_buffer); LOG_DEBUG(Service_FS, "called, savedata_high={:08X} savedata_low={:08X} unknown={:08X} " "files={:08X} directories={:08X} size_limit={:016x} icon_size={:08X}", - save_high, save_low, unknown, directories, files, size_limit, icon_size); + ext_data_info.save_id_high, ext_data_info.save_id_low, ext_data_info.unknown, + directories, files, size_limit, icon_size); } void FS_USER::DeleteExtSaveData(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - MediaType media_type = static_cast(rp.Pop()); // the other bytes are unknown - u32 save_low = rp.Pop(); - u32 save_high = rp.Pop(); - u32 unknown = rp.Pop(); // TODO(Subv): Figure out what this is + ExtSaveDataInfo info = rp.PopRaw(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteExtSaveData(media_type, save_high, save_low)); + rb.Push(archives.DeleteExtSaveData(static_cast(info.media_type), info.unknown, + info.save_id_high, info.save_id_low)); LOG_DEBUG(Service_FS, - "called, save_low={:08X} save_high={:08X} media_type={:08X} unknown={:08X}", save_low, - save_high, media_type, unknown); + "called, save_low={:08X} save_high={:08X} media_type={:08X} unknown={:08X}", + info.save_id_low, info.save_id_high, info.media_type, info.unknown); } void FS_USER::CardSlotIsInserted(Kernel::HLERequestContext& ctx) { @@ -578,7 +1026,9 @@ void FS_USER::CreateSystemSaveData(Kernel::HLERequestContext& ctx) { file_buckets, duplicate); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.CreateSystemSaveData(savedata_high, savedata_low)); + rb.Push(archives.CreateSystemSaveData(savedata_high, savedata_low, total_size, block_size, + directories, files, directory_buckets, file_buckets, + duplicate ? 1 : 0)); } void FS_USER::CreateLegacySystemSaveData(Kernel::HLERequestContext& ctx) { @@ -600,7 +1050,8 @@ void FS_USER::CreateLegacySystemSaveData(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); // With this command, the SystemSaveData always has save_high = 0 (Always created in the NAND) - rb.Push(archives.CreateSystemSaveData(0, savedata_id)); + rb.Push(archives.CreateSystemSaveData(0, savedata_id, total_size, block_size, directories, + files, directory_buckets, file_buckets, duplicate)); } void FS_USER::InitializeWithSdkVersion(Kernel::HLERequestContext& ctx) { @@ -757,13 +1208,12 @@ void FS_USER::ObsoletedCreateExtSaveData(Kernel::HLERequestContext& ctx) { FileSys::ArchiveFormatInfo format_info; format_info.number_directories = directories; format_info.number_files = files; - format_info.duplicate_data = false; - format_info.total_size = 0; + format_info.total_size = -1; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); ClientSlot* slot = GetSessionData(ctx.Session()); - rb.Push(archives.CreateExtSaveData(media_type, save_high, save_low, icon, format_info, - slot->program_id)); + rb.Push(archives.CreateExtSaveData(media_type, 0, save_high, save_low, icon, format_info, + slot->program_id, -1)); rb.PushMappedBuffer(icon_buffer); LOG_DEBUG(Service_FS, @@ -778,7 +1228,7 @@ void FS_USER::ObsoletedDeleteExtSaveData(Kernel::HLERequestContext& ctx) { u32 save_low = rp.Pop(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(archives.DeleteExtSaveData(media_type, 0, save_low)); + rb.Push(archives.DeleteExtSaveData(media_type, 0, 0, save_low)); LOG_DEBUG(Service_FS, "called, save_low={:08X} media_type={:08X}", save_low, media_type); } @@ -832,16 +1282,39 @@ void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 unique_id = rp.Pop(); const u8 title_variation = rp.Pop(); - // TODO: Generate and Save the Secure Value + if (!secure_value_backend->BackendIsSlow()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(secure_value_backend->ObsoletedSetSaveDataSecureValue(unique_id, title_variation, + secure_value_slot, value)); + return; + } - LOG_WARNING(Service_FS, - "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:08X} " - "unqiue_id=0x{:08X} title_variation=0x{:02X}", - value, secure_value_slot, unique_id, title_variation); + struct AsyncData { + u64 value; + u32 secure_value_slot; + u32 unique_id; + u8 title_variation; - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->value = value; + async_data->secure_value_slot = secure_value_slot; + async_data->unique_id = unique_id; + async_data->title_variation = title_variation; - rb.Push(ResultSuccess); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = secure_value_backend->ObsoletedSetSaveDataSecureValue( + async_data->unique_id, async_data->title_variation, async_data->secure_value_slot, + async_data->value); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -850,19 +1323,74 @@ void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 unique_id = rp.Pop(); const u8 title_variation = rp.Pop(); - LOG_WARNING( - Service_FS, - "(STUBBED) called secure_value_slot=0x{:08X} unqiue_id=0x{:08X} title_variation=0x{:02X}", - secure_value_slot, unique_id, title_variation); + if (!secure_value_backend->BackendIsSlow()) { + auto res = secure_value_backend->ObsoletedGetSaveDataSecureValue(unique_id, title_variation, + secure_value_slot); + if (res.Failed()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res.Code()); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(4, 0); + rb.Push(res.Code()); + rb.Push(std::get<0>(*res)); // indicates if the secure value exists + rb.Push(std::get<1>(*res)); // the secure value + } + return; + } - IPC::RequestBuilder rb = rp.MakeBuilder(4, 0); + struct AsyncData { + u32 secure_value_slot; + u32 unique_id; + u8 title_variation; - rb.Push(ResultSuccess); + ResultVal> res; + }; + auto async_data = std::make_shared(); + async_data->secure_value_slot = secure_value_slot; + async_data->unique_id = unique_id; + async_data->title_variation = title_variation; - // TODO: Implement Secure Value Lookup & Generation + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = secure_value_backend->ObsoletedGetSaveDataSecureValue( + async_data->unique_id, async_data->title_variation, async_data->secure_value_slot); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.Failed()) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res.Code()); + } else { + IPC::RequestBuilder rb(ctx, 4, 0); + rb.Push(async_data->res.Code()); + rb.Push( + std::get<0>(*async_data->res)); // indicates if the secure value exists + rb.Push(std::get<1>(*async_data->res)); // the secure value + } + }, + true); +} - rb.Push(false); // indicates that the secure value doesn't exist - rb.Push(0); // the secure value +void FS_USER::ControlSecureSave(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const auto action = rp.Pop(); + const auto input_size = rp.Pop(); + const auto output_size = rp.Pop(); + auto input = rp.PopMappedBuffer(); + auto output = rp.PopMappedBuffer(); + + std::vector in_data(input_size); + input.Read(in_data.data(), 0, in_data.size()); + std::vector out_data(output_size); + + Result res = secure_value_backend->ControlSecureSave(action, in_data.data(), in_data.size(), + out_data.data(), out_data.size()); + + if (res.IsSuccess() && output_size != 0) { + output.Write(out_data.data(), 0, out_data.size()); + } + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res); } void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -870,31 +1398,85 @@ void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 secure_value_slot = rp.Pop(); const u64 value = rp.Pop(); - // TODO: Generate and Save the Secure Value + if (!secure_value_backend->BackendIsSlow()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(secure_value_backend->SetThisSaveDataSecureValue(secure_value_slot, value)); + return; + } - LOG_WARNING(Service_FS, "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:08X}", value, - secure_value_slot); + struct AsyncData { + u64 value; + u32 secure_value_slot; - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->value = value; + async_data->secure_value_slot = secure_value_slot; - rb.Push(ResultSuccess); + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = secure_value_backend->SetThisSaveDataSecureValue( + async_data->secure_value_slot, async_data->value); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::GetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 secure_value_slot = rp.Pop(); - LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X}", secure_value_slot); + if (!secure_value_backend->BackendIsSlow()) { + auto res = secure_value_backend->GetThisSaveDataSecureValue(secure_value_slot); - IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + if (res.Failed()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res.Code()); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + rb.Push(res.Code()); + rb.Push(std::get<0>(*res)); // indicates if the secure value exists + rb.Push(std::get<1>( + *res)); // indicates if the requesting process is a gamecard, overriding the check + rb.Push(std::get<2>(*res)); // the secure value + } + return; + } - rb.Push(ResultSuccess); + struct AsyncData { + u32 secure_value_slot; - // TODO: Implement Secure Value Lookup & Generation + ResultVal> res; + }; + auto async_data = std::make_shared(); + async_data->secure_value_slot = secure_value_slot; - rb.Push(false); // indicates that the secure value doesn't exist - rb.Push(true); // indicates the requesting process is a gamecard, overriding the check - rb.Push(0); // the secure value + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = + secure_value_backend->GetThisSaveDataSecureValue(async_data->secure_value_slot); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.Failed()) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res.Code()); + } else { + IPC::RequestBuilder rb(ctx, 5, 0); + rb.Push(async_data->res.Code()); + rb.Push( + std::get<0>(*async_data->res)); // indicates if the secure value exists + rb.Push(std::get<1>(*async_data->res)); // indicates if the requesting process + // is a gamecard, overriding the check + rb.Push(std::get<2>(*async_data->res)); // the secure value + } + }, + true); } void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -904,16 +1486,39 @@ void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u64 value = rp.Pop(); const bool flush = rp.Pop(); - // TODO: Generate and Save the Secure Value + if (!archives.ArchiveIsSlow(archive_handle)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - LOG_WARNING(Service_FS, - "(STUBBED) called, value=0x{:016x} secure_value_slot=0x{:04X} " - "archive_handle=0x{:08X} flush={}", - value, secure_value_slot, archive_handle, flush); + rb.Push(archives.SetSaveDataSecureValue(archive_handle, secure_value_slot, value, flush)); + return; + } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + struct AsyncData { + ArchiveHandle archive_handle; + u64 value; + u32 secure_value_slot; + bool flush; - rb.Push(ResultSuccess); + Result res{0}; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->value = value; + async_data->secure_value_slot = secure_value_slot; + async_data->flush = flush; + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.SetSaveDataSecureValue(async_data->archive_handle, + async_data->secure_value_slot, + async_data->value, async_data->flush); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res); + }, + true); } void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { @@ -921,18 +1526,53 @@ void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const auto archive_handle = rp.PopRaw(); const u32 secure_value_slot = rp.Pop(); - LOG_WARNING(Service_FS, "(STUBBED) called secure_value_slot=0x{:08X} archive_handle=0x{:08X}", - secure_value_slot, archive_handle); + if (!archives.ArchiveIsSlow(archive_handle)) { + auto res = archives.GetSaveDataSecureValue(archive_handle, secure_value_slot); + if (res.Failed()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(res.Code()); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + rb.Push(res.Code()); + rb.Push(std::get<0>(*res)); // indicates if the secure value exists + rb.Push(std::get<1>( + *res)); // indicates if the requesting process is a gamecard, overriding the check + rb.Push(std::get<2>(*res)); // the secure value + } + return; + } - IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); + struct AsyncData { + ArchiveHandle archive_handle; + u32 secure_value_slot; - rb.Push(ResultSuccess); + ResultVal> res; + }; + auto async_data = std::make_shared(); + async_data->archive_handle = archive_handle; + async_data->secure_value_slot = secure_value_slot; - // TODO: Implement Secure Value Lookup & Generation - - rb.Push(false); // indicates that the secure value doesn't exist - rb.Push(true); // indicates the requesting process is a gamecard, overriding the check - rb.Push(0); // the secure value + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + async_data->res = archives.GetSaveDataSecureValue(async_data->archive_handle, + async_data->secure_value_slot); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + if (async_data->res.Failed()) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(async_data->res.Code()); + } else { + IPC::RequestBuilder rb(ctx, 5, 0); + rb.Push(async_data->res.Code()); + rb.Push( + std::get<0>(*async_data->res)); // indicates if the secure value exists + rb.Push(std::get<1>(*async_data->res)); // indicates if the requesting process + // is a gamecard, overriding the check + rb.Push(std::get<2>(*async_data->res)); // the secure value + } + }, + true); } void FS_USER::RegisterProgramInfo(u32 process_id, u64 program_id, const std::string& filepath) { @@ -1118,7 +1758,7 @@ FS_USER::FS_USER(Core::System& system) {0x0864, nullptr, "GetNandInfo"}, {0x0865, &FS_USER::ObsoletedSetSaveDataSecureValue, "SetSaveDataSecureValue"}, {0x0866, &FS_USER::ObsoletedGetSaveDataSecureValue, "GetSaveDataSecureValue"}, - {0x0867, nullptr, "ControlSecureSave"}, + {0x0867, &FS_USER::ControlSecureSave, "ControlSecureSave"}, {0x0868, nullptr, "GetMediaType"}, {0x0869, nullptr, "GetNandEraseCount"}, {0x086A, nullptr, "ReadNandReport"}, @@ -1132,6 +1772,13 @@ FS_USER::FS_USER(Core::System& system) // clang-format on }; RegisterHandlers(functions); + secure_value_backend = std::make_shared(); +} +template +void Service::FS::FS_USER::serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + ar& priority; + ar& secure_value_backend; } void InstallInterfaces(Core::System& system) { diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index 57285995a..206a9c3b6 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -9,6 +9,7 @@ #include #include "common/common_types.h" #include "core/file_sys/errors.h" +#include "core/file_sys/secure_value_backend.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/service.h" @@ -77,6 +78,10 @@ public: } } + void RegisterSecureValueBackend(const std::shared_ptr& backend) { + secure_value_backend = backend; + } + private: void Initialize(Kernel::HLERequestContext& ctx); @@ -657,6 +662,21 @@ private: */ void ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx); + /** + * FS_User::ControlSecureSave service function + * Inputs: + * 1 : Action + * 2 : Input Size + * 3 : Output Size + * 4 : (Input Size << 4) | 0xA + * 5 : Input Pointer + * 6 : (Output Size << 4) | 0xC + * 7 : Output Pointer + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void ControlSecureSave(Kernel::HLERequestContext& ctx); + /** * FS_User::SetThisSaveDataSecureValue service function. * Inputs: @@ -722,11 +742,10 @@ private: Core::System& system; ArchiveManager& archives; + std::shared_ptr secure_value_backend; + template - void serialize(Archive& ar, const unsigned int) { - ar& boost::serialization::base_object(*this); - ar& priority; - } + void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; }; diff --git a/src/core/hle/service/gsp/gsp_command.h b/src/core/hle/service/gsp/gsp_command.h index ddc203615..c7d0da927 100644 --- a/src/core/hle/service/gsp/gsp_command.h +++ b/src/core/hle/service/gsp/gsp_command.h @@ -71,7 +71,12 @@ struct CacheFlushCommand { /// GSP command struct Command { - BitField<0, 8, CommandId> id; + union { + BitField<0, 8, CommandId> id; + BitField<8, 8, u32> unknown1; + BitField<16, 8, u32> stop; + BitField<24, 8, u32> unknown2; + }; union { DmaCommand dma_request; SubmitCmdListCommand submit_gpu_cmdlist; @@ -86,6 +91,8 @@ static_assert(sizeof(Command) == 0x20, "Command struct has incorrect size"); /// GSP shared memory GX command buffer header struct CommandBuffer { + static constexpr u32 STATUS_STOPPED = 0x1; + static constexpr u32 STATUS_CMD_FAILED = 0x80; union { u32 hex; @@ -99,6 +106,11 @@ struct CommandBuffer { // application when writing a command to shared memory, after increasing this value // TriggerCmdReqQueue is only used if this field is value 1. BitField<8, 8, u32> number_commands; + + // When any of the following flags are set to 1, the GSP module stops processing the + // commands in the command buffer. + BitField<16, 8, u32> status; + BitField<24, 8, u32> should_stop; }; u32 unk[7]; diff --git a/src/core/hle/service/gsp/gsp_gpu.cpp b/src/core/hle/service/gsp/gsp_gpu.cpp index 374bbe3c6..1b2dd29f1 100644 --- a/src/core/hle/service/gsp/gsp_gpu.cpp +++ b/src/core/hle/service/gsp/gsp_gpu.cpp @@ -9,6 +9,7 @@ #include #include "common/archives.h" #include "common/bit_field.h" +#include "common/settings.h" #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/shared_memory.h" @@ -408,21 +409,54 @@ void GSP_GPU::SetLcdForceBlack(Kernel::HLERequestContext& ctx) { void GSP_GPU::TriggerCmdReqQueue(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - // Iterate through each command. auto* command_buffer = GetCommandBuffer(active_thread_id); auto& gpu = system.GPU(); - for (u32 i = 0; i < command_buffer->number_commands; i++) { - gpu.Debugger().GXCommandProcessed(command_buffer->commands[i]); + + bool requires_delay = false; + + while (command_buffer->number_commands) { + if (command_buffer->should_stop) { + command_buffer->status.Assign(CommandBuffer::STATUS_STOPPED); + break; + } + if (command_buffer->status == CommandBuffer::STATUS_STOPPED) { + break; + } + + Command command = command_buffer->commands[command_buffer->index]; + if (command.id == CommandId::SubmitCmdList && !requires_delay && + Settings::values.delay_game_render_thread_us.GetValue() != 0) { + requires_delay = true; + } + + // Decrease the number of commands remaining and increase the current index + command_buffer->number_commands.Assign(command_buffer->number_commands - 1); + command_buffer->index.Assign((command_buffer->index + 1) % 0xF); + + gpu.Debugger().GXCommandProcessed(command); // Decode and execute command - gpu.Execute(command_buffer->commands[i]); + gpu.Execute(command); - // Indicates that command has completed - command_buffer->number_commands.Assign(command_buffer->number_commands - 1); + if (command.stop) { + command_buffer->should_stop.Assign(1); + } } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + if (requires_delay) { + ctx.RunAsync( + [](Kernel::HLERequestContext& ctx) { + return Settings::values.delay_game_render_thread_us.GetValue() * 1000; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + false); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + } } void GSP_GPU::ImportDisplayCaptureInfo(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp index 9013fd4b1..1d11acd20 100644 --- a/src/core/hle/service/hid/hid.cpp +++ b/src/core/hle/service/hid/hid.cpp @@ -20,6 +20,8 @@ #include "core/hle/service/hid/hid.h" #include "core/hle/service/hid/hid_spvr.h" #include "core/hle/service/hid/hid_user.h" +#include "core/hle/service/ir/ir_rst.h" +#include "core/hle/service/ir/ir_user.h" #include "core/hle/service/service.h" #include "core/movie.h" @@ -53,6 +55,32 @@ void Module::serialize(Archive& ar, const unsigned int file_version) { } SERIALIZE_IMPL(Module) +ArticBaseController::ArticBaseController( + const std::shared_ptr& client) { + + udp_stream = + client->NewUDPStream("ArticController", sizeof(ArticBaseController::ControllerData), + std::chrono::milliseconds(2)); + if (udp_stream.get()) { + udp_stream->Start(); + } +} + +ArticBaseController::ControllerData ArticBaseController::GetControllerData() { + + if (udp_stream.get() && udp_stream->IsReady()) { + auto data = udp_stream->GetLastPacket(); + if (data.size() == sizeof(ControllerData)) { + u32 id = *reinterpret_cast(data.data()); + if ((id - last_packet_id) < (std::numeric_limits::max() / 2)) { + last_packet_id = id; + memcpy(&last_controller_data, data.data(), data.size()); + } + } + } + return last_controller_data; +} + constexpr float accelerometer_coef = 512.0f; // measured from hw test result constexpr float gyroscope_coef = 14.375f; // got from hwtest GetGyroscopeLowRawToDpsCoefficient call @@ -111,96 +139,151 @@ void Module::UpdatePadCallback(std::uintptr_t user_data, s64 cycles_late) { LoadInputDevices(); using namespace Settings::NativeButton; - state.a.Assign(buttons[A - BUTTON_HID_BEGIN]->GetStatus()); - state.b.Assign(buttons[B - BUTTON_HID_BEGIN]->GetStatus()); - state.x.Assign(buttons[X - BUTTON_HID_BEGIN]->GetStatus()); - state.y.Assign(buttons[Y - BUTTON_HID_BEGIN]->GetStatus()); - state.right.Assign(buttons[Right - BUTTON_HID_BEGIN]->GetStatus()); - state.left.Assign(buttons[Left - BUTTON_HID_BEGIN]->GetStatus()); - state.up.Assign(buttons[Up - BUTTON_HID_BEGIN]->GetStatus()); - state.down.Assign(buttons[Down - BUTTON_HID_BEGIN]->GetStatus()); - state.l.Assign(buttons[L - BUTTON_HID_BEGIN]->GetStatus()); - state.r.Assign(buttons[R - BUTTON_HID_BEGIN]->GetStatus()); - state.start.Assign(buttons[Start - BUTTON_HID_BEGIN]->GetStatus()); - state.select.Assign(buttons[Select - BUTTON_HID_BEGIN]->GetStatus()); - state.debug.Assign(buttons[Debug - BUTTON_HID_BEGIN]->GetStatus()); - state.gpio14.Assign(buttons[Gpio14 - BUTTON_HID_BEGIN]->GetStatus()); - // Get current circle pad position and update circle pad direction - float circle_pad_x_f, circle_pad_y_f; - std::tie(circle_pad_x_f, circle_pad_y_f) = circle_pad->GetStatus(); + if (artic_controller.get() && artic_controller->IsReady()) { + constexpr u32 HID_VALID_KEYS = 0xF0003FFF; + constexpr u32 LIBCTRU_TOUCH_KEY = (1 << 20); - // xperia64: 0x9A seems to be the calibrated limit of the circle pad - // Verified by using Input Redirector with very large-value digital inputs - // on the circle pad and calibrating using the system settings application - constexpr int MAX_CIRCLEPAD_POS = 0x9A; // Max value for a circle pad position + ArticBaseController::ControllerData data = artic_controller->GetControllerData(); - // These are rounded rather than truncated on actual hardware - s16 circle_pad_new_x = static_cast(std::roundf(circle_pad_x_f * MAX_CIRCLEPAD_POS)); - s16 circle_pad_new_y = static_cast(std::roundf(circle_pad_y_f * MAX_CIRCLEPAD_POS)); - s16 circle_pad_x = - (circle_pad_new_x + std::accumulate(circle_pad_old_x.begin(), circle_pad_old_x.end(), 0)) / - CIRCLE_PAD_AVERAGING; - s16 circle_pad_y = - (circle_pad_new_y + std::accumulate(circle_pad_old_y.begin(), circle_pad_old_y.end(), 0)) / - CIRCLE_PAD_AVERAGING; - circle_pad_old_x.erase(circle_pad_old_x.begin()); - circle_pad_old_x.push_back(circle_pad_new_x); - circle_pad_old_y.erase(circle_pad_old_y.begin()); - circle_pad_old_y.push_back(circle_pad_new_y); + state.hex = data.pad & HID_VALID_KEYS; - system.Movie().HandlePadAndCircleStatus(state, circle_pad_x, circle_pad_y); + s16 circle_pad_x = data.c_pad_x; + s16 circle_pad_y = data.c_pad_y; - const DirectionState direction = GetStickDirectionState(circle_pad_x, circle_pad_y); - state.circle_up.Assign(direction.up); - state.circle_down.Assign(direction.down); - state.circle_left.Assign(direction.left); - state.circle_right.Assign(direction.right); + system.Movie().HandlePadAndCircleStatus(state, circle_pad_x, circle_pad_y); - mem->pad.current_state.hex = state.hex; - mem->pad.index = next_pad_index; - next_pad_index = (next_pad_index + 1) % mem->pad.entries.size(); + mem->pad.current_state.hex = state.hex; + mem->pad.index = next_pad_index; + next_pad_index = (next_pad_index + 1) % mem->pad.entries.size(); - // Get the previous Pad state - u32 last_entry_index = (mem->pad.index - 1) % mem->pad.entries.size(); - PadState old_state = mem->pad.entries[last_entry_index].current_state; + // Get the previous Pad state + u32 last_entry_index = (mem->pad.index - 1) % mem->pad.entries.size(); + PadState old_state = mem->pad.entries[last_entry_index].current_state; - // Compute bitmask with 1s for bits different from the old state - PadState changed = {{(state.hex ^ old_state.hex)}}; + // Compute bitmask with 1s for bits different from the old state + PadState changed = {{(state.hex ^ old_state.hex)}}; - // Get the current Pad entry - PadDataEntry& pad_entry = mem->pad.entries[mem->pad.index]; + // Get the current Pad entry + PadDataEntry& pad_entry = mem->pad.entries[mem->pad.index]; - // Update entry properties - pad_entry.current_state.hex = state.hex; - pad_entry.delta_additions.hex = changed.hex & state.hex; - pad_entry.delta_removals.hex = changed.hex & old_state.hex; - pad_entry.circle_pad_x = circle_pad_x; - pad_entry.circle_pad_y = circle_pad_y; + // Update entry properties + pad_entry.current_state.hex = state.hex; + pad_entry.delta_additions.hex = changed.hex & state.hex; + pad_entry.delta_removals.hex = changed.hex & old_state.hex; + pad_entry.circle_pad_x = circle_pad_x; + pad_entry.circle_pad_y = circle_pad_y; - // If we just updated index 0, provide a new timestamp - if (mem->pad.index == 0) { - mem->pad.index_reset_ticks_previous = mem->pad.index_reset_ticks; - mem->pad.index_reset_ticks = (s64)system.CoreTiming().GetTicks(); + // If we just updated index 0, provide a new timestamp + if (mem->pad.index == 0) { + mem->pad.index_reset_ticks_previous = mem->pad.index_reset_ticks; + mem->pad.index_reset_ticks = (s64)system.CoreTiming().GetTicks(); + } + + mem->touch.index = next_touch_index; + next_touch_index = (next_touch_index + 1) % mem->touch.entries.size(); + + // Get the current touch entry + TouchDataEntry& touch_entry = mem->touch.entries[mem->touch.index]; + bool pressed = (data.pad & LIBCTRU_TOUCH_KEY) != 0; + + touch_entry.x = static_cast(data.touch_x); + touch_entry.y = static_cast(data.touch_y); + touch_entry.valid.Assign(pressed ? 1 : 0); + + system.Movie().HandleTouchStatus(touch_entry); + } else { + state.a.Assign(buttons[A - BUTTON_HID_BEGIN]->GetStatus()); + state.b.Assign(buttons[B - BUTTON_HID_BEGIN]->GetStatus()); + state.x.Assign(buttons[X - BUTTON_HID_BEGIN]->GetStatus()); + state.y.Assign(buttons[Y - BUTTON_HID_BEGIN]->GetStatus()); + state.right.Assign(buttons[Right - BUTTON_HID_BEGIN]->GetStatus()); + state.left.Assign(buttons[Left - BUTTON_HID_BEGIN]->GetStatus()); + state.up.Assign(buttons[Up - BUTTON_HID_BEGIN]->GetStatus()); + state.down.Assign(buttons[Down - BUTTON_HID_BEGIN]->GetStatus()); + state.l.Assign(buttons[L - BUTTON_HID_BEGIN]->GetStatus()); + state.r.Assign(buttons[R - BUTTON_HID_BEGIN]->GetStatus()); + state.start.Assign(buttons[Start - BUTTON_HID_BEGIN]->GetStatus()); + state.select.Assign(buttons[Select - BUTTON_HID_BEGIN]->GetStatus()); + state.debug.Assign(buttons[Debug - BUTTON_HID_BEGIN]->GetStatus()); + state.gpio14.Assign(buttons[Gpio14 - BUTTON_HID_BEGIN]->GetStatus()); + + // Get current circle pad position and update circle pad direction + float circle_pad_x_f, circle_pad_y_f; + std::tie(circle_pad_x_f, circle_pad_y_f) = circle_pad->GetStatus(); + + // xperia64: 0x9A seems to be the calibrated limit of the circle pad + // Verified by using Input Redirector with very large-value digital inputs + // on the circle pad and calibrating using the system settings application + constexpr int MAX_CIRCLEPAD_POS = 0x9A; // Max value for a circle pad position + + // These are rounded rather than truncated on actual hardware + s16 circle_pad_new_x = static_cast(std::roundf(circle_pad_x_f * MAX_CIRCLEPAD_POS)); + s16 circle_pad_new_y = static_cast(std::roundf(circle_pad_y_f * MAX_CIRCLEPAD_POS)); + s16 circle_pad_x = (circle_pad_new_x + + std::accumulate(circle_pad_old_x.begin(), circle_pad_old_x.end(), 0)) / + CIRCLE_PAD_AVERAGING; + s16 circle_pad_y = (circle_pad_new_y + + std::accumulate(circle_pad_old_y.begin(), circle_pad_old_y.end(), 0)) / + CIRCLE_PAD_AVERAGING; + circle_pad_old_x.erase(circle_pad_old_x.begin()); + circle_pad_old_x.push_back(circle_pad_new_x); + circle_pad_old_y.erase(circle_pad_old_y.begin()); + circle_pad_old_y.push_back(circle_pad_new_y); + + system.Movie().HandlePadAndCircleStatus(state, circle_pad_x, circle_pad_y); + + const DirectionState direction = GetStickDirectionState(circle_pad_x, circle_pad_y); + state.circle_up.Assign(direction.up); + state.circle_down.Assign(direction.down); + state.circle_left.Assign(direction.left); + state.circle_right.Assign(direction.right); + + mem->pad.current_state.hex = state.hex; + mem->pad.index = next_pad_index; + next_pad_index = (next_pad_index + 1) % mem->pad.entries.size(); + + // Get the previous Pad state + u32 last_entry_index = (mem->pad.index - 1) % mem->pad.entries.size(); + PadState old_state = mem->pad.entries[last_entry_index].current_state; + + // Compute bitmask with 1s for bits different from the old state + PadState changed = {{(state.hex ^ old_state.hex)}}; + + // Get the current Pad entry + PadDataEntry& pad_entry = mem->pad.entries[mem->pad.index]; + + // Update entry properties + pad_entry.current_state.hex = state.hex; + pad_entry.delta_additions.hex = changed.hex & state.hex; + pad_entry.delta_removals.hex = changed.hex & old_state.hex; + pad_entry.circle_pad_x = circle_pad_x; + pad_entry.circle_pad_y = circle_pad_y; + + // If we just updated index 0, provide a new timestamp + if (mem->pad.index == 0) { + mem->pad.index_reset_ticks_previous = mem->pad.index_reset_ticks; + mem->pad.index_reset_ticks = (s64)system.CoreTiming().GetTicks(); + } + + mem->touch.index = next_touch_index; + next_touch_index = (next_touch_index + 1) % mem->touch.entries.size(); + + // Get the current touch entry + TouchDataEntry& touch_entry = mem->touch.entries[mem->touch.index]; + bool pressed = false; + float x, y; + std::tie(x, y, pressed) = touch_device->GetStatus(); + if (!pressed && touch_btn_device) { + std::tie(x, y, pressed) = touch_btn_device->GetStatus(); + } + touch_entry.x = static_cast(x * Core::kScreenBottomWidth); + touch_entry.y = static_cast(y * Core::kScreenBottomHeight); + touch_entry.valid.Assign(pressed ? 1 : 0); + + system.Movie().HandleTouchStatus(touch_entry); } - mem->touch.index = next_touch_index; - next_touch_index = (next_touch_index + 1) % mem->touch.entries.size(); - - // Get the current touch entry - TouchDataEntry& touch_entry = mem->touch.entries[mem->touch.index]; - bool pressed = false; - float x, y; - std::tie(x, y, pressed) = touch_device->GetStatus(); - if (!pressed && touch_btn_device) { - std::tie(x, y, pressed) = touch_btn_device->GetStatus(); - } - touch_entry.x = static_cast(x * Core::kScreenBottomWidth); - touch_entry.y = static_cast(y * Core::kScreenBottomHeight); - touch_entry.valid.Assign(pressed ? 1 : 0); - - system.Movie().HandleTouchStatus(touch_entry); - // TODO(bunnei): We're not doing anything with offset 0xA8 + 0x18 of HID SharedMemory, which // supposedly is "Touch-screen entry, which contains the raw coordinate data prior to being // converted to pixel coordinates." (http://3dbrew.org/wiki/HID_Shared_Memory#Offset_0xA8). @@ -231,19 +314,27 @@ void Module::UpdateAccelerometerCallback(std::uintptr_t user_data, s64 cycles_la mem->accelerometer.index = next_accelerometer_index; next_accelerometer_index = (next_accelerometer_index + 1) % mem->accelerometer.entries.size(); - Common::Vec3 accel; - std::tie(accel, std::ignore) = motion_device->GetStatus(); - accel *= accelerometer_coef; - // TODO(wwylele): do a time stretch like the one in UpdateGyroscopeCallback - // The time stretch formula should be like - // stretched_vector = (raw_vector - gravity) * stretch_ratio + gravity - AccelerometerDataEntry& accelerometer_entry = mem->accelerometer.entries[mem->accelerometer.index]; - accelerometer_entry.x = static_cast(accel.x); - accelerometer_entry.y = static_cast(accel.y); - accelerometer_entry.z = static_cast(accel.z); + if (artic_controller.get() && artic_controller->IsReady()) { + ArticBaseController::ControllerData data = artic_controller->GetControllerData(); + + accelerometer_entry.x = data.accel_x; + accelerometer_entry.y = data.accel_y; + accelerometer_entry.z = data.accel_z; + } else { + Common::Vec3 accel; + std::tie(accel, std::ignore) = motion_device->GetStatus(); + accel *= accelerometer_coef; + // TODO(wwylele): do a time stretch like the one in UpdateGyroscopeCallback + // The time stretch formula should be like + // stretched_vector = (raw_vector - gravity) * stretch_ratio + gravity + + accelerometer_entry.x = static_cast(accel.x); + accelerometer_entry.y = static_cast(accel.y); + accelerometer_entry.z = static_cast(accel.z); + } system.Movie().HandleAccelerometerStatus(accelerometer_entry); @@ -278,13 +369,21 @@ void Module::UpdateGyroscopeCallback(std::uintptr_t user_data, s64 cycles_late) GyroscopeDataEntry& gyroscope_entry = mem->gyroscope.entries[mem->gyroscope.index]; - Common::Vec3 gyro; - std::tie(std::ignore, gyro) = motion_device->GetStatus(); - double stretch = system.perf_stats->GetLastFrameTimeScale(); - gyro *= gyroscope_coef * static_cast(stretch); - gyroscope_entry.x = static_cast(gyro.x); - gyroscope_entry.y = static_cast(gyro.y); - gyroscope_entry.z = static_cast(gyro.z); + if (artic_controller.get() && artic_controller->IsReady()) { + ArticBaseController::ControllerData data = artic_controller->GetControllerData(); + + gyroscope_entry.x = data.gyro_x; + gyroscope_entry.y = data.gyro_y; + gyroscope_entry.z = data.gyro_z; + } else { + Common::Vec3 gyro; + std::tie(std::ignore, gyro) = motion_device->GetStatus(); + double stretch = system.perf_stats->GetLastFrameTimeScale(); + gyro *= gyroscope_coef * static_cast(stretch); + gyroscope_entry.x = static_cast(gyro.x); + gyroscope_entry.y = static_cast(gyro.y); + gyroscope_entry.z = static_cast(gyro.z); + } system.Movie().HandleGyroscopeStatus(gyroscope_entry); @@ -316,6 +415,23 @@ void Module::Interface::GetIPCHandles(Kernel::HLERequestContext& ctx) { void Module::Interface::EnableAccelerometer(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto& artic_client = GetModule()->artic_client; + if (artic_client.get()) { + auto req = artic_client->NewRequest("HIDUSER_EnableAccelerometer"); + + auto resp = artic_client->Send(req); + + if (!resp.has_value()) { + rb.Push(ResultUnknown); + } else { + rb.Push(Result{static_cast(resp->GetMethodResult())}); + } + } else { + rb.Push(ResultSuccess); + } + ++hid->enable_accelerometer_count; // Schedules the accelerometer update event if the accelerometer was just enabled @@ -324,15 +440,29 @@ void Module::Interface::EnableAccelerometer(Kernel::HLERequestContext& ctx) { hid->accelerometer_update_event); } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); - LOG_DEBUG(Service_HID, "called"); } void Module::Interface::DisableAccelerometer(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto& artic_client = GetModule()->artic_client; + if (artic_client.get()) { + auto req = artic_client->NewRequest("HIDUSER_DisableAccelerometer"); + + auto resp = artic_client->Send(req); + + if (!resp.has_value()) { + rb.Push(ResultUnknown); + } else { + rb.Push(Result{static_cast(resp->GetMethodResult())}); + } + } else { + rb.Push(ResultSuccess); + } + --hid->enable_accelerometer_count; // Unschedules the accelerometer update event if the accelerometer was just disabled @@ -340,15 +470,29 @@ void Module::Interface::DisableAccelerometer(Kernel::HLERequestContext& ctx) { hid->system.CoreTiming().UnscheduleEvent(hid->accelerometer_update_event, 0); } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); - LOG_DEBUG(Service_HID, "called"); } void Module::Interface::EnableGyroscopeLow(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto& artic_client = GetModule()->artic_client; + if (artic_client.get()) { + auto req = artic_client->NewRequest("HIDUSER_EnableGyroscope"); + + auto resp = artic_client->Send(req); + + if (!resp.has_value()) { + rb.Push(ResultUnknown); + } else { + rb.Push(Result{static_cast(resp->GetMethodResult())}); + } + } else { + rb.Push(ResultSuccess); + } + ++hid->enable_gyroscope_count; // Schedules the gyroscope update event if the gyroscope was just enabled @@ -356,15 +500,29 @@ void Module::Interface::EnableGyroscopeLow(Kernel::HLERequestContext& ctx) { hid->system.CoreTiming().ScheduleEvent(gyroscope_update_ticks, hid->gyroscope_update_event); } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); - LOG_DEBUG(Service_HID, "called"); } void Module::Interface::DisableGyroscopeLow(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto& artic_client = GetModule()->artic_client; + if (artic_client.get()) { + auto req = artic_client->NewRequest("HIDUSER_DisableGyroscope"); + + auto resp = artic_client->Send(req); + + if (!resp.has_value()) { + rb.Push(ResultUnknown); + } else { + rb.Push(Result{static_cast(resp->GetMethodResult())}); + } + } else { + rb.Push(ResultSuccess); + } + --hid->enable_gyroscope_count; // Unschedules the gyroscope update event if the gyroscope was just disabled @@ -372,9 +530,6 @@ void Module::Interface::DisableGyroscopeLow(Kernel::HLERequestContext& ctx) { hid->system.CoreTiming().UnscheduleEvent(hid->gyroscope_update_event, 0); } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); - LOG_DEBUG(Service_HID, "called"); } @@ -382,25 +537,90 @@ void Module::Interface::GetGyroscopeLowRawToDpsCoefficient(Kernel::HLERequestCon IPC::RequestParser rp(ctx); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(ResultSuccess); - rb.Push(gyroscope_coef); + + auto& artic_client = GetModule()->artic_client; + if (artic_client.get()) { + auto req = artic_client->NewRequest("HIDUSER_GetGyroRawToDpsCoef"); + + auto resp = artic_client->Send(req); + + if (!resp.has_value()) { + rb.Push(ResultUnknown); + rb.Push(0.f); + return; + } + + Result res = Result{static_cast(resp->GetMethodResult())}; + if (res.IsError()) { + rb.Push(res); + rb.Push(0.f); + return; + } + + auto coef = resp->GetResponseFloat(0); + if (!coef.has_value()) { + rb.Push(ResultUnknown); + rb.Push(0.f); + return; + } + + rb.Push(res); + rb.Push(*coef); + } else { + rb.Push(ResultSuccess); + rb.Push(gyroscope_coef); + } } void Module::Interface::GetGyroscopeLowCalibrateParam(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); IPC::RequestBuilder rb = rp.MakeBuilder(6, 0); - rb.Push(ResultSuccess); - const s16 param_unit = 6700; // an approximate value taken from hw - GyroscopeCalibrateParam param = { - {0, param_unit, -param_unit}, - {0, param_unit, -param_unit}, - {0, param_unit, -param_unit}, - }; - rb.PushRaw(param); + auto& artic_client = GetModule()->artic_client; + if (artic_client.get()) { + GyroscopeCalibrateParam param; - LOG_WARNING(Service_HID, "(STUBBED) called"); + auto req = artic_client->NewRequest("HIDUSER_GetGyroCalibrateParam"); + + auto resp = artic_client->Send(req); + + if (!resp.has_value()) { + rb.Push(ResultUnknown); + rb.PushRaw(param); + return; + } + + Result res = Result{static_cast(resp->GetMethodResult())}; + if (res.IsError()) { + rb.Push(res); + rb.PushRaw(param); + return; + } + + auto param_buf = resp->GetResponseBuffer(0); + if (!param_buf.has_value() || param_buf->second != sizeof(param)) { + rb.Push(ResultUnknown); + rb.PushRaw(param); + return; + } + memcpy(¶m, param_buf->first, sizeof(param)); + + rb.Push(res); + rb.PushRaw(param); + } else { + rb.Push(ResultSuccess); + + const s16 param_unit = 6700; // an approximate value taken from hw + GyroscopeCalibrateParam param = { + {0, param_unit, -param_unit}, + {0, param_unit, -param_unit}, + {0, param_unit, -param_unit}, + }; + rb.PushRaw(param); + + LOG_WARNING(Service_HID, "(STUBBED) called"); + } } void Module::Interface::GetSoundVolume(Kernel::HLERequestContext& ctx) { @@ -454,6 +674,24 @@ Module::Module(Core::System& system) : system(system) { timing.ScheduleEvent(pad_update_ticks, pad_update_event); } +void Module::UseArticClient(const std::shared_ptr& client) { + artic_client = client; + artic_controller = std::make_shared(client); + if (!artic_controller->IsCreated()) { + artic_controller.reset(); + } else { + auto ir_user = system.ServiceManager().GetService("ir:USER"); + if (ir_user.get()) { + ir_user->UseArticController(artic_controller); + } + + auto ir_rst = system.ServiceManager().GetService("ir:rst"); + if (ir_rst.get()) { + ir_rst->UseArticController(artic_controller); + } + } +} + void Module::ReloadInputDevices() { is_device_reload_pending.store(true); } diff --git a/src/core/hle/service/hid/hid.h b/src/core/hle/service/hid/hid.h index 609cb9276..79713ca69 100644 --- a/src/core/hle/service/hid/hid.h +++ b/src/core/hle/service/hid/hid.h @@ -14,13 +14,11 @@ #include "common/common_funcs.h" #include "common/common_types.h" #include "common/settings.h" +#include "core/core.h" #include "core/core_timing.h" #include "core/frontend/input.h" #include "core/hle/service/service.h" - -namespace Core { -class System; -} +#include "network/artic_base/artic_base_client.h" namespace Kernel { class Event; @@ -199,6 +197,44 @@ struct DirectionState { /// Translates analog stick axes to directions. This is exposed for ir_rst module to use. DirectionState GetStickDirectionState(s16 circle_pad_x, s16 circle_pad_y); +class ArticBaseController { +public: + struct ControllerData { + u32 index{}; + u32 pad{}; + s16 c_pad_x{}; + s16 c_pad_y{}; + u16 touch_x{}; + u16 touch_y{}; + s16 c_stick_x{}; + s16 c_stick_y{}; + s16 accel_x{}; + s16 accel_y{}; + s16 accel_z{}; + s16 gyro_x{}; + s16 gyro_y{}; + s16 gyro_z{}; + }; + static_assert(sizeof(ControllerData) == 0x20, "Incorrect ControllerData size"); + + ArticBaseController(const std::shared_ptr& client); + + bool IsCreated() { + return udp_stream.get(); + } + + bool IsReady() { + return udp_stream.get() ? udp_stream->IsReady() : false; + } + + ControllerData GetControllerData(); + +private: + std::shared_ptr udp_stream; + u32 last_packet_id{}; + ControllerData last_controller_data{}; +}; + class Module final { public: explicit Module(Core::System& system); @@ -296,6 +332,8 @@ public: std::shared_ptr hid; }; + void UseArticClient(const std::shared_ptr& client); + void ReloadInputDevices(); const PadState& GetState() const; @@ -355,6 +393,9 @@ private: std::unique_ptr touch_device; std::unique_ptr touch_btn_device; + std::shared_ptr artic_controller; + std::shared_ptr artic_client; + template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; diff --git a/src/core/hle/service/http/http_c.cpp b/src/core/hle/service/http/http_c.cpp index 3337580a5..8fb9e4300 100644 --- a/src/core/hle/service/http/http_c.cpp +++ b/src/core/hle/service/http/http_c.cpp @@ -381,10 +381,10 @@ void Context::MakeRequestNonSSL(httplib::Request& request, const URLInfo& url_in if (!client->send(request, response, error)) { LOG_ERROR(Service_HTTP, "Request failed: {}: {}", error, httplib::to_string(error)); - state = RequestState::TimedOut; + state = RequestState::Completed; } else { LOG_DEBUG(Service_HTTP, "Request successful"); - state = RequestState::ReadyToDownloadContent; + state = RequestState::ReceivingBody; } } @@ -439,10 +439,10 @@ void Context::MakeRequestSSL(httplib::Request& request, const URLInfo& url_info, if (!client->send(request, response, error)) { LOG_ERROR(Service_HTTP, "Request failed: {}: {}", error, httplib::to_string(error)); - state = RequestState::TimedOut; + state = RequestState::Completed; } else { LOG_DEBUG(Service_HTTP, "Request successful"); - state = RequestState::ReadyToDownloadContent; + state = RequestState::ReceivingBody; } } @@ -696,6 +696,7 @@ void HTTP_C::ReceiveDataImpl(Kernel::HLERequestContext& ctx, bool timeout) { http_context.current_copied_data, 0, remaining_data); http_context.current_copied_data += remaining_data; + http_context.state = RequestState::Completed; rb.Push(ResultSuccess); } else { async_data->buffer->Write(http_context.response.body.data() + @@ -1969,7 +1970,7 @@ void HTTP_C::DecryptClCertA() { FileSys::NCCHFileOpenType::NCCHData, 0, FileSys::NCCHFilePathType::RomFS, exefs_filepath); FileSys::Mode open_mode = {}; open_mode.read_flag.Assign(1); - auto file_result = archive.OpenFile(file_path, open_mode); + auto file_result = archive.OpenFile(file_path, open_mode, 0); if (file_result.Failed()) { LOG_ERROR(Service_HTTP, "ClCertA file missing"); return; diff --git a/src/core/hle/service/http/http_c.h b/src/core/hle/service/http/http_c.h index 7f71aebcd..4a89bac27 100644 --- a/src/core/hle/service/http/http_c.h +++ b/src/core/hle/service/http/http_c.h @@ -48,12 +48,29 @@ enum class RequestMethod : u8 { constexpr u32 TotalRequestMethods = 8; enum class RequestState : u8 { - NotStarted = 0x1, // Request has not started yet. - ConnectingToServer = 0x5, // Request in progress, connecting to server. - SendingRequest = 0x6, // Request in progress, sending HTTP request. - ReceivingResponse = 0x7, // Request in progress, receiving HTTP response. - ReadyToDownloadContent = 0x8, // Ready to download the content. - TimedOut = 0xA, // Request timed out? + /// Request has not started yet. + NotStarted = 0x1, + + /// Request in progress, connecting to server. + ConnectingToServer = 0x5, + + /// Request in progress, sending HTTP request. + SendingRequest = 0x6, + + // Request in progress, receiving HTTP response and headers. + ReceivingResponse = 0x7, + + /// Request in progress, receiving HTTP body. The HTTP module may + /// get stuck in this state if the internal receive buffer gets full. + /// Once the user calls ReceiveData it will get unstuck. + ReceivingBody = 0x8, + + /// Request is finished and all data has been received. HTTP transitions + /// to the Completed state shortly afterwards after some cleanup. + Received = 0x9, + + /// Request is completed. + Completed = 0xA, }; enum class PostDataEncoding : u8 { diff --git a/src/core/hle/service/ir/extra_hid.cpp b/src/core/hle/service/ir/extra_hid.cpp index e159a70fe..e5dc486cb 100644 --- a/src/core/hle/service/ir/extra_hid.cpp +++ b/src/core/hle/service/ir/extra_hid.cpp @@ -2,10 +2,11 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include +#include #include "common/alignment.h" #include "common/settings.h" #include "core/core_timing.h" +#include "core/hle/service/hid/hid.h" #include "core/hle/service/ir/extra_hid.h" #include "core/movie.h" @@ -230,23 +231,47 @@ void ExtraHID::SendHIDStatus() { if (is_device_reload_pending.exchange(false)) LoadInputDevices(); + constexpr u32 ZL_BUTTON = (1 << 14); + constexpr u32 ZR_BUTTON = (1 << 15); + constexpr int C_STICK_CENTER = 0x800; // TODO(wwylele): this value is not accurately measured. We currently assume that the axis can // take values in the whole range of a 12-bit integer. constexpr int C_STICK_RADIUS = 0x7FF; - float x, y; - std::tie(x, y) = c_stick->GetStatus(); - ExtraHIDResponse response{}; - response.c_stick.header.Assign(static_cast(ResponseID::PollHID)); - response.c_stick.c_stick_x.Assign(static_cast(C_STICK_CENTER + C_STICK_RADIUS * x)); - response.c_stick.c_stick_y.Assign(static_cast(C_STICK_CENTER + C_STICK_RADIUS * y)); - response.buttons.battery_level.Assign(0x1F); - response.buttons.zl_not_held.Assign(!zl->GetStatus()); - response.buttons.zr_not_held.Assign(!zr->GetStatus()); - response.buttons.r_not_held.Assign(1); - response.unknown = 0; + + if (artic_controller.get() && artic_controller->IsReady()) { + Service::HID::ArticBaseController::ControllerData data = + artic_controller->GetControllerData(); + + constexpr int MAX_CSTICK_RADIUS = 0x9C; // Max value for a c-stick radius + + response.c_stick.header.Assign(static_cast(ResponseID::PollHID)); + response.c_stick.c_stick_x.Assign(static_cast( + (static_cast(data.c_stick_x) / MAX_CSTICK_RADIUS) * C_STICK_RADIUS + + C_STICK_CENTER)); + response.c_stick.c_stick_y.Assign(static_cast( + (static_cast(data.c_stick_y) / MAX_CSTICK_RADIUS) * C_STICK_RADIUS + + C_STICK_CENTER)); + response.buttons.battery_level.Assign(0x1F); + response.buttons.zl_not_held.Assign((data.pad & ZL_BUTTON) == 0); + response.buttons.zr_not_held.Assign((data.pad & ZR_BUTTON) == 0); + response.buttons.r_not_held.Assign(1); + response.unknown = 0; + } else { + float x, y; + std::tie(x, y) = c_stick->GetStatus(); + + response.c_stick.header.Assign(static_cast(ResponseID::PollHID)); + response.c_stick.c_stick_x.Assign(static_cast(C_STICK_CENTER + C_STICK_RADIUS * x)); + response.c_stick.c_stick_y.Assign(static_cast(C_STICK_CENTER + C_STICK_RADIUS * y)); + response.buttons.battery_level.Assign(0x1F); + response.buttons.zl_not_held.Assign(!zl->GetStatus()); + response.buttons.zr_not_held.Assign(!zr->GetStatus()); + response.buttons.r_not_held.Assign(1); + response.unknown = 0; + } movie.HandleExtraHidResponse(response); diff --git a/src/core/hle/service/ir/extra_hid.h b/src/core/hle/service/ir/extra_hid.h index 5dc36b4cd..4cceec580 100644 --- a/src/core/hle/service/ir/extra_hid.h +++ b/src/core/hle/service/ir/extra_hid.h @@ -19,6 +19,10 @@ class Timing; class Movie; } // namespace Core +namespace Service::HID { +class ArticBaseController; +}; + namespace Service::IR { struct ExtraHIDResponse { @@ -54,6 +58,10 @@ public: /// Requests input devices reload from current settings. Called when the input settings change. void RequestInputDevicesReload(); + void UseArticController(const std::shared_ptr& ac) { + artic_controller = ac; + } + private: void SendHIDStatus(); void HandleConfigureHIDPollingRequest(std::span request); @@ -70,6 +78,8 @@ private: std::unique_ptr c_stick; std::atomic is_device_reload_pending; + std::shared_ptr artic_controller = nullptr; + template void serialize(Archive& ar, const unsigned int) { ar& hid_period; diff --git a/src/core/hle/service/ir/ir_rst.cpp b/src/core/hle/service/ir/ir_rst.cpp index 2e2cd5b94..be7408619 100644 --- a/src/core/hle/service/ir/ir_rst.cpp +++ b/src/core/hle/service/ir/ir_rst.cpp @@ -72,25 +72,41 @@ void IR_RST::UpdateCallback(std::uintptr_t user_data, s64 cycles_late) { if (is_device_reload_pending.exchange(false)) LoadInputDevices(); + constexpr u32 VALID_EXTRAHID_KEYS = 0xF00C000; + PadState state; - state.zl.Assign(zl_button->GetStatus()); - state.zr.Assign(zr_button->GetStatus()); + s16 c_stick_x, c_stick_y; - // Get current c-stick position and update c-stick direction - float c_stick_x_f, c_stick_y_f; - std::tie(c_stick_x_f, c_stick_y_f) = c_stick->GetStatus(); - constexpr int MAX_CSTICK_RADIUS = 0x9C; // Max value for a c-stick radius - s16 c_stick_x = static_cast(c_stick_x_f * MAX_CSTICK_RADIUS); - s16 c_stick_y = static_cast(c_stick_y_f * MAX_CSTICK_RADIUS); + if (artic_controller.get() && artic_controller->IsReady()) { + Service::HID::ArticBaseController::ControllerData data = + artic_controller->GetControllerData(); - system.Movie().HandleIrRst(state, c_stick_x, c_stick_y); + state.hex = data.pad & VALID_EXTRAHID_KEYS; - if (!raw_c_stick) { - const HID::DirectionState direction = HID::GetStickDirectionState(c_stick_x, c_stick_y); - state.c_stick_up.Assign(direction.up); - state.c_stick_down.Assign(direction.down); - state.c_stick_left.Assign(direction.left); - state.c_stick_right.Assign(direction.right); + c_stick_x = data.c_stick_x; + c_stick_y = data.c_stick_y; + + system.Movie().HandleIrRst(state, c_stick_x, c_stick_y); + } else { + state.zl.Assign(zl_button->GetStatus()); + state.zr.Assign(zr_button->GetStatus()); + + // Get current c-stick position and update c-stick direction + float c_stick_x_f, c_stick_y_f; + std::tie(c_stick_x_f, c_stick_y_f) = c_stick->GetStatus(); + constexpr int MAX_CSTICK_RADIUS = 0x9C; // Max value for a c-stick radius + c_stick_x = static_cast(c_stick_x_f * MAX_CSTICK_RADIUS); + c_stick_y = static_cast(c_stick_y_f * MAX_CSTICK_RADIUS); + + system.Movie().HandleIrRst(state, c_stick_x, c_stick_y); + + if (!raw_c_stick) { + const HID::DirectionState direction = HID::GetStickDirectionState(c_stick_x, c_stick_y); + state.c_stick_up.Assign(direction.up); + state.c_stick_down.Assign(direction.down); + state.c_stick_left.Assign(direction.left); + state.c_stick_right.Assign(direction.right); + } } // TODO (wwylele): implement raw C-stick data for raw_c_stick = true diff --git a/src/core/hle/service/ir/ir_rst.h b/src/core/hle/service/ir/ir_rst.h index 2514ab6f9..d02d34aa3 100644 --- a/src/core/hle/service/ir/ir_rst.h +++ b/src/core/hle/service/ir/ir_rst.h @@ -21,6 +21,10 @@ namespace Core { struct TimingEventType; }; +namespace Service::HID { +class ArticBaseController; +}; + namespace Service::IR { union PadState { @@ -42,6 +46,10 @@ public: ~IR_RST(); void ReloadInputDevices(); + void UseArticController(const std::shared_ptr& ac) { + artic_controller = ac; + } + private: /** * GetHandles service function @@ -88,6 +96,8 @@ private: bool raw_c_stick{false}; int update_period{0}; + std::shared_ptr artic_controller = nullptr; + template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; diff --git a/src/core/hle/service/ir/ir_user.cpp b/src/core/hle/service/ir/ir_user.cpp index add0eb3d0..d6ced4b4c 100644 --- a/src/core/hle/service/ir/ir_user.cpp +++ b/src/core/hle/service/ir/ir_user.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include "common/archives.h" #include "common/swap.h" #include "core/core.h" @@ -480,6 +480,12 @@ void IR_USER::ReloadInputDevices() { extra_hid->RequestInputDevicesReload(); } +void IR_USER::UseArticController(const std::shared_ptr& ac) { + if (extra_hid.get()) { + extra_hid->UseArticController(ac); + } +} + IRDevice::IRDevice(SendFunc send_func_) : send_func(send_func_) {} IRDevice::~IRDevice() = default; diff --git a/src/core/hle/service/ir/ir_user.h b/src/core/hle/service/ir/ir_user.h index e724e9dc5..fac49edfa 100644 --- a/src/core/hle/service/ir/ir_user.h +++ b/src/core/hle/service/ir/ir_user.h @@ -14,6 +14,10 @@ class Event; class SharedMemory; } // namespace Kernel +namespace Service::HID { +class ArticBaseController; +}; + namespace Service::IR { class BufferManager; @@ -57,6 +61,8 @@ public: void ReloadInputDevices(); + void UseArticController(const std::shared_ptr& ac); + private: /** * InitializeIrNopShared service function diff --git a/src/core/hle/service/news/news.cpp b/src/core/hle/service/news/news.cpp index f1a32d168..919041f37 100644 --- a/src/core/hle/service/news/news.cpp +++ b/src/core/hle/service/news/news.cpp @@ -151,7 +151,7 @@ void Module::Interface::ResetNotifications(Kernel::HLERequestContext& ctx) { FileSys::Path archive_path(news_system_savedata_id); // Format the SystemSaveData archive 0x00010035 - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); news->news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); @@ -655,7 +655,7 @@ Result Module::LoadNewsDBSavedata() { // If the archive didn't exist, create the files inside if (archive_result.Code() == FileSys::ResultNotFound) { // Format the archive to create the directories - systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0); // Open it again to get a valid archive now that the folder exists news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap(); @@ -722,7 +722,7 @@ Result Module::SaveFileToSavedata(std::string filename, std::span buff ASSERT_MSG(result.Succeeded(), "could not open file"); auto file = std::move(result).Unwrap(); - file->Write(0, buffer.size(), 1, buffer.data()); + file->Write(0, buffer.size(), true, false, buffer.data()); file->Close(); return ResultSuccess; diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 6f609182b..a9851a19b 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -158,7 +158,8 @@ static void WriteGameCoinData(GameCoin gamecoin_data) { // If the archive didn't exist, create the files inside if (archive_result.Code() == FileSys::ResultNotFormatted) { // Format the archive to create the directories - extdata_archive_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0); + extdata_archive_factory.FormatAsExtData(archive_path, FileSys::ArchiveFormatInfo(), 0, 0, 0, + std::nullopt); // Open it again to get a valid archive now that the folder exists archive = extdata_archive_factory.Open(archive_path, 0).Unwrap(); // Create the game coin file @@ -174,7 +175,8 @@ static void WriteGameCoinData(GameCoin gamecoin_data) { auto gamecoin_result = archive->OpenFile(gamecoin_path, open_mode); if (gamecoin_result.Succeeded()) { auto gamecoin = std::move(gamecoin_result).Unwrap(); - gamecoin->Write(0, sizeof(GameCoin), true, reinterpret_cast(&gamecoin_data)); + gamecoin->Write(0, sizeof(GameCoin), true, false, + reinterpret_cast(&gamecoin_data)); gamecoin->Close(); } } diff --git a/src/core/hle/service/soc/soc_u.cpp b/src/core/hle/service/soc/soc_u.cpp index ae2ecfcb2..a40084249 100644 --- a/src/core/hle/service/soc/soc_u.cpp +++ b/src/core/hle/service/soc/soc_u.cpp @@ -19,6 +19,7 @@ #include "core/hle/kernel/shared_memory.h" #include "core/hle/result.h" #include "core/hle/service/soc/soc_u.h" +#include "network/socket_manager.h" #ifdef _WIN32 #include @@ -2221,17 +2222,12 @@ SOC_U::SOC_U() : ServiceFramework("soc:U", 18) { RegisterHandlers(functions); -#ifdef _WIN32 - WSADATA data; - WSAStartup(MAKEWORD(2, 2), &data); -#endif + Network::SocketManager::EnableSockets(); } SOC_U::~SOC_U() { CloseAndDeleteAllSockets(); -#ifdef _WIN32 - WSACleanup(); -#endif + Network::SocketManager::DisableSockets(); } std::optional SOC_U::GetDefaultInterfaceInfo() { @@ -2240,18 +2236,15 @@ std::optional SOC_U::GetDefaultInterfaceInfo() { } InterfaceInfo ret; -#ifdef _WIN32 - SOCKET sock_fd = -1; -#else - int sock_fd = -1; -#endif + + SocketHolder::SOCKET sock_fd = -1; bool interface_found = false; struct sockaddr_in s_in = {.sin_family = AF_INET, .sin_port = htons(53), .sin_addr = {}}; s_in.sin_addr.s_addr = inet_addr("8.8.8.8"); socklen_t s_info_len = sizeof(struct sockaddr_in); sockaddr_in s_info; - if (static_cast(sock_fd = ::socket(AF_INET, SOCK_STREAM, 0)) == -1) { + if ((sock_fd = ::socket(AF_INET, SOCK_STREAM, 0)) == static_cast(-1)) { return std::nullopt; } @@ -2269,7 +2262,7 @@ std::optional SOC_U::GetDefaultInterfaceInfo() { #ifdef _WIN32 sock_fd = WSASocket(AF_INET, SOCK_DGRAM, 0, 0, 0, 0); - if (static_cast(sock_fd) == SOCKET_ERROR) { + if (sock_fd == static_cast(SOCKET_ERROR)) { return std::nullopt; } diff --git a/src/core/hle/service/soc/soc_u.h b/src/core/hle/service/soc/soc_u.h index 2a7cfa8e8..b66607698 100644 --- a/src/core/hle/service/soc/soc_u.h +++ b/src/core/hle/service/soc/soc_u.h @@ -25,11 +25,11 @@ namespace Service::SOC { struct SocketHolder { #ifdef _WIN32 using SOCKET = unsigned long long; - SOCKET socket_fd; ///< The socket descriptor #else - int socket_fd; ///< The socket descriptor + using SOCKET = int; #endif // _WIN32 + SOCKET socket_fd; ///< The socket descriptor bool blocking = true; ///< Whether the socket is blocking or not. bool isGlobal = false; bool shutdown_rd = false; diff --git a/src/core/loader/artic.cpp b/src/core/loader/artic.cpp new file mode 100644 index 000000000..80365ed6e --- /dev/null +++ b/src/core/loader/artic.cpp @@ -0,0 +1,594 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "common/literals.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "common/string_util.h" +#include "common/swap.h" +#include "core/core.h" +#include "core/file_sys/ncch_container.h" +#include "core/file_sys/romfs_reader.h" +#include "core/file_sys/secure_value_backend_artic.h" +#include "core/file_sys/title_metadata.h" +#include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/process.h" +#include "core/hle/kernel/resource_limit.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/am/am_app.h" +#include "core/hle/service/am/am_net.h" +#include "core/hle/service/cfg/cfg.h" +#include "core/hle/service/cfg/cfg_u.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/fs/fs_user.h" +#include "core/hle/service/hid/hid_user.h" +#include "core/loader/artic.h" +#include "core/loader/smdh.h" +#include "core/memory.h" +#include "core/system_titles.h" +#include "network/network.h" + +namespace Loader { + +using namespace Common::Literals; + +Apploader_Artic::~Apploader_Artic() { + // TODO(PabloMK7) Find memory leak that prevents the romfs readers being destroyed + // when emulation stops. Looks like the mem leak comes from IVFCFile objects + // not being destroyed... + if (main_romfs_reader) { + static_cast(main_romfs_reader.get())->ClearCache(); + static_cast(main_romfs_reader.get())->CloseFile(); + main_romfs_reader.reset(); + } + if (update_romfs_reader) { + static_cast(update_romfs_reader.get())->ClearCache(); + static_cast(update_romfs_reader.get())->CloseFile(); + update_romfs_reader.reset(); + } + client->Stop(); +} + +FileType Apploader_Artic::IdentifyType(FileUtil::IOFile& file) { + return FileType::ARTIC; +} + +std::pair, ResultStatus> Apploader_Artic::LoadCoreVersion() { + if (!is_loaded) { + bool success = LoadExheader(); + if (!success) { + return std::make_pair(std::nullopt, ResultStatus::ErrorArtic); + } + } + + // Provide the core version from the exheader. + auto& ncch_caps = program_exheader.arm11_system_local_caps; + return std::make_pair(ncch_caps.core_version, ResultStatus::Success); +} + +std::pair, ResultStatus> Apploader_Artic::LoadKernelMemoryMode() { + if (!is_loaded) { + bool success = LoadExheader(); + if (!success) { + return std::make_pair(std::nullopt, ResultStatus::ErrorArtic); + } + } + + if (memory_mode_override.has_value()) { + return std::make_pair(memory_mode_override, ResultStatus::Success); + } + + // Provide the memory mode from the exheader. + auto& ncch_caps = program_exheader.arm11_system_local_caps; + auto mode = static_cast(ncch_caps.system_mode.Value()); + return std::make_pair(mode, ResultStatus::Success); +} + +std::pair, ResultStatus> +Apploader_Artic::LoadNew3dsHwCapabilities() { + if (!is_loaded) { + bool success = LoadExheader(); + if (!success) { + return std::make_pair(std::nullopt, ResultStatus::ErrorArtic); + } + } + + // Provide the capabilities from the exheader. + auto& ncch_caps = program_exheader.arm11_system_local_caps; + auto caps = Kernel::New3dsHwCapabilities{ + ncch_caps.enable_l2_cache != 0, + ncch_caps.enable_804MHz_cpu != 0, + static_cast(ncch_caps.n3ds_mode), + }; + return std::make_pair(std::move(caps), ResultStatus::Success); +} + +ResultStatus Apploader_Artic::LoadExec(std::shared_ptr& process) { + using Kernel::CodeSet; + + if (!is_loaded) + return ResultStatus::ErrorNotLoaded; + + std::vector code; + u64_le program_id; + if (ResultStatus::Success == ReadCode(code) && + ResultStatus::Success == ReadProgramId(program_id)) { + + std::string process_name = Common::StringFromFixedZeroTerminatedBuffer( + (const char*)program_exheader.codeset_info.name, 8); + + std::shared_ptr codeset = system.Kernel().CreateCodeSet(process_name, program_id); + + codeset->CodeSegment().offset = 0; + codeset->CodeSegment().addr = program_exheader.codeset_info.text.address; + codeset->CodeSegment().size = + program_exheader.codeset_info.text.num_max_pages * Memory::CITRA_PAGE_SIZE; + + codeset->RODataSegment().offset = + codeset->CodeSegment().offset + codeset->CodeSegment().size; + codeset->RODataSegment().addr = program_exheader.codeset_info.ro.address; + codeset->RODataSegment().size = + program_exheader.codeset_info.ro.num_max_pages * Memory::CITRA_PAGE_SIZE; + + // TODO(yuriks): Not sure if the bss size is added to the page-aligned .data size or just + // to the regular size. Playing it safe for now. + u32 bss_page_size = (program_exheader.codeset_info.bss_size + 0xFFF) & ~0xFFF; + code.resize(code.size() + bss_page_size, 0); + + codeset->DataSegment().offset = + codeset->RODataSegment().offset + codeset->RODataSegment().size; + codeset->DataSegment().addr = program_exheader.codeset_info.data.address; + codeset->DataSegment().size = + program_exheader.codeset_info.data.num_max_pages * Memory::CITRA_PAGE_SIZE + + bss_page_size; + + // Apply patches now that the entire codeset (including .bss) has been allocated + // const ResultStatus patch_result = overlay_ncch->ApplyCodePatch(code); + // if (patch_result != ResultStatus::Success && patch_result != ResultStatus::ErrorNotUsed) + // return patch_result; + + codeset->entrypoint = codeset->CodeSegment().addr; + codeset->memory = std::move(code); + + process = system.Kernel().CreateProcess(std::move(codeset)); + + // Attach a resource limit to the process based on the resource limit category + const auto category = static_cast( + program_exheader.arm11_system_local_caps.resource_limit_category); + process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(category); + + // When running N3DS-unaware titles pm will lie about the amount of memory available. + // This means RESLIMIT_COMMIT = APPMEMALLOC doesn't correspond to the actual size of + // APPLICATION. See: + // https://github.com/LumaTeam/Luma3DS/blob/e2778a45/sysmodules/pm/source/launch.c#L237 + auto& ncch_caps = program_exheader.arm11_system_local_caps; + const auto o3ds_mode = *LoadKernelMemoryMode().first; + const auto n3ds_mode = static_cast(ncch_caps.n3ds_mode); + const bool is_new_3ds = Settings::values.is_new_3ds.GetValue(); + if (is_new_3ds && n3ds_mode == Kernel::New3dsMemoryMode::Legacy && + category == Kernel::ResourceLimitCategory::Application) { + u64 new_limit = 0; + switch (o3ds_mode) { + case Kernel::MemoryMode::Prod: + new_limit = 64_MiB; + break; + case Kernel::MemoryMode::Dev1: + new_limit = 96_MiB; + break; + case Kernel::MemoryMode::Dev2: + new_limit = 80_MiB; + break; + default: + break; + } + process->resource_limit->SetLimitValue(Kernel::ResourceLimitType::Commit, + static_cast(new_limit)); + } + + // Set the default CPU core for this process + process->ideal_processor = program_exheader.arm11_system_local_caps.ideal_processor; + + // Copy data while converting endianness + using KernelCaps = std::array; + KernelCaps kernel_caps; + std::copy_n(program_exheader.arm11_kernel_caps.descriptors, kernel_caps.size(), + begin(kernel_caps)); + process->ParseKernelCaps(kernel_caps.data(), kernel_caps.size()); + + s32 priority = program_exheader.arm11_system_local_caps.priority; + u32 stack_size = program_exheader.codeset_info.stack_size; + + // On real HW this is done with FS:Reg, but we can be lazy + auto fs_user = system.ServiceManager().GetService("fs:USER"); + fs_user->RegisterProgramInfo(process->process_id, process->codeset->program_id, + "articbase://"); + + Service::FS::FS_USER::ProductInfo product_info{}; + if (LoadProductInfo(product_info) != ResultStatus::Success) { + return ResultStatus::ErrorArtic; + } + fs_user->RegisterProductInfo(process->process_id, product_info); + + process->Run(priority, stack_size); + return ResultStatus::Success; + } + return ResultStatus::ErrorArtic; +} + +void Apploader_Artic::ParseRegionLockoutInfo(u64 program_id) { + if (Settings::values.region_value.GetValue() != Settings::REGION_VALUE_AUTO_SELECT) { + return; + } + + preferred_regions.clear(); + + std::vector smdh_buffer; + if (ReadIcon(smdh_buffer) == ResultStatus::Success && smdh_buffer.size() >= sizeof(SMDH)) { + SMDH smdh; + std::memcpy(&smdh, smdh_buffer.data(), sizeof(SMDH)); + u32 region_lockout = smdh.region_lockout; + constexpr u32 REGION_COUNT = 7; + for (u32 region = 0; region < REGION_COUNT; ++region) { + if (region_lockout & 1) { + preferred_regions.push_back(region); + } + region_lockout >>= 1; + } + } else { + const auto region = Core::GetSystemTitleRegion(program_id); + if (region.has_value()) { + preferred_regions.push_back(region.value()); + } + } +} + +bool Apploader_Artic::LoadExheader() { + if (program_exheader_loaded) + return true; + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return false; + + auto req = client->NewRequest("Process_GetExheader"); + auto resp = client->Send(req); + if (!resp.has_value()) + return false; + + auto exheader_buf = resp->GetResponseBuffer(0); + if (!exheader_buf.has_value()) + return false; + + if (exheader_buf->second != sizeof(ExHeader_Header) - sizeof(ExHeader_Header::access_desc)) + return false; + + u8* prg_exh = reinterpret_cast(&program_exheader); + memcpy(prg_exh, exheader_buf->first, + sizeof(ExHeader_Header) - sizeof(ExHeader_Header::access_desc)); + memcpy(prg_exh + offsetof(ExHeader_Header, access_desc.arm11_system_local_caps), + reinterpret_cast(exheader_buf->first) + + offsetof(ExHeader_Header, arm11_system_local_caps), + offsetof(ExHeader_Header, access_desc) - + offsetof(ExHeader_Header, arm11_system_local_caps)); + program_exheader_loaded = true; + return true; +} + +ResultStatus Apploader_Artic::LoadProductInfo(Service::FS::FS_USER::ProductInfo& out_product_info) { + if (cached_product_info.has_value()) { + out_product_info = *cached_product_info; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_GetProductInfo"); + auto resp = client->Send(req); + if (!resp.has_value()) + return ResultStatus::ErrorArtic; + + auto pinfo_buf = resp->GetResponseBuffer(0); + if (!pinfo_buf.has_value() || pinfo_buf->second != sizeof(Service::FS::FS_USER::ProductInfo)) + return ResultStatus::ErrorArtic; + + out_product_info = *reinterpret_cast(pinfo_buf->first); + cached_product_info = out_product_info; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::Load(std::shared_ptr& process) { + u64_le ncch_program_id; + + if (is_loaded) + return ResultStatus::ErrorAlreadyLoaded; + + ResultStatus result = ReadProgramId(ncch_program_id); + if (result != ResultStatus::Success) { + return result; + } + + std::string program_id{fmt::format("{:016X}", ncch_program_id)}; + + LOG_INFO(Loader, "Program ID: {}", program_id); + + if (auto room_member = Network::GetRoomMember().lock()) { + Network::GameInfo game_info; + ReadTitle(game_info.name); + game_info.id = ncch_program_id; + room_member->SendGameInfo(game_info); + } + + is_loaded = true; // Set state to loaded + + result = LoadExec(process); // Load the executable into memory for booting + if (ResultStatus::Success != result) + return result; + + system.ArchiveManager().RegisterSelfNCCH(*this); + system.ArchiveManager().RegisterArticSaveDataSource(client); + system.ArchiveManager().RegisterArticExtData(client); + system.ArchiveManager().RegisterArticNCCH(client); + system.ArchiveManager().RegisterArticSystemSaveData(client); + + auto fs_user = system.ServiceManager().GetService("fs:USER"); + if (fs_user.get()) { + fs_user->RegisterSecureValueBackend( + std::make_shared(client)); + } + + auto cfg = system.ServiceManager().GetService("cfg:u"); + if (cfg.get()) { + cfg->UseArticClient(client); + } + + auto amnet = system.ServiceManager().GetService("am:net"); + if (amnet.get()) { + amnet->UseArticClient(client); + } + + auto amapp = system.ServiceManager().GetService("am:app"); + if (amapp.get()) { + amapp->UseArticClient(client); + } + + if (Settings::values.use_artic_base_controller.GetValue()) { + auto hid_user = system.ServiceManager().GetService("hid:USER"); + if (hid_user.get()) { + hid_user->GetModule()->UseArticClient(client); + } + } + + ParseRegionLockoutInfo(ncch_program_id); + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::IsExecutable(bool& out_executable) { + out_executable = true; + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadCode(std::vector& buffer) { + // Code is only read once, there is no need to cache it. + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + size_t code_size = program_exheader.codeset_info.text.num_max_pages * Memory::CITRA_PAGE_SIZE; + code_size += program_exheader.codeset_info.ro.num_max_pages * Memory::CITRA_PAGE_SIZE; + code_size += program_exheader.codeset_info.data.num_max_pages * Memory::CITRA_PAGE_SIZE; + + size_t read_amount = 0; + buffer.clear(); + + while (read_amount != code_size) { + size_t to_read = + std::min(client->GetServerRequestMaxSize() - 0x100, code_size - read_amount); + + auto req = client->NewRequest("Process_ReadCode"); + req.AddParameterS32(static_cast(read_amount)); + req.AddParameterS32(static_cast(to_read)); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto code_buff = resp->GetResponseBuffer(0); + if (!code_buff.has_value() || code_buff->second != to_read) + return ResultStatus::ErrorArtic; + + buffer.resize(read_amount + to_read); + memcpy(buffer.data() + read_amount, code_buff->first, to_read); + read_amount += to_read; + } + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadIcon(std::vector& buffer) { + if (!cached_icon.empty()) { + buffer = cached_icon; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_ReadIcon"); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto icon_buf = resp->GetResponseBuffer(0); + if (!icon_buf.has_value()) + return ResultStatus::ErrorArtic; + + cached_icon.resize(icon_buf->second); + memcpy(cached_icon.data(), icon_buf->first, icon_buf->second); + buffer = cached_icon; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadBanner(std::vector& buffer) { + if (!cached_banner.empty()) { + buffer = cached_banner; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_ReadBanner"); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto banner_buf = resp->GetResponseBuffer(0); + if (!banner_buf.has_value()) + return ResultStatus::ErrorArtic; + + cached_banner.resize(banner_buf->second); + memcpy(cached_banner.data(), banner_buf->first, banner_buf->second); + buffer = cached_banner; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadLogo(std::vector& buffer) { + if (!cached_logo.empty()) { + buffer = cached_logo; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_ReadLogo"); + auto resp = client->Send(req); + if (!resp.has_value() || !resp->Succeeded() || resp->GetMethodResult() != 0) + return ResultStatus::ErrorArtic; + + auto logo_buf = resp->GetResponseBuffer(0); + if (!logo_buf.has_value()) + return ResultStatus::ErrorArtic; + + cached_logo.resize(logo_buf->second); + memcpy(cached_logo.data(), logo_buf->first, logo_buf->second); + buffer = cached_logo; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadProgramId(u64& out_program_id) { + if (cached_title_id.has_value()) { + out_program_id = *cached_title_id; + return ResultStatus::Success; + } + + if (!client_connected) + client_connected = client->Connect(); + if (!client_connected) + return ResultStatus::ErrorArtic; + + auto req = client->NewRequest("Process_GetTitleID"); + auto resp = client->Send(req); + if (!resp.has_value()) + return ResultStatus::ErrorArtic; + + auto tid_buf = resp->GetResponseBuffer(0); + if (!tid_buf.has_value() || tid_buf->second != sizeof(u64)) + return ResultStatus::ErrorArtic; + + out_program_id = *reinterpret_cast(tid_buf->first); + cached_title_id = out_program_id; + + return ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadExtdataId(u64& out_extdata_id) { + if (program_exheader.arm11_system_local_caps.storage_info.other_attributes >> 1) { + // Using extended save data access + // There would be multiple possible extdata IDs in this case. The best we can do for now is + // guessing that the first one would be the main save. + const std::array extdata_ids{{ + program_exheader.arm11_system_local_caps.storage_info.extdata_id0.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id1.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id2.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id3.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id4.Value(), + program_exheader.arm11_system_local_caps.storage_info.extdata_id5.Value(), + }}; + for (u64 id : extdata_ids) { + if (id) { + // Found a non-zero ID, use it + out_extdata_id = id; + return ResultStatus::Success; + } + } + + return ResultStatus::ErrorNotUsed; + } + + out_extdata_id = program_exheader.arm11_system_local_caps.storage_info.ext_save_data_id; + return Loader::ResultStatus::Success; +} + +ResultStatus Apploader_Artic::ReadRomFS(std::shared_ptr& romfs_file) { + main_romfs_reader = romfs_file = std::make_shared(client, false); + return static_cast(romfs_file.get())->OpenStatus(); +} + +ResultStatus Apploader_Artic::ReadUpdateRomFS(std::shared_ptr& romfs_file) { + update_romfs_reader = romfs_file = std::make_shared(client, true); + return static_cast(romfs_file.get())->OpenStatus(); +} + +ResultStatus Apploader_Artic::DumpRomFS(const std::string& target_path) { + return ResultStatus::ErrorNotImplemented; +} + +ResultStatus Apploader_Artic::DumpUpdateRomFS(const std::string& target_path) { + return ResultStatus::ErrorNotImplemented; +} + +ResultStatus Apploader_Artic::ReadTitle(std::string& title) { + std::vector data; + Loader::SMDH smdh; + ResultStatus result = ReadIcon(data); + if (result != ResultStatus::Success) { + return result; + } + + if (!Loader::IsValidSMDH(data)) { + return ResultStatus::ErrorInvalidFormat; + } + + std::memcpy(&smdh, data.data(), sizeof(Loader::SMDH)); + + const auto& short_title = smdh.GetShortTitle(SMDH::TitleLanguage::English); + auto title_end = std::find(short_title.begin(), short_title.end(), u'\0'); + title = Common::UTF16ToUTF8(std::u16string{short_title.begin(), title_end}); + + return ResultStatus::Success; +} + +} // namespace Loader diff --git a/src/core/loader/artic.h b/src/core/loader/artic.h new file mode 100644 index 000000000..477153f08 --- /dev/null +++ b/src/core/loader/artic.h @@ -0,0 +1,136 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/common_types.h" +#include "common/swap.h" +#include "core/core.h" +#include "core/file_sys/ncch_container.h" +#include "core/hle/service/fs/fs_user.h" +#include "core/loader/loader.h" +#include "network/artic_base/artic_base_client.h" + +namespace Loader { + +/// Loads an NCCH file (e.g. from a CCI, or the first NCCH in a CXI) +class Apploader_Artic final : public AppLoader { +public: + Apploader_Artic(Core::System& system_, const std::string& server_addr, u16 server_port) + : AppLoader(system_, FileUtil::IOFile()) { + client = std::make_shared(server_addr, server_port); + client->SetCommunicationErrorCallback([&system_](const std::string& msg) { + system_.SetStatus(Core::System::ResultStatus::ErrorArticDisconnected, + msg.empty() ? nullptr : msg.c_str()); + }); + client->SetArticReportTrafficCallback( + [&system_](u32 bytes) { system_.ReportArticTraffic(bytes); }); + client->SetReportArticEventCallback([&system_](u64 event) { + Core::PerfStats::PerfArticEventBits ev = + static_cast(event & 0xFFFFFFFF); + bool set = (event > 32) != 0; + system_.ReportPerfArticEvent(ev, set); + }); + } + + ~Apploader_Artic() override; + + /** + * Returns the type of the file + * @param file FileUtil::IOFile open file + * @return FileType found, or FileType::Error if this loader doesn't know it + */ + static FileType IdentifyType(FileUtil::IOFile& file); + + FileType GetFileType() override { + return IdentifyType(file); + } + + [[nodiscard]] std::span GetPreferredRegions() const override { + return preferred_regions; + } + + ResultStatus Load(std::shared_ptr& process) override; + + std::pair, ResultStatus> LoadCoreVersion() override; + + /** + * Loads the Exheader and returns the system mode for this application. + * @returns A pair with the optional system mode, and and the status. + */ + std::pair, ResultStatus> LoadKernelMemoryMode() override; + + std::pair, ResultStatus> LoadNew3dsHwCapabilities() + override; + + ResultStatus IsExecutable(bool& out_executable) override; + + ResultStatus ReadCode(std::vector& buffer) override; + + ResultStatus ReadIcon(std::vector& buffer) override; + + ResultStatus ReadBanner(std::vector& buffer) override; + + ResultStatus ReadLogo(std::vector& buffer) override; + + ResultStatus ReadProgramId(u64& out_program_id) override; + + ResultStatus ReadExtdataId(u64& out_extdata_id) override; + + ResultStatus ReadRomFS(std::shared_ptr& romfs_file) override; + + ResultStatus ReadUpdateRomFS(std::shared_ptr& romfs_file) override; + + ResultStatus DumpRomFS(const std::string& target_path) override; + + ResultStatus DumpUpdateRomFS(const std::string& target_path) override; + + ResultStatus ReadTitle(std::string& title) override; + + bool SupportsSaveStates() override { + return false; + } + + bool SupportsMultipleInstancesForSameFile() override { + return false; + } + +private: + /** + * Loads .code section into memory for booting + * @param process The newly created process + * @return ResultStatus result of function + */ + ResultStatus LoadExec(std::shared_ptr& process); + + /// Reads the region lockout info in the SMDH and send it to CFG service + /// If an SMDH is not present, the program ID is compared against a list + /// of known system titles to determine the region. + void ParseRegionLockoutInfo(u64 program_id); + + bool LoadExheader(); + + ResultStatus LoadProductInfo(Service::FS::FS_USER::ProductInfo& out); + + ExHeader_Header program_exheader{}; + bool program_exheader_loaded = false; + + std::optional cached_title_id = std::nullopt; + std::optional cached_product_info = std::nullopt; + std::vector cached_icon; + std::vector cached_banner; + std::vector cached_logo; + + std::vector preferred_regions; + + std::string server_address; + std::shared_ptr client; + bool client_connected = false; + + std::shared_ptr main_romfs_reader = nullptr; + std::shared_ptr update_romfs_reader = nullptr; +}; + +} // namespace Loader diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index b282d0a24..f520e4f00 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -9,6 +9,7 @@ #include "core/core.h" #include "core/hle/kernel/process.h" #include "core/loader/3dsx.h" +#include "core/loader/artic.h" #include "core/loader/elf.h" #include "core/loader/ncch.h" @@ -74,6 +75,8 @@ const char* GetFileTypeString(FileType type) { return "ELF"; case FileType::THREEDSX: return "3DSX"; + case FileType::ARTIC: + return "ARTIC"; case FileType::Error: case FileType::Unknown: break; @@ -108,12 +111,39 @@ static std::unique_ptr GetFileLoader(Core::System& system, FileUtil:: case FileType::CCI: return std::make_unique(system, std::move(file), filepath); + case FileType::ARTIC: { + auto strToUInt = [](const std::string& str) -> int { + char* pEnd = NULL; + unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10); + if (*pEnd) + return -1; + return static_cast(ul); + }; + + u16 port = 5543; + std::string server_addr = filename; + auto pos = server_addr.find(":"); + if (pos != server_addr.npos) { + int newVal = strToUInt(server_addr.substr(pos + 1)); + if (newVal >= 0 && newVal <= 0xFFFF) { + port = static_cast(newVal); + server_addr = server_addr.substr(0, pos); + } + } + return std::make_unique(system, server_addr, port); + } + default: return nullptr; } } std::unique_ptr GetLoader(const std::string& filename) { + if (filename.starts_with("articbase://")) { + return GetFileLoader(Core::System::GetInstance(), FileUtil::IOFile(), FileType::ARTIC, + filename.substr(12), ""); + } + FileUtil::IOFile file(filename, "rb"); if (!file.IsOpen()) { LOG_ERROR(Loader, "Failed to load file {}", filename); diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 737075249..a47d5ae1d 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -31,6 +31,7 @@ enum class FileType { CIA, ELF, THREEDSX, // 3DSX + ARTIC, }; /** @@ -73,6 +74,7 @@ enum class ResultStatus { ErrorMemoryAllocationFailed, ErrorEncrypted, ErrorGbaTitle, + ErrorArtic, }; constexpr u32 MakeMagic(char a, char b, char c, char d) { @@ -264,6 +266,14 @@ public: return ResultStatus::ErrorNotImplemented; } + virtual bool SupportsSaveStates() { + return true; + } + + virtual bool SupportsMultipleInstancesForSameFile() { + return true; + } + protected: Core::System& system; FileUtil::IOFile file; diff --git a/src/core/movie.cpp b/src/core/movie.cpp index c7dd23c90..ee6bf3577 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "common/archives.h" #include "common/bit_field.h" #include "common/file_util.h" diff --git a/src/core/perf_stats.cpp b/src/core/perf_stats.cpp index 0d30a9187..c05cf80e5 100644 --- a/src/core/perf_stats.cpp +++ b/src/core/perf_stats.cpp @@ -101,6 +101,8 @@ PerfStats::Results PerfStats::GetAndResetStats(microseconds current_system_time_ last_stats.frametime = duration_cast(accumulated_frametime).count() / static_cast(system_frames); last_stats.emulation_speed = system_us_per_second.count() / 1'000'000.0; + last_stats.artic_transmitted = static_cast(artic_transmitted) / interval; + last_stats.artic_events.raw = artic_events.raw | prev_artic_event.raw; // Reset counters reset_point = now; @@ -108,6 +110,8 @@ PerfStats::Results PerfStats::GetAndResetStats(microseconds current_system_time_ accumulated_frametime = Clock::duration::zero(); system_frames = 0; game_frames = 0; + artic_transmitted = 0; + prev_artic_event.raw &= artic_events.raw; return last_stats; } diff --git a/src/core/perf_stats.h b/src/core/perf_stats.h index d2451dbc8..d68ff4dda 100644 --- a/src/core/perf_stats.h +++ b/src/core/perf_stats.h @@ -9,6 +9,7 @@ #include #include #include +#include "common/bit_field.h" #include "common/common_types.h" #include "common/thread.h" @@ -25,6 +26,29 @@ public: using Clock = std::chrono::high_resolution_clock; + enum class PerfArticEventBits { + NONE = 0, + ARTIC_SAVE_DATA = (1 << 0), + ARTIC_EXT_DATA = (1 << 1), + ARTIC_BOSS_EXT_DATA = (1 << 2), + ARTIC_SHARED_EXT_DATA = (1 << 3), + ARTIC_SYSTEM_SAVE_DATA = (1 << 4), + }; + union PerfArticEvents { + u32 raw{}; + BitField<0, 1, u32> artic_save_data; + BitField<1, 1, u32> artic_ext_data; + BitField<2, 1, u32> artic_boss_ext_data; + BitField<3, 1, u32> artic_shared_ext_data; + + void Set(PerfArticEventBits event, bool set) { + raw = (raw & ~static_cast(event)) | (set ? static_cast(event) : 0); + } + bool Get(PerfArticEventBits event) { + return (raw & static_cast(event)) != 0; + } + }; + struct Results { /// System FPS (LCD VBlanks) in Hz double system_fps; @@ -34,6 +58,10 @@ public: double frametime; /// Ratio of walltime / emulated time elapsed double emulation_speed; + /// Artic base bytes per second + double artic_transmitted = 0; + /// Artic base events + PerfArticEvents artic_events{}; }; void BeginSystemFrame(); @@ -55,6 +83,19 @@ public: */ double GetLastFrameTimeScale() const; + void AddArticBaseTraffic(u32 bytes) { + artic_transmitted += bytes; + } + + void ReportPerfArticEvent(PerfArticEventBits event, bool set) { + if (set) { + artic_events.Set(event, set); + prev_artic_event.Set(event, set); + } else { + artic_events.Set(event, set); + } + } + private: mutable std::mutex object_mutex; @@ -77,6 +118,12 @@ private: u32 system_frames = 0; /// Cumulative number of game frames (GSP frame submissions) since last reset u32 game_frames = 0; + /// Cumulative number of transmitted artic base traffic + std::atomic artic_transmitted = 0; + // System events that affect performance + PerfArticEvents artic_events; + + PerfArticEvents prev_artic_event; /// Point when the previous system frame ended Clock::time_point previous_frame_end = reset_point; diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp index bc1f53684..3390af273 100644 --- a/src/core/savestate.cpp +++ b/src/core/savestate.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include "common/archives.h" #include "common/file_util.h" #include "common/logging/log.h" @@ -13,6 +13,7 @@ #include "common/swap.h" #include "common/zstd_compression.h" #include "core/core.h" +#include "core/loader/loader.h" #include "core/movie.h" #include "core/savestate.h" #include "core/savestate_data.h" @@ -89,7 +90,7 @@ static bool ValidateSaveState(const CSTHeader& header, SaveStateInfo& info, u64 std::vector ListSaveStates(u64 program_id, u64 movie_id) { std::vector result; result.reserve(SaveStateSlotCount); - for (u32 slot = 1; slot <= SaveStateSlotCount; ++slot) { + for (u32 slot = 0; slot <= SaveStateSlotCount; ++slot) { const auto path = GetSaveStatePath(program_id, movie_id, slot); if (!FileUtil::Exists(path)) { continue; @@ -122,6 +123,12 @@ std::vector ListSaveStates(u64 program_id, u64 movie_id) { } void System::SaveState(u32 slot) const { + if (app_loader) { + if (!app_loader->SupportsSaveStates()) { + throw std::runtime_error("The current app loader doesn't support save states"); + } + } + std::ostringstream sstream{std::ios_base::binary}; // Serialize oarchive oa{sstream}; @@ -164,6 +171,11 @@ void System::SaveState(u32 slot) const { } void System::LoadState(u32 slot) { + if (app_loader) { + if (!app_loader->SupportsSaveStates()) { + throw std::runtime_error("The current app loader doesn't support save states"); + } + } if (Network::GetRoomMember().lock()->IsConnected()) { throw std::runtime_error("Unable to load while connected to multiplayer"); } diff --git a/src/core/savestate.h b/src/core/savestate.h index 2962cce86..5232e98c0 100644 --- a/src/core/savestate.h +++ b/src/core/savestate.h @@ -20,7 +20,7 @@ struct SaveStateInfo { std::string build_name; }; -constexpr u32 SaveStateSlotCount = 10; // Maximum count of savestate slots +constexpr u32 SaveStateSlotCount = 11; // Maximum count of savestate slots std::vector ListSaveStates(u64 program_id, u64 movie_id); diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index d8058dfd2..3feb6f479 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -1,6 +1,9 @@ add_library(network STATIC announce_multiplayer_session.cpp announce_multiplayer_session.h + artic_base/artic_base_client.cpp + artic_base/artic_base_client.h + artic_base/artic_base_common.h network.cpp network.h network_settings.cpp @@ -12,6 +15,8 @@ add_library(network STATIC room.h room_member.cpp room_member.h + socket_manager.cpp + socket_manager.h verify_user.cpp verify_user.h ) diff --git a/src/network/artic_base/artic_base_client.cpp b/src/network/artic_base/artic_base_client.cpp new file mode 100644 index 000000000..edc587f35 --- /dev/null +++ b/src/network/artic_base/artic_base_client.cpp @@ -0,0 +1,839 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "artic_base_client.h" +#include "common/assert.h" +#include "common/logging/log.h" + +#include "chrono" +#include "limits.h" +#include "memory" +#include "sstream" + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifdef _WIN32 +#define WSAEAGAIN WSAEWOULDBLOCK +#define WSAEMULTIHOP -1 // Invalid dummy value +#define ERRNO(x) WSA##x +#define GET_ERRNO WSAGetLastError() +#define poll(x, y, z) WSAPoll(x, y, z); +#define SHUT_RD SD_RECEIVE +#define SHUT_WR SD_SEND +#define SHUT_RDWR SD_BOTH +#else +#define ERRNO(x) x +#define GET_ERRNO errno +#define closesocket(x) close(x) +#endif + +// #define DISABLE_PING_TIMEOUT + +namespace Network::ArticBase { + +using namespace std::chrono_literals; + +bool Client::Request::AddParameterS8(s8 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_8; + std::memcpy(param.data, ¶meter, sizeof(s8)); + return true; +} + +bool Client::Request::AddParameterS16(s16 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_16; + std::memcpy(param.data, ¶meter, sizeof(s16)); + return true; +} + +bool Client::Request::AddParameterS32(s32 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_32; + std::memcpy(param.data, ¶meter, sizeof(s32)); + return true; +} + +bool Client::Request::AddParameterS64(s64 parameter) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + param.type = ArticBaseCommon::RequestParameterType::IN_INTEGER_64; + std::memcpy(param.data, ¶meter, sizeof(s64)); + return true; +} + +bool Client::Request::AddParameterBuffer(const void* buffer, size_t size) { + if (parameters.size() >= max_param_count) { + LOG_ERROR(Network, "Too many parameters added to method: {}", method_name); + return false; + } + auto& param = parameters.emplace_back(); + if (size <= sizeof(param.data)) { + param.type = ArticBaseCommon::RequestParameterType::IN_SMALL_BUFFER; + std::memcpy(param.data, buffer, size); + param.parameterSize = static_cast(size); + } else { + param.type = ArticBaseCommon::RequestParameterType::IN_BIG_BUFFER; + param.bigBufferID = static_cast(pending_big_buffers.size()); + s32 size_32 = static_cast(size); + std::memcpy(param.data, &size_32, sizeof(size_32)); + pending_big_buffers.push_back(std::make_pair(buffer, size)); + } + return true; +} + +Client::Request::Request(u32 request_id, const std::string& method, size_t max_params) { + method_name = method; + max_param_count = max_params; + request_packet.requestID = request_id; + std::memcpy(request_packet.method.data(), method.data(), + std::min(request_packet.method.size(), method.size())); +} + +void Client::UDPStream::Start() { + thread_run = true; + handle_thread = std::thread(&Client::UDPStream::Handle, this); +} + +void Client::UDPStream::Handle() { + struct sockaddr_in* servaddr = reinterpret_cast(serv_sockaddr_in.data()); + socklen_t serv_sockaddr_len = static_cast(serv_sockaddr_in.size()); + memcpy(servaddr, client.GetServerAddr().data(), client.GetServerAddr().size()); + servaddr->sin_port = htons(port); + + main_socket = ::socket(AF_INET, SOCK_DGRAM, 0); + if (main_socket == static_cast(-1) || !thread_run) { + LOG_ERROR(Network, "Failed to create socket"); + return; + } + + if (!SetNonBlock(main_socket, true) || !thread_run) { + closesocket(main_socket); + LOG_ERROR(Network, "Cannot set non-blocking socket mode"); + return; + } + + // Limit receive buffer so that packets don't get qeued and are dropped instead. + int buffer_size_int = static_cast(buffer_size); + if (::setsockopt(main_socket, SOL_SOCKET, SO_RCVBUF, reinterpret_cast(&buffer_size_int), + sizeof(buffer_size_int)) || + !thread_run) { + closesocket(main_socket); + LOG_ERROR(Network, "Cannot change receive buffer size"); + return; + } + + // Send data to server so that it knows client address. + char zero = '\0'; + int send_res = + ::sendto(main_socket, &zero, sizeof(char), 0, + reinterpret_cast(serv_sockaddr_in.data()), serv_sockaddr_len); + if (send_res < 0 || !thread_run) { + closesocket(main_socket); + LOG_ERROR(Network, "Cannot send data to socket"); + return; + } + + ready = true; + std::vector buffer(buffer_size); + while (thread_run) { + std::chrono::steady_clock::time_point before = std::chrono::steady_clock::now(); + + int packet_size = ::recvfrom( + main_socket, reinterpret_cast(buffer.data()), static_cast(buffer.size()), 0, + reinterpret_cast(serv_sockaddr_in.data()), &serv_sockaddr_len); + if (packet_size > 0) { + if (client.report_traffic_callback) { + client.report_traffic_callback(packet_size); + } + + buffer.resize(packet_size); + { + std::scoped_lock l(current_buffer_mutex); + current_buffer = buffer; + } + } + + auto elapsed = std::chrono::steady_clock::now() - before; + + std::unique_lock lk(thread_cv_mutex); + thread_cv.wait_for(lk, elapsed < read_interval ? (read_interval - elapsed) + : std::chrono::microseconds(50)); + } + ready = false; + + closesocket(main_socket); +} + +Client::~Client() { + StopImpl(false); + + for (auto it = handlers.begin(); it != handlers.end(); it++) { + Handler* h = *it; + h->thread->join(); + delete h; + } + + if (ping_thread.joinable()) { + ping_thread.join(); + } + + SocketManager::DisableSockets(); +} + +bool Client::Connect() { + if (connected) + return true; + + auto str_to_int = [](const std::string& str) -> int { + char* pEnd = NULL; + unsigned long ul = ::strtoul(str.c_str(), &pEnd, 10); + if (*pEnd) + return -1; + return static_cast(ul); + }; + + struct addrinfo hints, *addrinfo; + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_INET; + + LOG_INFO(Network, "Starting Artic Base Client"); + + if (getaddrinfo(address.data(), NULL, &hints, &addrinfo) != 0) { + LOG_ERROR(Network, "Failed to get server address"); + SignalCommunicationError(); + return false; + } + + main_socket = ::socket(AF_INET, SOCK_STREAM, 0); + if (main_socket == static_cast(-1)) { + LOG_ERROR(Network, "Failed to create socket"); + SignalCommunicationError(); + return false; + } + + if (!SetNonBlock(main_socket, true)) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Cannot set non-blocking socket mode"); + SignalCommunicationError(); + return false; + } + + struct sockaddr_in servaddr = {0}; + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = ((struct sockaddr_in*)(addrinfo->ai_addr))->sin_addr.s_addr; + servaddr.sin_port = htons(port); + freeaddrinfo(addrinfo); + memcpy(last_sockaddr_in.data(), &servaddr, last_sockaddr_in.size()); + + if (!ConnectWithTimeout(main_socket, &servaddr, sizeof(servaddr), 10)) { + closesocket(main_socket); + LOG_ERROR(Network, "Failed to connect"); + SignalCommunicationError(); + return false; + } + + auto version = SendSimpleRequest("VERSION"); + if (version.has_value()) { + int version_value = str_to_int(*version); + if (version_value != SERVER_VERSION) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Incompatible server version: {}", version_value); + SignalCommunicationError("\nIncompatible Artic Base Server version.\nCheck for updates " + "to Artic Base Server or Citra."); + return false; + } + } else { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server version."); + SignalCommunicationError(); + return false; + } + + auto max_work_size = SendSimpleRequest("MAXSIZE"); + int max_work_size_value = -1; + if (max_work_size.has_value()) { + max_work_size_value = str_to_int(*max_work_size); + } + if (max_work_size_value < 0) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server work ram size"); + SignalCommunicationError(); + return false; + } + max_server_work_ram = max_work_size_value; + + auto max_params = SendSimpleRequest("MAXPARAM"); + int max_param_value = -1; + if (max_params.has_value()) { + max_param_value = str_to_int(*max_params); + } + if (max_param_value < 0) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server max params"); + SignalCommunicationError(); + return false; + } + max_parameter_count = max_param_value; + + auto worker_ports = SendSimpleRequest("PORTS"); + if (!worker_ports.has_value()) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server worker ports"); + SignalCommunicationError(); + return false; + } + std::vector ports; + std::string str_port; + std::stringstream ss_port(worker_ports.value()); + while (std::getline(ss_port, str_port, ',')) { + int port_curr = str_to_int(str_port); + if (port_curr < 0 || port_curr > static_cast(USHRT_MAX)) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't parse server worker ports"); + SignalCommunicationError(); + return false; + } + ports.push_back(static_cast(port_curr)); + } + if (ports.empty()) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't parse server worker ports"); + SignalCommunicationError(); + return false; + } + + for (int i = 0; i < 101; i++) { + auto ready_server = SendSimpleRequest("READY"); + if (!ready_server.has_value() || i == 100) { + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); + LOG_ERROR(Network, "Couldn't fetch server readiness"); + SignalCommunicationError(); + return false; + } + if (*ready_server == "1") + break; + std::this_thread::sleep_for(100ms); + } + + ping_thread = std::thread(&Client::PingFunction, this); + + int i = 0; + running_handlers = ports.size(); + for (auto it = ports.begin(); it != ports.end(); it++) { + handlers.push_back(new Handler(*this, static_cast(servaddr.sin_addr.s_addr), *it, i)); + i++; + } + + connected = true; + return true; +} + +std::shared_ptr Client::NewUDPStream( + const std::string stream_id, size_t buffer_size, + const std::chrono::milliseconds& read_interval) { + + auto req = NewRequest("#" + stream_id); + + auto resp = Send(req); + + if (!resp.has_value()) { + return nullptr; + } + + auto port_udp = resp->GetResponseS32(0); + if (!port_udp.has_value()) { + return nullptr; + } + + udp_streams.push_back(std::make_shared(*this, static_cast(*port_udp), + buffer_size, read_interval)); + + return udp_streams.back(); +} + +void Client::StopImpl(bool from_error) { + bool expected = false; + if (!stopped.compare_exchange_strong(expected, true)) + return; + + if (!from_error) { + SendSimpleRequest("STOP"); + } + + for (auto it = udp_streams.begin(); it != udp_streams.end(); it++) { + it->get()->Stop(); + } + + if (ping_thread.joinable()) { + std::scoped_lock l2(ping_cv_mutex); + ping_run = false; + ping_cv.notify_one(); + } + + // Stop handlers + for (auto it = handlers.begin(); it != handlers.end(); it++) { + Handler* handler = *it; + handler->should_run = false; + // Shouldn't matter if the socket is shut down twice + shutdown(handler->handler_socket, SHUT_RDWR); + closesocket(handler->handler_socket); + } + + // Close main socket + shutdown(main_socket, SHUT_RDWR); + closesocket(main_socket); +} + +std::optional> Client::Response::GetResponseBuffer(u32 buffer_id) const { + if (!resp_data_buffer) + return std::nullopt; + + char* resp_data_buffer_end = resp_data_buffer + resp_data_size; + char* resp_data_buffer_start = resp_data_buffer; + while (resp_data_buffer_start + sizeof(ArticBaseCommon::Buffer) < resp_data_buffer_end) { + ArticBaseCommon::Buffer* curr_buffer = + reinterpret_cast(resp_data_buffer_start); + resp_data_buffer_start += sizeof(ArticBaseCommon::Buffer); + if (curr_buffer->bufferID == buffer_id) { + if (curr_buffer->data + curr_buffer->bufferSize <= resp_data_buffer_end) { + return std::make_pair(curr_buffer->data, curr_buffer->bufferSize); + } else { + return std::nullopt; + } + } + resp_data_buffer_start += curr_buffer->bufferSize; + } + return std::nullopt; +} + +std::optional Client::Send(Request& request) { + if (stopped) + return std::nullopt; + + request.request_packet.parameterCount = static_cast(request.parameters.size()); + PendingResponse resp(request); + + { + std::scoped_lock l(recv_map_mutex); + pending_responses[request.request_packet.requestID] = &resp; + } + + auto respPacket = SendRequestPacket(request.request_packet, false, request.parameters); + if (stopped || !respPacket.has_value()) { + std::scoped_lock l(recv_map_mutex); + pending_responses.erase(request.request_packet.requestID); + return std::nullopt; + } + + std::unique_lock cv_lk(resp.cv_mutex); + resp.cv.wait(cv_lk, [&resp]() { return resp.is_done; }); + + return std::optional(std::move(resp.response)); +} + +void Client::SignalCommunicationError(const std::string& msg) { + StopImpl(true); + LOG_CRITICAL(Network, "Communication error"); + if (communication_error_callback) + communication_error_callback(msg); +} + +void Client::PingFunction() { + // Max silence time => 7 secs interval + 3 secs wait + 10 seconds timeout = 25 seconds + while (ping_run) { + std::chrono::time_point last = last_sent_request; + if (std::chrono::steady_clock::now() - last > std::chrono::seconds(7)) { +#ifdef DISABLE_PING_TIMEOUT + client->last_sent_request = std::chrono::steady_clock::now(); +#else + auto ping_reply = SendSimpleRequest("PING"); + if (!ping_reply.has_value()) { + SignalCommunicationError(); + break; + } +#endif // DISABLE_PING_TIMEOUT + } + + std::unique_lock lk(ping_cv_mutex); + ping_cv.wait_for(lk, std::chrono::seconds(3)); + } +} + +bool Client::ConnectWithTimeout(SocketHolder sockFD, void* server_addr, size_t server_addr_len, + int timeout_seconds) { + + int res = ::connect(sockFD, (struct sockaddr*)server_addr, static_cast(server_addr_len)); + if (res == -1 && ((GET_ERRNO == ERRNO(EINPROGRESS) || GET_ERRNO == ERRNO(EWOULDBLOCK)))) { + struct timeval tv; + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(sockFD, &fdset); + + tv.tv_sec = timeout_seconds; + tv.tv_usec = 0; + int select_res = ::select(static_cast(sockFD + 1), NULL, &fdset, NULL, &tv); +#ifdef _WIN32 + if (select_res == 0) { + return false; + } +#else + bool select_good = false; + if (select_res == 1) { + int so_error; + socklen_t len = sizeof so_error; + + getsockopt(sockFD, SOL_SOCKET, SO_ERROR, &so_error, &len); + + if (so_error == 0) { + select_good = true; + } + } + if (!select_good) { + return false; + } +#endif // _WIN32 + + } else if (res == -1) { + return false; + } + return true; +} + +bool Client::SetNonBlock(SocketHolder sockFD, bool nonBlocking) { + bool blocking = !nonBlocking; +#ifdef _WIN32 + unsigned long nonblocking = (blocking) ? 0 : 1; + int ret = ioctlsocket(sockFD, FIONBIO, &nonblocking); + if (ret == -1) { + return false; + } +#else + int flags = ::fcntl(sockFD, F_GETFL, 0); + if (flags == -1) { + return false; + } + + flags &= ~O_NONBLOCK; + if (!blocking) { // O_NONBLOCK + flags |= O_NONBLOCK; + } + + const int ret = ::fcntl(sockFD, F_SETFL, flags); + if (ret == -1) { + return false; + } +#endif + return true; +} + +bool Client::Read(SocketHolder sockFD, void* buffer, size_t size, + const std::chrono::nanoseconds& timeout) { + size_t read_bytes = 0; + auto before = std::chrono::steady_clock::now(); + while (read_bytes != size) { + int new_read = + ::recv(sockFD, (char*)((uintptr_t)buffer + read_bytes), (int)(size - read_bytes), 0); + if (new_read < 0) { + if (GET_ERRNO == ERRNO(EWOULDBLOCK) && + (timeout == std::chrono::nanoseconds(0) || + std::chrono::steady_clock::now() - before < timeout)) { + continue; + } + read_bytes = 0; + break; + } + if (report_traffic_callback && new_read) { + report_traffic_callback(new_read); + } + read_bytes += new_read; + } + return read_bytes == size; +} + +bool Client::Write(SocketHolder sockFD, const void* buffer, size_t size, + const std::chrono::nanoseconds& timeout) { + size_t write_bytes = 0; + auto before = std::chrono::steady_clock::now(); + while (write_bytes != size) { + int new_written = ::send(sockFD, (const char*)((uintptr_t)buffer + write_bytes), + (int)(size - write_bytes), 0); + if (new_written < 0) { + if (GET_ERRNO == ERRNO(EWOULDBLOCK) && + (timeout == std::chrono::nanoseconds(0) || + std::chrono::steady_clock::now() - before < timeout)) { + continue; + } + write_bytes = 0; + break; + } + if (report_traffic_callback && new_written) { + report_traffic_callback(new_written); + } + write_bytes += new_written; + } + return write_bytes == size; +} + +std::optional Client::SendRequestPacket( + const ArticBaseCommon::RequestPacket& req, bool expect_response, + const std::vector& params, + const std::chrono::nanoseconds& read_timeout) { + std::scoped_lock l(send_mutex); + + if (main_socket == static_cast(-1)) { + return std::nullopt; + } + + if (!Write(main_socket, &req, sizeof(req))) { + LOG_WARNING(Network, "Failed to write to socket"); + SignalCommunicationError(); + return std::nullopt; + } + + if (!params.empty()) { + if (!Write(main_socket, params.data(), + params.size() * sizeof(ArticBaseCommon::RequestParameter))) { + LOG_WARNING(Network, "Failed to write to socket"); + SignalCommunicationError(); + return std::nullopt; + } + } + + ArticBaseCommon::DataPacket resp; + if (expect_response) { + if (!Read(main_socket, &resp, sizeof(resp), read_timeout)) { + LOG_WARNING(Network, "Failed to read from socket"); + SignalCommunicationError(); + return std::nullopt; + } + + if (resp.requestID != req.requestID) { + return std::nullopt; + } + } + + last_sent_request = std::chrono::steady_clock::now(); + return resp; +} + +std::optional Client::SendSimpleRequest(const std::string& method) { + ArticBaseCommon::RequestPacket req{}; + req.requestID = GetNextRequestID(); + const std::string final_method = "$" + method; + if (final_method.size() > sizeof(req.method)) { + return std::nullopt; + } + std::memcpy(req.method.data(), final_method.data(), final_method.size()); + auto resp = SendRequestPacket(req, true, {}, std::chrono::seconds(10)); + if (!resp.has_value() || resp->requestID != req.requestID) { + return std::nullopt; + } + char respBody[sizeof(ArticBaseCommon::DataPacket::dataRaw) + 1] = {0}; + std::memcpy(respBody, resp->dataRaw, sizeof(ArticBaseCommon::DataPacket::dataRaw)); + return respBody; +} + +Client::Handler::Handler(Client& _client, u32 _addr, u16 _port, int _id) + : id(_id), client(_client), addr(_addr), port(_port) { + thread = new std::thread( + [](Handler* handler) { + handler->RunLoop(); + handler->should_run = false; + if (--handler->client.running_handlers == 0) { + handler->client.OnAllHandlersFinished(); + } + }, + this); +} + +void Client::Handler::RunLoop() { + handler_socket = ::socket(AF_INET, SOCK_STREAM, 0); + if (handler_socket == static_cast(-1)) { + LOG_ERROR(Network, "Failed to create socket"); + return; + } + + if (!SetNonBlock(handler_socket, true)) { + closesocket(handler_socket); + client.SignalCommunicationError(); + LOG_ERROR(Network, "Cannot set non-blocking socket mode"); + return; + } + + struct sockaddr_in servaddr = {0}; + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = static_cast(addr); + servaddr.sin_port = htons(port); + + if (!ConnectWithTimeout(handler_socket, &servaddr, sizeof(servaddr), 10)) { + closesocket(handler_socket); + LOG_ERROR(Network, "Failed to connect"); + client.SignalCommunicationError(); + return; + } + + const auto signal_error = [&] { + if (should_run) { + client.SignalCommunicationError(); + } + }; + + ArticBaseCommon::DataPacket dataPacket; + u32 retry_count = 0; + while (should_run) { + if (!client.Read(handler_socket, &dataPacket, sizeof(dataPacket))) { + if (should_run) { + LOG_WARNING(Network, "Failed to read from socket"); + std::this_thread::sleep_for(100ms); + if (++retry_count == 300) { + signal_error(); + break; + } + continue; + } else { + break; + } + } + retry_count = 0; + + PendingResponse* pending_response; + { + std::scoped_lock l(client.recv_map_mutex); + auto it = client.pending_responses.find(dataPacket.requestID); + if (it == client.pending_responses.end()) { + continue; + } + pending_response = it->second; + } + + switch (dataPacket.resp.articResult) { + case ArticBaseCommon::ResponseMethod::ArticResult::SUCCESS: { + pending_response->response.articResult = dataPacket.resp.articResult; + pending_response->response.methodResult = dataPacket.resp.methodResult; + if (dataPacket.resp.bufferSize) { + pending_response->response.resp_data_buffer = + reinterpret_cast(operator new(dataPacket.resp.bufferSize)); + ASSERT_MSG(pending_response->response.resp_data_buffer != nullptr, + "ArticBase Handler: Cannot allocate buffer"); + pending_response->response.resp_data_size = + static_cast(dataPacket.resp.bufferSize); + if (!client.Read(handler_socket, pending_response->response.resp_data_buffer, + dataPacket.resp.bufferSize)) { + signal_error(); + } + } + } break; + case ArticBaseCommon::ResponseMethod::ArticResult::METHOD_NOT_FOUND: { + LOG_ERROR(Network, "Method {} not found by server", + pending_response->request.method_name); + pending_response->response.articResult = dataPacket.resp.articResult; + } break; + + case ArticBaseCommon::ResponseMethod::ArticResult::PROVIDE_INPUT: { + size_t bufferID = static_cast(dataPacket.resp.provideInputBufferID); + if (bufferID >= pending_response->request.pending_big_buffers.size() || + pending_response->request.pending_big_buffers[bufferID].second != + static_cast(dataPacket.resp.bufferSize)) { + LOG_ERROR(Network, "Method {} incorrect big buffer state {}", + pending_response->request.method_name, bufferID); + dataPacket.resp.articResult = + ArticBaseCommon::ResponseMethod::ArticResult::METHOD_ERROR; + if (client.Write(handler_socket, &dataPacket, sizeof(dataPacket))) { + continue; + } else { + signal_error(); + } + } else { + auto& buffer = pending_response->request.pending_big_buffers[bufferID]; + if (client.Write(handler_socket, &dataPacket, sizeof(dataPacket))) { + if (client.Write(handler_socket, buffer.first, buffer.second)) { + continue; + } else { + signal_error(); + } + } else { + signal_error(); + } + } + } break; + case ArticBaseCommon::ResponseMethod::ArticResult::METHOD_ERROR: + default: { + LOG_ERROR(Network, "Method {} error {}", pending_response->request.method_name, + dataPacket.resp.methodResult); + pending_response->response.articResult = dataPacket.resp.articResult; + pending_response->response.methodState = + static_cast(dataPacket.resp.methodResult); + } break; + } + + { + std::scoped_lock l(client.recv_map_mutex); + client.pending_responses.erase(dataPacket.requestID); + } + + { + std::scoped_lock lk(pending_response->cv_mutex); + pending_response->is_done = true; + pending_response->cv.notify_one(); + } + } + should_run = false; + shutdown(handler_socket, SHUT_RDWR); + closesocket(handler_socket); +} + +void Client::OnAllHandlersFinished() { + // If no handlers are running, signal all pending requests so that + // they don't become stuck. + std::scoped_lock l(recv_map_mutex); + for (auto& [id, response] : pending_responses) { + std::scoped_lock l2(response->cv_mutex); + response->is_done = true; + response->cv.notify_one(); + } + pending_responses.clear(); +} + +} // namespace Network::ArticBase \ No newline at end of file diff --git a/src/network/artic_base/artic_base_client.h b/src/network/artic_base/artic_base_client.h new file mode 100644 index 000000000..040d50c9c --- /dev/null +++ b/src/network/artic_base/artic_base_client.h @@ -0,0 +1,363 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once +#include "condition_variable" +#include "cstring" +#include "functional" +#include "map" +#include "memory" +#include "mutex" +#include "optional" +#include "string" +#include "thread" +#include "utility" + +#include "artic_base_common.h" +#include "network/socket_manager.h" + +#ifdef _WIN32 +using SocketHolder = unsigned long long; +#else +using SocketHolder = int; +#endif // _WIN32 + +namespace Network::ArticBase { + +class Client { +public: + class Request { + public: + bool AddParameterS8(s8 parameter); + bool AddParameterU8(u8 parameter) { + return AddParameterS8(static_cast(parameter)); + } + bool AddParameterS16(s16 parameter); + bool AddParameterU16(u16 parameter) { + return AddParameterS16(static_cast(parameter)); + } + bool AddParameterS32(s32 parameter); + bool AddParameterU32(u32 parameter) { + return AddParameterS32(static_cast(parameter)); + } + bool AddParameterS64(s64 parameter); + bool AddParameterU64(u64 parameter) { + return AddParameterS64(static_cast(parameter)); + } + + // NOTE: Buffer pointer must remain alive until the response is received + bool AddParameterBuffer(const void* buffer, size_t bufferSize); + + private: + friend class Client; + Request(u32 request_id, const std::string& method, size_t max_params); + + ArticBaseCommon::RequestPacket request_packet{}; + std::vector parameters; + std::string method_name; + size_t max_param_count; + std::vector> pending_big_buffers; + }; + + class UDPStream { + public: + std::vector GetLastPacket() { + std::scoped_lock l(current_buffer_mutex); + return current_buffer; + } + + bool IsReady() { + return ready; + } + + void Start(); + void Stop() { + if (thread_run && handle_thread.joinable()) { + std::scoped_lock l2(thread_cv_mutex); + thread_run = false; + thread_cv.notify_one(); + } + } + + UDPStream(Client& _client, u16 _port, size_t _buffer_size, + const std::chrono::milliseconds& _read_interval) + : client(_client), port(_port), buffer_size(_buffer_size), + read_interval(_read_interval) {} + + ~UDPStream() { + Stop(); + if (handle_thread.joinable()) { + handle_thread.join(); + } + } + + private: + void Handle(); + + Client& client; + u16 port; + size_t buffer_size; + std::chrono::milliseconds read_interval; + + std::array serv_sockaddr_in{}; + bool ready = false; + + std::mutex current_buffer_mutex; + std::vector current_buffer; + + SocketHolder main_socket = -1; + + std::thread handle_thread; + std::condition_variable thread_cv; + std::mutex thread_cv_mutex; + std::atomic thread_run = true; + }; + friend class UDPStream; + + Client(const std::string& _address, u16 _port) : address(_address), port(_port) { + SocketManager::EnableSockets(); + } + ~Client(); + + bool Connect(); + bool connected = false; + + size_t GetServerRequestMaxSize() { + return max_server_work_ram; + } + + Request NewRequest(const std::string& method) { + return Request(GetNextRequestID(), method, max_parameter_count); + } + + std::shared_ptr NewUDPStream( + const std::string stream_id, size_t buffer_size, + const std::chrono::milliseconds& read_interval = std::chrono::milliseconds(0)); + + void Stop() { + StopImpl(false); + } + + void SetCommunicationErrorCallback(const std::function& callback) { + communication_error_callback = callback; + } + + void SetArticReportTrafficCallback(const std::function& callback) { + report_traffic_callback = callback; + } + + void ReportArticEvent(u64 event) { + if (report_artic_event_callback) { + report_artic_event_callback(event); + } + } + void SetReportArticEventCallback(const std::function& callback) { + report_artic_event_callback = callback; + } + + // Returns the server address as a sockaddr_in struct + const std::array& GetServerAddr() { + return last_sockaddr_in; + } + +private: + static constexpr const int SERVER_VERSION = 2; + + std::string address; + u16 port; + std::array last_sockaddr_in; + + SocketHolder main_socket = -1; + std::atomic currRequestID; + u32 GetNextRequestID() { + return currRequestID++; + } + + void SignalCommunicationError(const std::string& msg = ""); + std::function communication_error_callback; + + std::function report_artic_event_callback; + + size_t max_server_work_ram = 0; + size_t max_parameter_count = 0; + std::mutex send_mutex; + + std::atomic stopped = false; + + std::atomic> last_sent_request; + std::thread ping_thread; + std::condition_variable ping_cv; + std::mutex ping_cv_mutex; + std::atomic ping_run = true; + + void StopImpl(bool from_error); + + void PingFunction(); + + static bool ConnectWithTimeout(SocketHolder sockFD, void* server_addr, size_t server_addr_len, + int timeout_seconds); + static bool SetNonBlock(SocketHolder sockFD, bool blocking); + bool Read(SocketHolder sockFD, void* buffer, size_t size, + const std::chrono::nanoseconds& timeout = std::chrono::nanoseconds(0)); + bool Write(SocketHolder sockFD, const void* buffer, size_t size, + const std::chrono::nanoseconds& timeout = std::chrono::nanoseconds(0)); + std::function report_traffic_callback; + + std::optional SendRequestPacket( + const ArticBaseCommon::RequestPacket& req, bool expect_response, + const std::vector& params, + const std::chrono::nanoseconds& read_timeout = std::chrono::nanoseconds(0)); + std::optional SendSimpleRequest(const std::string& method); + + std::vector> udp_streams; + + class Handler { + public: + Handler(Client& _client, u32 _addr, u16 _port, int _id); + ~Handler() { + delete thread; + } + void RunLoop(); + + int id = 0; + bool should_run = true; + SocketHolder handler_socket = -1; + std::thread* thread = nullptr; + + private: + Client& client; + u32 addr; + u16 port; + }; + + class PendingResponse; + +public: + class Response { + public: + Response() {} + Response(Response& other) + : articResult(other.articResult), methodResult(other.methodResult), + resp_data_size(other.resp_data_size) { + if (resp_data_size) { + resp_data_buffer = reinterpret_cast(operator new(resp_data_size)); + std::memcpy(resp_data_buffer, other.resp_data_buffer, resp_data_size); + } + } + Response(Response&& other) noexcept + : articResult(other.articResult), methodResult(other.methodResult), + resp_data_buffer(std::exchange(other.resp_data_buffer, nullptr)), + resp_data_size(other.resp_data_size) {} + + Response& operator=(Response& other) { + articResult = other.articResult; + methodResult = other.methodResult; + resp_data_size = other.resp_data_size; + if (resp_data_size) { + resp_data_buffer = reinterpret_cast(operator new(resp_data_size)); + std::memcpy(resp_data_buffer, other.resp_data_buffer, resp_data_size); + } + return *this; + } + + Response& operator=(Response&& other) noexcept { + articResult = other.articResult; + methodResult = other.methodResult; + resp_data_size = other.resp_data_size; + resp_data_buffer = std::exchange(other.resp_data_buffer, nullptr); + return *this; + } + + ~Response() { + if (resp_data_buffer) { + operator delete(resp_data_buffer); + } + } + + bool Succeeded() const { + return articResult == ArticBaseCommon::ResponseMethod::ArticResult::SUCCESS; + } + + int GetMethodResult() const { + return methodResult; + } + + std::optional> GetResponseBuffer(u32 buffer_id) const; + + std::optional GetResponseS32(u32 buffer_id) const { + auto buf = GetResponseBuffer(buffer_id); + if (!buf.has_value() || buf->second != sizeof(s32)) { + return std::nullopt; + } + return *reinterpret_cast(buf->first); + } + + std::optional GetResponseS64(u32 buffer_id) const { + auto buf = GetResponseBuffer(buffer_id); + if (!buf.has_value() || buf->second != sizeof(s64)) { + return std::nullopt; + } + return *reinterpret_cast(buf->first); + } + + std::optional GetResponseU64(u32 buffer_id) const { + auto buf = GetResponseBuffer(buffer_id); + if (!buf.has_value() || buf->second != sizeof(u64)) { + return std::nullopt; + } + return *reinterpret_cast(buf->first); + } + + std::optional GetResponseFloat(u32 buffer_id) const { + auto buf = GetResponseBuffer(buffer_id); + if (!buf.has_value() || buf->second != sizeof(float)) { + return std::nullopt; + } + return *reinterpret_cast(buf->first); + } + + private: + friend class Client; + friend class Client::Handler; + friend class PendingResponse; + + // Start in error state in case the request is not fullfilled properly. + ArticBaseCommon::ResponseMethod::ArticResult articResult = + ArticBaseCommon::ResponseMethod::ArticResult::METHOD_ERROR; + union { + ArticBaseCommon::MethodState methodState = + ArticBaseCommon::MethodState::INTERNAL_METHOD_ERROR; + int methodResult; + }; + char* resp_data_buffer{}; + size_t resp_data_size = 0; + }; + + std::optional Send(Request& request); + +private: + class PendingResponse { + public: + bool is_done = false; + + private: + friend class Client; + friend class Client::Handler; + PendingResponse(const Request& req) : request(req) {} + std::condition_variable cv; + std::mutex cv_mutex; + + const Request& request; + + Response response{}; + }; + + std::mutex recv_map_mutex; + std::map pending_responses; + + std::vector handlers; + std::atomic running_handlers; + void OnAllHandlersFinished(); +}; +} // namespace Network::ArticBase diff --git a/src/network/artic_base/artic_base_common.h b/src/network/artic_base/artic_base_common.h new file mode 100644 index 000000000..671da6e99 --- /dev/null +++ b/src/network/artic_base/artic_base_common.h @@ -0,0 +1,92 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once +#include +#include "common/common_types.h" + +namespace Network { +namespace ArticBaseCommon { +enum class MethodState : int { + PARSING_INPUT = 0, + PARAMETER_TYPE_MISMATCH = 1, + PARAMETER_COUNT_MISMATCH = 2, + BIG_BUFFER_READ_FAIL = 3, + BIG_BUFFER_WRITE_FAIL = 4, + OUT_OF_MEMORY = 5, + + GENERATING_OUTPUT = 6, + UNEXPECTED_PARSING_INPUT = 7, + OUT_OF_MEMORY_OUTPUT = 8, + + INTERNAL_METHOD_ERROR = 9, + FINISHED = 10, +}; +enum class RequestParameterType : u16 { + IN_INTEGER_8 = 0, + IN_INTEGER_16 = 1, + IN_INTEGER_32 = 2, + IN_INTEGER_64 = 3, + IN_SMALL_BUFFER = 4, + IN_BIG_BUFFER = 5, +}; +struct RequestParameter { + RequestParameterType type{}; + union { + u16 parameterSize{}; + u16 bigBufferID; + }; + + char data[0x1C]{}; +}; +struct RequestPacket { + u32 requestID{}; + std::array method{}; + u32 parameterCount{}; +}; +static_assert(sizeof(RequestPacket) == 0x28); + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4200) +#endif +struct Buffer { + u32 bufferID; + u32 bufferSize; + + char data[]; +}; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +struct ResponseMethod { + enum class ArticResult : u32 { + SUCCESS = 0, + METHOD_NOT_FOUND = 1, + METHOD_ERROR = 2, + PROVIDE_INPUT = 3, + }; + ArticResult articResult{}; + union { + int methodResult{}; + int provideInputBufferID; + }; + int bufferSize{}; + u8 padding[0x10]{}; +}; + +struct DataPacket { + DataPacket() {} + u32 requestID{}; + + union { + char dataRaw[0x1C]{}; + ResponseMethod resp; + }; +}; + +static_assert(sizeof(DataPacket) == 0x20); +}; // namespace ArticBaseCommon +} // namespace Network diff --git a/src/network/socket_manager.cpp b/src/network/socket_manager.cpp new file mode 100644 index 000000000..0157e3e84 --- /dev/null +++ b/src/network/socket_manager.cpp @@ -0,0 +1,31 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#ifdef _WIN32 +#include +#include +#endif + +#include "socket_manager.h" + +namespace Network { +std::atomic SocketManager::count = 0; + +void SocketManager::EnableSockets() { + if (count++ == 0) { +#ifdef _WIN32 + WSADATA data; + WSAStartup(MAKEWORD(2, 2), &data); +#endif + } +} + +void SocketManager::DisableSockets() { + if (--count == 0) { +#ifdef _WIN32 + WSACleanup(); +#endif + } +} +} // namespace Network \ No newline at end of file diff --git a/src/network/socket_manager.h b/src/network/socket_manager.h new file mode 100644 index 000000000..f54aef2c4 --- /dev/null +++ b/src/network/socket_manager.h @@ -0,0 +1,19 @@ +// Copyright 2024 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once +#include "atomic" +#include "common/common_types.h" + +namespace Network { +class SocketManager { +public: + static void EnableSockets(); + static void DisableSockets(); + +private: + SocketManager(); + static std::atomic count; +}; +} // namespace Network \ No newline at end of file diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index baddc1ada..4ac9368b1 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -13,7 +13,7 @@ add_executable(tests audio_core/lle/lle.cpp audio_core/audio_fixures.h audio_core/decoder_tests.cpp - video_core/shader/shader_jit_compiler.cpp + video_core/shader.cpp audio_core/merryhime_3ds_audio/merry_audio/merry_audio.cpp audio_core/merryhime_3ds_audio/merry_audio/merry_audio.h audio_core/merryhime_3ds_audio/merry_audio/service_fixture.cpp diff --git a/src/tests/video_core/shader/shader_jit_compiler.cpp b/src/tests/video_core/shader.cpp similarity index 86% rename from src/tests/video_core/shader/shader_jit_compiler.cpp rename to src/tests/video_core/shader.cpp index 01698a2e1..28cb1ec44 100644 --- a/src/tests/video_core/shader/shader_jit_compiler.cpp +++ b/src/tests/video_core/shader.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include "video_core/pica/shader_setup.h" @@ -73,18 +75,19 @@ static std::unique_ptr CompileShaderSetup( class ShaderTest { public: explicit ShaderTest(std::initializer_list code) - : shader_setup(CompileShaderSetup(code)) { - shader_jit.Compile(&shader_setup->program_code, &shader_setup->swizzle_data); - } + : shader_setup(CompileShaderSetup(code)) {} explicit ShaderTest(std::unique_ptr input_shader_setup) - : shader_setup(std::move(input_shader_setup)) { - shader_jit.Compile(&shader_setup->program_code, &shader_setup->swizzle_data); - } + : shader_setup(std::move(input_shader_setup)) {} + + virtual ~ShaderTest() = default; + + virtual void RunShader(Pica::ShaderUnit& shader_unit, + std::span inputs) = 0; Common::Vec4f Run(std::span inputs) { Pica::ShaderUnit shader_unit; - RunJit(shader_unit, inputs); + RunShader(shader_unit, inputs); return {shader_unit.output[0].x.ToFloat32(), shader_unit.output[0].y.ToFloat32(), shader_unit.output[0].z.ToFloat32(), shader_unit.output[0].w.ToFloat32()}; } @@ -105,24 +108,23 @@ public: return Run(std::vector{inputs}); } - void RunJit(Pica::ShaderUnit& shader_unit, std::span inputs) { - for (std::size_t i = 0; i < inputs.size(); ++i) { - const Common::Vec4f& input = inputs[i]; - shader_unit.input[i].x = Pica::f24::FromFloat32(input.x); - shader_unit.input[i].y = Pica::f24::FromFloat32(input.y); - shader_unit.input[i].z = Pica::f24::FromFloat32(input.z); - shader_unit.input[i].w = Pica::f24::FromFloat32(input.w); - } - shader_unit.temporary.fill(Common::Vec4::AssignToAll(Pica::f24::Zero())); - shader_jit.Run(*shader_setup, shader_unit, 0); - } - - void RunJit(Pica::ShaderUnit& shader_unit, float input) { + void Run(Pica::ShaderUnit& shader_unit, float input) { const Common::Vec4f input_vec(input, 0, 0, 0); - RunJit(shader_unit, {&input_vec, 1}); + RunShader(shader_unit, {&input_vec, 1}); } - void RunInterpreter(Pica::ShaderUnit& shader_unit, std::span inputs) { + std::unique_ptr shader_setup; +}; + +class ShaderInterpreterTest : public ShaderTest { +public: + explicit ShaderInterpreterTest(std::initializer_list code) + : ShaderTest(code) {} + + explicit ShaderInterpreterTest(std::unique_ptr input_shader_setup) + : ShaderTest(std::move(input_shader_setup)) {} + + void RunShader(Pica::ShaderUnit& shader_unit, std::span inputs) override { for (std::size_t i = 0; i < inputs.size(); ++i) { const Common::Vec4f& input = inputs[i]; shader_unit.input[i].x = Pica::f24::FromFloat32(input.x); @@ -134,23 +136,46 @@ public: shader_interpreter.Run(*shader_setup, shader_unit); } - void RunInterpreter(Pica::ShaderUnit& shader_unit, float input) { - const Common::Vec4f input_vec(input, 0, 0, 0); - RunInterpreter(shader_unit, {&input_vec, 1}); - } - -public: - JitShader shader_jit; +private: ShaderInterpreter shader_interpreter; - std::unique_ptr shader_setup; }; -TEST_CASE("ADD", "[video_core][shader][shader_jit]") { +class ShaderJitTest : public ShaderTest { +public: + explicit ShaderJitTest(std::initializer_list code) : ShaderTest(code) { + shader_jit.Compile(&shader_setup->program_code, &shader_setup->swizzle_data); + } + + explicit ShaderJitTest(std::unique_ptr input_shader_setup) + : ShaderTest(std::move(input_shader_setup)) { + shader_jit.Compile(&shader_setup->program_code, &shader_setup->swizzle_data); + } + + void RunShader(Pica::ShaderUnit& shader_unit, std::span inputs) override { + for (std::size_t i = 0; i < inputs.size(); ++i) { + const Common::Vec4f& input = inputs[i]; + shader_unit.input[i].x = Pica::f24::FromFloat32(input.x); + shader_unit.input[i].y = Pica::f24::FromFloat32(input.y); + shader_unit.input[i].z = Pica::f24::FromFloat32(input.z); + shader_unit.input[i].w = Pica::f24::FromFloat32(input.w); + } + shader_unit.temporary.fill(Common::Vec4::AssignToAll(Pica::f24::Zero())); + shader_jit.Run(*shader_setup, shader_unit, 0); + } + +private: + JitShader shader_jit; +}; + +#define SHADER_TEST_CASE(NAME, TAG) \ + TEMPLATE_TEST_CASE(NAME, TAG, ShaderInterpreterTest, ShaderJitTest) + +SHADER_TEST_CASE("ADD", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::ADD, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -162,7 +187,7 @@ TEST_CASE("ADD", "[video_core][shader][shader_jit]") { REQUIRE(std::isinf(shader.Run({INFINITY, -1.0f}).x)); } -TEST_CASE("CALL", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("CALL", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); @@ -192,17 +217,17 @@ TEST_CASE("CALL", "[video_core][shader][shader_jit]") { CALL.flow_control.num_instructions = 1; shader_setup->program_code[2] = CALL.hex; - auto shader = ShaderTest(std::move(shader_setup)); + auto shader = TestType(std::move(shader_setup)); REQUIRE(shader.Run(0.f).x == Catch::Approx(1.f)); } -TEST_CASE("DP3", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("DP3", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::DP3, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -213,12 +238,12 @@ TEST_CASE("DP3", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({vec4_one, vec4_one}).x == 3.0f); } -TEST_CASE("DP4", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("DP4", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::DP4, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -229,12 +254,12 @@ TEST_CASE("DP4", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({vec4_one, vec4_one}).x == 4.0f); } -TEST_CASE("DPH", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("DPH", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::DPH, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -246,11 +271,11 @@ TEST_CASE("DPH", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({vec4_zero, vec4_one}).x == 1.0f); } -TEST_CASE("LG2", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("LG2", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::LG2, sh_output, sh_input}, {OpCode::Id::END}, }); @@ -263,11 +288,11 @@ TEST_CASE("LG2", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run(1.e24f).x == Catch::Approx(79.7262742773f)); } -TEST_CASE("EX2", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("EX2", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::EX2, sh_output, sh_input}, {OpCode::Id::END}, }); @@ -281,12 +306,12 @@ TEST_CASE("EX2", "[video_core][shader][shader_jit]") { REQUIRE(std::isinf(shader.Run(800.f).x)); } -TEST_CASE("MUL", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("MUL", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::MUL, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -300,12 +325,12 @@ TEST_CASE("MUL", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({+INFINITY, -INFINITY}).x == -INFINITY); } -TEST_CASE("SGE", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("SGE", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::SGE, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -321,12 +346,12 @@ TEST_CASE("SGE", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({-1.0f, +1.0f}).x == 0.0f); } -TEST_CASE("SLT", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("SLT", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::SLT, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -342,11 +367,11 @@ TEST_CASE("SLT", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({-1.0f, +1.0f}).x == 1.0f); } -TEST_CASE("FLR", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("FLR", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::FLR, sh_output, sh_input1}, {OpCode::Id::END}, }); @@ -359,12 +384,12 @@ TEST_CASE("FLR", "[video_core][shader][shader_jit]") { REQUIRE(std::isinf(shader.Run({INFINITY}).x)); } -TEST_CASE("MAX", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("MAX", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::MAX, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -378,12 +403,12 @@ TEST_CASE("MAX", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({-INFINITY, +INFINITY}).x == +INFINITY); } -TEST_CASE("MIN", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("MIN", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::MIN, sh_output, sh_input1, sh_input2}, {OpCode::Id::END}, }); @@ -397,11 +422,11 @@ TEST_CASE("MIN", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({-INFINITY, +INFINITY}).x == -INFINITY); } -TEST_CASE("RCP", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("RCP", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::RCP, sh_output, sh_input}, {OpCode::Id::END}, }); @@ -422,11 +447,11 @@ TEST_CASE("RCP", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({0.0625f}).x == Catch::Approx(16.0f).margin(0.004f)); } -TEST_CASE("RSQ", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("RSQ", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ {OpCode::Id::RSQ, sh_output, sh_input}, {OpCode::Id::END}, }); @@ -448,12 +473,12 @@ TEST_CASE("RSQ", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({0.0625f}).x == Catch::Approx(4.0f).margin(0.004f)); } -TEST_CASE("Uniform Read", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("Uniform Read", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_c0 = SourceRegister::MakeFloat(0); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ // mova a0.x, sh_input.x {OpCode::Id::MOVA, DestRegister{}, "x", sh_input, "x", SourceRegister{}, "", nihstro::InlineAsm::RelativeAddress::A1}, @@ -481,12 +506,12 @@ TEST_CASE("Uniform Read", "[video_core][shader][shader_jit]") { } } -TEST_CASE("Address Register Offset", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("Address Register Offset", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_c40 = SourceRegister::MakeFloat(40); const auto sh_output = DestRegister::MakeOutput(0); - auto shader = ShaderTest({ + auto shader = TestType({ // mova a0.x, sh_input.x {OpCode::Id::MOVA, DestRegister{}, "x", sh_input, "x", SourceRegister{}, "", nihstro::InlineAsm::RelativeAddress::A1}, @@ -531,12 +556,12 @@ TEST_CASE("Address Register Offset", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run(-129.f) == f_uniforms[40]); } -TEST_CASE("Dest Mask", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("Dest Mask", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); const auto shader = [&sh_input, &sh_output](const char* dest_mask) { - return std::unique_ptr(new ShaderTest{ + return std::unique_ptr(new TestType{ {OpCode::Id::MOV, sh_output, dest_mask, sh_input, "xyzw", SourceRegister{}, ""}, {OpCode::Id::END}, }); @@ -561,7 +586,7 @@ TEST_CASE("Dest Mask", "[video_core][shader][shader_jit]") { REQUIRE(shader("xyzw")->Run({iota_vec}) == iota_vec); } -TEST_CASE("MAD", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("MAD", "[video_core][shader]") { const auto sh_input1 = SourceRegister::MakeInput(0); const auto sh_input2 = SourceRegister::MakeInput(1); const auto sh_input3 = SourceRegister::MakeInput(2); @@ -601,7 +626,7 @@ TEST_CASE("MAD", "[video_core][shader][shader_jit]") { swizzle.SetSelectorSrc3(3, SwizzlePattern::Selector::w); shader_setup->swizzle_data[0] = swizzle.hex; - auto shader = ShaderTest(std::move(shader_setup)); + auto shader = TestType(std::move(shader_setup)); REQUIRE(shader.Run({vec4_zero, vec4_zero, vec4_zero}) == vec4_zero); REQUIRE(shader.Run({vec4_one, vec4_one, vec4_one}) == (vec4_one * 2.0f)); @@ -609,12 +634,14 @@ TEST_CASE("MAD", "[video_core][shader][shader_jit]") { REQUIRE(shader.Run({vec4_nan, vec4_zero, vec4_zero}) == vec4_nan); } -TEST_CASE("Nested Loop", "[video_core][shader][shader_jit]") { +// Nested Loops are bugged on on the Shader-Interpreter at the moment +// SHADER_TEST_CASE("Nested Loop", "[video_core][shader]") { +TEMPLATE_TEST_CASE("Nested Loop", "[video_core][shader]", ShaderJitTest) { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_temp = SourceRegister::MakeTemporary(0); const auto sh_output = DestRegister::MakeOutput(0); - auto shader_test = ShaderTest({ + auto shader_test = TestType({ // clang-format off {OpCode::Id::MOV, sh_temp, sh_input}, {OpCode::Id::LOOP, 0}, @@ -628,8 +655,8 @@ TEST_CASE("Nested Loop", "[video_core][shader][shader_jit]") { }); { - shader_test.shader_setup->uniforms.i[0] = {4, 0, 1, 0}; - shader_test.shader_setup->uniforms.i[1] = {4, 0, 1, 0}; + shader_test.shader_setup->uniforms.i[0] = {(u8)GENERATE(4, 9), 0, (u8)GENERATE(1, 2), 0}; + shader_test.shader_setup->uniforms.i[1] = {(u8)GENERATE(4, 7), 0, (u8)GENERATE(1, 1), 0}; Common::Vec4 loop_parms{shader_test.shader_setup->uniforms.i[0]}; const int expected_aL = loop_parms[1] + ((loop_parms[0] + 1) * loop_parms[2]); @@ -639,37 +666,20 @@ TEST_CASE("Nested Loop", "[video_core][shader][shader_jit]") { input) + input; - Pica::ShaderUnit shader_unit_jit; - shader_test.RunJit(shader_unit_jit, input); + Pica::ShaderUnit shader_unit; + shader_test.Run(shader_unit, input); - REQUIRE(shader_unit_jit.address_registers[2] == expected_aL); - REQUIRE(shader_unit_jit.output[0].x.ToFloat32() == Catch::Approx(expected_out)); - } - { - shader_test.shader_setup->uniforms.i[0] = {9, 0, 2, 0}; - shader_test.shader_setup->uniforms.i[1] = {7, 0, 1, 0}; - - const Common::Vec4 loop_parms{shader_test.shader_setup->uniforms.i[0]}; - const int expected_aL = loop_parms[1] + ((loop_parms[0] + 1) * loop_parms[2]); - const float input = 1.0f; - const float expected_out = (((shader_test.shader_setup->uniforms.i[0][0] + 1) * - (shader_test.shader_setup->uniforms.i[1][0] + 1)) * - input) + - input; - Pica::ShaderUnit shader_unit_jit; - shader_test.RunJit(shader_unit_jit, input); - - REQUIRE(shader_unit_jit.address_registers[2] == expected_aL); - REQUIRE(shader_unit_jit.output[0].x.ToFloat32() == Catch::Approx(expected_out)); + REQUIRE(shader_unit.address_registers[2] == expected_aL); + REQUIRE(shader_unit.output[0].x.ToFloat32() == Catch::Approx(expected_out)); } } -TEST_CASE("Source Swizzle", "[video_core][shader][shader_jit]") { +SHADER_TEST_CASE("Source Swizzle", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_output = DestRegister::MakeOutput(0); const auto shader = [&sh_input, &sh_output](const char* swizzle) { - return std::unique_ptr(new ShaderTest{ + return std::unique_ptr(new TestType{ {OpCode::Id::MOV, sh_output, "xyzw", sh_input, swizzle, SourceRegister{}, ""}, {OpCode::Id::END}, }); diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index cd9c080ef..0144cbd61 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -647,10 +647,11 @@ typename T::Surface& RasterizerCache::GetTextureCube(const TextureCubeConfig& Surface& cube_surface = slot_surfaces[cube.surface_id]; for (u32 i = 0; i < addresses.size(); i++) { - if (!addresses[i]) { + const SurfaceId& face_id = cube.face_ids[i]; + if (!addresses[i] || !face_id) { continue; } - Surface& surface = slot_surfaces[cube.face_ids[i]]; + Surface& surface = slot_surfaces[face_id]; if (cube.ticks[i] == surface.modification_tick) { continue; } diff --git a/src/video_core/renderer_opengl/gl_driver.cpp b/src/video_core/renderer_opengl/gl_driver.cpp index 63b05bd85..1e110fd58 100644 --- a/src/video_core/renderer_opengl/gl_driver.cpp +++ b/src/video_core/renderer_opengl/gl_driver.cpp @@ -199,6 +199,15 @@ void Driver::FindBugs() { if (vendor == Vendor::Intel && !is_linux) { bugs |= DriverBug::BrokenClearTexture; } + + if (vendor == Vendor::ARM && gpu_model.find("Mali") != gpu_model.npos) { + constexpr GLint MIN_TEXTURE_BUFFER_SIZE = static_cast((1 << 16)); + GLint max_texel_buffer_size; + glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &max_texel_buffer_size); + if (max_texel_buffer_size == MIN_TEXTURE_BUFFER_SIZE) { + bugs |= DriverBug::SlowTextureBufferWithBigSize; + } + } } } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_driver.h b/src/video_core/renderer_opengl/gl_driver.h index 2d67c3dc9..3801babad 100644 --- a/src/video_core/renderer_opengl/gl_driver.h +++ b/src/video_core/renderer_opengl/gl_driver.h @@ -36,6 +36,9 @@ enum class DriverBug { BrokenTextureView = 1 << 2, // On Haswell and Broadwell Intel drivers glClearTexSubImage produces a black screen BrokenClearTexture = 1 << 3, + // On some Mali GPUs, the texture buffer size is small and has reduced performance + // if the buffer is close to the maximum texture size + SlowTextureBufferWithBigSize = 1 << 4, }; /** diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 0487905de..3a9dc7831 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -63,12 +63,19 @@ GLenum MakeAttributeType(Pica::PipelineRegs::VertexAttributeFormat format) { return GL_UNSIGNED_BYTE; } -[[nodiscard]] GLsizeiptr TextureBufferSize() { +[[nodiscard]] GLsizeiptr TextureBufferSize(const Driver& driver, bool is_lf) { // Use the smallest texel size from the texel views // which corresponds to GL_RG32F GLint max_texel_buffer_size; glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &max_texel_buffer_size); - return std::min(max_texel_buffer_size * 8ULL, TEXTURE_BUFFER_SIZE); + GLsizeiptr candidate = std::min(max_texel_buffer_size * 8ULL, TEXTURE_BUFFER_SIZE); + + if (driver.HasBug(DriverBug::SlowTextureBufferWithBigSize) && !is_lf) { + constexpr GLsizeiptr FIXUP_TEXTURE_BUFFER_SIZE = static_cast(1 << 14); // 16384 + return FIXUP_TEXTURE_BUFFER_SIZE; + } + + return candidate; } } // Anonymous namespace @@ -79,13 +86,11 @@ RasterizerOpenGL::RasterizerOpenGL(Memory::MemorySystem& memory, Pica::PicaCore& : VideoCore::RasterizerAccelerated{memory, pica}, driver{driver_}, shader_manager{renderer.GetRenderWindow(), driver, !driver.IsOpenGLES()}, runtime{driver, renderer}, res_cache{memory, custom_tex_manager, runtime, regs, renderer}, - texture_buffer_size{TextureBufferSize()}, vertex_buffer{driver, GL_ARRAY_BUFFER, - VERTEX_BUFFER_SIZE}, + vertex_buffer{driver, GL_ARRAY_BUFFER, VERTEX_BUFFER_SIZE}, uniform_buffer{driver, GL_UNIFORM_BUFFER, UNIFORM_BUFFER_SIZE}, index_buffer{driver, GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE}, - texture_buffer{driver, GL_TEXTURE_BUFFER, texture_buffer_size}, texture_lf_buffer{ - driver, GL_TEXTURE_BUFFER, - texture_buffer_size} { + texture_buffer{driver, GL_TEXTURE_BUFFER, TextureBufferSize(driver, false)}, + texture_lf_buffer{driver, GL_TEXTURE_BUFFER, TextureBufferSize(driver, true)} { // Clipping plane 0 is always enabled for PICA fixed clip plane z <= 0 state.clip_distance[0] = true; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h index 3fe5e8dde..b63b34e86 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.h +++ b/src/video_core/renderer_opengl/gl_rasterizer.h @@ -145,7 +145,6 @@ private: OGLVertexArray hw_vao; // VAO for hardware shader / accelerate draw std::array hw_vao_enabled_attributes{}; - GLsizeiptr texture_buffer_size; OGLStreamBuffer vertex_buffer; OGLStreamBuffer uniform_buffer; OGLStreamBuffer index_buffer; diff --git a/src/video_core/renderer_software/sw_clipper.cpp b/src/video_core/renderer_software/sw_clipper.cpp index 03a287a37..531eb0175 100644 --- a/src/video_core/renderer_software/sw_clipper.cpp +++ b/src/video_core/renderer_software/sw_clipper.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "video_core/pica/regs_texturing.h" #include "video_core/renderer_software/sw_clipper.h" diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index bb81a1037..f00966085 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -73,6 +73,7 @@ RendererVulkan::RendererVulkan(Core::System& system, Pica::PicaCore& pica_, RendererVulkan::~RendererVulkan() { vk::Device device = instance.GetDevice(); scheduler.Finish(); + main_window.WaitPresent(); device.waitIdle(); device.destroyShaderModule(present_vertex_shader); diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 8f77f25e0..2d91ff220 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include "common/assert.h" #include "common/settings.h" diff --git a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp index f8542524d..0e5ec6d97 100644 --- a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp +++ b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp @@ -68,9 +68,9 @@ void MasterSemaphoreTimeline::SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore const std::array signal_values{signal_value, u64(0)}; const std::array signal_semaphores{Handle(), signal}; - const u32 num_wait_semaphores = wait ? 2U : 1U; - const std::array wait_values{signal_value - 1, u64(1)}; - const std::array wait_semaphores{Handle(), wait}; + const u32 num_wait_semaphores = wait ? 1U : 0U; + const std::array wait_values{u64(1)}; + const std::array wait_semaphores{wait}; static constexpr std::array wait_stage_masks = { vk::PipelineStageFlagBits::eAllCommands, diff --git a/src/video_core/renderer_vulkan/vk_present_window.cpp b/src/video_core/renderer_vulkan/vk_present_window.cpp index ef879776a..62178c690 100644 --- a/src/video_core/renderer_vulkan/vk_present_window.cpp +++ b/src/video_core/renderer_vulkan/vk_present_window.cpp @@ -473,7 +473,7 @@ void PresentWindow::CopyToSwapchain(Frame* frame) { .pSignalSemaphores = &present_ready, }; - std::scoped_lock submit_lock{scheduler.submit_mutex}; + std::scoped_lock submit_lock{scheduler.submit_mutex, recreate_surface_mutex}; try { graphics_queue.submit(submit_info, frame->present_done); diff --git a/src/video_core/renderer_vulkan/vk_shader_util.cpp b/src/video_core/renderer_vulkan/vk_shader_util.cpp index 09e3eb883..3d2f2f1d1 100644 --- a/src/video_core/renderer_vulkan/vk_shader_util.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_util.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 498a921a8..12b12d27d 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -78,9 +78,13 @@ void Swapchain::Create(u32 width_, u32 height_, vk::SurfaceKHR surface_) { } bool Swapchain::AcquireNextImage() { + if (needs_recreation) { + return false; + } + MICROPROFILE_SCOPE(Vulkan_Acquire); - vk::Device device = instance.GetDevice(); - vk::Result result = + const vk::Device device = instance.GetDevice(); + const vk::Result result = device.acquireNextImageKHR(swapchain, std::numeric_limits::max(), image_acquired[frame_index], VK_NULL_HANDLE, &image_index); @@ -102,10 +106,6 @@ bool Swapchain::AcquireNextImage() { } void Swapchain::Present() { - if (needs_recreation) { - return; - } - const vk::PresentInfoKHR present_info = { .waitSemaphoreCount = 1, .pWaitSemaphores = &present_ready[image_index], @@ -119,6 +119,10 @@ void Swapchain::Present() { [[maybe_unused]] vk::Result result = instance.GetPresentQueue().presentKHR(present_info); } catch (vk::OutOfDateKHRError&) { needs_recreation = true; + return; + } catch (vk::SurfaceLostKHRError&) { + needs_recreation = true; + return; } catch (const vk::SystemError& err) { LOG_CRITICAL(Render_Vulkan, "Swapchain presentation failed {}", err.what()); UNREACHABLE(); @@ -268,4 +272,4 @@ void Swapchain::SetupImages() { } } -} // namespace Vulkan +} // namespace Vulkan \ No newline at end of file