Merge branch 'PabloMK7:master' into wayland

This commit is contained in:
Miguel 2024-07-20 18:20:45 +02:00 committed by GitHub
commit 9d64ddb121
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
171 changed files with 9190 additions and 1771 deletions

View file

@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Engelsk</translation> <translation>Engelsk</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Form</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord-presence</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Vis kørende spil som din Discord-status</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

18
dist/languages/de.ts vendored
View file

@ -3585,6 +3585,24 @@ Ziehe Punkte, um ihre Position zu verändern, oder doppelklicke auf Zellen in de
<translation>Englisch</translation> <translation>Englisch</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Form</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord Presence</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Aktuelles Spiel in Ihrem Discordstatus anzeigen</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

18
dist/languages/el.ts vendored
View file

@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Αγγλικά</translation> <translation>Αγγλικά</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Φόρμα</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Παρουσία Discord</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Εμφάνιση τρέχοντος παιχνιδιού στην κατάσταση Discord σας</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3585,6 +3585,24 @@ Mueve los puntos para cambiar la posición, o haz doble click en las celdas de l
<translation>Inglés (English)</translation> <translation>Inglés (English)</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Formulario</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Presencia en Discord</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Mostrar Juego Actual en el Estado de Discord</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

18
dist/languages/fi.ts vendored
View file

@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Englanti</translation> <translation>Englanti</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Muot</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord läsnäolo</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Näytä nykyinen peli Discord tilassa</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

18
dist/languages/fr.ts vendored
View file

@ -3585,6 +3585,24 @@ Glissez les points pour modifier la position, ou double-cliquez les cellules pou
<translation>Anglais</translation> <translation>Anglais</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Forme</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Présence sur Discord</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Afficher votre jeu en cours dans votre statut Discord</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3582,6 +3582,72 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Angol</translation> <translation>Angol</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Forma</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord jelenlét</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Jelenlegi játék megjelenítése a Discord állapotodban</translation>
</message>
</context>
<context>
<name>DirectConnect</name>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="14"/>
<source>Direct Connect</source>
<translation>Közvetlen Kapcsolódás</translation>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="47"/>
<source>Server Address</source>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="54"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Server address of the host&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="64"/>
<source>Port</source>
<translation>Port</translation>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="71"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Port number the host is listening on&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annak a portnak a száma, amire a gazda figyel&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="77"/>
<source>24872</source>
<translation>24872</translation>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="97"/>
<source>Nickname</source>
<translation>Becenév</translation>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="111"/>
<source>Password</source>
<translation>Jelszó</translation>
</message>
<message>
<location filename="../../src/citra_qt/multiplayer/direct_connect.ui" line="153"/>
<source>Connect</source>
<translation>Kapcsolás</translation>
</message>
</context>
<context> <context>
<name>DirectConnectWindow</name> <name>DirectConnectWindow</name>
<message> <message>

18
dist/languages/id.ts vendored
View file

@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Inggris</translation> <translation>Inggris</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Formulir</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Status Discord</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Tampilkan Game Saat Ini ke Status Discord Anda</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

18
dist/languages/it.ts vendored
View file

@ -3585,6 +3585,24 @@ Trascina i punti per cambiarne la posizione, o fai doppio clic sulla tabella per
<translation>Inglese</translation> <translation>Inglese</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Modulo</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord Presence</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Mostra il gioco attuale nel tuo stato di Discord</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation></translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord Presence</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Discordに表示</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>English</translation> <translation>English</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation></translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation> </translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation> </translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3581,6 +3581,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Anglų k.</translation> <translation>Anglų k.</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Forma</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord nustatymai</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Rodyti jūsų žaidžiamą žaidimą Discord&apos;e</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

18
dist/languages/nb.ts vendored
View file

@ -3584,6 +3584,24 @@ Dra punkter for å endre posisjon, eller dobbeltklikk på tabellceller for å re
<translation>Engelsk</translation> <translation>Engelsk</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Form</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord tilstedeværelse</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Vis Gjeldende Spill i Discord Statusen din.</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

18
dist/languages/nl.ts vendored
View file

@ -3585,6 +3585,24 @@ Sleep punten om de positie te wijzigen of dubbelklik op tabelcellen om waarden t
<translation>Engels</translation> <translation>Engels</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Formulier</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord Presence</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Toon Huidige Spel in je Discord Status</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Angielski (English)</translation> <translation>Angielski (English)</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Formularz</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Widoczność na Discordzie</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Pokaż obecnie włączoną grę w statusie na Discrodzie</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3585,6 +3585,24 @@ Arraste os pontos para alterar a posição ou clique duas vezes nas células da
<translation>Inglês (English)</translation> <translation>Inglês (English)</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Formulário</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Presença no Discord</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Mostrar o jogo atual no seu perfil do Discord</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Engleză</translation> <translation>Engleză</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Model</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Prezență pe Discord</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Afișează Jocul Prezent pe Statusul Discord</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3587,6 +3587,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Английский</translation> <translation>Английский</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Форма</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Интеграция с Discord</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Показывать текущую игру в статусе Discord</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>İngilizce</translation> <translation>İngilizce</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Form</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord Görünümü</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Şu Anki Oyunu Discord Durumunda Göster</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3583,6 +3583,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>Tiếng Anh</translation> <translation>Tiếng Anh</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Đnh dạng</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation type="unfinished"/>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation>Hiển thị game đang chơi trên trạng thái Discord</translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3585,6 +3585,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation></translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord </translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation> Discord </translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

View file

@ -3584,6 +3584,24 @@ Drag points to change position, or double-click table cells to edit values.</sou
<translation>English</translation> <translation>English</translation>
</message> </message>
</context> </context>
<context>
<name>ConfigureWeb</name>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="14"/>
<source>Form</source>
<translation>Form</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="177"/>
<source>Discord Presence</source>
<translation>Discord </translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="183"/>
<source>Show Current Game in your Discord Status</source>
<translation> Discord </translation>
</message>
</context>
<context> <context>
<name>DirectConnect</name> <name>DirectConnect</name>
<message> <message>

2
externals/dynarmic vendored

@ -1 +1 @@
Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c Subproject commit a41c380246d3d9f9874f0f792d234dc0cc17c180

2
externals/fmt vendored

@ -1 +1 @@
Subproject commit 2dd4fa8742fdac36468f8d8ea3e06e78215551f8 Subproject commit fcd3e1e19c8d2df94bb6cb40d7f1c97a9872cf2b

View file

@ -29,7 +29,7 @@ android {
namespace = "org.citra.citra_emu" namespace = "org.citra.citra_emu"
compileSdkVersion = "android-34" compileSdkVersion = "android-34"
ndkVersion = "26.1.10909125" ndkVersion = "26.3.11579264"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17

View file

@ -183,13 +183,13 @@ object NativeLibrary {
private var coreErrorAlertResult = false private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object() private val coreErrorAlertLock = Object()
private fun onCoreErrorImpl(title: String, message: String) { private fun onCoreErrorImpl(title: String, message: String, canContinue: Boolean) {
val emulationActivity = sEmulationActivity.get() val emulationActivity = sEmulationActivity.get()
if (emulationActivity == null) { if (emulationActivity == null) {
Log.error("[NativeLibrary] EmulationActivity not present") Log.error("[NativeLibrary] EmulationActivity not present")
return return
} }
val fragment = CoreErrorDialogFragment.newInstance(title, message) val fragment = CoreErrorDialogFragment.newInstance(title, message, canContinue)
fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG) fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG)
} }
@ -207,6 +207,7 @@ object NativeLibrary {
} }
val title: String val title: String
val message: String val message: String
val canContinue: Boolean
when (error) { when (error) {
CoreError.ErrorSystemFiles -> { CoreError.ErrorSystemFiles -> {
title = emulationActivity.getString(R.string.system_archive_not_found) title = emulationActivity.getString(R.string.system_archive_not_found)
@ -214,16 +215,25 @@ object NativeLibrary {
R.string.system_archive_not_found_message, R.string.system_archive_not_found_message,
details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
) )
canContinue = true
} }
CoreError.ErrorSavestate -> { CoreError.ErrorSavestate -> {
title = emulationActivity.getString(R.string.save_load_error) title = emulationActivity.getString(R.string.save_load_error)
message = details 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 -> { CoreError.ErrorUnknown -> {
title = emulationActivity.getString(R.string.fatal_error) title = emulationActivity.getString(R.string.fatal_error)
message = emulationActivity.getString(R.string.fatal_error_message) message = emulationActivity.getString(R.string.fatal_error_message)
canContinue = true
} }
else -> { else -> {
@ -232,7 +242,7 @@ object NativeLibrary {
} }
// Show the AlertDialog on the main thread. // 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. // Wait for the lock to notify that it is complete.
synchronized(coreErrorAlertLock) { synchronized(coreErrorAlertLock) {
@ -346,6 +356,11 @@ object NativeLibrary {
return return
} }
if (resultCode == EmulationErrorDialogFragment.ShutdownRequested) {
emulationActivity.finish()
return
}
emulationActivity.runOnUiThread { emulationActivity.runOnUiThread {
EmulationErrorDialogFragment.newInstance(resultCode).showNow( EmulationErrorDialogFragment.newInstance(resultCode).showNow(
emulationActivity.supportFragmentManager, emulationActivity.supportFragmentManager,
@ -361,16 +376,23 @@ object NativeLibrary {
emulationActivity = requireActivity() as EmulationActivity emulationActivity = requireActivity() as EmulationActivity
var captionId = R.string.loader_error_invalid_format 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 captionId = R.string.loader_error_encrypted
} }
if (result == ErrorArticDisconnected) {
captionId = R.string.artic_base
}
val alert = MaterialAlertDialogBuilder(requireContext()) val alert = MaterialAlertDialogBuilder(requireContext())
.setTitle(captionId) .setTitle(captionId)
.setMessage( .setMessage(
Html.fromHtml( Html.fromHtml(
CitraApplication.appContext.resources.getString(R.string.redump_games), if (result == ErrorArticDisconnected)
Html.FROM_HTML_MODE_LEGACY 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 -> .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
@ -398,7 +420,10 @@ object NativeLibrary {
const val ErrorLoader = 4 const val ErrorLoader = 4
const val ErrorLoader_ErrorEncrypted = 5 const val ErrorLoader_ErrorEncrypted = 5
const val ErrorLoader_ErrorInvalidFormat = 6 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 ShutdownRequested = 11
const val ErrorUnknown = 12 const val ErrorUnknown = 12
@ -502,12 +527,28 @@ object NativeLibrary {
external fun removeAmiibo() external fun removeAmiibo()
const val SAVESTATE_SLOT_COUNT = 10 const val SAVESTATE_SLOT_COUNT = 11
const val QUICKSAVE_SLOT = 0
external fun getSavestateInfo(): Array<SaveStateInfo>? external fun getSavestateInfo(): Array<SaveStateInfo>?
external fun saveState(slot: Int) 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) external fun loadState(slot: Int)
/** /**
@ -619,6 +660,7 @@ object NativeLibrary {
enum class CoreError { enum class CoreError {
ErrorSystemFiles, ErrorSystemFiles,
ErrorSavestate, ErrorSavestate,
ErrorArticDisconnected,
ErrorUnknown ErrorUnknown
} }
@ -633,23 +675,33 @@ object NativeLibrary {
} }
class CoreErrorDialogFragment : DialogFragment() { class CoreErrorDialogFragment : DialogFragment() {
private var userChosen = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = requireArguments().getString(TITLE) val title = requireArguments().getString(TITLE)
val message = requireArguments().getString(MESSAGE) val message = requireArguments().getString(MESSAGE)
return MaterialAlertDialogBuilder(requireContext()) val canContinue = requireArguments().getBoolean(CAN_CONTINUE)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(title) .setTitle(title)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int -> if (canContinue) {
dialog.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int ->
coreErrorAlertResult = true coreErrorAlertResult = true
userChosen = true
} }
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int -> }
coreErrorAlertResult = false dialog.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
}.show() coreErrorAlertResult = false
userChosen = true
}
return dialog.show()
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog) super.onDismiss(dialog)
coreErrorAlertResult = true val canContinue = requireArguments().getBoolean(CAN_CONTINUE)
if (!userChosen) {
coreErrorAlertResult = canContinue
}
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() } synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
} }
@ -658,12 +710,14 @@ object NativeLibrary {
const val TITLE = "title" const val TITLE = "title"
const val MESSAGE = "message" 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 frag = CoreErrorDialogFragment()
val args = Bundle() val args = Bundle()
args.putString(TITLE, title) args.putString(TITLE, title)
args.putString(MESSAGE, message) args.putString(MESSAGE, message)
args.putBoolean(CAN_CONTINUE, canContinue)
frag.arguments = args frag.arguments = args
return frag return frag
} }

View file

@ -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.fragments.MessageDialogFragment
import org.citra.citra_emu.utils.ControllerMappingHelper import org.citra.citra_emu.utils.ControllerMappingHelper
import org.citra.citra_emu.utils.FileBrowserHelper 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.EmulationLifecycleUtil
import org.citra.citra_emu.utils.EmulationMenuSettings import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.ThemeUtil import org.citra.citra_emu.utils.ThemeUtil
@ -47,7 +46,6 @@ import org.citra.citra_emu.viewmodel.EmulationViewModel
class EmulationActivity : AppCompatActivity() { class EmulationActivity : AppCompatActivity() {
private val preferences: SharedPreferences private val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
private var foregroundService: Intent? = null
var isActivityRecreated = false var isActivityRecreated = false
private val settingsViewModel: SettingsViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels()
@ -66,7 +64,7 @@ class EmulationActivity : AppCompatActivity() {
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings) screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil) hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this)
setContentView(binding.root) setContentView(binding.root)
val navHostFragment = val navHostFragment =
@ -85,10 +83,6 @@ class EmulationActivity : AppCompatActivity() {
windowManager.defaultDisplay.rotation 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() }) EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() })
} }
@ -112,7 +106,6 @@ class EmulationActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
EmulationLifecycleUtil.clear() EmulationLifecycleUtil.clear()
stopForegroundService(this)
super.onDestroy() super.onDestroy()
} }
@ -186,8 +179,7 @@ class EmulationActivity : AppCompatActivity() {
return false return false
} }
val button = val button = preferences.getInt(InputBindingSetting.getInputButtonKey(event), event.scanCode)
preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
val action: Int = when (event.action) { val action: Int = when (event.action) {
KeyEvent.ACTION_DOWN -> { KeyEvent.ACTION_DOWN -> {
// On some devices, the back gesture / button press is not intercepted by androidx // On some devices, the back gesture / button press is not intercepted by androidx
@ -453,12 +445,4 @@ class EmulationActivity : AppCompatActivity() {
OnFilePickerResult(result.toString()) OnFilePickerResult(result.toString())
} }
companion object {
fun stopForegroundService(activity: Activity) {
val startIntent = Intent(activity, ForegroundService::class.java)
startIntent.action = ForegroundService.ACTION_STOP
activity.startForegroundService(startIntent)
}
}
} }

View file

@ -8,5 +8,7 @@ enum class Hotkey(val button: Int) {
SWAP_SCREEN(10001), SWAP_SCREEN(10001),
CYCLE_LAYOUT(10002), CYCLE_LAYOUT(10002),
CLOSE_GAME(10003), CLOSE_GAME(10003),
PAUSE_OR_RESUME(10004); PAUSE_OR_RESUME(10004),
QUICKSAVE(10005),
QUICKLOAD(10006);
} }

View file

@ -4,10 +4,14 @@
package org.citra.citra_emu.features.hotkeys 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.utils.EmulationLifecycleUtil
import org.citra.citra_emu.display.ScreenAdjustmentUtil 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 } val hotkeyButtons = Hotkey.entries.map { it.button }
@ -18,6 +22,23 @@ class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() 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 -> {} else -> {}
} }
return true return true

View file

@ -40,7 +40,9 @@ enum class IntSetting(
VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1), VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1),
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0), DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0),
TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 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 override var int: Int = defaultValue
@ -68,7 +70,8 @@ enum class IntSetting(
DEBUG_RENDERER, DEBUG_RENDERER,
CPU_JIT, CPU_JIT,
ASYNC_CUSTOM_LOADING, ASYNC_CUSTOM_LOADING,
AUDIO_INPUT_TYPE AUDIO_INPUT_TYPE,
USE_ARTIC_BASE_CONTROLLER
) )
fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key } fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }

View file

@ -136,6 +136,8 @@ class Settings {
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout" const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
const val HOTKEY_CLOSE_GAME = "hotkey_close_game" const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_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( val buttonKeys = listOf(
KEY_BUTTON_A, KEY_BUTTON_A,
@ -187,13 +189,17 @@ class Settings {
HOTKEY_SCREEN_SWAP, HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT, HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME, HOTKEY_CLOSE_GAME,
HOTKEY_PAUSE_OR_RESUME HOTKEY_PAUSE_OR_RESUME,
HOTKEY_QUICKSAVE,
HOTKEY_QUICKlOAD
) )
val hotkeyTitles = listOf( val hotkeyTitles = listOf(
R.string.emulation_swap_screens, R.string.emulation_swap_screens,
R.string.emulation_cycle_landscape_layouts, R.string.emulation_cycle_landscape_layouts,
R.string.emulation_close_game, 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" const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"

View file

@ -133,6 +133,8 @@ class InputBindingSetting(
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.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 else -> -1
} }
@ -222,8 +224,10 @@ class InputBindingSetting(
Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show()
return 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 value = uiString
} }
@ -283,9 +287,17 @@ class InputBindingSetting(
/** /**
* Helper function to get the settings key for an gamepad button. * 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}" 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. * Helper function to get the settings key for an gamepad axis.
*/ */
@ -301,5 +313,23 @@ class InputBindingSetting(
*/ */
fun getInputAxisOrientationKey(axis: Int): String = fun getInputAxisOrientationKey(axis: Int): String =
"${getInputAxisKey(axis)}_GuestOrientation" "${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
}
}
} }
} }

View file

@ -626,6 +626,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
val button = getInputObject(key) val button = getInputObject(key)
add(InputBindingSetting(button, Settings.hotkeyTitles[i])) 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 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(HeaderSetting(R.string.stereoscopy))
add( add(

View file

@ -481,12 +481,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.setOnMenuItemClickListener { popupMenu.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_emulation_save_state -> { R.id.menu_emulation_save_state -> {
showSaveStateSubmenu() showStateSubmenu(true)
true true
} }
R.id.menu_emulation_load_state -> { R.id.menu_emulation_load_state -> {
showLoadStateSubmenu() showStateSubmenu(false)
true true
} }
@ -497,7 +497,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show() popupMenu.show()
} }
private fun showSaveStateSubmenu() { private fun showStateSubmenu(isSaving: Boolean) {
val savestates = NativeLibrary.getSavestateInfo() val savestates = NativeLibrary.getSavestateInfo()
val popupMenu = PopupMenu( val popupMenu = PopupMenu(
@ -507,19 +508,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menu.apply { popupMenu.menu.apply {
for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) { for (i in 0 until NativeLibrary.SAVESTATE_SLOT_COUNT) {
val slot = i + 1 val slot = i
val text = getString(R.string.emulation_empty_state_slot, slot) var enableClick = isSaving
add(text).setEnabled(true).setOnMenuItemClickListener { val text = if (slot == NativeLibrary.QUICKSAVE_SLOT) {
displaySavestateWarning() enableClick = false
NativeLibrary.saveState(slot) 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 true
} }
} }
} }
savestates?.forEach { savestates?.forEach {
val text = getString(R.string.emulation_occupied_state_slot, it.slot, it.time) var enableClick = true
popupMenu.menu.getItem(it.slot - 1).setTitle(text) 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() popupMenu.show()

View file

@ -16,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -23,14 +24,19 @@ import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.R import org.citra.citra_emu.R
import org.citra.citra_emu.adapters.HomeSettingAdapter 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.databinding.FragmentHomeSettingsBinding
import org.citra.citra_emu.features.settings.model.Settings 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.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile 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.model.HomeSetting
import org.citra.citra_emu.ui.main.MainActivity import org.citra.citra_emu.ui.main.MainActivity
import org.citra.citra_emu.utils.GameHelper import org.citra.citra_emu.utils.GameHelper
@ -76,6 +82,44 @@ class HomeSettingsFragment : Fragment() {
R.drawable.ic_settings, R.drawable.ic_settings,
{ SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } { 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( HomeSetting(
R.string.system_files, R.string.system_files,
R.string.system_files_description, R.string.system_files_description,

View file

@ -156,9 +156,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
} }
// Dismiss previous notifications (should not happen unless a crash occurred)
EmulationActivity.stopForegroundService(this)
setInsets() setInsets()
} }
@ -170,7 +167,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
override fun onDestroy() { override fun onDestroy() {
EmulationActivity.stopForegroundService(this)
super.onDestroy() super.onDestroy()
} }

View file

@ -128,6 +128,8 @@ void Config::ReadValues() {
static_cast<u16>(sdl2_config->GetInteger("Controls", "udp_input_port", static_cast<u16>(sdl2_config->GetInteger("Controls", "udp_input_port",
InputCommon::CemuhookUDP::DEFAULT_PORT)); InputCommon::CemuhookUDP::DEFAULT_PORT));
ReadSetting("Controls", Settings::values.use_artic_base_controller);
// Core // Core
ReadSetting("Core", Settings::values.use_cpu_jit); ReadSetting("Core", Settings::values.use_cpu_jit);
ReadSetting("Core", Settings::values.cpu_clock_percentage); 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_red);
ReadSetting("Renderer", Settings::values.bg_green); ReadSetting("Renderer", Settings::values.bg_green);
ReadSetting("Renderer", Settings::values.bg_blue); ReadSetting("Renderer", Settings::values.bg_blue);
ReadSetting("Renderer", Settings::values.delay_game_render_thread_us);
// Layout // Layout
Settings::values.layout_option = static_cast<Settings::LayoutOption>(sdl2_config->GetInteger( Settings::values.layout_option = static_cast<Settings::LayoutOption>(sdl2_config->GetInteger(

View file

@ -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) # The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0)
udp_pad_index= udp_pad_index=
# Use Artic Controller when connected to Artic Base Server. (Default 0)
use_artic_base_controller=
[Core] [Core]
# Whether to use the Just-In-Time (JIT) compiler for CPU emulation # Whether to use the Just-In-Time (JIT) compiler for CPU emulation
# 0: Interpreter (slow), 1 (default): JIT (fast) # 0: Interpreter (slow), 1 (default): JIT (fast)
@ -175,6 +178,10 @@ anaglyph_shader_name =
# 0: Nearest, 1 (default): Linear # 0: Nearest, 1 (default): Linear
filter_mode = 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]
# Layout for the screen inside the render window. # 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 # 0 (default): Default Top Bottom Screen, 1: Single Screen Only, 2: Large Screen Small Screen, 3: Side by Side

View file

@ -27,14 +27,18 @@ static void UpdateLandscapeScreenLayout() {
IDCache::GetNativeLibraryClass(), IDCache::GetLandscapeScreenLayout())); IDCache::GetNativeLibraryClass(), IDCache::GetLandscapeScreenLayout()));
} }
void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
render_window = surface; if (render_window == surface) {
return false;
}
render_window = surface;
window_info.type = Frontend::WindowSystemType::Android; window_info.type = Frontend::WindowSystemType::Android;
window_info.render_surface = surface; window_info.render_surface = surface;
StopPresenting(); StopPresenting();
OnFramebufferSizeChanged(); OnFramebufferSizeChanged();
return true;
} }
bool EmuWindow_Android::OnTouchEvent(int x, int y, bool pressed) { bool EmuWindow_Android::OnTouchEvent(int x, int y, bool pressed) {

View file

@ -17,7 +17,7 @@ public:
~EmuWindow_Android(); ~EmuWindow_Android();
/// Called by the onSurfaceChanges() method to change the surface /// 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) /// Handles touch event that occur.(Touched or released)
bool OnTouchEvent(int x, int y, bool pressed); bool OnTouchEvent(int x, int y, bool pressed);

View file

@ -82,6 +82,7 @@ static jobject ToJavaCoreError(Core::System::ResultStatus result) {
static const std::map<Core::System::ResultStatus, const char*> CoreErrorNameMap{ static const std::map<Core::System::ResultStatus, const char*> CoreErrorNameMap{
{Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"},
{Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"},
{Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"},
{Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"},
}; };
@ -178,6 +179,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
auto app_loader = Loader::GetLoader(filepath); auto app_loader = Loader::GetLoader(filepath);
if (app_loader) { if (app_loader) {
app_loader->ReadProgramId(program_id); app_loader->ReadProgramId(program_id);
system.RegisterAppLoaderEarly(app_loader);
GameSettings::LoadOverrides(program_id); GameSettings::LoadOverrides(program_id);
} }
system.ApplySettings(); system.ApplySettings();
@ -231,6 +233,10 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
InputManager::NDKMotionHandler()->DisableSensors(); InputManager::NDKMotionHandler()->DisableSensors();
if (!HandleCoreError(result, system.GetStatusDetails())) { if (!HandleCoreError(result, system.GetStatusDetails())) {
// Frontend requests us to abort // 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; return result;
} }
InputManager::NDKMotionHandler()->EnableSensors(); InputManager::NDKMotionHandler()->EnableSensors();
@ -288,12 +294,13 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
jobject surf) { jobject surf) {
s_surf = ANativeWindow_fromSurface(env, surf); s_surf = ANativeWindow_fromSurface(env, surf);
bool notify = false;
if (window) { if (window) {
window->OnSurfaceChanged(s_surf); notify = window->OnSurfaceChanged(s_surf);
} }
auto& system = Core::System::GetInstance(); auto& system = Core::System::GetInstance();
if (system.IsPoweredOn()) { if (notify && system.IsPoweredOn()) {
system.GPU().Renderer().NotifySurfaceChanged(); 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) { if (stop_run || pause_emulation) {
return; return;
} }
window->TryPresenting(); if (window) {
window->TryPresenting();
}
} }
void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver( void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver(

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M200,840q-33,0 -56.5,-23.5T120,760q0,-33 23.5,-56.5T200,680q33,0 56.5,23.5T280,760q0,33 -23.5,56.5T200,840ZM680,840q0,-117 -44,-218.5T516,444q-76,-76 -177.5,-120T120,280v-120q142,0 265,53t216,146q93,93 146,216t53,265L680,840ZM440,840q0,-67 -25,-124.5T346,614q-44,-44 -101.5,-69T120,520v-120q92,0 171.5,34.5T431,529q60,60 94.5,139.5T560,840L440,840Z"/>
</vector>

View file

@ -657,4 +657,16 @@ Se esperan fallos gráficos temporales cuando ésta esté activado.</string>
<string name="november">Noviembre</string> <string name="november">Noviembre</string>
<string name="december">Diciembre</string> <string name="december">Diciembre</string>
<!-- Artic base -->
<string name="artic_server_comm_error">Fallo de comunicación con el servidor Artic Base. La emulación se detendrá.</string>
<string name="artic_base">Artic Base</string>
<string name="artic_base_connect">Conectar con Artic Base</string>
<string name="artic_base_connect_description">Conectar con una consola real que esté ejecutando un servidor Artic Base</string>
<string name="artic_base_enter_address">Introduce la dirección del servidor Artic Base</string>
<string name="delay_render_thread">Retrasa el hilo de dibujado del juego</string>
<string name="delay_render_thread_description">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.</string>
<string name="miscellaneous">Misceláneo</string>
<string name="use_artic_base_controller">Usar Artic Controller cuando se está conectado a Artic Base Server</string>
<string name="use_artic_base_controller_desc">Usa los controles proporcionados por Artic Base Server cuando esté conectado a él en lugar del dispositivo de entrada configurado.</string>
</resources> </resources>

View file

@ -683,4 +683,25 @@
<string name="november">November</string> <string name="november">November</string>
<string name="december">December</string> <string name="december">December</string>
<!-- Artic base -->
<string name="artic_server_comm_error">Failed to communicate with the Artic Base server. Emulation will stop.</string>
<string name="artic_base">Artic Base</string>
<string name="artic_base_connect_description">Connect to a real console that is running an Artic Base server</string>
<string name="artic_base_connect">Connect to Artic Base</string>
<string name="artic_base_enter_address">Enter Artic Base server address</string>
<string name="delay_render_thread">Delay game render thread</string>
<string name="delay_render_thread_description">Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) dynamic-fps games.</string>
<!-- Quickload&Save-->
<string name="emulation_quicksave_slot">Quicksave</string>
<string name="emulation_quicksave">Quicksave</string>
<string name="emulation_quickload">Quickload</string>
<string name="emulation_occupied_quicksave_slot">Quicksave - %1$tF %1$tR</string>
<string name="quicksave_saving">Saving…</string>
<string name="quickload_loading">Loading…</string>
<string name="quickload_not_found">No Quicksave available.</string>
<string name="miscellaneous">Miscellaneous</string>
<string name="use_artic_base_controller">Use Artic Controller when connected to Artic Base Server</string>
<string name="use_artic_base_controller_desc">Use the controls provided by Artic Base Server when connected to it instead of the configured input device.</string>
</resources> </resources>

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View file

@ -147,6 +147,7 @@ void Config::ReadValues() {
ReadSetting("Renderer", Settings::values.use_vsync_new); ReadSetting("Renderer", Settings::values.use_vsync_new);
ReadSetting("Renderer", Settings::values.texture_filter); ReadSetting("Renderer", Settings::values.texture_filter);
ReadSetting("Renderer", Settings::values.texture_sampling); 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.mono_render_option);
ReadSetting("Renderer", Settings::values.render_3d); ReadSetting("Renderer", Settings::values.render_3d);

View file

@ -81,6 +81,9 @@ add_executable(citra-qt
configuration/configure_ui.cpp configuration/configure_ui.cpp
configuration/configure_ui.h configuration/configure_ui.h
configuration/configure_ui.ui configuration/configure_ui.ui
configuration/configure_web.cpp
configuration/configure_web.h
configuration/configure_web.ui
configuration/configure_cheats.cpp configuration/configure_cheats.cpp
configuration/configure_cheats.h configuration/configure_cheats.h
configuration/configure_cheats.ui configuration/configure_cheats.ui

View file

@ -327,6 +327,8 @@ void Config::ReadCameraValues() {
void Config::ReadControlValues() { void Config::ReadControlValues() {
qt_config->beginGroup(QStringLiteral("Controls")); qt_config->beginGroup(QStringLiteral("Controls"));
ReadBasicSetting(Settings::values.use_artic_base_controller);
int num_touch_from_button_maps = int num_touch_from_button_maps =
qt_config->beginReadArray(QStringLiteral("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.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.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList();
UISettings::values.language = ReadSetting(QStringLiteral("language"), QString{}).toString(); UISettings::values.language = ReadSetting(QStringLiteral("language"), QString{}).toString();
} }
@ -665,6 +669,8 @@ void Config::ReadRendererValues() {
ReadGlobalSetting(Settings::values.texture_filter); ReadGlobalSetting(Settings::values.texture_filter);
ReadGlobalSetting(Settings::values.texture_sampling); ReadGlobalSetting(Settings::values.texture_sampling);
ReadGlobalSetting(Settings::values.delay_game_render_thread_us);
if (global) { if (global) {
ReadBasicSetting(Settings::values.use_shader_jit); ReadBasicSetting(Settings::values.use_shader_jit);
} }
@ -920,6 +926,8 @@ void Config::SaveCameraValues() {
void Config::SaveControlValues() { void Config::SaveControlValues() {
qt_config->beginGroup(QStringLiteral("Controls")); qt_config->beginGroup(QStringLiteral("Controls"));
WriteBasicSetting(Settings::values.use_artic_base_controller);
WriteSetting(QStringLiteral("profile"), Settings::values.current_input_profile_index, 0); WriteSetting(QStringLiteral("profile"), Settings::values.current_input_profile_index, 0);
qt_config->beginWriteArray(QStringLiteral("profiles")); qt_config->beginWriteArray(QStringLiteral("profiles"));
for (std::size_t p = 0; p < Settings::values.input_profiles.size(); ++p) { 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); WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true);
} }
qt_config->endArray(); 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("recentFiles"), UISettings::values.recent_files);
WriteSetting(QStringLiteral("language"), UISettings::values.language, QString{}); WriteSetting(QStringLiteral("language"), UISettings::values.language, QString{});
} }
@ -1164,6 +1174,8 @@ void Config::SaveRendererValues() {
WriteGlobalSetting(Settings::values.texture_filter); WriteGlobalSetting(Settings::values.texture_filter);
WriteGlobalSetting(Settings::values.texture_sampling); WriteGlobalSetting(Settings::values.texture_sampling);
WriteGlobalSetting(Settings::values.delay_game_render_thread_us);
if (global) { if (global) {
WriteSetting(QStringLiteral("use_shader_jit"), Settings::values.use_shader_jit.GetValue(), WriteSetting(QStringLiteral("use_shader_jit"), Settings::values.use_shader_jit.GetValue(),
true); true);

View file

@ -97,6 +97,12 @@
<header>configuration/configure_enhancements.h</header> <header>configuration/configure_enhancements.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>ConfigureWeb</class>
<extends>QWidget</extends>
<header>configuration/configure_web.h</header>
<container>1</container>
</customwidget>
<customwidget> <customwidget>
<class>ConfigureUi</class> <class>ConfigureUi</class>
<extends>QWidget</extends> <extends>QWidget</extends>

View file

@ -86,7 +86,7 @@
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout1">
<item> <item>
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
@ -100,7 +100,7 @@
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout2">
<item> <item>
<widget class="QCheckBox" name="toggle_console"> <widget class="QCheckBox" name="toggle_console">
<property name="text"> <property name="text">
@ -125,7 +125,7 @@
<property name="title"> <property name="title">
<string>CPU</string> <string>CPU</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="clock_speed_GLayout">
<item row="1" column="0"> <item row="1" column="0">
<widget class="QWidget" name="clock_speed_widget" native="true"> <widget class="QWidget" name="clock_speed_widget" native="true">
<layout class="QHBoxLayout" name="clock_speed_layout"> <layout class="QHBoxLayout" name="clock_speed_layout">

View file

@ -16,6 +16,7 @@
#include "citra_qt/configuration/configure_storage.h" #include "citra_qt/configuration/configure_storage.h"
#include "citra_qt/configuration/configure_system.h" #include "citra_qt/configuration/configure_system.h"
#include "citra_qt/configuration/configure_ui.h" #include "citra_qt/configuration/configure_ui.h"
#include "citra_qt/configuration/configure_web.h"
#include "citra_qt/hotkeys.h" #include "citra_qt/hotkeys.h"
#include "common/settings.h" #include "common/settings.h"
#include "core/core.h" #include "core/core.h"
@ -28,7 +29,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor
system{system_}, is_powered_on{system.IsPoweredOn()}, system{system_}, is_powered_on{system.IsPoweredOn()},
general_tab{std::make_unique<ConfigureGeneral>(this)}, general_tab{std::make_unique<ConfigureGeneral>(this)},
system_tab{std::make_unique<ConfigureSystem>(system, this)}, system_tab{std::make_unique<ConfigureSystem>(system, this)},
input_tab{std::make_unique<ConfigureInput>(this)}, input_tab{std::make_unique<ConfigureInput>(system, this)},
hotkeys_tab{std::make_unique<ConfigureHotkeys>(this)}, hotkeys_tab{std::make_unique<ConfigureHotkeys>(this)},
graphics_tab{ graphics_tab{
std::make_unique<ConfigureGraphics>(gl_renderer, physical_devices, is_powered_on, this)}, std::make_unique<ConfigureGraphics>(gl_renderer, physical_devices, is_powered_on, this)},
@ -37,7 +38,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor
camera_tab{std::make_unique<ConfigureCamera>(this)}, camera_tab{std::make_unique<ConfigureCamera>(this)},
debug_tab{std::make_unique<ConfigureDebug>(is_powered_on, this)}, debug_tab{std::make_unique<ConfigureDebug>(is_powered_on, this)},
storage_tab{std::make_unique<ConfigureStorage>(is_powered_on, this)}, storage_tab{std::make_unique<ConfigureStorage>(is_powered_on, this)},
ui_tab{std::make_unique<ConfigureUi>(this)} { web_tab{std::make_unique<ConfigureWeb>(this)}, ui_tab{std::make_unique<ConfigureUi>(this)} {
Settings::SetConfiguringGlobal(true); Settings::SetConfiguringGlobal(true);
ui->setupUi(this); 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(camera_tab.get(), tr("Camera"));
ui->tabWidget->addTab(debug_tab.get(), tr("Debug")); ui->tabWidget->addTab(debug_tab.get(), tr("Debug"));
ui->tabWidget->addTab(storage_tab.get(), tr("Storage")); ui->tabWidget->addTab(storage_tab.get(), tr("Storage"));
ui->tabWidget->addTab(web_tab.get(), tr("Web"));
ui->tabWidget->addTab(ui_tab.get(), tr("UI")); ui->tabWidget->addTab(ui_tab.get(), tr("UI"));
hotkeys_tab->Populate(registry); hotkeys_tab->Populate(registry);
@ -87,6 +89,7 @@ void ConfigureDialog::SetConfiguration() {
audio_tab->SetConfiguration(); audio_tab->SetConfiguration();
camera_tab->SetConfiguration(); camera_tab->SetConfiguration();
debug_tab->SetConfiguration(); debug_tab->SetConfiguration();
web_tab->SetConfiguration();
ui_tab->SetConfiguration(); ui_tab->SetConfiguration();
storage_tab->SetConfiguration(); storage_tab->SetConfiguration();
} }
@ -102,6 +105,7 @@ void ConfigureDialog::ApplyConfiguration() {
audio_tab->ApplyConfiguration(); audio_tab->ApplyConfiguration();
camera_tab->ApplyConfiguration(); camera_tab->ApplyConfiguration();
debug_tab->ApplyConfiguration(); debug_tab->ApplyConfiguration();
web_tab->ApplyConfiguration();
ui_tab->ApplyConfiguration(); ui_tab->ApplyConfiguration();
storage_tab->ApplyConfiguration(); storage_tab->ApplyConfiguration();
system.ApplySettings(); system.ApplySettings();
@ -114,7 +118,7 @@ void ConfigureDialog::PopulateSelectionList() {
ui->selectorList->clear(); ui->selectorList->clear();
const std::array<std::pair<QString, QList<QWidget*>>, 5> items{ const std::array<std::pair<QString, QList<QWidget*>>, 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("System"), {system_tab.get(), camera_tab.get(), storage_tab.get()}},
{tr("Graphics"), {enhancements_tab.get(), graphics_tab.get()}}, {tr("Graphics"), {enhancements_tab.get(), graphics_tab.get()}},
{tr("Audio"), {audio_tab.get()}}, {tr("Audio"), {audio_tab.get()}},
@ -154,6 +158,7 @@ void ConfigureDialog::RetranslateUI() {
audio_tab->RetranslateUI(); audio_tab->RetranslateUI();
camera_tab->RetranslateUI(); camera_tab->RetranslateUI();
debug_tab->RetranslateUI(); debug_tab->RetranslateUI();
web_tab->RetranslateUI();
ui_tab->RetranslateUI(); ui_tab->RetranslateUI();
storage_tab->RetranslateUI(); storage_tab->RetranslateUI();
} }
@ -173,6 +178,7 @@ void ConfigureDialog::UpdateVisibleTabs() {
{camera_tab.get(), tr("Camera")}, {camera_tab.get(), tr("Camera")},
{debug_tab.get(), tr("Debug")}, {debug_tab.get(), tr("Debug")},
{storage_tab.get(), tr("Storage")}, {storage_tab.get(), tr("Storage")},
{web_tab.get(), tr("Web")},
{ui_tab.get(), tr("UI")}}; {ui_tab.get(), tr("UI")}};
ui->tabWidget->clear(); ui->tabWidget->clear();

View file

@ -29,6 +29,7 @@ class ConfigureAudio;
class ConfigureCamera; class ConfigureCamera;
class ConfigureDebug; class ConfigureDebug;
class ConfigureStorage; class ConfigureStorage;
class ConfigureWeb;
class ConfigureUi; class ConfigureUi;
class ConfigureDialog : public QDialog { class ConfigureDialog : public QDialog {
@ -69,5 +70,6 @@ private:
std::unique_ptr<ConfigureCamera> camera_tab; std::unique_ptr<ConfigureCamera> camera_tab;
std::unique_ptr<ConfigureDebug> debug_tab; std::unique_ptr<ConfigureDebug> debug_tab;
std::unique_ptr<ConfigureStorage> storage_tab; std::unique_ptr<ConfigureStorage> storage_tab;
std::unique_ptr<ConfigureWeb> web_tab;
std::unique_ptr<ConfigureUi> ui_tab; std::unique_ptr<ConfigureUi> ui_tab;
}; };

View file

@ -26,6 +26,10 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::span<const QStrin
// Set the index to -1 to ensure the below lambda is called with setCurrentIndex // Set the index to -1 to ensure the below lambda is called with setCurrentIndex
ui->graphics_api_combo->setCurrentIndex(-1); ui->graphics_api_combo->setCurrentIndex(-1);
const auto width = static_cast<int>(QString::fromStdString("000000000").size() * 6);
ui->delay_render_display_label->setMinimumWidth(width);
ui->delay_render_combo->setVisible(!Settings::IsConfiguringGlobal());
auto graphics_api_combo_model = auto graphics_api_combo_model =
qobject_cast<QStandardItemModel*>(ui->graphics_api_combo->model()); qobject_cast<QStandardItemModel*>(ui->graphics_api_combo->model());
#ifndef ENABLE_SOFTWARE_RENDERER #ifndef ENABLE_SOFTWARE_RENDERER
@ -82,12 +86,25 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::span<const QStrin
connect(ui->graphics_api_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, connect(ui->graphics_api_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
&ConfigureGraphics::SetPhysicalDeviceComboVisibility); &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(); SetConfiguration();
} }
ConfigureGraphics::~ConfigureGraphics() = default; ConfigureGraphics::~ConfigureGraphics() = default;
void ConfigureGraphics::SetConfiguration() { 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()) { if (!Settings::IsConfiguringGlobal()) {
ConfigurationShared::SetHighlight(ui->graphics_api_group, ConfigurationShared::SetHighlight(ui->graphics_api_group,
!Settings::values.graphics_api.UsingGlobal()); !Settings::values.graphics_api.UsingGlobal());
@ -101,6 +118,16 @@ void ConfigureGraphics::SetConfiguration() {
&Settings::values.texture_sampling); &Settings::values.texture_sampling);
ConfigurationShared::SetHighlight(ui->widget_texture_sampling, ConfigurationShared::SetHighlight(ui->widget_texture_sampling,
!Settings::values.texture_sampling.UsingGlobal()); !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 { } else {
ui->graphics_api_combo->setCurrentIndex( ui->graphics_api_combo->setCurrentIndex(
static_cast<int>(Settings::values.graphics_api.GetValue())); static_cast<int>(Settings::values.graphics_api.GetValue()));
@ -144,6 +171,9 @@ void ConfigureGraphics::ApplyConfiguration() {
ui->toggle_disk_shader_cache, use_disk_shader_cache); ui->toggle_disk_shader_cache, use_disk_shader_cache);
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync_new, ui->toggle_vsync_new, ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync_new, ui->toggle_vsync_new,
use_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()) { if (Settings::IsConfiguringGlobal()) {
Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked(); 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->toggle_async_present->setEnabled(Settings::values.async_presentation.UsingGlobal());
ui->graphics_api_combo->setEnabled(Settings::values.graphics_api.UsingGlobal()); ui->graphics_api_combo->setEnabled(Settings::values.graphics_api.UsingGlobal());
ui->physical_device_combo->setEnabled(Settings::values.physical_device.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; return;
} }
connect(ui->delay_render_combo, qOverload<int>(&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); ui->toggle_shader_jit->setVisible(false);
ConfigurationShared::SetColoredComboBox( ConfigurationShared::SetColoredComboBox(

View file

@ -307,6 +307,83 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QWidget" name="delay_render_layout" native="true">
<layout class="QHBoxLayout" name="delay_render_layout_inner">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="delay_render_combo">
<item>
<property name="text">
<string>Use global</string>
</property>
</item>
<item>
<property name="text">
<string>Use per-game</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_delay_render">
<property name="text">
<string>Delay game render thread:</string>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Delays the emulated game render thread the specified amount of milliseconds every time it submits render commands to the GPU.&lt;/p&gt;&lt;p&gt;Adjust this feature in the (very few) dynamic-fps games to fix performance issues.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="delay_render_slider">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>16000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="pageStep">
<number>250</number>
</property>
<property name="value">
<number>0</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="delay_render_display_label">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View file

@ -16,6 +16,7 @@
#include "citra_qt/configuration/configure_input.h" #include "citra_qt/configuration/configure_input.h"
#include "citra_qt/configuration/configure_motion_touch.h" #include "citra_qt/configuration/configure_motion_touch.h"
#include "common/param_package.h" #include "common/param_package.h"
#include "core/core.h"
#include "ui_configure_input.h" #include "ui_configure_input.h"
const std::array<std::string, ConfigureInput::ANALOG_SUB_BUTTONS_NUM> const std::array<std::string, ConfigureInput::ANALOG_SUB_BUTTONS_NUM>
@ -145,8 +146,8 @@ static QString AnalogToText(const Common::ParamPackage& param, const std::string
return QObject::tr("[unknown]"); return QObject::tr("[unknown]");
} }
ConfigureInput::ConfigureInput(QWidget* parent) ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent)
: QWidget(parent), ui(std::make_unique<Ui::ConfigureInput>()), : QWidget(parent), system(_system), ui(std::make_unique<Ui::ConfigureInput>()),
timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) { timeout_timer(std::make_unique<QTimer>()), poll_timer(std::make_unique<QTimer>()) {
ui->setupUi(this); ui->setupUi(this);
setFocusPolicy(Qt::ClickFocus); setFocusPolicy(Qt::ClickFocus);
@ -400,6 +401,9 @@ ConfigureInput::ConfigureInput(QWidget* parent)
ConfigureInput::~ConfigureInput() = default; ConfigureInput::~ConfigureInput() = default;
void ConfigureInput::ApplyConfiguration() { void ConfigureInput::ApplyConfiguration() {
Settings::values.use_artic_base_controller = ui->use_artic_controller->isChecked();
std::transform(buttons_param.begin(), buttons_param.end(), std::transform(buttons_param.begin(), buttons_param.end(),
Settings::values.current_input_profile.buttons.begin(), Settings::values.current_input_profile.buttons.begin(),
[](const Common::ParamPackage& param) { return param.Serialize(); }); [](const Common::ParamPackage& param) { return param.Serialize(); });
@ -444,6 +448,10 @@ QList<QKeySequence> ConfigureInput::GetUsedKeyboardKeys() {
} }
void ConfigureInput::LoadConfiguration() { 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(), std::transform(Settings::values.current_input_profile.buttons.begin(),
Settings::values.current_input_profile.buttons.end(), buttons_param.begin(), Settings::values.current_input_profile.buttons.end(), buttons_param.begin(),
[](const std::string& str) { return Common::ParamPackage(str); }); [](const std::string& str) { return Common::ParamPackage(str); });

View file

@ -30,7 +30,7 @@ class ConfigureInput : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit ConfigureInput(QWidget* parent = nullptr); explicit ConfigureInput(Core::System& system, QWidget* parent = nullptr);
~ConfigureInput() override; ~ConfigureInput() override;
/// Save all button configurations to settings file /// Save all button configurations to settings file
@ -50,6 +50,7 @@ signals:
void InputKeysChanged(QList<QKeySequence> new_key_list); void InputKeysChanged(QList<QKeySequence> new_key_list);
private: private:
Core::System& system;
std::unique_ptr<Ui::ConfigureInput> ui; std::unique_ptr<Ui::ConfigureInput> ui;
std::unique_ptr<QTimer> timeout_timer; std::unique_ptr<QTimer> timeout_timer;

View file

@ -841,6 +841,13 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QCheckBox" name="use_artic_controller">
<property name="text">
<string>Use Artic Controller when connected to Artic Base Server</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>

View file

@ -151,7 +151,14 @@ void ConfigurePerGame::LoadConfiguration() {
ui->display_title_id->setText( ui->display_title_id->setText(
QStringLiteral("%1").arg(title_id, 16, 16, QLatin1Char{'0'}).toUpper()); QStringLiteral("%1").arg(title_id, 16, 16, QLatin1Char{'0'}).toUpper());
const auto loader = Loader::GetLoader(filename); std::unique_ptr<Loader::AppLoader> loader_ptr;
Loader::AppLoader* loader;
if (system.IsPoweredOn()) {
loader = &system.GetAppLoader();
} else {
loader_ptr = Loader::GetLoader(filename);
loader = loader_ptr.get();
}
std::string title; std::string title;
if (loader->ReadTitle(title) == Loader::ResultStatus::Success) if (loader->ReadTitle(title) == Loader::ResultStatus::Success)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,36 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <QIcon>
#include <QMessageBox>
#include <QtConcurrent/QtConcurrentRun>
#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::ConfigureWeb>()) {
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);
}

View file

@ -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 <memory>
#include <QFutureWatcher>
#include <QWidget>
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::ConfigureWeb> ui;
};

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConfigureWeb</class>
<widget class="QWidget" name="ConfigureWeb">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>996</width>
<height>561</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="discord_group">
<property name="title">
<string>Discord Presence</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_21">
<item>
<widget class="QCheckBox" name="toggle_discordrpc">
<property name="text">
<string>Show Current Game in your Discord Status</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<tabstops>
<tabstop>toggle_discordrpc</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -381,6 +381,10 @@ void GMainWindow::InitializeWidgets() {
progress_bar->hide(); progress_bar->hide();
statusBar()->addPermanentWidget(progress_bar); 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 = new QLabel();
emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% "
"indicate emulation is running faster or slower than a 3DS.")); "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 " 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.")); "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->setVisible(false);
label->setFrameStyle(QFrame::NoFrame); label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0); label->setContentsMargins(4, 0, 4, 0);
@ -866,6 +871,7 @@ void GMainWindow::ConnectMenuEvents() {
// File // File
connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile);
connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); 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++) { for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) {
connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), connect_menu(ui->menu_Boot_Home_Menu->actions().at(region),
[this, region] { OnMenuBootHomeMenu(region); }); [this, region] { OnMenuBootHomeMenu(region); });
@ -935,6 +941,10 @@ void GMainWindow::ConnectMenuEvents() {
// Help // Help
connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder); 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, []() { connect_menu(ui->action_FAQ, []() {
QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/")));
}); });
@ -964,7 +974,7 @@ void GMainWindow::UpdateMenuState() {
action->setEnabled(emulation_running); 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) { if (emulation_running && is_paused) {
ui->action_Pause->setText(tr("&Continue")); 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.")); tr("GBA Virtual Console ROMs are not supported by Citra."));
break; 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: default:
QMessageBox::critical( QMessageBox::critical(
this, tr("Error while loading ROM!"), this, tr("Error while loading ROM!"),
@ -1223,7 +1241,13 @@ bool GMainWindow::LoadROM(const QString& filename) {
} }
void GMainWindow::BootGame(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( const auto answer = QMessageBox::question(
this, tr("CIA must be installed before usage"), this, tr("CIA must be installed before usage"),
tr("Before using this CIA, you must install it. Do you want to install it now?"), 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; return;
} }
show_artic_label = is_artic;
LOG_INFO(Frontend, "Citra starting..."); 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) { if (movie_record_on_start) {
movie.PrepareForRecording(); movie.PrepareForRecording();
@ -1246,16 +1274,26 @@ void GMainWindow::BootGame(const QString& filename) {
} }
const std::string path = filename.toStdString(); const std::string path = filename.toStdString();
const auto loader = Loader::GetLoader(path); auto loader = Loader::GetLoader(path);
u64 title_id{0}; 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(); system.ApplySettings();
Settings::LogSettings(); Settings::LogSettings();
@ -1265,8 +1303,11 @@ void GMainWindow::BootGame(const QString& filename) {
game_list->SaveInterfaceLayout(); game_list->SaveInterfaceLayout();
config->Save(); config->Save();
if (!LoadROM(filename)) if (!LoadROM(filename)) {
render_window->ReleaseRenderTarget();
secondary_window->ReleaseRenderTarget();
return; return;
}
// Set everything up // Set everything up
if (movie_record_on_start) { if (movie_record_on_start) {
@ -1420,6 +1461,8 @@ void GMainWindow::ShutdownGame() {
// Disable status bar updates // Disable status bar updates
status_bar_update_timer.stop(); status_bar_update_timer.stop();
message_label_used_for_movie = false; message_label_used_for_movie = false;
show_artic_label = false;
artic_traffic_label->setVisible(false);
emu_speed_label->setVisible(false); emu_speed_label->setVisible(false);
game_fps_label->setVisible(false); game_fps_label->setVisible(false);
emu_frametime_label->setVisible(false); emu_frametime_label->setVisible(false);
@ -1759,6 +1802,17 @@ void GMainWindow::OnMenuInstallCIA() {
InstallCIA(filepaths); 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) { void GMainWindow::OnMenuBootHomeMenu(u32 region) {
BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region)));
} }
@ -2365,33 +2419,47 @@ void GMainWindow::OnSaveMovie() {
} }
void GMainWindow::OnCaptureScreenshot() { void GMainWindow::OnCaptureScreenshot() {
if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { if (!emu_thread) [[unlikely]] {
return; return;
} }
OnPauseGame(); const bool was_running = emu_thread->IsRunning();
std::string path = UISettings::values.screenshot_path.GetValue();
if (!FileUtil::IsDirectory(path)) { if (was_running ||
if (!FileUtil::CreateFullPath(path)) { (QMessageBox::question(
QMessageBox::information(this, tr("Invalid Screenshot Directory"), this, tr("Game will unpause"),
tr("Cannot create specified screenshot directory. Screenshot " tr("The game will be unpaused, and the next frame will be captured. Is this okay?"),
"path is set back to its default value.")); QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) {
path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); if (was_running) {
path.append("screenshots/"); OnPauseGame();
UISettings::values.screenshot_path = path; }
}; 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() { void GMainWindow::OnDumpVideo() {
@ -2575,6 +2643,53 @@ void GMainWindow::UpdateStatusBar() {
auto results = system.GetAndResetPerfStats(); 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<std::pair<Core::PerfStats::PerfArticEventBits, QString>, 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) { if (Settings::values.frame_limit.GetValue() == 0) {
emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
} else { } else {
@ -2585,6 +2700,9 @@ void GMainWindow::UpdateStatusBar() {
game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); 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)); 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); emu_speed_label->setVisible(true);
game_fps_label->setVisible(true); game_fps_label->setVisible(true);
emu_frametime_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; QString title, message;
QMessageBox::Icon error_severity_icon; QMessageBox::Icon error_severity_icon;
bool can_continue = true;
if (result == Core::System::ResultStatus::ErrorSystemFiles) { if (result == Core::System::ResultStatus::ErrorSystemFiles) {
const QString common_message = const QString common_message =
tr("%1 is missing. Please <a " tr("%1 is missing. Please <a "
@ -2756,6 +2875,13 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
title = tr("Save/load Error"); title = tr("Save/load Error");
message = QString::fromStdString(details); message = QString::fromStdString(details);
error_severity_icon = QMessageBox::Icon::Warning; error_severity_icon = QMessageBox::Icon::Warning;
} else if (result == Core::System::ResultStatus::ErrorArticDisconnected) {
title = tr("Artic Base Server");
message =
tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details)
.c_str());
error_severity_icon = QMessageBox::Icon::Critical;
can_continue = false;
} else { } else {
title = tr("Fatal Error"); title = tr("Fatal Error");
message = message =
@ -2772,12 +2898,14 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
message_box.setText(message); message_box.setText(message);
message_box.setIcon(error_severity_icon); message_box.setIcon(error_severity_icon);
if (error_severity_icon == QMessageBox::Icon::Critical) { if (error_severity_icon == QMessageBox::Icon::Critical) {
message_box.addButton(tr("Continue"), QMessageBox::RejectRole); if (can_continue) {
message_box.addButton(tr("Continue"), QMessageBox::RejectRole);
}
QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole);
if (result != Core::System::ResultStatus::ShutdownRequested) if (result != Core::System::ResultStatus::ShutdownRequested)
message_box.exec(); message_box.exec();
if (result == Core::System::ResultStatus::ShutdownRequested || if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested ||
message_box.clickedButton() == abort_button) { message_box.clickedButton() == abort_button) {
if (emu_thread) { if (emu_thread) {
ShutdownGame(); ShutdownGame();

View file

@ -216,6 +216,7 @@ private slots:
void OnConfigurePerGame(); void OnConfigurePerGame();
void OnMenuLoadFile(); void OnMenuLoadFile();
void OnMenuInstallCIA(); void OnMenuInstallCIA();
void OnMenuConnectArticBase();
void OnMenuBootHomeMenu(u32 region); void OnMenuBootHomeMenu(u32 region);
void OnUpdateProgress(std::size_t written, std::size_t total); void OnUpdateProgress(std::size_t written, std::size_t total);
void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath); void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath);
@ -302,6 +303,8 @@ private:
// Status bar elements // Status bar elements
QProgressBar* progress_bar = nullptr; QProgressBar* progress_bar = nullptr;
QLabel* message_label = nullptr; QLabel* message_label = nullptr;
bool show_artic_label = false;
QLabel* artic_traffic_label = nullptr;
QLabel* emu_speed_label = nullptr; QLabel* emu_speed_label = nullptr;
QLabel* game_fps_label = nullptr; QLabel* game_fps_label = nullptr;
QLabel* emu_frametime_label = nullptr; QLabel* emu_frametime_label = nullptr;

View file

@ -78,6 +78,7 @@
</widget> </widget>
<addaction name="action_Load_File"/> <addaction name="action_Load_File"/>
<addaction name="action_Install_CIA"/> <addaction name="action_Install_CIA"/>
<addaction name="action_Connect_Artic"/>
<addaction name="menu_Boot_Home_Menu"/> <addaction name="menu_Boot_Home_Menu"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="menu_recent_files"/> <addaction name="menu_recent_files"/>
@ -202,6 +203,7 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_Report_Compatibility"/> <addaction name="action_Report_Compatibility"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_Open_Log_Folder"/>
<addaction name="action_FAQ"/> <addaction name="action_FAQ"/>
<addaction name="action_About"/> <addaction name="action_About"/>
</widget> </widget>
@ -222,6 +224,11 @@
<string>Install CIA...</string> <string>Install CIA...</string>
</property> </property>
</action> </action>
<action name="action_Connect_Artic">
<property name="text">
<string>Connect to Artic Base...</string>
</property>
</action>
<action name="action_Boot_Home_Menu_JPN"> <action name="action_Boot_Home_Menu_JPN">
<property name="text"> <property name="text">
<string>JPN</string> <string>JPN</string>
@ -473,6 +480,14 @@
<string>Fullscreen</string> <string>Fullscreen</string>
</property> </property>
</action> </action>
<action name="action_Open_Log_Folder">
<property name="text">
<string>Open Log Folder</string>
</property>
<property name="toolTip">
<string>Opens the Citra Log folder</string>
</property>
</action>
<action name="action_Open_Maintenance_Tool"> <action name="action_Open_Maintenance_Tool">
<property name="text"> <property name="text">
<string>Modify Citra Install</string> <string>Modify Citra Install</string>

View file

@ -116,6 +116,7 @@ struct Values {
bool game_dir_deprecated_deepscan; bool game_dir_deprecated_deepscan;
QVector<UISettings::GameDir> game_dirs; QVector<UISettings::GameDir> game_dirs;
QStringList recent_files; QStringList recent_files;
QString last_artic_base_addr;
QString language; QString language;
QString theme; QString theme;

View file

@ -12,9 +12,9 @@
#if FMT_VERSION >= 80100 #if FMT_VERSION >= 80100
template <typename T> template <typename T>
struct fmt::formatter<T, std::enable_if_t<std::is_enum_v<T>, char>> struct fmt::formatter<T, std::enable_if_t<std::is_enum_v<T>, char>>
: formatter<std::underlying_type_t<T>> { : fmt::formatter<std::underlying_type_t<T>> {
template <typename FormatContext> template <typename FormatContext>
auto format(const T& value, FormatContext& ctx) -> decltype(ctx.out()) { auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) {
return fmt::formatter<std::underlying_type_t<T>>::format( return fmt::formatter<std::underlying_type_t<T>>::format(
static_cast<std::underlying_type_t<T>>(value), ctx); static_cast<std::underlying_type_t<T>>(value), ctx);
} }

View file

@ -83,6 +83,7 @@ void LogSettings() {
LOG_INFO(Config, "Citra Configuration:"); LOG_INFO(Config, "Citra Configuration:");
log_setting("Core_UseCpuJit", values.use_cpu_jit.GetValue()); log_setting("Core_UseCpuJit", values.use_cpu_jit.GetValue());
log_setting("Core_CPUClockPercentage", values.cpu_clock_percentage.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_UseGLES", values.use_gles.GetValue());
log_setting("Renderer_GraphicsAPI", GetGraphicsAPIName(values.graphics_api.GetValue())); log_setting("Renderer_GraphicsAPI", GetGraphicsAPIName(values.graphics_api.GetValue()));
log_setting("Renderer_AsyncShaders", values.async_shader_compilation.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_TextureFilter", GetTextureFilterName(values.texture_filter.GetValue()));
log_setting("Renderer_TextureSampling", log_setting("Renderer_TextureSampling",
GetTextureSamplingName(values.texture_sampling.GetValue())); 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_Render3d", values.render_3d.GetValue());
log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue()); log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue());
log_setting("Stereoscopy_MonoRenderOption", values.mono_render_option.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.frame_limit.SetGlobal(true);
values.texture_filter.SetGlobal(true); values.texture_filter.SetGlobal(true);
values.texture_sampling.SetGlobal(true); values.texture_sampling.SetGlobal(true);
values.delay_game_render_thread_us.SetGlobal(true);
values.layout_option.SetGlobal(true); values.layout_option.SetGlobal(true);
values.swap_screen.SetGlobal(true); values.swap_screen.SetGlobal(true);
values.upright_screen.SetGlobal(true); values.upright_screen.SetGlobal(true);

View file

@ -425,6 +425,7 @@ struct Values {
int current_input_profile_index; ///< The current input profile index int current_input_profile_index; ///< The current input profile index
std::vector<InputProfile> input_profiles; ///< The list of input profiles std::vector<InputProfile> input_profiles; ///< The list of input profiles
std::vector<TouchFromButtonMap> touch_from_button_maps; std::vector<TouchFromButtonMap> touch_from_button_maps;
Setting<bool> use_artic_base_controller{false, "use_artic_base_controller"};
SwitchableSetting<bool> enable_gamemode{true, "enable_gamemode"}; SwitchableSetting<bool> enable_gamemode{true, "enable_gamemode"};
@ -479,6 +480,8 @@ struct Values {
SwitchableSetting<TextureFilter> texture_filter{TextureFilter::None, "texture_filter"}; SwitchableSetting<TextureFilter> texture_filter{TextureFilter::None, "texture_filter"};
SwitchableSetting<TextureSampling> texture_sampling{TextureSampling::GameControlled, SwitchableSetting<TextureSampling> texture_sampling{TextureSampling::GameControlled,
"texture_sampling"}; "texture_sampling"};
SwitchableSetting<u16, true> delay_game_render_thread_us{0, 0, 16000,
"delay_game_render_thread_us"};
SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, "layout_option"}; SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, "layout_option"};
SwitchableSetting<bool> swap_screen{false, "swap_screen"}; SwitchableSetting<bool> swap_screen{false, "swap_screen"};

View file

@ -14,6 +14,7 @@
//---------------------------------------------------------------------------// //---------------------------------------------------------------------------//
#pragma once #pragma once
#include <algorithm>
#include <array> #include <array>
#include <list> #include <list>
#include <tuple> #include <tuple>

View file

@ -40,6 +40,8 @@ add_library(citra_core STATIC
dumping/backend.h dumping/backend.h
dumping/ffmpeg_backend.cpp dumping/ffmpeg_backend.cpp
dumping/ffmpeg_backend.h dumping/ffmpeg_backend.h
file_sys/archive_artic.cpp
file_sys/archive_artic.h
file_sys/archive_backend.cpp file_sys/archive_backend.cpp
file_sys/archive_backend.h file_sys/archive_backend.h
file_sys/archive_extsavedata.cpp file_sys/archive_extsavedata.cpp
@ -60,6 +62,8 @@ add_library(citra_core STATIC
file_sys/archive_source_sd_savedata.h file_sys/archive_source_sd_savedata.h
file_sys/archive_systemsavedata.cpp file_sys/archive_systemsavedata.cpp
file_sys/archive_systemsavedata.h file_sys/archive_systemsavedata.h
file_sys/artic_cache.cpp
file_sys/artic_cache.h
file_sys/cia_common.h file_sys/cia_common.h
file_sys/cia_container.cpp file_sys/cia_container.cpp
file_sys/cia_container.h file_sys/cia_container.h
@ -87,6 +91,10 @@ add_library(citra_core STATIC
file_sys/romfs_reader.h file_sys/romfs_reader.h
file_sys/savedata_archive.cpp file_sys/savedata_archive.cpp
file_sys/savedata_archive.h 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.cpp
file_sys/seed_db.h file_sys/seed_db.h
file_sys/ticket.cpp file_sys/ticket.cpp
@ -445,6 +453,8 @@ add_library(citra_core STATIC
hw/y2r.h hw/y2r.h
loader/3dsx.cpp loader/3dsx.cpp
loader/3dsx.h loader/3dsx.h
loader/artic.cpp
loader/artic.h
loader/elf.cpp loader/elf.cpp
loader/elf.h loader/elf.h
loader/loader.cpp loader/loader.cpp
@ -470,7 +480,7 @@ add_library(citra_core STATIC
tracer/citrace.h tracer/citrace.h
tracer/recorder.cpp tracer/recorder.cpp
tracer/recorder.h tracer/recorder.h
) )
create_target_directory_groups(citra_core) create_target_directory_groups(citra_core)

View file

@ -256,7 +256,11 @@ System::ResultStatus System::SingleStep() {
System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath, System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath,
Frontend::EmuWindow* secondary_window) { Frontend::EmuWindow* secondary_window) {
FileUtil::SetCurrentRomPath(filepath); 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) { if (!app_loader) {
LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath); LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);
return ResultStatus::ErrorGetLoader; return ResultStatus::ErrorGetLoader;
@ -286,6 +290,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorInvalidFormat; return ResultStatus::ErrorLoader_ErrorInvalidFormat;
case Loader::ResultStatus::ErrorGbaTitle: case Loader::ResultStatus::ErrorGbaTitle:
return ResultStatus::ErrorLoader_ErrorGbaTitle; return ResultStatus::ErrorLoader_ErrorGbaTitle;
case Loader::ResultStatus::ErrorArtic:
return ResultStatus::ErrorArticDisconnected;
default: default:
return ResultStatus::ErrorSystemMode; return ResultStatus::ErrorSystemMode;
} }
@ -334,6 +340,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorInvalidFormat; return ResultStatus::ErrorLoader_ErrorInvalidFormat;
case Loader::ResultStatus::ErrorGbaTitle: case Loader::ResultStatus::ErrorGbaTitle:
return ResultStatus::ErrorLoader_ErrorGbaTitle; return ResultStatus::ErrorLoader_ErrorGbaTitle;
case Loader::ResultStatus::ErrorArtic:
return ResultStatus::ErrorArticDisconnected;
default: default:
return ResultStatus::ErrorLoader; return ResultStatus::ErrorLoader;
} }
@ -691,6 +699,10 @@ void System::ApplySettings() {
} }
} }
void System::RegisterAppLoaderEarly(std::unique_ptr<Loader::AppLoader>& loader) {
early_app_loader = std::move(loader);
}
template <class Archive> template <class Archive>
void System::serialize(Archive& ar, const unsigned int file_version) { void System::serialize(Archive& ar, const unsigned int file_version) {

View file

@ -99,6 +99,7 @@ public:
///< Console ///< Console
ErrorSystemFiles, ///< Error in finding system files ErrorSystemFiles, ///< Error in finding system files
ErrorSavestate, ///< Error saving or loading ErrorSavestate, ///< Error saving or loading
ErrorArticDisconnected, ///< Error when artic base disconnects
ShutdownRequested, ///< Emulated program requested a system shutdown ShutdownRequested, ///< Emulated program requested a system shutdown
ErrorUnknown ///< Any other error ErrorUnknown ///< Any other error
}; };
@ -169,6 +170,18 @@ public:
[[nodiscard]] PerfStats::Results GetAndResetPerfStats(); [[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(); [[nodiscard]] PerfStats::Results GetLastPerfStats();
/** /**
@ -346,6 +359,8 @@ public:
/// Applies any changes to settings to this core instance. /// Applies any changes to settings to this core instance.
void ApplySettings(); void ApplySettings();
void RegisterAppLoaderEarly(std::unique_ptr<Loader::AppLoader>& loader);
private: private:
/** /**
* Initialize the emulated system. * Initialize the emulated system.
@ -366,6 +381,9 @@ private:
/// AppLoader used to load the current executing application /// AppLoader used to load the current executing application
std::unique_ptr<Loader::AppLoader> app_loader; std::unique_ptr<Loader::AppLoader> app_loader;
// Temporary app loader passed from frontend
std::unique_ptr<Loader::AppLoader> early_app_loader;
/// ARM11 CPU core /// ARM11 CPU core
std::vector<std::shared_ptr<ARM_Interface>> cpu_cores; std::vector<std::shared_ptr<ARM_Interface>> cpu_cores;
ARM_Interface* running_core = nullptr; ARM_Interface* running_core = nullptr;

View file

@ -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<u8> ArticArchive::BuildFSPath(const Path& path) {
std::vector<u8> ret(sizeof(u32) * 2);
u32* raw_data = reinterpret_cast<u32*>(ret.data());
auto path_type = path.GetType();
auto binary = path.AsBinary();
raw_data[0] = static_cast<u32>(path_type);
raw_data[1] = static_cast<u32>(binary.size());
if (!binary.empty()) {
ret.insert(ret.end(), binary.begin(), binary.end());
}
// The insert may have invalidated the pointer
raw_data = reinterpret_cast<u32*>(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<Network::ArticBase::Client::Response>& resp) {
if (!resp.has_value() || !resp->Succeeded()) {
return ResultUnknown;
}
return Result(static_cast<u32>(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<u64>(report_artic_event));
}
}
}
ResultVal<std::unique_ptr<ArchiveBackend>> ArticArchive::Open(
std::shared_ptr<Network::ArticBase::Client>& 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<s32>(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<u32>(resp->GetMethodResult()));
if (res.IsError())
return res;
auto handle_opt = resp->GetResponseS64(0);
if (!handle_opt.has_value()) {
return ResultUnknown;
}
return std::make_unique<ArticArchive>(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<u64>(report_artic_event));
}
}
}
std::string ArticArchive::GetName() const {
return "ArticArchive";
}
ResultVal<std::unique_ptr<FileBackend>> 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_t>(*size_opt));
}
}
if (open_reporter->open_files++ == 0 &&
report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
client->ReportArticEvent(static_cast<u64>(report_artic_event) | (1ULL << 32));
}
return std::make_unique<ArticFileBackend>(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<std::unique_ptr<DirectoryBackend>> 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<u64>(report_artic_event) | (1ULL << 32));
}
return std::make_unique<ArticDirectoryBackend>(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<u64>(*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<u32>(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<std::tuple<bool, bool, u64>> 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<u64>(report_artic_event));
}
}
void ArticArchive::OpenFileReporter::OnDirectoryClosed() {
if (--open_files == 0 && report_artic_event != Core::PerfStats::PerfArticEventBits::NONE) {
client->ReportArticEvent(static_cast<u64>(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<std::size_t> 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<u64>(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<size_t>(client->GetServerRequestMaxSize() - 0x100, length - read_amount);
auto req = client->NewRequest("FSFILE_Read");
req.AddParameterS32(file_handle);
req.AddParameterS64(static_cast<s64>(offset + read_amount));
req.AddParameterS32(static_cast<s32>(to_read));
auto resp = client->Send(req);
if (!resp.has_value() || !resp->Succeeded())
return Result(-1);
auto res = Result(static_cast<u32>(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<std::size_t> 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<size_t>(client->GetServerRequestMaxSize() - 0x100,
length - written_amount);
auto req = client->NewRequest("FSFILE_Write");
req.AddParameterS32(file_handle);
req.AddParameterS64(static_cast<s64>(offset + written_amount));
req.AddParameterS32(static_cast<s32>(to_write));
req.AddParameterS32(static_cast<s32>(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<u32>(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<size_t>(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<u32>(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

View file

@ -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 <boost/serialization/unique_ptr.hpp>
#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<u8> BuildFSPath(const Path& path);
static Result RespResult(const std::optional<Network::ArticBase::Client::Response>& resp);
explicit ArticArchive(std::shared_ptr<Network::ArticBase::Client>& _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<OpenFileReporter>(_client, _report_artic_event);
}
~ArticArchive() override;
static ResultVal<std::unique_ptr<ArchiveBackend>> Open(
std::shared_ptr<Network::ArticBase::Client>& 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<std::unique_ptr<FileBackend>> 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<std::unique_ptr<DirectoryBackend>> 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<std::tuple<bool, bool, u64>> 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<Network::ArticBase::Client>& cli,
Core::PerfStats::PerfArticEventBits _report_artic_event)
: client(cli), report_artic_event(_report_artic_event) {}
void OnFileClosed();
void OnDirectoryClosed();
std::shared_ptr<Network::ArticBase::Client> client;
Core::PerfStats::PerfArticEventBits report_artic_event =
Core::PerfStats::PerfArticEventBits::NONE;
std::atomic<u32> open_files = 0;
};
std::shared_ptr<Network::ArticBase::Client> client;
s64 archive_handle;
std::shared_ptr<OpenFileReporter> 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 <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<ArchiveBackend>(*this);
ar& archive_handle;
}
friend class boost::serialization::access;
};
class ArticFileBackend : public FileBackend {
public:
explicit ArticFileBackend(std::shared_ptr<Network::ArticBase::Client>& _client,
s32 _file_handle,
const std::shared_ptr<ArticArchive::OpenFileReporter>& _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<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
ResultVal<std::size_t> 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<Network::ArticBase::Client> client;
s32 file_handle;
std::shared_ptr<ArticArchive::OpenFileReporter> open_reporter;
Path archive_path;
ArticCacheProvider* cache_provider = nullptr;
Path file_path;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<FileBackend>(*this);
ar& file_handle;
}
friend class boost::serialization::access;
};
class ArticDirectoryBackend : public DirectoryBackend {
public:
explicit ArticDirectoryBackend(
std::shared_ptr<Network::ArticBase::Client>& _client, s32 _dir_handle,
const Path& _archive_path,
const std::shared_ptr<ArticArchive::OpenFileReporter>& _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<Network::ArticBase::Client> client;
s32 dir_handle;
Path archive_path;
std::shared_ptr<ArticArchive::OpenFileReporter> open_reporter;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<DirectoryBackend>(*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)

View file

@ -105,8 +105,7 @@ std::vector<u8> Path::AsBinary() const {
std::vector<u8> to_return(u16str.size() * 2); std::vector<u8> to_return(u16str.size() * 2);
for (std::size_t i = 0; i < u16str.size(); ++i) { for (std::size_t i = 0; i < u16str.size(); ++i) {
u16 tmp_char = u16str.at(i); u16 tmp_char = u16str.at(i);
to_return[i * 2] = (tmp_char & 0xFF00) >> 8; *reinterpret_cast<u16*>(to_return.data() + i * 2) = tmp_char;
to_return[i * 2 + 1] = (tmp_char & 0x00FF);
} }
return to_return; return to_return;
} }

View file

@ -103,6 +103,7 @@ struct ArchiveFormatInfo {
u8 duplicate_data; ///< Whether the archive should duplicate the data. u8 duplicate_data; ///< Whether the archive should duplicate the data.
}; };
static_assert(std::is_trivial_v<ArchiveFormatInfo>, "ArchiveFormatInfo is not POD"); static_assert(std::is_trivial_v<ArchiveFormatInfo>, "ArchiveFormatInfo is not POD");
static_assert(sizeof(ArchiveFormatInfo) == 16, "Invalid ArchiveFormatInfo size");
class ArchiveBackend : NonCopyable { class ArchiveBackend : NonCopyable {
public: public:
@ -119,8 +120,8 @@ public:
* @param mode Mode to open the file with * @param mode Mode to open the file with
* @return Opened file, or error code * @return Opened file, or error code
*/ */
virtual ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, virtual ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode& mode,
const Mode& mode) const = 0; u32 attributes = 0) = 0;
/** /**
* Delete a file specified by its path * Delete a file specified by its path
@ -157,14 +158,14 @@ public:
* @param size The size of the new file, filled with zeroes * @param size The size of the new file, filled with zeroes
* @return Result of the operation * @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 * Create a directory specified by its path
* @param path Path relative to the archive * @param path Path relative to the archive
* @return Result of the operation * @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 * Rename a Directory specified by its path
@ -179,7 +180,7 @@ public:
* @param path Path relative to the archive * @param path Path relative to the archive
* @return Opened directory, or error code * @return Opened directory, or error code
*/ */
virtual ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const = 0; virtual ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) = 0;
/** /**
* Get the free space * Get the free space
@ -187,6 +188,20 @@ public:
*/ */
virtual u64 GetFreeBytes() const = 0; 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() { u64 GetOpenDelayNs() {
if (delay_generator != nullptr) { if (delay_generator != nullptr) {
return delay_generator->GetOpenDelayNs(); return delay_generator->GetOpenDelayNs();
@ -196,6 +211,31 @@ public:
return delay_generator->GetOpenDelayNs(); 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<std::tuple<bool, bool, u64>> 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<bool, bool, u64>(false, true, 0);
}
virtual bool IsSlow() {
return false;
}
protected: protected:
std::unique_ptr<DelayGenerator> delay_generator; std::unique_ptr<DelayGenerator> delay_generator;
@ -232,7 +272,7 @@ public:
* @return Result of the operation, 0 on success * @return Result of the operation, 0 on success
*/ */
virtual Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, 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 * Retrieves the format info about the archive with the specified path
@ -242,6 +282,10 @@ public:
*/ */
virtual ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const = 0; virtual ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const = 0;
virtual bool IsSlow() {
return false;
}
template <class Archive> template <class Archive>
void serialize(Archive& ar, const unsigned int) {} void serialize(Archive& ar, const unsigned int) {}
friend class boost::serialization::access; friend class boost::serialization::access;

View file

@ -10,6 +10,7 @@
#include "common/common_types.h" #include "common/common_types.h"
#include "common/file_util.h" #include "common/file_util.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "core/file_sys/archive_artic.h"
#include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/archive_extsavedata.h"
#include "core/file_sys/disk_archive.h" #include "core/file_sys/disk_archive.h"
#include "core/file_sys/errors.h" #include "core/file_sys/errors.h"
@ -37,7 +38,7 @@ public:
return false; return false;
} }
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override { const u8* buffer) override {
if (offset > size) { if (offset > size) {
return ResultWriteBeyondEnd; return ResultWriteBeyondEnd;
@ -49,7 +50,7 @@ public:
length = size - offset; length = size - offset;
} }
return DiskFile::Write(offset, length, flush, buffer); return DiskFile::Write(offset, length, flush, update_timestamp, buffer);
} }
private: private:
@ -100,8 +101,8 @@ public:
return "ExtSaveDataArchive: " + mount_point; return "ExtSaveDataArchive: " + mount_point;
} }
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode& mode,
const Mode& mode) const override { u32 attributes) override {
LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex); LOG_DEBUG(Service_FS, "called path={} mode={:01X}", path.DebugStr(), mode.hex);
const PathParser path_parser(path); const PathParser path_parser(path);
@ -234,69 +235,193 @@ Path ArchiveFactory_ExtSaveData::GetCorrectedPath(const Path& path) {
return {binary_data}; return {binary_data};
} }
ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_ExtSaveData::Open(const Path& path, static Service::FS::ArchiveIdCode ExtSaveDataTypeToArchiveID(ExtSaveDataType type) {
u64 program_id) { switch (type) {
const auto directory = type == ExtSaveDataType::Boss ? "boss/" : "user/"; case FileSys::ExtSaveDataType::Normal:
const auto fullpath = GetExtSaveDataPath(mount_point, GetCorrectedPath(path)) + directory; return Service::FS::ArchiveIdCode::ExtSaveData;
if (!FileUtil::Exists(fullpath)) { case FileSys::ExtSaveDataType::Shared:
// TODO(Subv): Verify the archive behavior of SharedExtSaveData compared to ExtSaveData. return Service::FS::ArchiveIdCode::SharedExtSaveData;
// ExtSaveData seems to return FS_NotFound (120) when the archive doesn't exist. case FileSys::ExtSaveDataType::Boss:
if (type != ExtSaveDataType::Shared) { return Service::FS::ArchiveIdCode::BossExtSaveData;
return ResultNotFoundInvalidState; default:
} else { return Service::FS::ArchiveIdCode::ExtSaveData;
return ResultNotFormatted;
}
} }
std::unique_ptr<DelayGenerator> delay_generator = std::make_unique<ExtSaveDataDelayGenerator>();
return std::make_unique<ExtSaveDataArchive>(fullpath, std::move(delay_generator));
} }
Result ArchiveFactory_ExtSaveData::Format(const Path& path, static Core::PerfStats::PerfArticEventBits ExtSaveDataTypeToPerfArtic(ExtSaveDataType type) {
const FileSys::ArchiveFormatInfo& format_info, switch (type) {
u64 program_id) { case FileSys::ExtSaveDataType::Normal:
auto corrected_path = GetCorrectedPath(path); return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA;
case FileSys::ExtSaveDataType::Shared:
// These folders are always created with the ExtSaveData return Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA;
std::string user_path = GetExtSaveDataPath(mount_point, corrected_path) + "user/"; case FileSys::ExtSaveDataType::Boss:
std::string boss_path = GetExtSaveDataPath(mount_point, corrected_path) + "boss/"; return Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA;
FileUtil::CreateFullPath(user_path); default:
FileUtil::CreateFullPath(boss_path); return Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA;
// 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)); ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_ExtSaveData::Open(const Path& path,
return ResultSuccess; 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<DelayGenerator> delay_generator =
std::make_unique<ExtSaveDataDelayGenerator>();
return std::make_unique<ExtSaveDataArchive>(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<std::span<const u8>> 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<u8>(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<u8>(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<u32>(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<ArchiveFormatInfo> ArchiveFactory_ExtSaveData::GetFormatInfo(const Path& path, ResultVal<ArchiveFormatInfo> ArchiveFactory_ExtSaveData::GetFormatInfo(const Path& path,
u64 program_id) const { u64 program_id) const {
std::string metadata_path = GetExtSaveDataPath(mount_point, path) + "metadata"; if (IsUsingArtic()) {
FileUtil::IOFile file(metadata_path, "rb"); auto req = artic_client->NewRequest("FSUSER_GetFormatInfo");
if (!file.IsOpen()) { req.AddParameterS32(static_cast<u32>(ExtSaveDataTypeToArchiveID(type)));
LOG_ERROR(Service_FS, "Could not open metadata information for archive"); auto path_artic = ArticArchive::BuildFSPath(path);
// TODO(Subv): Verify error code req.AddParameterBuffer(path_artic.data(), path_artic.size());
return ResultNotFormatted;
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<const u8> 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 } // namespace FileSys
SERIALIZE_EXPORT_IMPL(FileSys::ExtSaveDataDelayGenerator) SERIALIZE_EXPORT_IMPL(FileSys::ExtSaveDataDelayGenerator)

View file

@ -5,13 +5,17 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <optional>
#include <span> #include <span>
#include <string> #include <string>
#include <boost/serialization/export.hpp> #include <boost/serialization/export.hpp>
#include <boost/serialization/string.hpp> #include <boost/serialization/string.hpp>
#include "common/common_types.h" #include "common/common_types.h"
#include "core/file_sys/archive_backend.h" #include "core/file_sys/archive_backend.h"
#include "core/file_sys/artic_cache.h"
#include "core/hle/result.h" #include "core/hle/result.h"
#include "core/hle/service/fs/archive.h"
#include "network/artic_base/artic_base_client.h"
namespace FileSys { namespace FileSys {
@ -22,7 +26,7 @@ enum class ExtSaveDataType {
}; };
/// File system interface to the ExtSaveData archive /// File system interface to the ExtSaveData archive
class ArchiveFactory_ExtSaveData final : public ArchiveFactory { class ArchiveFactory_ExtSaveData final : public ArchiveFactory, public ArticCacheProvider {
public: public:
ArchiveFactory_ExtSaveData(const std::string& mount_point, ExtSaveDataType type_); ArchiveFactory_ExtSaveData(const std::string& mount_point, ExtSaveDataType type_);
@ -31,21 +35,35 @@ public:
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
u64 program_id) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
bool IsSlow() override {
return IsUsingArtic();
}
const std::string& GetMountPoint() const { const std::string& GetMountPoint() const {
return mount_point; return mount_point;
} }
/** Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
* Writes the SMDH icon of the ExtSaveData to file u32 directory_buckets, u32 file_buckets) override {
* @param path Path of this ExtSaveData return UnimplementedFunction(ErrorModule::FS);
* @param icon_data Binary data of the icon };
* @param icon_size Size of the icon data
*/ Result FormatAsExtData(const Path& path, const FileSys::ArchiveFormatInfo& format_info,
void WriteIcon(const Path& path, std::span<const u8> icon); u8 unknown, u64 program_id, u64 total_size,
std::optional<std::span<const u8>> icon);
Result DeleteExtData(Service::FS::MediaType media_type, u8 unknown, u32 high, u32 low);
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& client) {
artic_client = client;
}
bool IsUsingArtic() const {
return artic_client.get() != nullptr;
}
private: private:
/// Type of ext save data archive being accessed. /// 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. /// Returns a path with the correct SaveIdHigh value for Shared extdata paths.
Path GetCorrectedPath(const Path& path); Path GetCorrectedPath(const Path& path);
std::shared_ptr<Network::ArticBase::Client> artic_client = nullptr;
ArchiveFactory_ExtSaveData() = default; ArchiveFactory_ExtSaveData() = default;
template <class Archive> template <class Archive>
void serialize(Archive& ar, const unsigned int) { void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<ArchiveFactory>(*this); ar& boost::serialization::base_object<ArchiveFactory>(*this);
ar& boost::serialization::base_object<ArticCacheProvider>(*this);
ar& type; ar& type;
ar& mount_point; ar& mount_point;
} }

View file

@ -15,6 +15,7 @@
#include "common/string_util.h" #include "common/string_util.h"
#include "common/swap.h" #include "common/swap.h"
#include "core/core.h" #include "core/core.h"
#include "core/file_sys/archive_artic.h"
#include "core/file_sys/archive_ncch.h" #include "core/file_sys/archive_ncch.h"
#include "core/file_sys/errors.h" #include "core/file_sys/errors.h"
#include "core/file_sys/ivfc_archive.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)); return FileSys::Path(std::move(file));
} }
ResultVal<std::unique_ptr<FileBackend>> NCCHArchive::OpenFile(const Path& path, ResultVal<std::unique_ptr<FileBackend>> NCCHArchive::OpenFile(const Path& path, const Mode& mode,
const Mode& mode) const { u32 attributes) {
if (path.GetType() != LowPathType::Binary) { if (path.GetType() != LowPathType::Binary) {
LOG_ERROR(Service_FS, "Path need to be Binary"); LOG_ERROR(Service_FS, "Path need to be Binary");
return ResultInvalidPath; return ResultInvalidPath;
@ -207,14 +209,14 @@ Result NCCHArchive::DeleteDirectoryRecursively(const Path& path) const {
return ResultUnknown; 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()); LOG_CRITICAL(Service_FS, "Attempted to create a file in an NCCH archive ({}).", GetName());
// TODO: Verify error code // TODO: Verify error code
return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported,
ErrorLevel::Permanent); 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()); LOG_CRITICAL(Service_FS, "Attempted to create a directory in an NCCH archive ({}).", GetName());
// TODO(wwylele): Use correct error code // TODO(wwylele): Use correct error code
return ResultUnknown; return ResultUnknown;
@ -226,7 +228,7 @@ Result NCCHArchive::RenameDirectory(const Path& src_path, const Path& dest_path)
return ResultUnknown; return ResultUnknown;
} }
ResultVal<std::unique_ptr<DirectoryBackend>> NCCHArchive::OpenDirectory(const Path& path) const { ResultVal<std::unique_ptr<DirectoryBackend>> NCCHArchive::OpenDirectory(const Path& path) {
LOG_CRITICAL(Service_FS, "Attempted to open a directory within an NCCH archive ({}).", LOG_CRITICAL(Service_FS, "Attempted to open a directory within an NCCH archive ({}).",
GetName().c_str()); GetName().c_str());
// TODO(shinyquagsire23): Use correct error code // TODO(shinyquagsire23): Use correct error code
@ -255,7 +257,7 @@ ResultVal<std::size_t> NCCHFile::Read(const u64 offset, const std::size_t length
} }
ResultVal<std::size_t> NCCHFile::Write(const u64 offset, const std::size_t length, const bool flush, ResultVal<std::size_t> 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"); LOG_ERROR(Service_FS, "Attempted to write to NCCH file");
// TODO(shinyquagsire23): Find error code // TODO(shinyquagsire23): Find error code
return 0ULL; return 0ULL;
@ -274,6 +276,13 @@ ArchiveFactory_NCCH::ArchiveFactory_NCCH() {}
ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_NCCH::Open(const Path& path, ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_NCCH::Open(const Path& path,
u64 program_id) { 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) { if (path.GetType() != LowPathType::Binary) {
LOG_ERROR(Service_FS, "Path need to be Binary"); LOG_ERROR(Service_FS, "Path need to be Binary");
return ResultInvalidPath; return ResultInvalidPath;
@ -293,7 +302,7 @@ ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_NCCH::Open(const Path&
} }
Result ArchiveFactory_NCCH::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, 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."); LOG_ERROR(Service_FS, "Attempted to format a NCCH archive.");
// TODO: Verify error code // TODO: Verify error code
return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported, return Result(ErrorDescription::NotAuthorized, ErrorModule::FS, ErrorSummary::NotSupported,

View file

@ -11,8 +11,10 @@
#include <boost/serialization/export.hpp> #include <boost/serialization/export.hpp>
#include <boost/serialization/vector.hpp> #include <boost/serialization/vector.hpp>
#include "core/file_sys/archive_backend.h" #include "core/file_sys/archive_backend.h"
#include "core/file_sys/artic_cache.h"
#include "core/file_sys/file_backend.h" #include "core/file_sys/file_backend.h"
#include "core/hle/result.h" #include "core/hle/result.h"
#include "network/artic_base/artic_base_client.h"
namespace Service::FS { namespace Service::FS {
enum class MediaType : u32; enum class MediaType : u32;
@ -48,16 +50,16 @@ public:
return "NCCHArchive"; return "NCCHArchive";
} }
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode& mode,
const Mode& mode) const override; u32 attributes) override;
Result DeleteFile(const Path& path) const override; Result DeleteFile(const Path& path) const override;
Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override;
Result DeleteDirectory(const Path& path) const override; Result DeleteDirectory(const Path& path) const override;
Result DeleteDirectoryRecursively(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override;
Result CreateFile(const Path& path, u64 size) const override; Result CreateFile(const Path& path, u64 size, u32 attributes) const override;
Result CreateDirectory(const Path& path) const override; Result CreateDirectory(const Path& path, u32 attributes) const override;
Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override;
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override; ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override;
u64 GetFreeBytes() const override; u64 GetFreeBytes() const override;
protected: protected:
@ -82,11 +84,11 @@ public:
explicit NCCHFile(std::vector<u8> buffer, std::unique_ptr<DelayGenerator> delay_generator_); explicit NCCHFile(std::vector<u8> buffer, std::unique_ptr<DelayGenerator> delay_generator_);
ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override; ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override; const u8* buffer) override;
u64 GetSize() const override; u64 GetSize() const override;
bool SetSize(u64 size) const override; bool SetSize(u64 size) const override;
bool Close() const override { bool Close() override {
return false; return false;
} }
void Flush() const override {} void Flush() const override {}
@ -105,7 +107,7 @@ private:
}; };
/// File system interface to the NCCH archive /// File system interface to the NCCH archive
class ArchiveFactory_NCCH final : public ArchiveFactory { class ArchiveFactory_NCCH final : public ArchiveFactory, public ArticCacheProvider {
public: public:
explicit ArchiveFactory_NCCH(); explicit ArchiveFactory_NCCH();
@ -114,14 +116,29 @@ public:
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
bool IsSlow() override {
return IsUsingArtic();
}
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& client) {
artic_client = client;
}
bool IsUsingArtic() const {
return artic_client.get() != nullptr;
}
private: private:
std::shared_ptr<Network::ArticBase::Client> artic_client = nullptr;
template <class Archive> template <class Archive>
void serialize(Archive& ar, const unsigned int) { void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<ArchiveFactory>(*this); ar& boost::serialization::base_object<ArchiveFactory>(*this);
ar& boost::serialization::base_object<ArticCacheProvider>(*this);
} }
friend class boost::serialization::access; friend class boost::serialization::access;
}; };

View file

@ -75,12 +75,14 @@ ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_OtherSaveDataPermitted
return ResultGamecardNotInserted; 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, Result ArchiveFactory_OtherSaveDataPermitted::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info, 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."); LOG_ERROR(Service_FS, "Attempted to format a OtherSaveDataPermitted archive.");
return ResultInvalidPath; return ResultInvalidPath;
} }
@ -96,7 +98,8 @@ ResultVal<ArchiveFormatInfo> ArchiveFactory_OtherSaveDataPermitted::GetFormatInf
return ResultGamecardNotInserted; 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( ArchiveFactory_OtherSaveDataGeneral::ArchiveFactory_OtherSaveDataGeneral(
@ -114,12 +117,14 @@ ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_OtherSaveDataGeneral::
return ResultGamecardNotInserted; 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, Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info, const FileSys::ArchiveFormatInfo& format_info,
u64 /*client_program_id*/) { u64 /*client_program_id*/, u32 directory_buckets,
u32 file_buckets) {
MediaType media_type; MediaType media_type;
u64 program_id; u64 program_id;
CASCADE_RESULT(std::tie(media_type, program_id), ParsePathGeneral(path)); CASCADE_RESULT(std::tie(media_type, program_id), ParsePathGeneral(path));
@ -129,7 +134,9 @@ Result ArchiveFactory_OtherSaveDataGeneral::Format(const Path& path,
return ResultGamecardNotInserted; 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<ArchiveFormatInfo> ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo( ResultVal<ArchiveFormatInfo> ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo(
@ -143,7 +150,8 @@ ResultVal<ArchiveFormatInfo> ArchiveFactory_OtherSaveDataGeneral::GetFormatInfo(
return ResultGamecardNotInserted; return ResultGamecardNotInserted;
} }
return sd_savedata_source->GetFormatInfo(program_id); return sd_savedata_source->GetFormatInfo(
program_id, Service::FS::ArchiveIdCode::OtherSaveDataPermitted, path);
} }
} // namespace FileSys } // namespace FileSys

View file

@ -22,10 +22,14 @@ public:
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
bool IsSlow() override {
return sd_savedata_source->IsUsingArtic();
}
private: private:
std::shared_ptr<ArchiveSource_SDSaveData> sd_savedata_source; std::shared_ptr<ArchiveSource_SDSaveData> sd_savedata_source;
@ -49,8 +53,8 @@ public:
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
private: private:

View file

@ -18,18 +18,20 @@ ArchiveFactory_SaveData::ArchiveFactory_SaveData(
ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SaveData::Open(const Path& path, ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SaveData::Open(const Path& path,
u64 program_id) { 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, Result ArchiveFactory_SaveData::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info, const FileSys::ArchiveFormatInfo& format_info,
u64 program_id) { u64 program_id, u32 directory_buckets, u32 file_buckets) {
return sd_savedata_source->Format(program_id, format_info); return sd_savedata_source->Format(program_id, format_info, Service::FS::ArchiveIdCode::SaveData,
path, directory_buckets, file_buckets);
} }
ResultVal<ArchiveFormatInfo> ArchiveFactory_SaveData::GetFormatInfo(const Path& path, ResultVal<ArchiveFormatInfo> ArchiveFactory_SaveData::GetFormatInfo(const Path& path,
u64 program_id) const { 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 } // namespace FileSys

View file

@ -20,11 +20,15 @@ public:
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
bool IsSlow() override {
return sd_savedata_source->IsUsingArtic();
}
private: private:
std::shared_ptr<ArchiveSource_SDSaveData> sd_savedata_source; std::shared_ptr<ArchiveSource_SDSaveData> sd_savedata_source;

View file

@ -43,8 +43,8 @@ public:
SERIALIZE_DELAY_GENERATOR SERIALIZE_DELAY_GENERATOR
}; };
ResultVal<std::unique_ptr<FileBackend>> SDMCArchive::OpenFile(const Path& path, ResultVal<std::unique_ptr<FileBackend>> SDMCArchive::OpenFile(const Path& path, const Mode& mode,
const Mode& mode) const { u32 attributes) {
Mode modified_mode; Mode modified_mode;
modified_mode.hex = mode.hex; 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); }); 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); const PathParser path_parser(path);
if (!path_parser.IsValid()) { if (!path_parser.IsValid()) {
@ -267,7 +267,7 @@ Result SDMCArchive::CreateFile(const FileSys::Path& path, u64 size) const {
ErrorLevel::Info); ErrorLevel::Info);
} }
Result SDMCArchive::CreateDirectory(const Path& path) const { Result SDMCArchive::CreateDirectory(const Path& path, u32 attributes) const {
const PathParser path_parser(path); const PathParser path_parser(path);
if (!path_parser.IsValid()) { if (!path_parser.IsValid()) {
@ -331,7 +331,7 @@ Result SDMCArchive::RenameDirectory(const Path& src_path, const Path& dest_path)
ErrorSummary::NothingHappened, ErrorLevel::Status); ErrorSummary::NothingHappened, ErrorLevel::Status);
} }
ResultVal<std::unique_ptr<DirectoryBackend>> SDMCArchive::OpenDirectory(const Path& path) const { ResultVal<std::unique_ptr<DirectoryBackend>> SDMCArchive::OpenDirectory(const Path& path) {
const PathParser path_parser(path); const PathParser path_parser(path);
if (!path_parser.IsValid()) { if (!path_parser.IsValid()) {
@ -392,7 +392,7 @@ ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SDMC::Open(const Path&
} }
Result ArchiveFactory_SDMC::Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, 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. :) // This is kind of an undesirable operation, so let's just ignore it. :)
return ResultSuccess; return ResultSuccess;
} }

View file

@ -27,16 +27,16 @@ public:
return "SDMCArchive: " + mount_point; return "SDMCArchive: " + mount_point;
} }
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode& mode,
const Mode& mode) const override; u32 attributes) override;
Result DeleteFile(const Path& path) const override; Result DeleteFile(const Path& path) const override;
Result RenameFile(const Path& src_path, const Path& dest_path) const override; Result RenameFile(const Path& src_path, const Path& dest_path) const override;
Result DeleteDirectory(const Path& path) const override; Result DeleteDirectory(const Path& path) const override;
Result DeleteDirectoryRecursively(const Path& path) const override; Result DeleteDirectoryRecursively(const Path& path) const override;
Result CreateFile(const Path& path, u64 size) const override; Result CreateFile(const Path& path, u64 size, u32 attributes) const override;
Result CreateDirectory(const Path& path) const override; Result CreateDirectory(const Path& path, u32 attributes) const override;
Result RenameDirectory(const Path& src_path, const Path& dest_path) const override; Result RenameDirectory(const Path& src_path, const Path& dest_path) const override;
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override; ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override;
u64 GetFreeBytes() const override; u64 GetFreeBytes() const override;
protected: protected:
@ -68,8 +68,8 @@ public:
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
private: private:

View file

@ -41,7 +41,8 @@ public:
}; };
ResultVal<std::unique_ptr<FileBackend>> SDMCWriteOnlyArchive::OpenFile(const Path& path, ResultVal<std::unique_ptr<FileBackend>> SDMCWriteOnlyArchive::OpenFile(const Path& path,
const Mode& mode) const { const Mode& mode,
u32 attributes) {
if (mode.read_flag) { if (mode.read_flag) {
LOG_ERROR(Service_FS, "Read flag is not supported"); LOG_ERROR(Service_FS, "Read flag is not supported");
return ResultInvalidReadFlag; return ResultInvalidReadFlag;
@ -49,8 +50,7 @@ ResultVal<std::unique_ptr<FileBackend>> SDMCWriteOnlyArchive::OpenFile(const Pat
return SDMCArchive::OpenFileBase(path, mode); return SDMCArchive::OpenFileBase(path, mode);
} }
ResultVal<std::unique_ptr<DirectoryBackend>> SDMCWriteOnlyArchive::OpenDirectory( ResultVal<std::unique_ptr<DirectoryBackend>> SDMCWriteOnlyArchive::OpenDirectory(const Path& path) {
const Path& path) const {
LOG_ERROR(Service_FS, "Not supported"); LOG_ERROR(Service_FS, "Not supported");
return ResultUnsupportedOpenFlags; return ResultUnsupportedOpenFlags;
} }
@ -83,7 +83,8 @@ ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SDMCWriteOnly::Open(co
Result ArchiveFactory_SDMCWriteOnly::Format(const Path& path, Result ArchiveFactory_SDMCWriteOnly::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info, const FileSys::ArchiveFormatInfo& format_info,
u64 program_id) { u64 program_id, u32 directory_buckets,
u32 file_buckets) {
// TODO(wwylele): hwtest this // TODO(wwylele): hwtest this
LOG_ERROR(Service_FS, "Attempted to format a SDMC write-only archive."); LOG_ERROR(Service_FS, "Attempted to format a SDMC write-only archive.");
return ResultUnknown; return ResultUnknown;

View file

@ -24,10 +24,10 @@ public:
return "SDMCWriteOnlyArchive: " + mount_point; return "SDMCWriteOnlyArchive: " + mount_point;
} }
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode& mode,
const Mode& mode) const override; u32 attributes) override;
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override; ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override;
private: private:
SDMCWriteOnlyArchive() = default; SDMCWriteOnlyArchive() = default;
@ -54,8 +54,8 @@ public:
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
private: private:

View file

@ -51,7 +51,7 @@ public:
return data->size(); return data->size();
} }
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override { const u8* buffer) override {
LOG_ERROR(Service_FS, "The file is read-only!"); LOG_ERROR(Service_FS, "The file is read-only!");
return ResultUnsupportedOpenFlags; return ResultUnsupportedOpenFlags;
@ -65,7 +65,7 @@ public:
return false; return false;
} }
bool Close() const override { bool Close() override {
return true; return true;
} }
@ -94,7 +94,8 @@ public:
return "SelfNCCHArchive"; return "SelfNCCHArchive";
} }
ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode&) const override { ResultVal<std::unique_ptr<FileBackend>> OpenFile(const Path& path, const Mode&,
u32 attributes) override {
// Note: SelfNCCHArchive doesn't check the open mode. // Note: SelfNCCHArchive doesn't check the open mode.
if (path.GetType() != LowPathType::Binary) { if (path.GetType() != LowPathType::Binary) {
@ -154,12 +155,12 @@ public:
return ResultUnsupportedOpenFlags; 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"); LOG_ERROR(Service_FS, "Unsupported");
return ResultUnsupportedOpenFlags; return ResultUnsupportedOpenFlags;
} }
Result CreateDirectory(const Path& path) const override { Result CreateDirectory(const Path& path, u32 attributes) const override {
LOG_ERROR(Service_FS, "Unsupported"); LOG_ERROR(Service_FS, "Unsupported");
return ResultUnsupportedOpenFlags; return ResultUnsupportedOpenFlags;
} }
@ -169,7 +170,7 @@ public:
return ResultUnsupportedOpenFlags; return ResultUnsupportedOpenFlags;
} }
ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) const override { ResultVal<std::unique_ptr<DirectoryBackend>> OpenDirectory(const Path& path) override {
LOG_ERROR(Service_FS, "Unsupported"); LOG_ERROR(Service_FS, "Unsupported");
return ResultUnsupportedOpenFlags; return ResultUnsupportedOpenFlags;
} }
@ -297,7 +298,7 @@ ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SelfNCCH::Open(const P
} }
Result ArchiveFactory_SelfNCCH::Format(const Path&, const FileSys::ArchiveFormatInfo&, 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."); LOG_ERROR(Service_FS, "Attempted to format a SelfNCCH archive.");
return ResultInvalidPath; return ResultInvalidPath;
} }

View file

@ -50,8 +50,8 @@ public:
return "SelfNCCH"; return "SelfNCCH";
} }
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override;
private: private:

View file

@ -6,6 +6,7 @@
#include "common/archives.h" #include "common/archives.h"
#include "common/file_util.h" #include "common/file_util.h"
#include "common/logging/log.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/archive_source_sd_savedata.h"
#include "core/file_sys/errors.h" #include "core/file_sys/errors.h"
#include "core/file_sys/savedata_archive.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); LOG_DEBUG(Service_FS, "Directory {} set as SaveData.", mount_point);
} }
ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveSource_SDSaveData::Open(u64 program_id) { ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveSource_SDSaveData::Open(
std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); Service::FS::ArchiveIdCode archive_id, const Path& path, u64 program_id) {
if (!FileUtil::Exists(concrete_mount_point)) { if (IsUsingArtic()) {
// When a SaveData archive is created for the first time, it is not yet formatted and the EnsureCacheCreated();
// save file/directory structure expected by the game has not yet been initialized. return ArticArchive::Open(artic_client, archive_id, path,
// Returning the NotFormatted error code will signal the game to provision the SaveData Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, *this,
// archive with the files and folders that it expects. archive_id != Service::FS::ArchiveIdCode::SaveData);
return ResultNotFormatted; } 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<SaveDataArchive>(std::move(concrete_mount_point)); return std::make_unique<SaveDataArchive>(std::move(concrete_mount_point));
}
} }
Result ArchiveSource_SDSaveData::Format(u64 program_id, Result ArchiveSource_SDSaveData::Format(u64 program_id,
const FileSys::ArchiveFormatInfo& format_info) { const FileSys::ArchiveFormatInfo& format_info,
std::string concrete_mount_point = GetSaveDataPath(mount_point, program_id); Service::FS::ArchiveIdCode archive_id, const Path& path,
FileUtil::DeleteDirRecursively(concrete_mount_point); u32 directory_buckets, u32 file_buckets) {
FileUtil::CreateFullPath(concrete_mount_point); if (IsUsingArtic()) {
ClearAllCache();
auto req = artic_client->NewRequest("FSUSER_FormatSaveData");
// Write the format metadata req.AddParameterS32(static_cast<u32>(archive_id));
std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); auto artic_path = ArticArchive::BuildFSPath(path);
FileUtil::IOFile file(metadata_path, "wb"); 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()) { auto resp = artic_client->Send(req);
file.WriteBytes(&format_info, sizeof(format_info)); 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;
} }
return ResultSuccess;
} }
ResultVal<ArchiveFormatInfo> ArchiveSource_SDSaveData::GetFormatInfo(u64 program_id) const { ResultVal<ArchiveFormatInfo> ArchiveSource_SDSaveData::GetFormatInfo(
std::string metadata_path = GetSaveDataMetadataPath(mount_point, program_id); u64 program_id, Service::FS::ArchiveIdCode archive_id, const Path& path) const {
FileUtil::IOFile file(metadata_path, "rb"); if (IsUsingArtic()) {
auto req = artic_client->NewRequest("FSUSER_GetFormatInfo");
if (!file.IsOpen()) { req.AddParameterS32(static_cast<u32>(archive_id));
LOG_ERROR(Service_FS, "Could not open metadata information for archive"); auto path_artic = ArticArchive::BuildFSPath(path);
// TODO(Subv): Verify error code req.AddParameterBuffer(path_artic.data(), path_artic.size());
return ResultNotFormatted;
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, std::string ArchiveSource_SDSaveData::GetSaveDataPathFor(const std::string& mount_point,

View file

@ -9,27 +9,48 @@
#include <boost/serialization/export.hpp> #include <boost/serialization/export.hpp>
#include <boost/serialization/string.hpp> #include <boost/serialization/string.hpp>
#include "core/file_sys/archive_backend.h" #include "core/file_sys/archive_backend.h"
#include "core/file_sys/artic_cache.h"
#include "core/hle/result.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 { namespace FileSys {
/// A common source of SD save data archive /// A common source of SD save data archive
class ArchiveSource_SDSaveData { class ArchiveSource_SDSaveData : public ArticCacheProvider {
public: public:
explicit ArchiveSource_SDSaveData(const std::string& mount_point); explicit ArchiveSource_SDSaveData(const std::string& mount_point);
ResultVal<std::unique_ptr<ArchiveBackend>> Open(u64 program_id); ResultVal<std::unique_ptr<ArchiveBackend>> Open(Service::FS::ArchiveIdCode archive_id,
Result Format(u64 program_id, const FileSys::ArchiveFormatInfo& format_info); const Path& path, u64 program_id);
ResultVal<ArchiveFormatInfo> GetFormatInfo(u64 program_id) const; 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<ArchiveFormatInfo> 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); static std::string GetSaveDataPathFor(const std::string& mount_point, u64 program_id);
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& client) {
artic_client = client;
}
bool IsUsingArtic() const {
return artic_client.get() != nullptr;
}
private: private:
std::string mount_point; std::string mount_point;
std::shared_ptr<Network::ArticBase::Client> artic_client = nullptr;
ArchiveSource_SDSaveData() = default; ArchiveSource_SDSaveData() = default;
template <class Archive> template <class Archive>
void serialize(Archive& ar, const unsigned int) { void serialize(Archive& ar, const unsigned int) {
ar& boost::serialization::base_object<ArticCacheProvider>(*this);
ar& mount_point; ar& mount_point;
} }
friend class boost::serialization::access; friend class boost::serialization::access;

View file

@ -10,6 +10,7 @@
#include "common/archives.h" #include "common/archives.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/file_util.h" #include "common/file_util.h"
#include "core/file_sys/archive_artic.h"
#include "core/file_sys/archive_systemsavedata.h" #include "core/file_sys/archive_systemsavedata.h"
#include "core/file_sys/errors.h" #include "core/file_sys/errors.h"
#include "core/file_sys/savedata_archive.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) ArchiveFactory_SystemSaveData::ArchiveFactory_SystemSaveData(const std::string& nand_path)
: base_path(GetSystemSaveDataContainerPath(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<u8> path_data = path.AsBinary();
return path_data.size() == 8 &&
(*reinterpret_cast<u32*>(path_data.data() + 4) & APP_SYSTEM_SAVE_DATA_MASK) != 0;
}
return false;
}
ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SystemSaveData::Open(const Path& path, ResultVal<std::unique_ptr<ArchiveBackend>> ArchiveFactory_SystemSaveData::Open(const Path& path,
u64 program_id) { u64 program_id) {
std::string fullpath = GetSystemSaveDataPath(base_path, path); if (IsUsingArtic() && AllowArticSystemSaveData(path)) {
if (!FileUtil::Exists(fullpath)) { EnsureCacheCreated();
// TODO(Subv): Check error code, this one is probably wrong return ArticArchive::Open(artic_client, Service::FS::ArchiveIdCode::SystemSaveData, path,
return ResultNotFound; 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<SaveDataArchive>(fullpath);
} }
return std::make_unique<SaveDataArchive>(fullpath);
} }
Result ArchiveFactory_SystemSaveData::Format(const Path& path, Result ArchiveFactory_SystemSaveData::Format(const Path& path,
const FileSys::ArchiveFormatInfo& format_info, const FileSys::ArchiveFormatInfo& format_info,
u64 program_id) { u64 program_id, u32 directory_buckets,
std::string fullpath = GetSystemSaveDataPath(base_path, path); u32 file_buckets) {
FileUtil::DeleteDirRecursively(fullpath); const std::vector<u8> vec_data = path.AsBinary();
FileUtil::CreateFullPath(fullpath); u32 save_low;
return ResultSuccess; 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<ArchiveFormatInfo> ArchiveFactory_SystemSaveData::GetFormatInfo(const Path& path, ResultVal<ArchiveFormatInfo> ArchiveFactory_SystemSaveData::GetFormatInfo(const Path& path,
@ -78,4 +101,45 @@ ResultVal<ArchiveFormatInfo> ArchiveFactory_SystemSaveData::GetFormatInfo(const
return ResultUnknown; 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<u32>(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 } // namespace FileSys

View file

@ -10,27 +10,48 @@
#include <boost/serialization/string.hpp> #include <boost/serialization/string.hpp>
#include "common/common_types.h" #include "common/common_types.h"
#include "core/file_sys/archive_backend.h" #include "core/file_sys/archive_backend.h"
#include "core/file_sys/artic_cache.h"
#include "core/hle/result.h" #include "core/hle/result.h"
#include "core/hle/service/fs/archive.h"
#include "network/artic_base/artic_base_client.h"
namespace FileSys { namespace FileSys {
/// File system interface to the SystemSaveData archive /// File system interface to the SystemSaveData archive
class ArchiveFactory_SystemSaveData final : public ArchiveFactory { class ArchiveFactory_SystemSaveData final : public ArchiveFactory, public ArticCacheProvider {
public: public:
explicit ArchiveFactory_SystemSaveData(const std::string& mount_point); explicit ArchiveFactory_SystemSaveData(const std::string& mount_point);
ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override; ResultVal<std::unique_ptr<ArchiveBackend>> Open(const Path& path, u64 program_id) override;
Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, Result Format(const Path& path, const FileSys::ArchiveFormatInfo& format_info, u64 program_id,
u64 program_id) override; u32 directory_buckets, u32 file_buckets) override;
ResultVal<ArchiveFormatInfo> GetFormatInfo(const Path& path, u64 program_id) const override; ResultVal<ArchiveFormatInfo> 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 { std::string GetName() const override {
return "SystemSaveData"; return "SystemSaveData";
} }
bool IsSlow() override {
return IsUsingArtic();
}
void RegisterArtic(std::shared_ptr<Network::ArticBase::Client>& client) {
artic_client = client;
}
bool IsUsingArtic() const {
return artic_client.get() != nullptr;
}
private: private:
std::string base_path; std::string base_path;
std::shared_ptr<Network::ArticBase::Client> artic_client = nullptr;
ArchiveFactory_SystemSaveData() = default; ArchiveFactory_SystemSaveData() = default;
template <class Archive> template <class Archive>
void serialize(Archive& ar, const unsigned int) { void serialize(Archive& ar, const unsigned int) {

View file

@ -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<std::size_t> 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<u8*>(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<u8*>(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<size_t> 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<size_t>(client->GetServerRequestMaxSize() - 0x100, length - written_amount);
auto req = client->NewRequest("FSFILE_Write");
req.AddParameterS32(file_handle);
req.AddParameterS64(static_cast<s64>(offset + written_amount));
req.AddParameterS32(static_cast<s32>(to_write));
req.AddParameterS32(static_cast<s32>(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<u32>(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<size_t>(actually_written_opt.value());
written_amount += actually_written;
if (actually_written != to_write)
break;
}
return written_amount;
}
ResultVal<size_t> 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<u32>(resp->GetMethodResult()));
if (res.IsError())
return res;
auto size_buf = resp->GetResponseS64(0);
if (!size_buf) {
return Result(-1);
}
data_size = static_cast<size_t>(*size_buf);
return data_size.value();
}
ResultVal<size_t> 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<size_t>(client->GetServerRequestMaxSize() - 0x100, len - read_amount);
auto req = client->NewRequest("FSFILE_Read");
req.AddParameterS32(file_handle);
req.AddParameterS64(static_cast<s64>(offset + read_amount));
req.AddParameterS32(static_cast<s32>(to_read));
auto resp = client->Send(req);
if (!resp.has_value() || !resp->Succeeded())
return Result(-1);
auto res = Result(static_cast<u32>(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<std::pair<std::size_t, std::size_t>> ArticCache::BreakupRead(std::size_t offset,
std::size_t length) {
std::vector<std::pair<std::size_t, std::size_t>> 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

View file

@ -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 <array>
#include <shared_mutex>
#include "vector"
#include <boost/serialization/array.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/export.hpp>
#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<Network::ArticBase::Client>& cli) : client(cli) {}
ResultVal<std::size_t> 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<std::size_t> Write(s32 file_handle, std::size_t offset, std::size_t length,
const u8* buffer, u32 flags);
ResultVal<size_t> GetSize(s32 file_handle);
void ForceSetSize(const std::optional<size_t>& size) {
data_size = size;
}
private:
std::shared_ptr<Network::ArticBase::Client> client;
std::optional<size_t> 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<std::size_t, std::array<u8, cache_line_size>, 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::pair<std::size_t, std::size_t>, std::vector<NoInitChar>,
big_cache_lines>
big_cache;
std::shared_mutex big_cache_mutex;
Common::StaticLRUCache<std::pair<std::size_t, std::size_t>, std::vector<NoInitChar>,
very_big_cache_lines>
very_big_cache;
std::shared_mutex very_big_cache_mutex;
ResultVal<std::size_t> ReadFromArtic(s32 file_handle, u8* buffer, size_t len, size_t offset);
std::size_t OffsetToPage(std::size_t offset) {
return Common::AlignDown<std::size_t>(offset, cache_line_size);
}
std::vector<std::pair<std::size_t, std::size_t>> BreakupRead(std::size_t offset,
std::size_t length);
protected:
template <class Archive>
void serialize(Archive& ar, const unsigned int) {}
friend class boost::serialization::access;
};
class ArticCacheProvider {
public:
virtual ~ArticCacheProvider() {}
std::vector<u8> 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<u8> ret;
ret.push_back(static_cast<u8>(file_path.GetType()));
ret.insert(ret.end(), archive_path_binary.begin(), archive_path_binary.end());
ret.push_back(static_cast<u8>(archive_path.GetType()));
ret.insert(ret.end(), file_path_binary.begin(), file_path_binary.end());
return ret;
}
virtual std::shared_ptr<ArticCache> ProvideCache(
const std::shared_ptr<Network::ArticBase::Client>& cli, const std::vector<u8>& 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<ArticCache>(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::map<std::vector<u8>, std::shared_ptr<ArticCache>>>();
}
}
protected:
template <class Archive>
void serialize(Archive& ar, const unsigned int) {}
friend class boost::serialization::access;
private:
std::unique_ptr<std::map<std::vector<u8>, std::shared_ptr<ArticCache>>> file_caches = nullptr;
std::shared_ptr<Network::ArticBase::Client> client;
};
} // namespace FileSys
BOOST_CLASS_EXPORT_KEY(FileSys::ArticCache)
BOOST_CLASS_EXPORT_KEY(FileSys::ArticCacheProvider)

View file

@ -49,7 +49,11 @@ public:
* Close the directory * Close the directory
* @return true if the directory closed correctly * @return true if the directory closed correctly
*/ */
virtual bool Close() const = 0; virtual bool Close() = 0;
virtual bool IsSlow() {
return false;
}
private: private:
template <class Archive> template <class Archive>

View file

@ -26,7 +26,7 @@ ResultVal<std::size_t> DiskFile::Read(const u64 offset, const std::size_t length
} }
ResultVal<std::size_t> DiskFile::Write(const u64 offset, const std::size_t length, const bool flush, ResultVal<std::size_t> 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) if (!mode.write_flag)
return ResultInvalidOpenFlags; return ResultInvalidOpenFlags;
@ -47,7 +47,7 @@ bool DiskFile::SetSize(const u64 size) const {
return true; return true;
} }
bool DiskFile::Close() const { bool DiskFile::Close() {
return file->Close(); return file->Close();
} }

View file

@ -30,11 +30,11 @@ public:
} }
ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override; ResultVal<std::size_t> Read(u64 offset, std::size_t length, u8* buffer) const override;
ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, ResultVal<std::size_t> Write(u64 offset, std::size_t length, bool flush, bool update_timestamp,
const u8* buffer) override; const u8* buffer) override;
u64 GetSize() const override; u64 GetSize() const override;
bool SetSize(u64 size) const override; bool SetSize(u64 size) const override;
bool Close() const override; bool Close() override;
void Flush() const override { void Flush() const override {
file->Flush(); file->Flush();
@ -66,7 +66,7 @@ public:
u32 Read(u32 count, Entry* entries) override; u32 Read(u32 count, Entry* entries) override;
bool Close() const override { bool Close() override {
return true; return true;
} }

Some files were not shown because too many files have changed in this diff Show more