diff --git a/.ci/docker.sh b/.ci/docker.sh index d4df835c4..1747f8832 100755 --- a/.ci/docker.sh +++ b/.ci/docker.sh @@ -14,4 +14,7 @@ echo "Tag name is: $TAG_NAME" docker build -f docker/azahar-room/Dockerfile -t azahar-room:$TAG_NAME . mkdir -p build -docker save azahar-room:$TAG_NAME > build/azahar-room-$TAG_NAME.dockerimage +FILENAME="azahar-room-$TAG_NAME.dockerimage" +docker save azahar-room:$TAG_NAME > build/$FILENAME + +echo "DOCKER_IMAGE_PATH=artifacts/$FILENAME" >> $GITHUB_ENV \ No newline at end of file diff --git a/.ci/macos-universal.sh b/.ci/macos-universal.sh index 54541a1c5..7b3a2a673 100755 --- a/.ci/macos-universal.sh +++ b/.ci/macos-universal.sh @@ -2,13 +2,14 @@ ARTIFACTS_LIST=($ARTIFACTS) -BUNDLE_DIR=build/bundle -mkdir build +BUILD_DIR=build +UNIVERSAL_DIR=$BUILD_DIR/universal +BUNDLE_DIR=$UNIVERSAL_DIR/bundle +OTHER_BUNDLE_DIR=$BUILD_DIR/x86_64/bundle -# Set up the base artifact to combine into. -BASE_ARTIFACT=${ARTIFACTS_LIST[0]} -BASE_ARTIFACT_ARCH="${BASE_ARTIFACT##*-}" -mv $BASE_ARTIFACT $BUNDLE_DIR +# Set up the base bundle to combine into. +mkdir $UNIVERSAL_DIR +cp -a $BUILD_DIR/arm64/bundle $UNIVERSAL_DIR # Executable binary paths that need to be combined. BIN_PATHS=(Azahar.app/Contents/MacOS/azahar) @@ -19,21 +20,18 @@ DYLIB_PATHS=($(cd $BUNDLE_DIR && find . -name '*.dylib')) unset IFS # Combine all of the executable binaries and dylibs. -for OTHER_ARTIFACT in "${ARTIFACTS_LIST[@]:1}"; do - OTHER_ARTIFACT_ARCH="${OTHER_ARTIFACT##*-}" +for BIN_PATH in "${BIN_PATHS[@]}"; do + lipo -create -output $BUNDLE_DIR/$BIN_PATH $BUNDLE_DIR/$BIN_PATH $OTHER_BUNDLE_DIR/$BIN_PATH +done - for BIN_PATH in "${BIN_PATHS[@]}"; do - lipo -create -output $BUNDLE_DIR/$BIN_PATH $BUNDLE_DIR/$BIN_PATH $OTHER_ARTIFACT/$BIN_PATH - done +for DYLIB_PATH in "${DYLIB_PATHS[@]}"; do + # Only merge if the libraries do not have conflicting arches, otherwise it will fail. + DYLIB_INFO=`file $BUNDLE_DIR/$DYLIB_PATH` - for DYLIB_PATH in "${DYLIB_PATHS[@]}"; do - # Only merge if the libraries do not have conflicting arches, otherwise it will fail. - DYLIB_INFO=`file $BUNDLE_DIR/$DYLIB_PATH` - OTHER_DYLIB_INFO=`file $OTHER_ARTIFACT/$DYLIB_PATH` - if ! [[ "$DYLIB_INFO" =~ "$OTHER_ARTIFACT_ARCH" ]] && ! [[ "$OTHER_DYLIB_INFO" =~ "$BASE_ARTIFACT_ARCH" ]]; then - lipo -create -output $BUNDLE_DIR/$DYLIB_PATH $BUNDLE_DIR/$DYLIB_PATH $OTHER_ARTIFACT/$DYLIB_PATH - fi - done + OTHER_DYLIB_INFO=`file $OTHER_BUNDLE_DIR/$DYLIB_PATH` + if ! [[ "$DYLIB_INFO" =~ "x86_64" ]] && ! [[ "$OTHER_DYLIB_INFO" =~ "arm64" ]]; then + lipo -create -output $BUNDLE_DIR/$DYLIB_PATH $BUNDLE_DIR/$DYLIB_PATH $OTHER_BUNDLE_DIR/$DYLIB_PATH + fi done # Remove leftover libs so that they aren't distributed diff --git a/.ci/macos.sh b/.ci/macos.sh index 4c353d1c3..b6fb54b1e 100755 --- a/.ci/macos.sh +++ b/.ci/macos.sh @@ -4,12 +4,10 @@ if [ "$GITHUB_REF_TYPE" == "tag" ]; then export EXTRA_CMAKE_FLAGS=(-DENABLE_QT_UPDATE_CHECKER=ON) fi -mkdir build && cd build -cmake .. -GNinja \ +mkdir -p build/$BUILD_ARCH && cd build/$BUILD_ARCH +cmake ../.. -GNinja \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_OSX_ARCHITECTURES="$TARGET" \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_OSX_ARCHITECTURES="$BUILD_ARCH" \ -DENABLE_QT_TRANSLATION=ON \ -DENABLE_ROOM_STANDALONE=OFF \ -DUSE_DISCORD_PRESENCE=ON \ @@ -18,9 +16,8 @@ ninja ninja bundle mv ./bundle/azahar.app ./bundle/Azahar.app # TODO: Can this be done in CMake? -ccache -s -v CURRENT_ARCH=`arch` -if [ "$TARGET" = "$CURRENT_ARCH" ]; then +if [ "$BUILD_ARCH" = "$CURRENT_ARCH" ]; then ctest -VV -C Release fi diff --git a/.ci/mxe.sh b/.ci/mxe.sh new file mode 100755 index 000000000..2aa2ccfa9 --- /dev/null +++ b/.ci/mxe.sh @@ -0,0 +1,26 @@ +#!/bin/bash -ex + +# TODO: Why doesn't the CI environment use the PATH set in the Dockerimage? +# It works fine when using the image locally. +export PATH="/mxe/usr/bin:${PATH}" + +mkdir build && cd build + +if [ "$GITHUB_REF_TYPE" == "tag" ]; then + export EXTRA_CMAKE_FLAGS=(-DENABLE_QT_UPDATE_CHECKER=ON) +fi + +x86_64-w64-mingw32.shared-cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DENABLE_QT_TRANSLATION=ON \ + -DUSE_DISCORD_PRESENCE=ON \ + -DUSE_SYSTEM_BOOST=ON \ + -DUSE_SYSTEM_CRYPTOPP=ON \ + "${EXTRA_CMAKE_FLAGS[@]}" +x86_64-w64-mingw32.shared-cmake --build . -- -j$(nproc) +x86_64-w64-mingw32.shared-strip -s bin/Release/*.exe +make bundle + +ccache -s -v diff --git a/.ci/pack.sh b/.ci/pack.sh index 3d4ce9c9e..71c2339b4 100755 --- a/.ci/pack.sh +++ b/.ci/pack.sh @@ -3,20 +3,21 @@ # Determine the full revision name. GITDATE="`git show -s --date=short --format='%ad' | sed 's/-//g'`" GITREV="`git show -s --format='%h'`" -REV_NAME="azahar-$OS-$TARGET-$GITDATE-$GITREV" - -# Determine the name of the release being built. -if [ "$GITHUB_REF_TYPE" = "tag" ]; then - RELEASE_NAME=azahar-$GITHUB_REF_NAME - REV_NAME="azahar-$OS-$TARGET-$GITHUB_REF_NAME" -else - RELEASE_NAME=azahar-head -fi # Archive and upload the artifacts. mkdir -p artifacts function pack_artifacts() { + REV_NAME="azahar-$OS-$TARGET-$GITDATE-$GITREV" + + # Determine the name of the release being built. + if [ "$GITHUB_REF_TYPE" = "tag" ]; then + RELEASE_NAME=azahar-$GITHUB_REF_NAME + REV_NAME="azahar-$OS-$TARGET-$GITHUB_REF_NAME" + else + RELEASE_NAME=azahar-head + fi + ARTIFACTS_PATH="$1" # Set up root directory for archive. @@ -35,10 +36,10 @@ function pack_artifacts() { fi # Create .zip/.tar.gz - if [ "$OS" = "windows" ]; then + if [ "$OS" = "windows" ] && [ "$TARGET" != "mxe" ]; then ARCHIVE_FULL_NAME="$ARCHIVE_NAME.zip" powershell Compress-Archive "$REV_NAME" "$ARCHIVE_FULL_NAME" - elif [ "$OS" = "android" ] || [ "$OS" = "macos" ]; then + elif [ "$OS" = "android" ] || [ "$OS" = "macos" ] || [ "$TARGET" = "mxe" ]; then ARCHIVE_FULL_NAME="$ARCHIVE_NAME.zip" zip -r "$ARCHIVE_FULL_NAME" "$REV_NAME" else @@ -56,11 +57,23 @@ if [ -n "$UNPACKED" ]; then FILENAME=$(basename "$ARTIFACT") EXTENSION="${FILENAME##*.}" + # TODO: Deduplicate + REV_NAME="azahar-$OS-$TARGET-$GITDATE-$GITREV" + + # Determine the name of the release being built. + if [ "$GITHUB_REF_TYPE" = "tag" ]; then + RELEASE_NAME=azahar-$GITHUB_REF_NAME + REV_NAME="azahar-$OS-$TARGET-$GITHUB_REF_NAME" + else + RELEASE_NAME=azahar-head + fi + mv "$ARTIFACT" "artifacts/$REV_NAME.$EXTENSION" done elif [ -n "$PACK_INDIVIDUALLY" ]; then # Pack and upload the artifacts one-by-one. for ARTIFACT in build/bundle/*; do + TARGET=$(basename "$ARTIFACT") pack_artifacts "$ARTIFACT" done else diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d23923c94..69460c695 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,6 @@ +- [ ] I have read the [Azahar AI Policy document](https://github.com/azahar-emu/azahar/blob/master/AI-POLICY.md) and have disclosed any use of AI if applicable under those terms. +--------- + --- + Mapatge de Comandament Automàtic + Aplica l\'assignació de comandament estàndard per a tots els botons i eixos + Pressiona este botó en el teu comandament! + Botons de 3DS amb el botó A ressaltat + Netejar Totes les Assignacions + Això eliminarà totes els assignacions del comandament actual. Pad circular Palanca C Tecles de drecera + Si la tecla \"Habilitar tecles d\'accés ràpid\" està assignada, s\'ha de pressionar eixa tecla a més de la tecla d\'accés ràpid assignada + Habilitar tecles d\'accés ràpid Botons de darrere Botó de darrere Pad de control @@ -110,6 +126,8 @@ És possible que alguns controladors no puguen assignar el D-pad com un eix. Si aquest és el cas, utilitza la secció D-Pad (botons). D-Pad (Botó) Assigna només el D-pad a aquests si tens problemes amb les assignacions de botons del D-Pad (Eix). + Eix Vertical + Eix Horitzontal Amunt Avall Esquerra @@ -118,6 +136,8 @@ Pulsa o mou un botó/palanca. Assignació de botons Prem o mou una entrada per enllaçar-la a %1$s. + Pressiona A DALT en el teu joystick. + Pressiona DRETA en el teu joystick. HOME Intercanviar Pantalles Turbo @@ -156,6 +176,8 @@ Nom d\'usuari/a Mode New 3DS Usar Applets LLE (si están instal·lades) + Aplicar modificació de regió lliure a les aplicacions instal·lades. + Modifica la regió de les aplicacions instal·lades perquè siguen de regió lliure, de manera que sempre apareguen en el menú Home. Habilite els mòduls LLE necessaris per a les funcions en línia (si estan instal·lats) Habilita els mòduls LLE necessaris per al mode multijugador en línia, accés a la eShop, etc. Rellotge @@ -211,23 +233,30 @@ Usa SPIR-V en vez de GLSL per a emetre el fragment de ombrejador utilitzat per a emular PICA. Desativar l\'optimitzador SPIR-V Desactiva la passada d\'optimització SPIR-V reduint considerablement el quequeig i afectant poc el rendiment. + Activar compilació de ombrejadors asíncrona Compila els ombrejats en segón pla per a reduir les aturades durant la partida. S\'esperen errors gràfics temporals quan estigue activat. Filtre Linear Activa el filtre linear, que fa que els gràfics del joc es vegen més suaus. + Escalat a múltiples sencers + Escala les pantalles amb un multiplicador enter de la pantalla original de 3DS. Per a dissenys amb dos grandàries de pantalla diferents, la pantalla més gran s\'escala amb un multiplicador enter. Filtre de Textures Millora l\'aspecte visual de les aplicacions aplicant un filtre a les textures. Els filtres compatibles són Anime4K, Ultrafast, Bicubic, ScaleForce, xBRZ Freescale i MMPX. + Endarrerir fil de renderitzat del joc Retarda el fil de renderitzat del joc en enviar dades a la GPU. Ajuda a solucionar problemes de rendiment en les (poques) aplicacions amb velocitats de fotogrames dinàmiques. Avançat Mostreig de Textures Sobreescriu el filtre de mostreig usat en jocs. Pot ser útil en uns certs casos de jocs amb baix rendiment en pujar la resolució. Si no estàs segur, possa\'l en Controlat per Joc. Multiplicació Precisa Usa multiplicacions més precises en els ombrejos de Hardware, que podrien arreglar uns certs problemes gràfics. Quan s\'active, el rendiment es reduirà. + Activar Emulació Asíncrona de la GPU Usa un fil separat per a emular la GPU de manera asíncrona. Quan s\'active, el rendiment millorarà. Límit de velocitat Quan s\'active, la velocitat d\'emulació estarà limitada a un percentatge determinat de la velocitat normal. Quan es desactive, la velocitat d\'emulació no tindrà límit i la tecla d\'accés ràpid de velocitat turbo no funcionarà. Limitar percentatge de velocitat Especifica el valor al qual es limita la velocitat d\'emulació. Amb el valor per defecte del 100%, l\'emulació es limitarà a la velocitat normal. Els valors alts o baixos incrementaran o reduiran el límit de velocitat. + Ocultar les imatges de 3DS en Android + Evita que Android indexe les imatges de la càmera, captures de pantalla i textures personalitzades de la 3DS i les mostre en la galeria. És possible que hages de reiniciar el dispositiu després de canviar esta configuració perquè tinga efecte. Límit de Velocitat Turbo Límit de velocitat d\'emulació utilitzat mentres la tecla d\'accés ràpid turbo està activa. Expandir a l\'àrea de retallada @@ -249,10 +278,18 @@ S\'esperen errors gràfics temporals quan estigue activat. Avís: Modificar estes configuracions reduiran la velocitat d\'emulació. Estereoscopia Mode 3D Estereoscòpic + Seleccione el mode 3D estereoscòpic per a renderitzat 3D. Els modes costat a costat són els més comuns en l\'actualitat. Els modes Anaglifo i Entrellaçat sempre s\'apliquen a totes les pantalles connectades. Profunditat Especifica el valor del regulador 3D. Hauria d\'estar posat a més enllà del 0% quan el Mode 3D Estereoscòpic està activat.\nNota: Els valors de profunditat superiors al 100% no són possibles en hardware real i poden causar problemes gràfics. Desactivar Renderitzat d\'Ull Dret Millora enormement el rendiment en algunes aplicacions, però pot provocar parpellejos en unes altres. + Intercanviar Ulls + Intercanvia quin ull es mostra en cada costat. Combinat amb el mode Costat a Costat, permet veure en 3D creuant els ulls! + Renderitzat 3D Estereoscòpic + Decidix si s\'activa el 3D estereoscòpic i en quines pantalles. Les opcions de pantalla única només són rellevants quan es connecten diverses pantalles. + Activat (totes les pantalles) + Activat (només pantalla principal) + Activat (només pantalla secundària) Cardboard VR Grandària de la pantalla Cardboard Escala la pantalla a un percentatge de la seua grandària original. @@ -277,6 +314,7 @@ S\'esperen errors gràfics temporals quan estigue activat. Volum Extensió d\'Àudio Estén l\'àudio per a reduir les aturades. Quan s\'active, la latència d\'àudio s\'incrementarà i reduirà un poc el rendiment. + Activar àudio en temps real Ajusta la velocitat de reproducció d\'àudio per a compensar les caigudes en la velocitat d\'emulació de quadres. Això significa que l\'àudio es reproduirà a velocitat completa fins i tot quan la velocitat de quadres del joc siga baixa. Pot causar problemes de desincronització d\'àudio. Dispositiu d\'entrada d\'àudio Mode d\'eixida de l\'àudio @@ -288,14 +326,19 @@ S\'esperen errors gràfics temporals quan estigue activat. Usa el hardware per a emular els ombrejadors de 3DS. Quan s\'active, el rendiment millorarà notablement. Velocitat de rellotge de la CPU Activar Sincronització Vertical + Sincronitza la freqüència de fotogrames del joc amb la freqüència d\'actualització del teu dispositiu. Pot causar latència d\'entrada addicional, però pot reduir el tearing en alguns casos. Renderitzador de depuració Arxiva informació addicional gràfica relacionada amb la depuració. Quan està activada, el rendiment dels jocs serà reduït considerablement + Guardar l\'eixida del registre en cada missatge Envia immediatament el registre de depuració a un arxiu. Usa-ho si Azahar falla i es talla l\'eixida del registre. + Inici diferit amb mòduls LLE Retarda l\'inici de l\'aplicació quan els mòduls LLE estan habilitats. Operacions asíncrones deterministes Fa que les operacions asíncrones siguen deterministes per a la depuració. Habilitar esta opció pot causar bloquejos. Activar Servidor RPC Activa el servidor RPC en el port 45987. Això permet llegir/escriure de manera remota la memòria emulada. + Canviar tipus de consola en les dades úniques de consola + Permet alternar el tipus de consola (Old 3DS ↔ New 3DS) per a poder descarregar el firmware del sistema oposat des de la configuració del sistema. Activar Ombreig JIT Usar el motor JIT en lloc de l\'intèrpret per a l\'emulació del ombrejador de software. @@ -306,6 +349,8 @@ S\'esperen errors gràfics temporals quan estigue activat. Horitzontal invertida Vertical Vertical invertida + Cicle d\'Estils + Selecciona quins estils es poden ciclar amb la tecla d\'accés ràpid Per omissió 16:9 4:3 @@ -413,6 +458,13 @@ S\'esperen errors gràfics temporals quan estigue activat. Per omissió Per defecte del sistema (espill) Estil Personalitzat + Color de fons + El color que apareix darrere de les pantalles durant l\'emulació, representat com un valor RGB. + Roig + Verd + Azul + Opacitat personalitzada de la segona pantalla + L\'opacitat de la segona pantalla de 3DS en usar la pantalla personalitzada. Útil si la segona pantalla esta posicionada en la part superior de la primera pantalla. Posició de Pantalla Xicoteta On hauria d\'aparéixer la pantalla xicoteta en relació amb la gran en Proporció de Pantalla Gran? Amunt a la dreta @@ -508,8 +560,17 @@ S\'esperen errors gràfics temporals quan estigue activat. Error Fatal Ha ocorregut un error fatal. Mira el registre per a més detalls.\nSeguir amb l\'emulació podria resultar en diversos penges i problemes. Aplicació cifrada no suportada + Mode de sistema no vàlid + Les aplicacions exclusives de New 3DS no es poden carregar sense activar el mode New 3DS. + Preparant ombrejadors + Construint%s + Eliminar cache d\'ombrejadors + Selecciona l\'API gràfica per a eliminar la cache d\'ombrejadors + Eliminant la caché d\'ombrejadors, per favor espere… + Caché d\'ombrejadors eliminada + Jugar Desinstal·lar Aplicació @@ -532,11 +593,37 @@ S\'esperen errors gràfics temporals quan estigue activat. ID: Fitxer: Tipus: + Inserir Cartutx + Expulsar Cartutx + Mostrar informació de rendiment Informació de rendiment Activar informació de rendiment Configura la informació de rendiment + Mostrar FPS + Mostra els fotogrames per segon actuals. + Mostrar duració de fotogrames + Mostra la duració actual de cada fotograma. + Mostrar velocitat + Mostra el percentatge de velocitat d\'emulació actual. + Mostrar l\'ús de memòria de l\'aplicació + Mostra la quantitat de memòria RAM que esta usant l\'emulador. + Mostrar memòria disponible + Mostra la quantitat de memòria RAM que esta disponible. + Mostrar la temperatura de la bateria + Mostra la temperatura actual de la bateria en Celsius i Fahrenheit. + Posició de la informació + Tria on la informació de rendiment serà mostrada en la pantalla. + Dalt a l\'esquerra + Dalt a la dreta + Avall a l\'esquerra + Avall a la dreta + Dalt al centre + Avall al centre + Fons de la informació + Agrega un fons darrere de la informació per a fer-la més llegible. + Trucs Afegir trucs @@ -639,6 +726,7 @@ S\'esperen errors gràfics temporals quan estigue activat. De costat a costat + De costat a costat ample complet Anàglifo Entrellaçat Entrellaçat invers @@ -827,4 +915,19 @@ S\'esperen errors gràfics temporals quan estigue activat. Guardat ràpid - %1$tF %1$tR Guardat ràpid no disponible. - + + Comprimir + Comprimint... + Descomprimir + Descomprimint... + Compressió completada amb èxit. + Compressió no suportada amb este fitxer. + Este fitxer ja està comprimit. + Va fallar la compressió. + Descompressió completada amb èxit. + Descompressió no suportada amb este fitxer. + Este fitxer no està comprimit. + Va fallar la descompressió. + Les aplicacions ja instal·lades no es poden comprimir ni descomprimir. + + diff --git a/src/android/app/src/main/res/values-b+da+DK/strings.xml b/src/android/app/src/main/res/values-b+da+DK/strings.xml index ad918f0a7..562bec31b 100644 --- a/src/android/app/src/main/res/values-b+da+DK/strings.xml +++ b/src/android/app/src/main/res/values-b+da+DK/strings.xml @@ -355,7 +355,6 @@ Lær mere Luk Nulstil til standard - spilkassetter eller installerede titler.]]> Standard Ingen Auto diff --git a/src/android/app/src/main/res/values-b+es+419/strings.xml b/src/android/app/src/main/res/values-b+es+419/strings.xml new file mode 100644 index 000000000..0fec00c00 --- /dev/null +++ b/src/android/app/src/main/res/values-b+es+419/strings.xml @@ -0,0 +1,333 @@ + + + + Este software ejecutará aplicaciones para la consola portátil Nintendo 3DS. No se incluyen juegos.\n\nAntes de comenzar con la emulación, seleccione una carpeta para almacenar los datos de usuario de Azahar en.\n\nQué es ésto:\nWiki - Datos de usuario de Azahar Android y almacenamiento + Notificaciones del emulador 3DS Azahar + Azahar está ejecutándose + A continuación, deberás seleccionar una Carpeta de Aplicaciones. Azahar mostrará todas las ROM de 3DS de la carpeta seleccionada en la aplicación.\n\nLas ROM CIA, actualizaciones y los DLC deben instalarse por separado haciendo clic en el icono de la carpeta y seleccionando Instalar CIA. + + + Configuración + Opciones + Buscar + Aplicaciones + Configurar opciones del emulador + Instalar archivo CIA + Instalar aplicaciones, actualizaciones o DLC + Compartir Registro + Compartir el archivo de registro de Azahar para depurar problemas + Administrador de drivers de GPU + Instalar drivers de GPU + Instala drivers alternativos para intentar mejorar el rendimiento o la precisión + Driver ya instalado + Drivers personalizados no compatibles + La carga de drivers personalizados no es compatible en este dispositivo.\n¡Comprueba esta opción otra vez en el futuro para ver si ya está disponible! + No se encontró ningún archivo de registro + Selecciona el directorio de aplicaciones + Permite que Azahar llene la lista de aplicaciones + Acerca de + Un emulador de 3DS de código abierto + Versión de compilación, créditos y más + Directorio de aplicaciones seleccionado + Cambia los archivos que Azahar usa para cargar aplicaciones + Cambia la apariencia de la app + Instalar CIA + + + Seleccionar driver de GPU + ¿Quieres reemplazar tu driver actual de GPU? + Instalar + Por defecto + Instalado %s + Usando el driver por defecto de la GPU + ¡El driver seleccionado no es válido, se usará el driver por defecto del sistema! + Driver de la GPU del sistema + Instalando el driver... + + + Copiado al portapapeles + Colaboradores + Colaboradores que hicieron posible Azahar + Proyectos utilizados por Azahar para Android + Compilación + Licencias + + ¡Te damos la bienvenida! + Aprende a configurar <b>Azahar</b> y disfruta de la emulación. + Comenzar + ¡Completado! + Aplicaciones + Selecciona la carpeta de <b>Aplicaciones</b> con el botón de abajo. + Terminado + ¡Ya estás listo!\n¡Disfruta del emulador! + Continuar + Notificaciones + Concede el permiso de notificaciones con el botón de abajo. + Conceder permiso + ¿Omitir el permiso de notificaciones? + Azahar no podrá notificarte sobre información importante. + Faltan permisos + Azahar necesita permiso para administrar archivos en este dispositivo para poder almacenar y administrar los datos de usuario.\n\nPor favor, concede el permiso antes de continuar. + Cámara + Concede el permiso de cámara para emular la cámara de la 3DS. + Micrófono + Concede el permiso de micrófono para emular el micrófono de la 3DS. + Sistema de archivos + Concede el permiso de sistema de archivos para permitir que Azahar almacene archivos. + Permiso denegado + ¿Omitir la selección de la carpeta de aplicaciones? + No se mostrará nada en la lista de aplicaciones si no se selecciona una carpeta. + Permisos + Carpetas de datos + (Carpeta de usuario es necesaria)]]> + Concede permisos opcionales para usar funciones específicas del emulador + Ayuda + Omitir + Cancelar + Selecciona la carpeta de usuario + datos de usuario con el botón de abajo.]]> + Parece que tienes directorios de usuario configurados tanto para Lime3DS como para Azahar. Probablemente se deba a que actualizaste a Azahar y, cuando se te pidió, elegiste un directorio de usuario diferente al que usabas para Lime3DS.\n\nEsto puede haberte hecho pensar que perdiste partidas guardadas u otras configuraciones; te pedimos disculpas si eso ocurrió.\n\n¿Prefieres volver a usar tu directorio de usuario original de Lime3DS, restaurando la configuración y las partidas guardadas de Lime3DS, o conservar tu directorio de usuario actual de Azahar?\n\nNinguno de los directorios se eliminará, independientemente de tu elección, y puedes cambiar libremente entre ellos usando la opción \"Selecciona la carpeta de usuario\". + Mantener el directorio actual de Azahar + Usar el directorio de Lime3DS anterior + Seleccionar + No puedes omitir configurar la carpeta de usuario + Este paso es necesario para permitir que Azahar funcione. Por favor, selecciona un directorio y luego puedes continuar. + Has perdido los permisos de escritura en tu directorio de datos de usuario, donde se guardan las partidas guardadas y otra información. Esto puede ocurrir después de actualizar Android o algunas aplicaciones. Vuelve a seleccionar el directorio para recuperar los permisos y poder continuar. + Selección no válida + La selección del directorio de usuario no es válida.\nVuelve a seleccionar el directorio de usuario, asegurándote de navegar hasta él desde la raíz del almacenamiento del dispositivo. + Azahar ha perdido el permiso para administrar archivos en este dispositivo. Esto puede ocurrir después de actualizar Android o algunas aplicaciones. Vuelve a conceder este permiso en la siguiente pantalla para seguir usando la aplicación. + Configuración del tema + Configura tus preferencias de tema de Azahar. + Establecer tema + + + Buscar y filtrar aplicaciones + Buscar aplicaciones + Jugado recientemente + Añadido recientemente + Instalados + + + Asignar control automáticamente + Aplica la asignación de control estándar para todos los botones y ejes + ¡Presiona este botón en tu control! + Botones frontales de 3DS en rombo con el botón A resaltado + Eliminar todas las asignaciones + Esto eliminará todas los asignaciones del control actual. + Circle Pad + Palanca C + Teclas de atajo + Si la tecla \"Habilitar teclas de acceso rápido\" está asignada, se debe presionar esa tecla además de la tecla de acceso rápido asignada + Habilitar teclas de acceso rápido + Gatillos + Gatillo + Cruceta + Cruceta (eje) + Es posible que algunos controles no puedan asignar la Cruceta como un eje. Si ese es el caso, utilice la sección Cruceta (botones). + Cruceta (botón) + Solamente asigne la Cruceta a éstos si tiene problemas con las asignaciones de botones de la Cruceta (eje). + Eje vertical + Eje horizontal + Arriba + Abajo + Izquierda + Derecha + Asigna %1$s %2$s + Presiona un botón o mueve una palanca + Asignación de botones + Pulsa un botón o mueve una palanca para asignarlo a %1$s. + Presiona ARRIBA en tu joystick. + Presiona DERECHA en tu joystick. + HOME + Intercambiar pantallas + Turbo + ¡Este control debe asignarse a una palanca analógica o a un eje de la Cruceta! + ¡Este dispositivo debe asignarse a un botón del control! + Velocidad turbo + Velocidad turbo activada + Velocidad turbo desactivada + + + Archivos del sistema + Realizar operaciones de archivos del sistema, como instalar archivos del sistema o iniciar el Menú Home + Conectar con la herramienta de configuración Artic + herramienta de configuración Artic.
Notas:]]>
+ Obteniendo el estado actual de los archivos del sistema, por favor espere... + Desvincular datos únicos de la consola +
¿Continuar?]]>
+ Configuración de Old 3DS + Configuración de New 3DS + La configuración es posible. + La configuración Old 3DS es necesaria primero. + Configuración completa. + Ingresa la dirección de la herramienta de configuración Artic + Preparando configuración, por favor espere... + Cargar el Menú HOME + Mostrar las aplicaciones del menú HOME en la lista de aplicaciones + Ejecutar la configuración de la consola cuando se ejecute el Menú HOME + Menú HOME + + + Botones + Botón + + + Opciones de emulación + Nombre de usuario + Modo New 3DS + Usar Applets LLE (si están instaladas) + Aplicar parche de región libre a las aplicaciones instaladas + Parchea la región de las aplicaciones instaladas para que estén libres de región, de modo que siempre aparezcan en el menú home. + Habilitar los módulos LLE necesarios para las funciones en línea (si están instalados) + Habilita los módulos LLE necesarios para el modo multijugador en línea, acceso a la eShop, etc. + Reloj + Reloj + Configura el reloj emulado de la 3DS para que tenga la misma fecha y hora de tu dispositivo o para simular una distinta. + Reloj del sistema + Reloj simulado + Si el reloj está en \"Reloj simulado\", esto cambia la fecha y hora de inicio. + Opciones de perfil + Región + Idioma + Cumpleaños + Mes + Día + País + Monedas de juego + Pasos por hora del podómetro + Número de pasos por hora reportados por el podómetro. Rango de 0 a 65.535. + ID de la Consola + Regenerar ID de la consola + Esto reemplazará tu ID de consola de 3DS virtual por una nueva. Tu ID virtual actual será irrecuperable. Esto puede tener efectos inesperados en algunas aplicaciones. Si usas un archivo de configuración obsoleto esto podría fallar. ¿Quieres continuar? + Dirección MAC + Regenerar dirección MAC + Esto reemplazará tu dirección MAC actual por una nueva. No se recomienda hacerlo si obtuviste la dirección MAC de tu consola real con la herramienta de configuración. ¿Quieres continuar? + Cargador de complementos 3GX + Carga los complementos 3GX de la SD emulada si están disponibles. + Permite que las aplicaciones cambien el estado del cargador de complementos. + Permite que las apps de homebrew activen el cargador de complementos incluso cuando está desactivado. + Advertencia de región no válida + La configuración del país no es válida para la región emulada seleccionada. + La configuración del país no es válida para la consola vinculada actual. + Almacenamiento + Comprimir contenido CIA instalado + Comprime el contenido de archivos CIA cuando son instalados a la SD emulada. Solo afecta contenido CIA instalado con esta opción activada. + + + Cámara interior + Cámara izquierda externa + Cámara derecha externa + Fuente de la imagen de la cámara + Configura la fuente de imagen de la cámara virtual. Puedes usar un archivo de imagen o una cámara si es compatible. + Cámara + Si la \"Fuente de imagen\" está en \"Cámara del dispositivo\", se usará la cámara del propio dispositivo. + Frontal + Trasera + Externa + Rotación + + + Motor gráfico + API gráfica + Activar generación de shaders SPIR-V + Usa SPIR-V en vez de GLSL para emitir el shader de fragmentos usado para emular PICA. + Desactivar el optimizador de SPIR-V + Desactiva el paso de optimización SPIR-V reduciendo considerablemente los tirones y afectando muy poco el rendimiento. + Activar compilación asíncrona de shaders + Compila los shaders en segundo plano para reducir los tirones durante la partida. Habrá fallos gráficos temporales cuando esté activado. + Filtrado lineal + Activa el filtrado linear, que hace que los gráficos del juego se vean más suaves. + Escalado de enteros + Escala las pantallas usando un multiplicador entero basado en la resolución original de 3DS. En diseños con dos tamaños de pantalla diferentes, la más grande usará el escalado de enteros. + Filtrado de texturas + Mejora los gráficos visuales de las aplicaciones aplicando un filtro a las texturas. Los filtros compatibles son Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale y MMPX. + Atrasar hilo del motor gráfico del juego + Retrasa el hilo del motor gráfico del juego cuando envía datos a la GPU. Ayuda con los problemas de rendimiento en las (muy pocas) aplicaciones con tasas de fotogramas dinámicas. + Avanzado + Muestreo de texturas + Sobrescribe el filtro de muestreo original del juego. Puede ser útil en ciertos casos con juegos con problemas gráficos al aumentar la resolución. Si no estás seguro, déjalo en Controlado por juego. + Multiplicación precisa + Usa una multiplicación más precisa en los shaders por hardware, lo cual puede solucionar ciertos errores gráficos. Al activarse, el rendimiento disminuirá. + Activar emulación asíncrona de la GPU + Usa un hilo separado para emular la GPU de manera asíncrona. Al activarse, el rendimiento mejorará. + Límite de velocidad + Si se activa, la velocidad de emulación se limitará a un porcentaje determinado de velocidad normal. Si se desactiva, la velocidad no tendrá límite y el atajo para la velocidad velocidad turbo no funcionará. + Limitar porcentaje de velocidad + Especifica el valor al que se limita la velocidad de emulación. Con el valor por defecto del 100%, la emulación se limitará a la velocidad normal. Los valores altos o bajos aumentarán o reducirán el límite de velocidad. + Ocultar las imágenes de 3DS de la galería de Android + Evita que Android indexe las imágenes de la cámara, capturas de pantalla y texturas personalizadas de la 3DS y las muestre en la galería. Es posible que tengas que reiniciar el dispositivo después de cambiar esta configuración para que surta efecto. + Límite de velocidad turbo + Límite de velocidad de emulación que se aplica al activar el turbo. + Expandir al área de recorte + Amplía la pantalla para incluir el recorte de la cámara (o notch). + Resolución interna + Especifica la resolución de renderizado. Una alta resolución mejorará considerablemente la calidad visual, pero tendrá un gran impacto en el rendimiento y puede causar fallos en ciertas aplicaciones. + Auto (Tamaño pantalla) + Nativa (400x240) + 2x Nativa (800x480) + 3x Nativa (1200x720) + 4x Nativa (1600x960) + 5x Nativa (2000x1200) + 6x Nativa (2400x1440) + 7x Nativa (2800x1680) + 8x Nativa (3200x1920) + 9x Nativa (3600x2160) + 10x Nativa (4000x2400) + ¡Desactivar esta opción reducirá notablemente el rendimiento de la emulación! Para obtener la mejor experiencia, se recomienda dejarla activada. + Aviso: Modificar estos ajustes reducirá la velocidad de emulación. + Estereoscopia + Modo 3D estereoscópico + Elige el modo 3D estereoscópico para el renderizado 3D. Los modos de lado a lado son los más comunes en la actualidad. Los modos anaglifo y entrelazado se aplicarán siempre a todas las pantallas conectadas. + Profundidad + Especifica el valor del regulador 3D. Debería ajustarse por encima del 0% cuando el 3D estereoscópico esté activado.\nNota: Los valores de profundidad superiores al 100% no son posibles en el hardware real y pueden causar problemas gráficos. + Desactivar el renderizado de ojo derecho + Mejora mucho el rendimiento en algunas aplicaciones, pero puede causar parpadeo en otras. + Intercambiar ojos + Intercambia qué ojo se muestra en cada lado. Combinado con el modo lado a lado, ¡permite ver en 3D cruzando los ojos! + Renderizado 3D estereoscópico + Si se enciende el 3D estereoscópico, y en que pantallas. Las opciones para una sola pantalla solo son relevantes si hay varias pantallas conectadas. + Activado (todas las pantallas) + Encendido (Solo Pantalla Principal) + Encendido (Solo Pantalla Secundaria) + Cardboard VR + Tamaño de la pantalla para Cardboard + Escala la pantalla a un porcentaje de su tamaño original. + Desplazamiento horizontal + Especifica el porcentaje de espacio vacío para desplazar las pantallas horizontalmente. Los valores positivos acercan los dos ojos al medio, mientras que los negativos los alejan. + Desplazamiento vertical + Especifica el porcentaje de espacio vacío para desplazar las pantallas verticalmente. Los valores positivos mueven los dos ojos hacia abajo, mientras que los negativos los mueve hacia arriba. + Shader JIT + Caché de shader en disco + Reduce los tirones al guardar y cargar shaders generados en el disco. No se puede usar sin activar la opción shader por hardware. + Utilidades + Volcar texturas + Las texturas se vuelcan en dump/textures/[Title ID]/. + Texturas personalizadas + Las texturas se cargan desde load/textures/[Title ID]/. + Precargar texturas personalizadas + Carga todas las texturas personalizadas en la memoria. Esta función puede usar mucha memoria. + Carga asíncrona de texturas personalizadas + Carga las texturas personalizadas de manera asíncrona con hilos de fondo para reducir los tirones de carga. + + + Volumen + Estiramiento de audio + Estira el audio para reducir los tirones. Al activarse, la latencia del audio se incrementará y reducirá un poco el rendimiento. + Activar audio en tiempo real + Escala la velocidad de reproducción de audio para compensar por perdidas en la velocidad de fotogramas durante la emulación. Esto significa que el audio se reproducirá a velocidad completa incluso cuando la velocidad de fotogramas del juego sea baja. Puede causar problemas de desincronización de audio. + Dispositivo de entrada de audio + Modo de salida del audio + + + CPU JIT + Usa el compilador Just-in-Time (JIT) para emular la CPU. Cuando este encendido, el rendimiento de los juegos mejorará significativamente. + Activar shader por hardware + Usa el hardware para emular los shaders de la 3DS. Al activarse, el rendimiento mejorará notablemente. + Velocidad de reloj de la CPU + Activar V-Sync + Sincroniza la velocidad de fotogramas con la tasa de actualización de tu dispositivo. Puede causar latencia de entrada adicional, pero puede reducir el rasgado de imagen en algunos casos. + Renderizador de depuración + Alternar tipo de consola. + Alternar Controles + El diseño usado por una pantalla secundaria conectada, alámbrica o inalámbrica (Chromecast, Miracast) +
diff --git a/src/android/app/src/main/res/values-b+es+ES/strings.xml b/src/android/app/src/main/res/values-b+es+ES/strings.xml index a77cf724a..a12876632 100644 --- a/src/android/app/src/main/res/values-b+es+ES/strings.xml +++ b/src/android/app/src/main/res/values-b+es+ES/strings.xml @@ -463,8 +463,8 @@ Se esperan fallos gráficos temporales cuando ésta esté activado. Rojo Verde Azul - Opacidad personalizado de la segunda pantalla - La opacidad de la segunda pantalla de 3DS al usar el pantalla personalizado. Útil si la segunda pantalla ésta posicionada en la parte superior de la primera pantalla. + Opacidad personalizada de la segunda pantalla + La opacidad de la segunda pantalla de 3DS al usar la pantalla personalizada. Útil si la segunda pantalla ésta posicionada en la parte superior de la primera pantalla. Posición Pantalla Pequeña ¿Dónde debería aparecer la pantalla pequeña en relación con la grande en Disposicion de Pantalla Grande? Arriba a la Derecha @@ -566,6 +566,10 @@ Se esperan fallos gráficos temporales cuando ésta esté activado. Preparando shaders Construyendo%s + Eliminar caché de sombreadores + Selecciona la API gráfica para eliminar la caché de sombreadores + Eliminando la caché de sombreadores, por favor espere… + Caché de sombreadores eliminada Jugar diff --git a/src/android/app/src/main/res/values-b+pl+PL/strings.xml b/src/android/app/src/main/res/values-b+pl+PL/strings.xml index 460a12431..1d59d225d 100644 --- a/src/android/app/src/main/res/values-b+pl+PL/strings.xml +++ b/src/android/app/src/main/res/values-b+pl+PL/strings.xml @@ -107,9 +107,18 @@ Ostatnio dodane Zainstalowane + + Auto-mapowanie Kontrolera + Zastosuj standardowe mapowanie gamepada dla wszystkich przycisków i osi. + Naciśnij ten przycisk na kontrolerze! + Przycisk kierunkowy 3DS z zaznaczonym przyciskiem A + Wyczyść wszystkie mapowania + Spowoduje to usunięcie wszystkich bieżących mapowań kontrolera. Analog C-Stick Skróty klawiszowe + Jeśli opcja \"Włącz skrót klawiszowy” jest włączona, należy nacisnąć ten klawisz oprócz przypisanego skrótu klawiszowego. + Włącz skrót klawiszowy Spusty Spust Krzyżak @@ -327,6 +336,8 @@ Sprawia, że operacje asynchroniczne są deterministyczne dla debugowania. Włączenie tej opcji może powodować zawieszanie się gry. Włącz serwer RPC Włącza serwer RPC na porcie 45987. Pozwala to na zdalny odczyt/zapis pamięci gościa. Nie włączaj tej opcji, jeśli nie wiesz, co robisz. + Przełącz typ indywidualnych danych konsoli + Przełącza unikalny typ konsoli danych (Old 3DS ↔ New 3DS), aby umożliwić pobranie oprogramowania układowego przeciwnego typu z ustawień systemowych. Aktywuj Shader JIT Używa silnika JIT zamiast interpretera do programowej emulacji shaderów. @@ -337,6 +348,8 @@ Odwrócony widok Ekran Odwrócony Ekran + Układy do cyklu + Które układy są przełączane za pomocą skrótu do układu cyklu Domyślny 16:9 4:3 @@ -357,7 +370,7 @@ Dowiedz się więcej Zamknij Przywróć ustawienia domyślne - kartridże z grami lub zainstalowane tytuły.]]> + kartridże z grami lub zainstalowane tytuły.]]> Domyślne Brak Automatyczne @@ -552,7 +565,6 @@ Przygotowanie shaderów Tworzenie%s - Odtwórz Odinstaluj aktualizacje diff --git a/src/android/app/src/main/res/values-b+pt+BR/strings.xml b/src/android/app/src/main/res/values-b+pt+BR/strings.xml index 1055be5e3..859768387 100644 --- a/src/android/app/src/main/res/values-b+pt+BR/strings.xml +++ b/src/android/app/src/main/res/values-b+pt+BR/strings.xml @@ -3,7 +3,7 @@ Este software irá executar aplicativos feitos para o console portátil Nintendo 3DS. Nenhum jogo está incluído.\n\nAntes de começar a emulação, selecione uma pasta para armazenar os dados do usuário do Azahar.\n\nO que é isto:\nWiki - Dados e armazenamento do usuário do Azahar para Android Notificações do emulador Azahar 3DS - Azahar está Executando + Azahar em Execução Em seguida, você precisará selecionar uma pasta de Aplicativos. O Azahar exibirá todas as ROMs de 3DS dentro da pasta selecionada no aplicativo.\n\nROMs, atualizações e DLC no formato CIA precisarão ser instaladas separadamente clicando no ícone da pasta e selecionando Instalar CIA. @@ -23,8 +23,8 @@ Drivers personalizados não suportados O carregamento de driver personalizado não é suportado atualmente para este dispositivo.\nVerifique esta opção novamente no futuro para ver se o suporte foi adicionado! Nenhum arquivo de log encontrado - Selecione a Pasta de Aplicativos - Permitir que o Azahar preencha a lista de aplicativos + Selecionar Pasta de Aplicativos + Permite que o Azahar preencha a lista de aplicativos Sobre Um emulador de 3DS de código aberto Versão da compilação, créditos e mais @@ -107,9 +107,18 @@ Recentemente adicionado Instalado + + Mapeamento Automático do Controle + Aplicar mapeamento padrão do controle para todos os botões e eixos + Pressione este botão no seu controle! + Botões de face em diamante do 3DS com o botão A destacado + Limpar Todos os Mapeamentos + Isso removerá todos os mapeamentos de controle atuais. Analógico Direcional C-Stick Teclas de atalho + Se a tecla \"Ativar Atalho\" estiver mapeada, ela deve ser pressionada em conjunto com o atalho mapeado + Ativar Atalho Gatilhos Gatilho Direcional D-Pad @@ -228,6 +237,8 @@ Compila shaders em segundo plano para reduzir travamentos durante o jogo. Quando ativado, espere falhas gráficas temporárias Filtragem Linear Ativa a filtragem linear, que suaviza o visual do jogo. + Escala Inteira + Garante que a tela maior em todos os layouts tenha uma escala inteira de 240px de altura, correspondente à tela original do 3DS. Filtro de texturas Aprimora o visual dos aplicativos ao aplicar filtros às texturas. Os filtros compatíveis são: Anime4K Ultrafast, Bicúbico, ScaleForce, xBRZ Freescale e MMPX. Atrasar Thread de Renderização do Aplicativo @@ -325,6 +336,8 @@ Torna as operações assíncronas determinísticas para depuração. Ativar essa opção pode causar congelamentos. Ativar servidor RPC Ativa o servidor RPC na porta 45987. Isso permite ler e escrever remotamente a memória do sistema emulado. + Alternar tipo de console de dados únicos + Alterna o tipo de console de dados únicos (Old 3DS ↔ New 3DS) para permitir o download do firmware do sistema oposto nas configurações do sistema. Ativar Shader JIT Usa o mecanismo JIT em vez do interpretador para a emulação de shaders por software. @@ -335,6 +348,8 @@ Paisagem Reversa Retrato Retrato Reverso + Layouts para Alternar + Quais layouts são percorridos pela tecla de atalho \"Alternar Layout\" Padrão 16:9 4:3 @@ -355,7 +370,7 @@ Saber mais Fechar Redefinir para o Padrão - cartuchos de jogos ou títulos instalados.]]> + cartuchos de jogo ou títulos instalados.]]> Padrão Nenhum Automático @@ -472,7 +487,7 @@ Posição Y Largura Altura - Trocar Disposições + Alternar Layout Trocar telas Girar Tela para Posição Vertical Redefinir sobreposição @@ -550,6 +565,10 @@ Preparando Shaders Construindo %s + Excluir Cache de Shaders + Selecione a API gráfica para excluir o cache de shaders + Excluindo cache de shaders do título, aguarde... + Cache de shaders excluído Jogar diff --git a/src/android/app/src/main/res/values-b+tr+TR/strings.xml b/src/android/app/src/main/res/values-b+tr+TR/strings.xml index da2a72592..bf540bdbb 100644 --- a/src/android/app/src/main/res/values-b+tr+TR/strings.xml +++ b/src/android/app/src/main/res/values-b+tr+TR/strings.xml @@ -306,7 +306,6 @@ Daha Fazla Öğren Kapat Varsayılanlara Sıfırla - oyun kartuşlarınızı veya yüklü başlıklarınızıyeniden yüklemek için kılavuzları izleyin.]]> Varsayılan Otomatik Kapalı diff --git a/src/android/app/src/main/res/values-b+zh+CN/strings.xml b/src/android/app/src/main/res/values-b+zh+CN/strings.xml index f9c07aa30..74ad152d7 100644 --- a/src/android/app/src/main/res/values-b+zh+CN/strings.xml +++ b/src/android/app/src/main/res/values-b+zh+CN/strings.xml @@ -334,7 +334,6 @@ 了解更多 关闭 恢复默认 - 游戏卡带或已安装的应用。]]> 默认 自动 diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml index a2d812a84..578072879 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml @@ -66,6 +66,7 @@ Berechtigung erteilen Möchtest du keine Benachrichtigungsberechtigung gewähren? Azahar wird dich nicht über wichtige Informationen benachrichtigen. + Fehlende Berechtigungen Kamera Teile unten die Kameraberechtigung, um die 3DS-Kamera zu emulieren Mikrofon @@ -89,6 +90,7 @@ Der Nutzer Ordner muss gesetzt sein Dieser Schritt ist nötig, damit Azahar funktionieren kann. Bitte wähle ein Verzeichnis aus, damit du fortfahren kannst. Die Schreibrechte auf dein Nutzerdaten-Verzeichnis, wo deine Speicherstände und andere Informationen gespeichert sind, fehlen. Dies kann durch ein Anwendungsupdate, oder ein Androidupdate passiert sein. Bitte wähle das Verzeichnis erneut aus, damit die Berechtigungen wiederhergestellt werden können. + Ungültige Auswahl Design-Einstellung Konfigurieren sie ihre Designeinstellungen für Azahar. Design wählen @@ -100,6 +102,7 @@ Kürzlich hinzugefügt Installiert + Alle Bindungen zurücksetzen Schiebepad C-Stick Tastenkürzel @@ -214,6 +217,7 @@ Kompiliere Shader im Hintergrund, um das Stottern des Spiels zu reduzieren. Dadurch kann es zu temporären grafischen Fehlern während des Spielens kommen. Lineare Filterung Aktiviert lineare Filterung, welche die Spieltexturen glättet. + Integer-Skalierung Texturfilter Verbessert die Optik von Anwendungen durch Anwenden eines Filters auf Texturen. Die unterstützten Filter sind Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale und MMPX. Verzögert den Render-Thread des Spiels, wenn er Daten an die GPU sendet. Hilft bei Leistungsproblemen in den (sehr wenigen) Anwendungen mit dynamischen Frameraten. @@ -222,6 +226,7 @@ Setzt den von Spielen verwendeten Sampling-Filter außer Kraft. Dies kann in bestimmten Fällen nützlich sein, wenn sich die Spiele beim Hochskalieren schlecht verhalten. Wenn du dir unsicher bist, setze diese Einstellung auf „Spielgesteuert“ Genaue Multiplikation Benutzt genauere Multiplikation in Hardware-Shadern, welche einige Grafikbugs fixen kann. Wenn aktiviert, ist die Leistung reduziert. + Aktiviere Asynchrone GPU-Emulation Verwendet einen separaten Thread, um die GPU asynchron zu emulieren. Wenn aktiviert, wird die Leistung verbessert. Höchstgeschwindigkeit Wenn aktiviert, wird die Emulationsgeschwindigkeit auf einen angegebenen Prozentsatz der normalen Geschwindigkeit begrenzt. Wenn diese Option deaktiviert ist, wird die Emulationsgeschwindigkeit nicht begrenzt und der Hotkey für die Turbogeschwindigkeit funktioniert nicht. @@ -252,6 +257,7 @@ Gibt den Wert des 3D-Schiebereglers an. Dieser Wert sollte auf mehr als 0 % eingestellt werden, wenn Stereoskopisches 3D aktiviert ist.\nHinweis: Tiefenwerte über 100% werden nicht von echter Hardware unterstützt und könnten zu grafischen Fehlern führen. Rendering für das rechte Auge deaktivieren Verbessert die Leistung in einigen Anwendungen erheblich, kann in anderen jedoch zu Flackern führen. + Augen tauschen Karton-VR Karton-Bildschirmgröße Skaliert den Bildschirm auf einen Prozentsatz seiner Originalgröße. @@ -276,6 +282,7 @@ Lautstärke Audiodehnung Dehnt Audio, um Stottern zu reduzieren. Wenn aktiviert, wird die Audiolatenz erhöht und die Leistung leicht verschlechtert. + Echtzeitaudio aktivieren Skaliert die Tonabspielgeschwindigkeit, um Einbrüche in der Emulationsframerate zu minimieren. Das bedeutet, dass der Ton in voller Geschwindigkeit abspielt, selbst wenn die Framerate des Spiels niedrig ist. Kann zu Tonverschiebungen führen. Audioeingabegerät Tonausgabemodus @@ -289,7 +296,9 @@ V-Sync aktivieren Debug-Renderer Zusätzliche grafisch spezifische Debuginformationen werden protokolliert. Wenn dies aktiviert ist, ist die Leistung des Spiels minimal reduziert. + Log Output bei jeder Nachricht leeren Überträgt das Debugprotokoll sofort in eine Datei. Verwenden Sie dies, wenn Azahar abstürzt und die Protokollausgabe abgeschnitten wird. + Start mit LLE-Module verzögern Verzögert den Start der App, wenn die LLE-Module aktiviert sind. Deterministische asynchrone Operationen Asynchrone Operationen werden für Debug-Zwecke deterministisch. Die Aktivierung dieser Funktion kann zum Einfrieren führen. @@ -325,7 +334,6 @@ Mehr erfahren Schließen Auf Standard zurücksetzen - Karten oder Downloadtitel)erneut zu dumpen.]]> Standard Keine Auto @@ -405,6 +413,10 @@ Original Standard Benutzerdefinierte Anordnung + Hintergrundfarbe + Rot + Grün + Blau Position des kleinen Bildschirms Wo soll der kleine Bildschirm im Verhältnis zum großen Bildschirm in der Großbild-Anordnung erscheinen? Oben Rechts @@ -521,11 +533,27 @@ Verknüpfung erstellen Verknüpfungsname darf nicht leer sein Bildgröße anpassen + ID: + Datei: + Spielkarte einsetzen + Spielkarte Entfernen + Leistungs-Overlay anzeigen Leistungs-Overlay Leistungsoverlay aktivieren Konfigurieren Sie, ob das Leistungs-Overlay angezeigt wird und welche Informationen angezeigt werden. + FPS anzeigen + Frametime anzeigen + Geschwindigkeit anzeigen + App-Speichernutzung anzeigen + Verfügbaren Speicher anzeigen + Batterietemperatur anzeigen + Overlay-Position + Oben Links + Oben Rechts + Unten Links + Unten Rechts Cheats Cheat hinzufügen @@ -816,4 +844,10 @@ Schnellspeichern - %1$tF %1$tR Kein Schnellspeicher vorhanden. + + Komprimiere + Wird komprimiert... + Dekomprimiere + Wird dekomprimiert... + Alle Dateien wurden erfolgreich komprimiert. diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index 9c36dcc6f..875e6b658 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -370,7 +370,7 @@ En savoir plus Fermer Par défaut - cartouches de jeu ou les jeux installés.]]> + cartouches ou titres installés.]]> Par défaut Aucun Auto @@ -565,6 +565,10 @@ Préparation des shaders Construction %s + Supprimer le cache de shaders + Sélectionnez l\'API graphique pour laquelle vous voulez supprimer le cache de shaders + Suppression du cache de shaders pour cette application, veuillez patienter ... + Le cache de shaders a été supprimé. Jouer diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index 847041389..2af71cb42 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -80,7 +80,7 @@ Permessi Cartella dati (La cartella utente è richiesta)]]> - Concedere autorizzazioni opzionali per utilizzare specifiche funzionalità dell\'emulatore + Concedi permessi facoltativi per utilizzare funzionalità specifiche dell\'emulatore Aiuto Salta Annulla @@ -95,7 +95,7 @@ Hai perso i permessi di scrittura sulla tua cartella dei dati utente, dove sono memorizzati i salvataggi e altre informazioni. Questo può accadere dopo alcuni aggiornamenti app o Android. Seleziona nuovamente la cartella per ripristinare i permessi e continuare. Selezione non valida La cartella utente selezionata non è valida.\nSelezionala di nuovo assicurandoti di partire dalla memoria principale del dispositivo. - Azahar ha perso l\'autorizzazione a gestire i file su questo dispositivo. Ciò può accadere dopo alcuni aggiornamenti di app o Android. Si prega di concedere nuovamente questa autorizzazione nella schermata successiva per continuare a utilizzare l\'app. + Azahar ha perso l\'autorizzazione a gestire i file su questo dispositivo. Ciò può accadere dopo alcuni aggiornamenti dell\' app o di Android. Si prega di concedere nuovamente questa autorizzazione nella schermata successiva per continuare a utilizzare l\'app. Impostazioni tema Configura le impostazioni del tema di Azahar. Imposta tema @@ -370,7 +370,7 @@ Scopri di più Chiudi Reimposta - cartuccie di gioco o titoli installati.]]> + cartuccie di gioco o titoli installati.]]> Standard Nessuno Auto @@ -565,6 +565,10 @@ Preparazione degli shader Compilazione %s + Cancella cache shader + Seleziona per quale API grafica eliminare la cache shader + Eliminazione della cache shader per il titolo, attendi... + Cache shader cancellate Riproduci diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml index f94484f18..085690c5b 100644 --- a/src/android/app/src/main/res/values-nb/strings.xml +++ b/src/android/app/src/main/res/values-nb/strings.xml @@ -75,7 +75,7 @@ Konfigurer Kontroller Endre Utseende Ferdig - Veksle Kontroller + Bytt Kontrollene Juster Skala Åpne Innstillinger Landskap Skjermoppsett diff --git a/src/android/app/src/main/res/values-sv/strings.xml b/src/android/app/src/main/res/values-sv/strings.xml index fdc0ba794..52c276371 100644 --- a/src/android/app/src/main/res/values-sv/strings.xml +++ b/src/android/app/src/main/res/values-sv/strings.xml @@ -370,7 +370,7 @@ Lär dig mer Stäng Återställ till standard - spelkassetter igen eller installerade titlar.]]> + spelkassetter eller installerade titlarna.]]> Standard Ingen Auto @@ -565,7 +565,6 @@ Förbereder shaders Bygger %s - Spela Avinstallera applikation diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 70966639a..5eefd07ee 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -233,6 +233,8 @@ Storage Compress installed CIA content Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + Asynchronous filesystem operations + Makes emulated filesystem accesses asynchronous. Greatly reduces filesystem related stutter, but may slightly increase load times. Inner Camera @@ -264,6 +266,8 @@ Enhances the visuals of applications by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale, and MMPX. Delay Game Render Thread Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) applications with dynamic framerates. + Simulate 3DS GPU Timings + Delays GPU completion events based on measurements taken from real hardware, so that games have more realistic GPU time measurements. Helps stabilize dynamic FPS games. Disabling this feature may improve performance in some rare cases at the cost of stability. Advanced Texture Sampling Overrides the sampling filter used by games. This can be useful in certain cases with poorly behaved games when upscaling. If unsure, set this to Game Controlled. @@ -336,6 +340,8 @@ Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance. Enable Realtime Audio Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the game framerate is low. May cause audio desync issues. + Simulate Headphones Plugged In + Simulates whether headphones are plugged in to the emulated 3DS system. Audio Input Device Sound Output Mode @@ -434,11 +440,39 @@ Layout + Error loading application + Invalid application format + Please make sure you are using one of the compatible file formats:]]> + Invalid system mode + New 3DS exclusive applications cannot be loaded without enabling the New 3DS mode. + Error applying patches + A generic error occurred while applying a patch to the application. Please check the log for more details. + Failed to apply a patch because it is designed for a different application. Please make sure you are using the patches for the right application, region and version. Your ROM is Encrypted - Invalid ROM format + blog post for more information.]]> ROM file does not exist No bootable game present! + An error occurred while loading ROM: \"%s (%d)\" + Success + Not initialized + Loader for file not found, incompatible file type + Failed to parse file + Generic loader error + Encrypted file + Corrupted file + File is GBA title + Error applying patches + Patches are for a different application + Missing system files + Savestate failed + Artic Base disconnected + File is New 3DS application + Core exception raised + Memory exception raised + Shutdown requested + Unknown error + Press Back to access the menu. Save State @@ -586,6 +620,10 @@ Preparing Shaders Building %s + Delete Shader Cache + Select which graphics API to delete the shader cache + Deleting shader cache for title, please wait… + Shader cache deleted Play diff --git a/src/android/build.gradle.kts b/src/android/build.gradle.kts index a0d43cb49..194907da3 100644 --- a/src/android/build.gradle.kts +++ b/src/android/build.gradle.kts @@ -4,8 +4,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.1" apply false - id("com.android.library") version "8.13.1" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false id("org.jetbrains.kotlin.android") version "2.0.20" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20" } diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 6ea16672e..f6f8bf9ed 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(audio_core STATIC +add_library(audio_core STATIC EXCLUDE_FROM_ALL audio_types.h codec.cpp codec.h diff --git a/src/citra_libretro/CMakeLists.txt b/src/citra_libretro/CMakeLists.txt index cd2f6baaf..797deedb7 100644 --- a/src/citra_libretro/CMakeLists.txt +++ b/src/citra_libretro/CMakeLists.txt @@ -31,6 +31,11 @@ add_library(azahar_libretro SHARED create_target_directory_groups(azahar_libretro) +if (BSD STREQUAL "NetBSD") + include(DisablePaxMprotect) + disable_pax_mprotect(azahar_libretro) +endif() + target_link_libraries(citra_common PRIVATE libretro) target_link_libraries(citra_core PRIVATE libretro) target_link_libraries(video_core PRIVATE libretro) diff --git a/src/citra_libretro/citra_libretro.cpp b/src/citra_libretro/citra_libretro.cpp index 9263fc46b..c93931196 100644 --- a/src/citra_libretro/citra_libretro.cpp +++ b/src/citra_libretro/citra_libretro.cpp @@ -105,31 +105,6 @@ unsigned retro_api_version() { return RETRO_API_VERSION; } -void LibRetro::OnConfigureEnvironment() { - -#ifdef HAVE_LIBRETRO_VFS - struct retro_vfs_interface_info vfs_iface_info{1, nullptr}; - LibRetro::SetVFSCallback(&vfs_iface_info); -#endif - - LibRetro::RegisterCoreOptions(); - - static const struct retro_controller_description controllers[] = { - {"Nintendo 3DS", RETRO_DEVICE_JOYPAD}, - }; - - static const struct retro_controller_info ports[] = { - {controllers, 1}, - {nullptr, 0}, - }; - - LibRetro::SetControllerInfo(ports); -} - -uintptr_t LibRetro::GetFramebuffer() { - return emu_instance->hw_render.get_current_framebuffer(); -} - /** * Updates Citra's settings with Libretro's. */ @@ -598,6 +573,7 @@ bool retro_load_game(const struct retro_game_info* info) { LibRetro::DisplayMessage("Failed to set HW renderer"); return false; } + LibRetro::SetFramebufferCallback(emu_instance->hw_render.get_current_framebuffer); #endif break; case Settings::GraphicsAPI::Vulkan: diff --git a/src/citra_libretro/core_settings.cpp b/src/citra_libretro/core_settings.cpp index 3710709c4..085f7ac52 100644 --- a/src/citra_libretro/core_settings.cpp +++ b/src/citra_libretro/core_settings.cpp @@ -87,7 +87,8 @@ static constexpr const char* enable_mouse_touchscreen = citra_setting(BaseKeys::enable_mouse_touchscreen); static constexpr const char* enable_touch_touchscreen = citra_setting(BaseKeys::enable_touch_touchscreen); -static constexpr const char* render_touchscreen = citra_setting(BaseKeys::render_touchscreen); +static constexpr const char* enable_touch_pointer_timeout = + citra_setting(BaseKeys::enable_touch_pointer_timeout); static constexpr const char* enable_motion = citra_setting(BaseKeys::enable_motion); static constexpr const char* motion_sensitivity = citra_setting(BaseKeys::motion_sensitivity); } // namespace input @@ -609,10 +610,10 @@ static constexpr retro_core_option_v2_definition option_definitions[] = { config::enabled }, { - config::input::render_touchscreen, - "Show Touch Interactions", - "Show Touch", - "Visually indicate touchscreen interactions on screen.", + config::input::enable_touch_pointer_timeout, + "Touch Pointer Timeout", + "Touch Pointer Timeout", + "Whether or not the touchscreen pointer should disappear during inactivity.", nullptr, config::category::input, { @@ -620,7 +621,7 @@ static constexpr retro_core_option_v2_definition option_definitions[] = { { config::disabled, "Disabled" }, { nullptr, nullptr } }, - config::disabled + config::enabled }, { config::input::enable_motion, @@ -1025,9 +1026,10 @@ static void ParseInputOptions(void) { LibRetro::FetchVariable(config::input::enable_touch_touchscreen, config::enabled) == config::enabled; - LibRetro::settings.render_touchscreen = - LibRetro::FetchVariable(config::input::render_touchscreen, config::disabled) == + LibRetro::settings.enable_touch_pointer_timeout = + LibRetro::FetchVariable(config::input::enable_touch_pointer_timeout, config::enabled) == config::enabled; + LibRetro::settings.enable_motion = LibRetro::FetchVariable(config::input::enable_motion, config::enabled) == config::enabled; auto motion_sens = LibRetro::FetchVariable(config::input::motion_sensitivity, "1.0"); diff --git a/src/citra_libretro/core_settings.h b/src/citra_libretro/core_settings.h index e7297c839..6b3178875 100644 --- a/src/citra_libretro/core_settings.h +++ b/src/citra_libretro/core_settings.h @@ -25,7 +25,7 @@ struct CoreSettings { bool enable_touch_touchscreen; - bool render_touchscreen; + bool enable_touch_pointer_timeout; std::string swap_screen_mode; diff --git a/src/citra_libretro/emu_window/libretro_window.cpp b/src/citra_libretro/emu_window/libretro_window.cpp index a1bd863a4..8e8532566 100644 --- a/src/citra_libretro/emu_window/libretro_window.cpp +++ b/src/citra_libretro/emu_window/libretro_window.cpp @@ -341,7 +341,7 @@ void EmuWindow_LibRetro::DestroyContext() { } Frontend::EmuWindow::CursorInfo EmuWindow_LibRetro::GetCursorInfo() const { - if (enableEmulatedPointer && tracker && LibRetro::settings.render_touchscreen) { + if (enableEmulatedPointer && tracker) { return tracker->GetCursorInfo(); } return {}; diff --git a/src/citra_libretro/environment.cpp b/src/citra_libretro/environment.cpp index 04c78356f..03bf51053 100644 --- a/src/citra_libretro/environment.cpp +++ b/src/citra_libretro/environment.cpp @@ -8,6 +8,7 @@ #include "audio_core/libretro_sink.h" #include "common/scm_rev.h" #include "core/3ds.h" +#include "core_settings.h" #include "emu_window/libretro_window.h" #include "environment.h" @@ -26,6 +27,7 @@ static retro_audio_sample_batch_t audio_batch_cb; static retro_environment_t environ_cb; static retro_input_poll_t input_poll_cb; static retro_input_state_t input_state_cb; +static retro_hw_get_current_framebuffer_t framebuffer_cb; } // namespace @@ -61,7 +63,14 @@ bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface) { } Settings::GraphicsAPI GetPreferredRenderer() { - // try and maintain the current driver + // On Android, we really want to default to Vulkan if we can, so we'll ignore the frontend's + // recommendation if possible... +#if defined(ANDROID) && defined(ENABLE_VULKAN) + return Settings::GraphicsAPI::Vulkan; +#endif + // ...Otherwise negotiate with the RetroArch frontend as usual + + // Attempt to use the renderer recommended by the frontend if possible retro_hw_context_type context_type = RETRO_HW_CONTEXT_OPENGL; environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &context_type); switch (context_type) { @@ -80,7 +89,7 @@ Settings::GraphicsAPI GetPreferredRenderer() { default: break; } - // we can't maintain the current driver, need to switch + // We can't get a recommendation from the frontend, so fall back to whatever's available #if defined(ENABLE_VULKAN) return Settings::GraphicsAPI::Vulkan; #elif defined(ENABLE_OPENGL) @@ -225,6 +234,34 @@ bool CanUseJIT() { } #endif +void OnConfigureEnvironment() { +#ifdef HAVE_LIBRETRO_VFS + struct retro_vfs_interface_info vfs_iface_info{1, nullptr}; + SetVFSCallback(&vfs_iface_info); +#endif + + RegisterCoreOptions(); + + static const struct retro_controller_description controllers[] = { + {"Nintendo 3DS", RETRO_DEVICE_JOYPAD}, + }; + + static const struct retro_controller_info ports[] = { + {controllers, 1}, + {nullptr, 0}, + }; + + SetControllerInfo(ports); +} + +void SetFramebufferCallback(retro_hw_get_current_framebuffer_t cb) { + framebuffer_cb = cb; +} + +uintptr_t GetFramebuffer() { + return framebuffer_cb ? framebuffer_cb() : 0; +} + }; // namespace LibRetro void retro_get_system_info(struct retro_system_info* info) { diff --git a/src/citra_libretro/environment.h b/src/citra_libretro/environment.h index 2b84d8b85..18f2b04bd 100644 --- a/src/citra_libretro/environment.h +++ b/src/citra_libretro/environment.h @@ -109,6 +109,9 @@ bool HasUpdatedConfig(); /// Returns the current framebuffer. uintptr_t GetFramebuffer(); +/// Sets the callback used by GetFramebuffer(). +void SetFramebufferCallback(retro_hw_get_current_framebuffer_t cb); + /// Tells the frontend that we are done. bool Shutdown(); diff --git a/src/citra_libretro/input/mouse_tracker.cpp b/src/citra_libretro/input/mouse_tracker.cpp index d553ef2f4..c847f6028 100644 --- a/src/citra_libretro/input/mouse_tracker.cpp +++ b/src/citra_libretro/input/mouse_tracker.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include @@ -112,6 +113,8 @@ MouseTracker::MouseTracker() { cursor_renderer = std::make_unique(); break; } + + last_moved = std::chrono::steady_clock::time_point::min(); } MouseTracker::~MouseTracker() = default; @@ -129,6 +132,7 @@ void MouseTracker::Restrict(int minX, int minY, int maxX, int maxY) { void MouseTracker::Update(int bufferWidth, int bufferHeight, const Layout::FramebufferLayout& layout) { bool state = false; + bool wasMoved = false; if (LibRetro::settings.enable_mouse_touchscreen) { // Check mouse input @@ -203,16 +207,20 @@ void MouseTracker::Update(int bufferWidth, int bufferHeight, INT16_MAX); // Deadzone the controller inputs - float smoothedX = std::abs(controllerX); - float smoothedY = std::abs(controllerY); + float magnitudeX = std::abs(controllerX); + float magnitudeY = std::abs(controllerY); - if (smoothedX < LibRetro::settings.analog_deadzone) { + if (magnitudeX < LibRetro::settings.analog_deadzone) { controllerX = 0; } - if (smoothedY < LibRetro::settings.analog_deadzone) { + if (magnitudeY < LibRetro::settings.analog_deadzone) { controllerY = 0; } + if (controllerX != 0 || controllerY != 0) { + wasMoved = true; + } + OnMouseMove(static_cast(controllerX * widthSpeed), static_cast(controllerY * heightSpeed)); } @@ -225,11 +233,15 @@ void MouseTracker::Update(int bufferWidth, int bufferHeight, isPressed = state; + if (wasMoved) { + last_moved = std::chrono::steady_clock::now(); + } + this->framebuffer_layout = layout; } void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_data) { - if (!LibRetro::settings.render_touchscreen) { + if (GetCursorInfo().visible == false) { return; } @@ -245,6 +257,18 @@ void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_d } } +Frontend::EmuWindow::CursorInfo MouseTracker::GetCursorInfo() { + bool visible = true; + auto current = std::chrono::steady_clock::now(); + uint64_t since_last_moved = + std::chrono::duration_cast(current - last_moved).count(); + constexpr auto timeout_secs = 4; // TODO: Make this configurable maybe? -OS + if (LibRetro::settings.enable_touch_pointer_timeout && since_last_moved >= timeout_secs) { + visible = false; + } + return {visible, projectedX, projectedY}; +} + #ifdef ENABLE_OPENGL // OpenGL-specific cursor renderer implementation OpenGLCursorRenderer::OpenGLCursorRenderer() { diff --git a/src/citra_libretro/input/mouse_tracker.h b/src/citra_libretro/input/mouse_tracker.h index 9f0e7e179..0f77aaaf8 100644 --- a/src/citra_libretro/input/mouse_tracker.h +++ b/src/citra_libretro/input/mouse_tracker.h @@ -4,6 +4,7 @@ #pragma once +#include #include "common/math_util.h" #include "core/frontend/emu_window.h" #include "core/frontend/framebuffer_layout.h" @@ -55,9 +56,7 @@ public: } /// Get cursor rendering state for external renderers (e.g. Vulkan). - Frontend::EmuWindow::CursorInfo GetCursorInfo() const { - return {true, projectedX, projectedY}; - } + Frontend::EmuWindow::CursorInfo GetCursorInfo(); private: int x; @@ -69,6 +68,7 @@ private: float projectedX; float projectedY; + std::chrono::steady_clock::time_point last_moved; bool isPressed; Layout::FramebufferLayout framebuffer_layout; diff --git a/src/citra_meta/CMakeLists.txt b/src/citra_meta/CMakeLists.txt index a8ca812c3..a2398b5d6 100644 --- a/src/citra_meta/CMakeLists.txt +++ b/src/citra_meta/CMakeLists.txt @@ -6,6 +6,11 @@ add_executable(citra_meta set_target_properties(citra_meta PROPERTIES OUTPUT_NAME "azahar") +if (BSD STREQUAL "NetBSD") + include(DisablePaxMprotect) + disable_pax_mprotect(citra_meta) +endif() + if (APPLE) set(DIST_DIR "../../dist/apple") set(APPLE_RESOURCES @@ -43,6 +48,26 @@ if (APPLE) endif() endif() +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" AND MINGW) + # TODO: Do this for all executables, not just citra_meta + # TODO: Don't hardcode MXE directory to root? + set(dllwalker_command "${CMAKE_SOURCE_DIR}/externals/dllwalker/dllwalker.rb" + $ + /mxe/usr/x86_64-w64-mingw32.shared/bin/ + /mxe/usr/x86_64-w64-mingw32.shared/qt6/bin/) + set(dll_list_filename "${CMAKE_CURRENT_BINARY_DIR}/required_dlls_list.txt") + add_custom_command(TARGET citra_meta POST_BUILD + COMMAND ${dllwalker_command} > ${dll_list_filename} + ) + add_custom_command(TARGET citra_meta POST_BUILD + COMMAND cat ${dll_list_filename} | xargs -I {} ${CMAKE_COMMAND} -E copy {} $ + COMMAND ${CMAKE_COMMAND} -E make_directory "$/plugins/" + COMMAND ${CMAKE_COMMAND} -E copy_directory /mxe/usr/x86_64-w64-mingw32.shared/qt6/plugins/ "$/plugins/" + COMMAND_EXPAND_LISTS + DEPENDS ${dll_list_filename} + ) +endif() + target_link_libraries(citra_meta PRIVATE citra_common fmt) if (ENABLE_QT) diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 20971828a..4c20b510b 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -174,6 +174,8 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL multiplayer/state.cpp multiplayer/state.h multiplayer/validation.h + notification_led.cpp + notification_led.h precompiled_headers.h qt_image_interface.cpp qt_image_interface.h @@ -223,10 +225,13 @@ if (ENABLE_QT_TRANSLATION) if (GENERATE_QT_TRANSLATION) get_target_property(QT_SRCS citra_qt SOURCES) get_target_property(QT_INCLUDES citra_qt INCLUDE_DIRECTORIES) - qt_add_lupdate(citra_qt TS_FILES ${CITRA_QT_LANGUAGES}/en.ts + qt_add_lupdate( + LUPDATE_TARGET citra_qt_lupdate SOURCES ${QT_SRCS} ${UIS} + TS_FILES ${CITRA_QT_LANGUAGES}/en.ts INCLUDE_DIRECTORIES ${QT_INCLUDES} - NO_GLOBAL_TARGET) + NO_GLOBAL_TARGET + ) add_custom_target(translation ALL DEPENDS citra_qt_lupdate) endif() @@ -275,6 +280,10 @@ target_link_libraries(citra_qt PRIVATE audio_core citra_common citra_core input_ target_link_libraries(citra_qt PRIVATE Boost::boost nihstro-headers Qt6::Widgets Qt6::Multimedia Qt6::Concurrent) target_link_libraries(citra_qt PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) +if (MINGW) + target_link_libraries(citra_qt PRIVATE dwmapi) +endif() + if (ENABLE_OPENGL) target_link_libraries(citra_qt PRIVATE glad) endif() diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index f9329dd31..f2390d6d9 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -142,6 +142,61 @@ constexpr int default_mouse_timeout = 2500; const int GMainWindow::max_recent_files_item; +// There is a bug in the QT implementation on MSYS2 builds +// that cause corners to appear when the app is switched to +// fullscreen. The following code aims to fix that issue +// until it is addressed upstream. It works by manually +// disabling corners through the DWM API. +// TODO(PabloMK7): Remove once the upstream bug is solved. +#if defined(_WIN32) && !defined(_MSC_VER) +#define NEEDS_ROUND_CORNERS_FIX +#endif + +#ifdef NEEDS_ROUND_CORNERS_FIX +#include +class WindowCornerManager { +public: + static WindowCornerManager& instance() { + static WindowCornerManager inst; + return inst; + } + + void blockRoundedCorners(QWidget* widget, bool block) { + HWND hwnd = reinterpret_cast(widget->winId()); + DWORD pref; + + if (block) { + pref = DWMWCP_DEFAULT; + if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, + sizeof(pref)))) { + original_prefs[hwnd] = pref; + } else { + original_prefs[hwnd] = DWMWCP_DEFAULT; + } + + pref = DWMWCP_DONOTROUND; + DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)); + } else { + auto it = original_prefs.find(hwnd); + if (it == original_prefs.end()) + return; + + pref = it->second; + + DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)); + + original_prefs.erase(it); + } + } + +private: + WindowCornerManager() = default; + ~WindowCornerManager() = default; + + std::unordered_map original_prefs; +}; +#endif + static QString PrettyProductName() { #ifdef _WIN32 // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 @@ -352,6 +407,8 @@ GMainWindow::GMainWindow(Core::System& system_) Camera::RegisterFactory("image", std::make_unique()); Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + system.RegisterInfoLEDColorChanged([this]() { emit InfoLEDColorChanged(); }); + LoadTranslation(); Pica::g_debug_context = Pica::DebugContext::Construct(); @@ -364,7 +421,7 @@ GMainWindow::GMainWindow(Core::System& system_) #ifdef USE_DISCORD_PRESENCE SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); - discord_rpc->Update(); + discord_rpc->Update(false); #endif play_time_manager = std::make_unique(); @@ -465,24 +522,10 @@ GMainWindow::GMainWindow(Core::System& system_) #ifdef ENABLE_OPENGL gl_renderer = GetOpenGLRenderer(); -#if defined(_WIN32) - if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { - // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ - // We can override the version here to allow Citra to work. - // TODO: Remove this when OpenGL 4.0+ is advertised. - qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); - } -#endif #endif #ifdef ENABLE_VULKAN physical_devices = GetVulkanPhysicalDevices(); - if (physical_devices.empty()) { - QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), - tr("Vulkan initialization failed during boot.
" - "Your GPU may not support Vulkan 1.1, or you do not " - "have the latest graphics driver.")); - } #endif if (!game_path.isEmpty()) { @@ -620,6 +663,20 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + QFrame* sep = new QFrame(this); + sep->setFrameShape(QFrame::VLine); + sep->setFrameShadow(QFrame::Sunken); + sep->setFixedHeight(16); + statusBar()->addPermanentWidget(sep); + + notification_led = new LedWidget(); + notification_led->setToolTip(tr("Emulated notification LED")); + statusBar()->addPermanentWidget(notification_led); + connect(this, &GMainWindow::InfoLEDColorChanged, this, [this] { + auto led_color = system.GetInfoLEDColor(); + notification_led->setColor(QColor(led_color.r(), led_color.g(), led_color.b())); + }); + statusBar()->setVisible(true); // Removes an ugly inner border from the status bar widgets under Linux @@ -798,10 +855,11 @@ void GMainWindow::InitializeHotkeys() { // QAction Hotkeys const auto link_action_shortcut = [&](QAction* action, const QString& action_name, - const bool primary_only = false) { + const bool primary_only = false, + const bool auto_repeat = false) { static const QString main_window = QStringLiteral("Main Window"); action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); - action->setAutoRepeat(false); + action->setAutoRepeat(auto_repeat); this->addAction(action); if (!primary_only) secondary_window->addAction(action); @@ -818,6 +876,9 @@ void GMainWindow::InitializeHotkeys() { link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); link_action_shortcut(ui->action_Fullscreen, fullscreen, true); link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + link_action_shortcut(ui->action_Debug_Pause, QStringLiteral("Debug Pause")); + link_action_shortcut(ui->action_Debug_Resume, QStringLiteral("Debug Resume")); + link_action_shortcut(ui->action_Debug_Step, QStringLiteral("Debug Step"), false, true); link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, QStringLiteral("Swap Screens")); link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, QStringLiteral("Rotate Screens Upright")); @@ -973,7 +1034,7 @@ void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { OnPauseGame(); } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { auto_paused = false; - OnStartGame(); + OnResumeGame(false); } } if (UISettings::values.mute_when_in_background) { @@ -1132,6 +1193,23 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); + // Tools debug + connect_menu(ui->action_Debug_Pause, [this] { + if (emu_thread) { + emu_thread->SetRunning(false); + } + }); + connect_menu(ui->action_Debug_Resume, [this] { + if (emu_thread) { + emu_thread->SetRunning(true); + } + }); + connect_menu(ui->action_Debug_Step, [this] { + if (emu_thread) { + emu_thread->ExecStep(); + } + }); + // Tools connect_menu(ui->action_Compress_ROM_File, &GMainWindow::OnCompressFile); connect_menu(ui->action_Decompress_ROM_File, &GMainWindow::OnDecompressFile); @@ -1288,49 +1366,39 @@ bool GMainWindow::LoadROM(const QString& filename) { system.Load(*render_window, filename.toStdString(), secondary_window)}; if (result != Core::System::ResultStatus::Success) { + QString invalid_format = tr("Invalid application format"); + QString invalid_format_description = + tr("The application file format not supported.
Please make sure you are using one " + "of the compatible file formats:
  • Cartridge images: " + ".cci/.zcci/.3ds
  • Installable archives: " + ".cia/.zcia
  • Homebrew titles: .3dsx/.z3dsx
  • NCCH " + "containers: .cxi/.zcxi/.app
  • ELF files: .elf/.axf
"); + switch (result) { case Core::System::ResultStatus::ErrorGetLoader: - LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); - QMessageBox::critical( - this, tr("Invalid App Format"), - tr("Your app format is not supported.
Please check the wiki and redump your " - "game " - "titles.")); + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}", filename.toStdString()); + QMessageBox::critical(this, invalid_format, invalid_format_description); break; case Core::System::ResultStatus::ErrorSystemMode: - LOG_CRITICAL(Frontend, "Failed to load App!"); - QMessageBox::critical( - this, tr("App Corrupted"), - tr("Your app is corrupted.
Please check the wiki and redump your " - "game " - "titles.")); + LOG_CRITICAL(Frontend, "Failed to load application!"); + QMessageBox::critical(this, invalid_format, invalid_format_description); break; case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { - QMessageBox::critical(this, tr("App Encrypted"), - tr("Your app is encrypted.
" + QMessageBox::critical(this, tr("Encrypted application"), + tr("Encrypted applications are not supported.
" "" "Please check our blog for more info.")); break; } case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: - QMessageBox::critical( - this, tr("Invalid App Format"), - tr("Your app format is not supported.
Please check the wiki and redump your " - "game " - "titles.")); + QMessageBox::critical(this, invalid_format, invalid_format_description); break; case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: - QMessageBox::critical(this, tr("Unsupported App"), + QMessageBox::critical(this, tr("Unsupported application"), tr("GBA Virtual Console is not supported by Azahar.")); break; @@ -1347,10 +1415,27 @@ bool GMainWindow::LoadROM(const QString& filename) { tr("New 3DS exclusive applications cannot be loaded without " "enabling the New 3DS mode.")); break; + case Core::System::ResultStatus::ErrorLoader: + QMessageBox::critical(this, tr("Generic load error"), + tr("An generic load error occurred while loading the " + "application.
Please check the log for more details.")); + break; + case Core::System::ResultStatus::ErrorLoader_ErrorPatches: + QMessageBox::critical(this, tr("Error applying patches"), + tr("A generic error occurred while applying a patch to the " + "application.
Please check the log for more details.")); + break; + case Core::System::ResultStatus::ErrorLoader_ErrorPatchesInvalidTitle: + QMessageBox::critical( + this, tr("Error applying patches"), + tr("Failed to apply a patch because it is designed for a different " + "application.
Please make sure you are using the patches for " + "the right application, region and version.")); + break; default: QMessageBox::critical( - this, tr("Error while loading App!"), - tr("An unknown error occurred. Please see the log for more details.")); + this, tr("Error while loading application"), + tr("An unknown error occurred.
Please see the log for more details.")); break; } return false; @@ -1409,7 +1494,8 @@ void GMainWindow::BootGame(const QString& filename) { auto loader = Loader::GetLoader(path); u64 title_id{0}; - Loader::ResultStatus res = loader->ReadProgramId(title_id); + Loader::ResultStatus res = + loader ? loader->ReadProgramId(title_id) : Loader::ResultStatus::Error; if (Loader::ResultStatus::Success == res) { // Load per game settings @@ -1422,7 +1508,7 @@ void GMainWindow::BootGame(const QString& filename) { // Artic 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()) { + if (loader && !loader->SupportsMultipleInstancesForSameFile()) { system.RegisterAppLoaderEarly(loader); } @@ -1516,7 +1602,7 @@ void GMainWindow::BootGame(const QString& filename) { ShowFullscreen(); } - OnStartGame(); + OnResumeGame(true); } void GMainWindow::ShutdownGame() { @@ -1569,7 +1655,7 @@ void GMainWindow::ShutdownGame() { OnCloseMovie(); #ifdef USE_DISCORD_PRESENCE - discord_rpc->Update(); + discord_rpc->Update(false); #endif #ifdef __unix__ Common::Linux::StopGamemode(); @@ -1602,6 +1688,7 @@ void GMainWindow::ShutdownGame() { emu_speed_label->setVisible(false); game_fps_label->setVisible(false); emu_frametime_label->setVisible(false); + notification_led->setColor(QColor(0, 0, 0)); UpdateSaveStates(); @@ -2493,7 +2580,7 @@ void GMainWindow::OnMenuRecentFile() { } } -void GMainWindow::OnStartGame() { +void GMainWindow::OnResumeGame(bool first_start) { qt_cameras->ResumeCameras(); PreventOSSleep(); @@ -2510,9 +2597,12 @@ void GMainWindow::OnStartGame() { play_time_manager->SetProgramId(game_title_id); play_time_manager->Start(); + if (first_start) { #ifdef USE_DISCORD_PRESENCE - discord_rpc->Update(); + discord_rpc->Update(true); #endif + } + #ifdef __unix__ Common::Linux::StartGamemode(); #endif @@ -2548,7 +2638,7 @@ void GMainWindow::OnPauseContinueGame() { if (emu_thread->IsRunning() && !system.frame_limiter.IsFrameAdvancing()) { OnPauseGame(); } else { - OnStartGame(); + OnResumeGame(false); } } } @@ -2587,8 +2677,14 @@ void GMainWindow::ToggleSecondaryFullscreen() { return; } if (secondary_window->isFullScreen()) { +#ifdef NEEDS_ROUND_CORNERS_FIX + WindowCornerManager::instance().blockRoundedCorners(secondary_window, false); +#endif secondary_window->showNormal(); } else { +#ifdef NEEDS_ROUND_CORNERS_FIX + WindowCornerManager::instance().blockRoundedCorners(secondary_window, true); +#endif secondary_window->showFullScreen(); } } @@ -2598,9 +2694,15 @@ void GMainWindow::ShowFullscreen() { UISettings::values.geometry = saveGeometry(); ui->menubar->hide(); statusBar()->hide(); +#ifdef NEEDS_ROUND_CORNERS_FIX + WindowCornerManager::instance().blockRoundedCorners(this, true); +#endif showFullScreen(); } else { UISettings::values.renderwindow_geometry = render_window->saveGeometry(); +#ifdef NEEDS_ROUND_CORNERS_FIX + WindowCornerManager::instance().blockRoundedCorners(render_window, true); +#endif render_window->showFullScreen(); } } @@ -2609,9 +2711,15 @@ void GMainWindow::HideFullscreen() { if (ui->action_Single_Window_Mode->isChecked()) { statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); ui->menubar->show(); +#ifdef NEEDS_ROUND_CORNERS_FIX + WindowCornerManager::instance().blockRoundedCorners(this, false); +#endif showNormal(); restoreGeometry(UISettings::values.geometry); } else { +#ifdef NEEDS_ROUND_CORNERS_FIX + WindowCornerManager::instance().blockRoundedCorners(render_window, false); +#endif render_window->showNormal(); render_window->restoreGeometry(UISettings::values.renderwindow_geometry); } @@ -2850,6 +2958,7 @@ void GMainWindow::OnConfigure() { #ifdef USE_DISCORD_PRESENCE if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(system.IsPoweredOn()); } #endif #ifdef __unix__ @@ -3017,7 +3126,7 @@ void GMainWindow::OnCloseMovie() { } if (was_running) { - OnStartGame(); + OnResumeGame(false); } } @@ -3039,7 +3148,7 @@ void GMainWindow::OnSaveMovie() { } if (was_running) { - OnStartGame(); + OnResumeGame(false); } } @@ -3083,7 +3192,7 @@ void GMainWindow::OnCaptureScreenshot() { screenshot_window->CaptureScreenshot( UISettings::values.screenshot_resolution_factor.GetValue(), QString::fromStdString(path)); - OnStartGame(); + OnResumeGame(false); } } @@ -3485,7 +3594,7 @@ void GMainWindow::OnStopVideoDumping() { ShutdownGame(); } else if (game_paused_for_dumping) { game_paused_for_dumping = false; - OnStartGame(); + OnResumeGame(false); } }); future_watcher->setFuture(future); @@ -3786,6 +3895,18 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det .c_str()); error_severity_icon = QMessageBox::Icon::Critical; can_continue = false; + } else if (result == Core::System::ResultStatus::ErrorCoreExceptionRaised) { + title = tr("An exception occurred"); + message = tr("An exception occurred while executing the emulated application.\n\n"); + message += QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else if (result == Core::System::ResultStatus::ErrorMemoryExceptionRaised) { + title = tr("An invalid memory access occurred"); + message = + tr("An invalid memory access occurred while executing the emulated application.\n\n"); + message += QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Critical; } else { title = tr("Fatal Error"); message = tr("A fatal error occurred. " @@ -4216,7 +4337,6 @@ void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { } else { discord_rpc = std::make_unique(); } - discord_rpc->Update(); } #endif diff --git a/src/citra_qt/citra_qt.h b/src/citra_qt/citra_qt.h index cdf9eaff6..01dcaabfb 100644 --- a/src/citra_qt/citra_qt.h +++ b/src/citra_qt/citra_qt.h @@ -21,6 +21,7 @@ #include #include "citra_qt/compatibility_list.h" #include "citra_qt/hotkeys.h" +#include "citra_qt/notification_led.h" #include "citra_qt/user_data_migration.h" #include "core/core.h" #include "core/savestate.h" @@ -147,6 +148,7 @@ signals: void CIAInstallReport(Service::AM::InstallStatus status, QString filepath); void CompressFinished(bool is_compress, bool success); void CIAInstallFinished(); + void InfoLEDColorChanged(); // Signal that tells widgets to update icons to use the current theme void UpdateThemedIcons(); @@ -229,7 +231,7 @@ private: void ShowFFmpegErrorMessage(); private slots: - void OnStartGame(); + void OnResumeGame(bool first_start); void OnRestartGame(); void OnPauseGame(); void OnPauseContinueGame(); @@ -364,6 +366,8 @@ private: MultiplayerState* multiplayer_state = nullptr; + LedWidget* notification_led = nullptr; + // Created before `config` to ensure that emu data directory // isn't created before the check is performed UserDataMigrator user_data_migrator; diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index a5aa9896b..81d1956af 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -57,12 +57,15 @@ const std::array, Settings::NativeAnalog::NumAnalogs> QtConfi // This must be in alphabetical order according to action name as it must have the same order as // UISetting::values.shortcuts, which is alphabetically ordered. // clang-format off -const std::array QtConfig::default_hotkeys {{ +const std::array QtConfig::default_hotkeys {{ {QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}}, {QStringLiteral("Audio Mute/Unmute"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}}, {QStringLiteral("Audio Volume Down"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}}, {QStringLiteral("Audio Volume Up"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}}, {QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}}, + {QStringLiteral("Debug Pause"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F4"),Qt::WidgetWithChildrenShortcut}}, + {QStringLiteral("Debug Resume"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F5"),Qt::WidgetWithChildrenShortcut}}, + {QStringLiteral("Debug Step"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F6"),Qt::WidgetWithChildrenShortcut}}, {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, {QStringLiteral("Decrease 3D Factor"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+-"), Qt::ApplicationShortcut}}, {QStringLiteral("Decrease Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("-"), Qt::ApplicationShortcut}}, @@ -287,6 +290,7 @@ void QtConfig::ReadAudioValues() { ReadGlobalSetting(Settings::values.audio_emulation); ReadGlobalSetting(Settings::values.enable_audio_stretching); ReadGlobalSetting(Settings::values.enable_realtime_audio); + ReadGlobalSetting(Settings::values.simulate_headphones_plugged); ReadGlobalSetting(Settings::values.volume); if (global) { @@ -483,6 +487,7 @@ void QtConfig::ReadDataStorageValues() { ReadBasicSetting(Settings::values.use_virtual_sd); ReadBasicSetting(Settings::values.use_custom_storage); ReadBasicSetting(Settings::values.compress_cia_installs); + ReadBasicSetting(Settings::values.async_fs_operations); const std::string nand_dir = ReadSetting(Settings::QKeys::nand_directory, QStringLiteral("")).toString().toStdString(); @@ -510,6 +515,7 @@ void QtConfig::ReadDebuggingValues() { ReadBasicSetting(Settings::values.instant_debug_log); ReadBasicSetting(Settings::values.enable_rpc_server); ReadBasicSetting(Settings::values.toggle_unique_data_console_type); + ReadBasicSetting(Settings::values.break_on_unmapped_memory_access); qt_config->beginGroup(QStringLiteral("LLE")); for (const auto& service_module : Service::service_module_map) { @@ -722,6 +728,8 @@ void QtConfig::ReadRendererValues() { ReadGlobalSetting(Settings::values.delay_game_render_thread_us); ReadGlobalSetting(Settings::values.disable_right_eye_render); + ReadGlobalSetting(Settings::values.simulate_3ds_gpu_timings); + if (global) { ReadBasicSetting(Settings::values.use_shader_jit); } @@ -937,6 +945,7 @@ void QtConfig::SaveAudioValues() { WriteGlobalSetting(Settings::values.audio_emulation); WriteGlobalSetting(Settings::values.enable_audio_stretching); WriteGlobalSetting(Settings::values.enable_realtime_audio); + WriteGlobalSetting(Settings::values.simulate_headphones_plugged); WriteGlobalSetting(Settings::values.volume); if (global) { @@ -1073,6 +1082,7 @@ void QtConfig::SaveDataStorageValues() { WriteBasicSetting(Settings::values.use_virtual_sd); WriteBasicSetting(Settings::values.use_custom_storage); WriteBasicSetting(Settings::values.compress_cia_installs); + WriteBasicSetting(Settings::values.async_fs_operations); WriteSetting(Settings::QKeys::nand_directory, QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)), QStringLiteral("")); @@ -1094,6 +1104,7 @@ void QtConfig::SaveDebuggingValues() { WriteBasicSetting(Settings::values.instant_debug_log); WriteBasicSetting(Settings::values.enable_rpc_server); WriteBasicSetting(Settings::values.toggle_unique_data_console_type); + WriteBasicSetting(Settings::values.break_on_unmapped_memory_access); qt_config->beginGroup(QStringLiteral("LLE")); for (const auto& service_module : Settings::values.lle_modules) { @@ -1266,6 +1277,8 @@ void QtConfig::SaveRendererValues() { WriteGlobalSetting(Settings::values.delay_game_render_thread_us); WriteGlobalSetting(Settings::values.disable_right_eye_render); + WriteGlobalSetting(Settings::values.simulate_3ds_gpu_timings); + if (global) { WriteSetting(Settings::QKeys::use_shader_jit, Settings::values.use_shader_jit.GetValue(), true); diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 3fba498ed..6cc87f7f0 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -26,7 +26,7 @@ public: static const std::array default_buttons; static const std::array, Settings::NativeAnalog::NumAnalogs> default_analogs; - static const std::array default_hotkeys; + static const std::array default_hotkeys; private: void Initialize(const std::string& config_name); diff --git a/src/citra_qt/configuration/configure_audio.cpp b/src/citra_qt/configuration/configure_audio.cpp index 6dd775926..1a36da86d 100644 --- a/src/citra_qt/configuration/configure_audio.cpp +++ b/src/citra_qt/configuration/configure_audio.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -67,6 +67,8 @@ void ConfigureAudio::SetConfiguration() { ui->toggle_audio_stretching->setChecked(Settings::values.enable_audio_stretching.GetValue()); ui->toggle_realtime_audio->setChecked(Settings::values.enable_realtime_audio.GetValue()); + ui->simulate_headphones_plugged->setChecked( + Settings::values.simulate_headphones_plugged.GetValue()); SetHleFeaturesEnabled(); const s32 volume = @@ -175,6 +177,9 @@ void ConfigureAudio::ApplyConfiguration() { &Settings::values.volume, ui->volume_combo_box, [this](s32) { return static_cast(ui->volume_slider->value()) / ui->volume_slider->maximum(); }); + ConfigurationShared::ApplyPerGameSetting(&Settings::values.simulate_headphones_plugged, + ui->simulate_headphones_plugged, + simulate_headphones_plugged); if (Settings::IsConfiguringGlobal()) { Settings::values.output_type = @@ -252,4 +257,7 @@ void ConfigureAudio::SetupPerGameUI() { ConfigurationShared::SetColoredTristate(ui->toggle_realtime_audio, Settings::values.enable_realtime_audio, realtime_audio); + ConfigurationShared::SetColoredTristate(ui->simulate_headphones_plugged, + Settings::values.simulate_headphones_plugged, + simulate_headphones_plugged); } diff --git a/src/citra_qt/configuration/configure_audio.h b/src/citra_qt/configuration/configure_audio.h index 2da6e3eea..fe6087e07 100644 --- a/src/citra_qt/configuration/configure_audio.h +++ b/src/citra_qt/configuration/configure_audio.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -41,5 +41,6 @@ private: ConfigurationShared::CheckState audio_stretching; ConfigurationShared::CheckState realtime_audio; + ConfigurationShared::CheckState simulate_headphones_plugged; std::unique_ptr ui; }; diff --git a/src/citra_qt/configuration/configure_audio.ui b/src/citra_qt/configuration/configure_audio.ui index 706e335c0..946f5c0e7 100644 --- a/src/citra_qt/configuration/configure_audio.ui +++ b/src/citra_qt/configuration/configure_audio.ui @@ -85,26 +85,6 @@ - - - - <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> - - - Enable audio stretching - - - - - - - <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> - - - Enable realtime audio - - - @@ -192,6 +172,36 @@ + + + + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + + + Enable audio stretching + + + + + + + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + + + Enable realtime audio + + + + + + + <html><head/><body><p>Simulates whether headphones are plugged in to the emulated 3DS system.</p></body></html> + + + Simulate headphones plugged in + + + diff --git a/src/citra_qt/configuration/configure_debug.cpp b/src/citra_qt/configuration/configure_debug.cpp index 0fe4f49cf..bdb945eb9 100644 --- a/src/citra_qt/configuration/configure_debug.cpp +++ b/src/citra_qt/configuration/configure_debug.cpp @@ -12,6 +12,7 @@ #include "common/file_util.h" #include "common/logging/backend.h" #include "common/settings.h" +#include "core/core.h" #include "ui_configure_debug.h" #ifdef ENABLE_VULKAN #include "video_core/renderer_vulkan/vk_instance.h" @@ -33,6 +34,17 @@ ConfigureDebug::ConfigureDebug(bool is_powered_on_, QWidget* parent) ui->setupUi(this); SetConfiguration(); + connect(ui->toggle_gdbstub, &QCheckBox::clicked, + [this](bool checked) { ui->debug_next_process->setEnabled(checked); }); + + connect(ui->debug_next_process, &QCheckBox::clicked, [](bool checked) { + if (checked) { + Core::System::GetInstance().SetDebugNextProcessFlag(); + } else { + Core::System::GetInstance().ClearDebugNextProcessFlag(); + } + }); + connect(ui->open_log_button, &QPushButton::clicked, []() { QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); QDesktopServices::openUrl(QUrl::fromLocalFile(path)); @@ -87,6 +99,10 @@ ConfigureDebug::ConfigureDebug(bool is_powered_on_, QWidget* parent) ui->clock_speed_label->setVisible(Settings::IsConfiguringGlobal()); ui->clock_speed_combo->setVisible(!Settings::IsConfiguringGlobal()); +#ifndef ENABLE_GDBSTUB + ui->gdb_groupbox->setVisible(false); +#endif + SetupPerGameUI(); } @@ -94,6 +110,9 @@ ConfigureDebug::~ConfigureDebug() = default; void ConfigureDebug::SetConfiguration() { ui->toggle_gdbstub->setChecked(Settings::values.use_gdbstub.GetValue()); + if (!ui->toggle_gdbstub->isChecked()) { + ui->debug_next_process->setEnabled(false); + } ui->gdbport_spinbox->setEnabled(Settings::values.use_gdbstub.GetValue()); ui->gdbport_spinbox->setValue(Settings::values.gdbstub_port.GetValue()); ui->toggle_console->setEnabled(!is_powered_on); @@ -112,6 +131,8 @@ void ConfigureDebug::SetConfiguration() { #endif // !ENABLE_SCRIPTING ui->toggle_unique_data_console_type->setChecked( Settings::values.toggle_unique_data_console_type.GetValue()); + ui->break_on_unmapped_memory_access->setChecked( + Settings::values.break_on_unmapped_memory_access.GetValue()); ui->toggle_renderer_debug->setChecked(Settings::values.renderer_debug.GetValue()); ui->toggle_dump_command_buffers->setChecked(Settings::values.dump_command_buffers.GetValue()); @@ -133,6 +154,10 @@ void ConfigureDebug::SetConfiguration() { ui->clock_display_label->setText( QStringLiteral("%1%").arg(Settings::values.cpu_clock_percentage.GetValue())); ui->instant_debug_log->setChecked(Settings::values.instant_debug_log.GetValue()); + + if (Core::System::GetInstance().GetDebugNextProcessFlag()) { + ui->debug_next_process->setChecked(true); + } } void ConfigureDebug::ApplyConfiguration() { @@ -153,6 +178,8 @@ void ConfigureDebug::ApplyConfiguration() { Settings::values.enable_rpc_server = ui->enable_rpc_server->isChecked(); Settings::values.toggle_unique_data_console_type = ui->toggle_unique_data_console_type->isChecked(); + Settings::values.break_on_unmapped_memory_access = + ui->break_on_unmapped_memory_access->isChecked(); Settings::values.renderer_debug = ui->toggle_renderer_debug->isChecked(); Settings::values.dump_command_buffers = ui->toggle_dump_command_buffers->isChecked(); Settings::values.instant_debug_log = ui->instant_debug_log->isChecked(); @@ -174,10 +201,11 @@ void ConfigureDebug::SetupPerGameUI() { ConfigurationShared::SetHighlight(ui->clock_speed_widget, index == 1); }); - ui->groupBox->setVisible(false); + ui->gdb_groupbox->setVisible(false); ui->groupBox_2->setVisible(false); ui->enable_rpc_server->setVisible(false); ui->toggle_unique_data_console_type->setVisible(false); + ui->break_on_unmapped_memory_access->setVisible(false); ui->toggle_cpu_jit->setVisible(false); } diff --git a/src/citra_qt/configuration/configure_debug.ui b/src/citra_qt/configuration/configure_debug.ui index 990835a80..d9c833949 100644 --- a/src/citra_qt/configuration/configure_debug.ui +++ b/src/citra_qt/configuration/configure_debug.ui @@ -17,7 +17,7 @@ - + GDB @@ -60,6 +60,13 @@ + + + + Pause next non-sysmodule process at start + + + @@ -299,6 +306,16 @@ + + + + <html><head/><body><p>Pauses emulation and shows an error message if an unmapped memory access is detected.</p></body></html> + + + Break on unmapped memory access + + + diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp index aac27ab16..2a2ea3f6c 100644 --- a/src/citra_qt/configuration/configure_graphics.cpp +++ b/src/citra_qt/configuration/configure_graphics.cpp @@ -154,6 +154,7 @@ void ConfigureGraphics::SetConfiguration() { ui->toggle_display_refresh_rate_detection->setChecked( Settings::values.use_display_refresh_rate_detection.GetValue()); } + ui->simulate_3ds_gpu_timings->setChecked(Settings::values.simulate_3ds_gpu_timings.GetValue()); } void ConfigureGraphics::ApplyConfiguration() { @@ -182,6 +183,9 @@ void ConfigureGraphics::ApplyConfiguration() { ConfigurationShared::ApplyPerGameSetting( &Settings::values.delay_game_render_thread_us, ui->delay_render_combo, [this](s32) { return ui->delay_render_slider->value(); }); + ConfigurationShared::ApplyPerGameSetting(&Settings::values.simulate_3ds_gpu_timings, + ui->simulate_3ds_gpu_timings, + simulate_3ds_gpu_timings); if (Settings::IsConfiguringGlobal()) { Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked(); @@ -212,6 +216,8 @@ void ConfigureGraphics::SetupPerGameUI() { ui->physical_device_combo->setEnabled(Settings::values.physical_device.UsingGlobal()); ui->delay_render_combo->setEnabled( Settings::values.delay_game_render_thread_us.UsingGlobal()); + ui->simulate_3ds_gpu_timings->setEnabled( + Settings::values.simulate_3ds_gpu_timings.UsingGlobal()); return; } @@ -254,6 +260,9 @@ void ConfigureGraphics::SetupPerGameUI() { ConfigurationShared::SetColoredTristate(ui->disable_spirv_optimizer, Settings::values.disable_spirv_optimizer, disable_spirv_optimizer); + ConfigurationShared::SetColoredTristate(ui->simulate_3ds_gpu_timings, + Settings::values.simulate_3ds_gpu_timings, + simulate_3ds_gpu_timings); } void ConfigureGraphics::SetPhysicalDeviceComboVisibility(int index) { diff --git a/src/citra_qt/configuration/configure_graphics.h b/src/citra_qt/configuration/configure_graphics.h index 61a462a67..f4ec4f104 100644 --- a/src/citra_qt/configuration/configure_graphics.h +++ b/src/citra_qt/configuration/configure_graphics.h @@ -44,6 +44,7 @@ private: ConfigurationShared::CheckState async_presentation; ConfigurationShared::CheckState spirv_shader_gen; ConfigurationShared::CheckState disable_spirv_optimizer; + ConfigurationShared::CheckState simulate_3ds_gpu_timings; std::unique_ptr ui; QColor bg_color; }; diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui index ff5ec613f..7f9a01956 100644 --- a/src/citra_qt/configuration/configure_graphics.ui +++ b/src/citra_qt/configuration/configure_graphics.ui @@ -372,7 +372,7 @@ 0 - 16000 + 65000 100 @@ -404,6 +404,16 @@ + + + + <html><head/><body><p>Delays GPU completion events based on measurements taken from real hardware, so that games have more realistic GPU time measurements. Helps stabilize dynamic FPS games. Disabling this feature may improve performance in some rare cases at the cost of stability.</p></body></html> + + + Simulate 3DS GPU timings + + + diff --git a/src/citra_qt/configuration/configure_motion_touch.cpp b/src/citra_qt/configuration/configure_motion_touch.cpp index f400f0f44..b69a754e8 100644 --- a/src/citra_qt/configuration/configure_motion_touch.cpp +++ b/src/citra_qt/configuration/configure_motion_touch.cpp @@ -229,8 +229,13 @@ void ConfigureMotionTouch::ConnectEvents() { poll_timer->start(200); // Check for new inputs every 200ms } }); +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) + connect(ui->touchpad_checkbox, &QCheckBox::stateChanged, this, [this]() { UpdateUiDisplay(); }); +#else connect(ui->touchpad_checkbox, &QCheckBox::checkStateChanged, this, [this]() { UpdateUiDisplay(); }); +#endif + connect(ui->touchpad_config_btn, &QPushButton::clicked, this, [this]() { if (QMessageBox::information(this, tr("Information"), tr("After pressing OK, tap the touchpad on the controller " diff --git a/src/citra_qt/configuration/configure_storage.cpp b/src/citra_qt/configuration/configure_storage.cpp index a8cabd987..aa468d4c1 100644 --- a/src/citra_qt/configuration/configure_storage.cpp +++ b/src/citra_qt/configuration/configure_storage.cpp @@ -78,6 +78,7 @@ void ConfigureStorage::SetConfiguration() { ui->toggle_virtual_sd->setChecked(Settings::values.use_virtual_sd.GetValue()); ui->toggle_custom_storage->setChecked(Settings::values.use_custom_storage.GetValue()); ui->toggle_compress_cia->setChecked(Settings::values.compress_cia_installs.GetValue()); + ui->async_fs_operations->setChecked(Settings::values.async_fs_operations.GetValue()); ui->storage_group->setEnabled(!is_powered_on); } @@ -86,6 +87,7 @@ void ConfigureStorage::ApplyConfiguration() { Settings::values.use_virtual_sd = ui->toggle_virtual_sd->isChecked(); Settings::values.use_custom_storage = ui->toggle_custom_storage->isChecked(); Settings::values.compress_cia_installs = ui->toggle_compress_cia->isChecked(); + Settings::values.async_fs_operations = ui->async_fs_operations->isChecked(); if (!Settings::values.use_custom_storage) { FileUtil::UpdateUserPath(FileUtil::UserPath::NANDDir, diff --git a/src/citra_qt/configuration/configure_storage.ui b/src/citra_qt/configuration/configure_storage.ui index 5cfd0c449..6e8909a3a 100644 --- a/src/citra_qt/configuration/configure_storage.ui +++ b/src/citra_qt/configuration/configure_storage.ui @@ -180,7 +180,7 @@ - + @@ -191,6 +191,16 @@ + + + + Asynchronous filesystem operations + + + <html><head/><body><p>Makes emulated filesystem accesses asynchronous. Greatly reduces filesystem related stutter, but may slightly increase load times.</p></body></html> + + + diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp index 9ed8bebfa..6b47a28d1 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/citra_qt/configuration/configure_system.cpp @@ -670,12 +670,16 @@ void ConfigureSystem::RefreshSecureDataStatus() { return tr("Status: Loaded (Invalid Signature)"); case HW::UniqueData::SecureDataLoadStatus::RegionChanged: return tr("Status: Loaded (Region Changed)"); + case HW::UniqueData::SecureDataLoadStatus::CannotValidateSignature: + return tr("Status: Loaded (Cannot Validate Signature)"); case HW::UniqueData::SecureDataLoadStatus::NotFound: return tr("Status: Not Found"); case HW::UniqueData::SecureDataLoadStatus::Invalid: return tr("Status: Invalid"); case HW::UniqueData::SecureDataLoadStatus::IOError: return tr("Status: IO Error"); + case HW::UniqueData::SecureDataLoadStatus::NoCryptoKeys: + return tr("Status: Missing Crypto Keys"); default: return QString(); } diff --git a/src/citra_qt/discord.h b/src/citra_qt/discord.h index a867cc4d6..37d800b71 100644 --- a/src/citra_qt/discord.h +++ b/src/citra_qt/discord.h @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -11,7 +11,7 @@ public: virtual ~DiscordInterface() = default; virtual void Pause() = 0; - virtual void Update() = 0; + virtual void Update(bool is_powered_on) = 0; }; class NullImpl : public DiscordInterface { @@ -19,7 +19,7 @@ public: ~NullImpl() = default; void Pause() override {} - void Update() override {} + void Update(bool is_powered_on) override {} }; } // namespace DiscordRPC diff --git a/src/citra_qt/discord_impl.cpp b/src/citra_qt/discord_impl.cpp index bef83c3b4..3a79c0c25 100644 --- a/src/citra_qt/discord_impl.cpp +++ b/src/citra_qt/discord_impl.cpp @@ -30,14 +30,21 @@ void DiscordImpl::Pause() { Discord_ClearPresence(); } -void DiscordImpl::Update() { +void DiscordImpl::Update(bool is_powered_on) { s64 start_time = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); + auto truncate = [](const std::string& str, std::size_t maxLen = 128) -> std::string { + if (str.length() <= maxLen) { + return str; + } + return str.substr(0, maxLen - 3) + "..."; + }; + std::string title; - const bool is_powered_on = system.IsPoweredOn(); if (is_powered_on) { system.GetAppLoader().ReadTitle(title); + title = truncate("Playing: " + title); } DiscordRichPresence presence{}; @@ -45,9 +52,6 @@ void DiscordImpl::Update() { presence.largeImageText = "An open source emulator for the Nintendo 3DS"; if (is_powered_on) { presence.state = title.c_str(); - presence.details = "Currently in game"; - } else { - presence.details = "Not in game"; } presence.startTimestamp = start_time; Discord_UpdatePresence(&presence); diff --git a/src/citra_qt/discord_impl.h b/src/citra_qt/discord_impl.h index 25bee0696..f2b94ad16 100644 --- a/src/citra_qt/discord_impl.h +++ b/src/citra_qt/discord_impl.h @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -18,7 +18,7 @@ public: ~DiscordImpl() override; void Pause() override; - void Update() override; + void Update(bool is_powered_on) override; private: const Core::System& system; diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index acca5361d..1e5dbb4e5 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -206,6 +206,16 @@ + + + Debug + + + + + + + @@ -453,6 +463,30 @@ Capture Screenshot + + + true + + + Debug Pause + + + + + true + + + Debug Resume + + + + + true + + + Debug Step + + true diff --git a/src/citra_qt/notification_led.cpp b/src/citra_qt/notification_led.cpp new file mode 100644 index 000000000..3966b8bda --- /dev/null +++ b/src/citra_qt/notification_led.cpp @@ -0,0 +1,87 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "citra_qt/notification_led.h" + +LedWidget::LedWidget(QWidget* parent) : QWidget(parent), color(0, 0, 0) { + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); +} + +QSize LedWidget::sizeHint() const { + return QSize(16, 16); +} + +QSize LedWidget::minimumSizeHint() const { + return QSize(16, 16); +} + +void LedWidget::setColor(const QColor& _color) { + if (color == _color) + return; + + color = _color; + update(); +} + +QColor LedWidget::lerpColor(const QColor& a, const QColor& b, float t) { + t = std::clamp(t, 0.0f, 1.0f); + + return QColor(int(a.red() + (b.red() - a.red()) * t), + int(a.green() + (b.green() - a.green()) * t), + int(a.blue() + (b.blue() - a.blue()) * t)); +} + +QColor LedWidget::blendLedColor(int r, int g, int b) const { + // Default "off" color + const QColor off_color(64, 64, 64); + + // If completely off, just show gray and skip further calculations + if (r == 0 && g == 0 && b == 0) + return off_color; + + // Normalize lit color so hue stays pure + int max_c = std::max({r, g, b}); + QColor lit_color((r * 255) / max_c, (g * 255) / max_c, (b * 255) / max_c); + + // Convert PWM duty to perceived brightness. + // This gives better results as LED RGB values + // are not linear. + constexpr float gamma = 2.4f; + float pwm = max_c / 255.0; + float t = std::pow(pwm, 1.f / gamma); + + return lerpColor(off_color, lit_color, t * 0.8f); +} + +void LedWidget::paintEvent(QPaintEvent*) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + QRectF rect = this->rect().adjusted(0, 2, 0, -2); + + qreal size = std::min(rect.width(), rect.height()); + QRectF circle((rect.center().x() - size / 2.f) - 2, rect.center().y() - size / 2.f, size, size); + + QPointF center = circle.center(); + qreal radius = circle.width() / 2.f; + + QColor base = blendLedColor(color.red(), color.green(), color.blue()); + + QRadialGradient g(center, radius); + + QColor inner = base.lighter(135); + QColor outer = base.darker(125); + + g.setColorAt(0.f, inner); + g.setColorAt(0.7f, base); + g.setColorAt(1.f, outer); + + p.setPen(Qt::NoPen); + p.setBrush(g); + p.drawEllipse(circle); +} diff --git a/src/citra_qt/notification_led.h b/src/citra_qt/notification_led.h new file mode 100644 index 000000000..3a139a202 --- /dev/null +++ b/src/citra_qt/notification_led.h @@ -0,0 +1,30 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class LedWidget : public QWidget { + Q_OBJECT + +public: + explicit LedWidget(QWidget* parent = nullptr); + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + void setColor(const QColor& color); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + QColor blendLedColor(int r, int g, int b) const; + static QColor lerpColor(const QColor& a, const QColor& b, float t); + +private: + QColor color; +}; diff --git a/src/citra_room_standalone/CMakeLists.txt b/src/citra_room_standalone/CMakeLists.txt index e48051469..3660b11aa 100644 --- a/src/citra_room_standalone/CMakeLists.txt +++ b/src/citra_room_standalone/CMakeLists.txt @@ -4,6 +4,11 @@ add_executable(citra_room_standalone set_target_properties(citra_room_standalone PROPERTIES OUTPUT_NAME "azahar-room") +if (BSD STREQUAL "NetBSD") + include(DisablePaxMprotect) + disable_pax_mprotect(citra_room_standalone) +endif() + target_link_libraries(citra_room_standalone PRIVATE citra_room) if(UNIX AND NOT APPLE) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 4b6e74c49..ae812c89d 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -1,6 +1,6 @@ include(GenerateSCMRev) -add_library(citra_common STATIC +add_library(citra_common STATIC EXCLUDE_FROM_ALL aarch64/cpu_detect.cpp aarch64/cpu_detect.h aarch64/oaknut_abi.h @@ -59,6 +59,7 @@ add_library(citra_common STATIC microprofile.cpp microprofile.h microprofileui.h + optional_helper.h param_package.cpp param_package.h play_time_manager.cpp diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 95b5dda5b..52f406226 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -122,6 +122,16 @@ typedef struct stat file_stat_t; #define FERROR ferror #define FFLUSH std::fflush +#ifdef _MSC_VER +#define DUP_FD _dup +#define FDOPEN _fdopen +#define CLOSE_FD _close +#else +#define DUP_FD dup +#define FDOPEN fdopen +#define CLOSE_FD close +#endif + #endif // This namespace has various generic functions related to files and paths. @@ -1262,6 +1272,44 @@ void IOFile::Swap(IOFile& other) noexcept { bool IOFile::Open() { Close(); + // Any filename with the format fd:// represents a file that + // must be opened by duplicating the provided file_descriptor. This is used + // on Android vanilla builds when the ROM absolute path is not known. + if (filename.starts_with("fd://")) { + +#if !defined(HAVE_LIBRETRO_VFS) + const std::string fd_str = filename.substr(5); + + // Check that fd_str is not empty and contains only digits + if (fd_str.empty() || !std::all_of(fd_str.begin(), fd_str.end(), ::isdigit)) { + m_good = false; + return false; + } + + int fd = std::stoi(fd_str); + + int dup_fd = DUP_FD(fd); + if (dup_fd == -1) { + m_good = false; + return false; + } + + m_file = FDOPEN(dup_fd, openmode.c_str()); + if (!m_file) { + CLOSE_FD(dup_fd); + m_good = false; + return false; + } + + m_good = true; + return true; +#else + // TODO: Add support for libretro vfs when needed. + m_good = false; + return false; +#endif + } + #ifdef _WIN32 // Open with FILE_SHARE_READ, FILE_SHARE_WRITE and FILE_SHARE_DELETE // flags. This mimics linux behaviour as much as possible, which diff --git a/src/common/hacks/hack_list.cpp b/src/common/hacks/hack_list.cpp index 5f320c673..73662c028 100644 --- a/src/common/hacks/hack_list.cpp +++ b/src/common/hacks/hack_list.cpp @@ -198,5 +198,18 @@ HackManager hack_manager = { 0x00040000001D1A00, // EUR }, }}, + {HackType::DELAY_TEXTURE_COPY_COMPLETION, + HackEntry{ + .mode = HackAllowMode::FORCE, + .affected_title_ids = + { + // Super Mario 3D Land + 0x0004000000054100, // JPN + 0x0004000000054000, // USA + 0x0004000000053F00, // EUR + 0x0004000000089E00, // CHN + 0x0004000000089D00, // KOR + }, + }}, }}; } \ No newline at end of file diff --git a/src/common/hacks/hack_list.h b/src/common/hacks/hack_list.h index 61a8cccfa..1c34d22e0 100644 --- a/src/common/hacks/hack_list.h +++ b/src/common/hacks/hack_list.h @@ -16,6 +16,7 @@ enum class HackType : int { REGION_FROM_SECURE, REQUIRES_SHADER_FIXUP, SPOOF_FRIEND_CODE_SEED, + DELAY_TEXTURE_COPY_COMPLETION, }; class UserHackData {}; diff --git a/src/common/memory_ref.h b/src/common/memory_ref.h index f5935bf4c..3e4ddc67f 100644 --- a/src/common/memory_ref.h +++ b/src/common/memory_ref.h @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -153,7 +153,9 @@ private: void serialize(Archive& ar, const unsigned int) { ar & backing_mem; ar & offset; - Init(); + if (Archive::is_loading::value) { + Init(); + } } friend class boost::serialization::access; }; diff --git a/src/common/optional_helper.h b/src/common/optional_helper.h new file mode 100644 index 000000000..f057973d9 --- /dev/null +++ b/src/common/optional_helper.h @@ -0,0 +1,42 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +namespace detail { +template +struct is_optional_trait : std::false_type { + using value_type = T; +}; + +template +struct is_optional_trait> : std::true_type { + using value_type = T; +}; +} // namespace detail + +/** + * Returns true if T is a std::optional, false otherwise. + * For example: + * using Test1 = u32; + * using Test2 = std::optional; + * is_optional_type -> false + * is_optional_type -> true + */ +template +inline constexpr bool is_optional_type = detail::is_optional_trait::value; + +/** + * Provides the inner type of T if it is a std::optional, or T itself if it is not. + * For example: + * using Test1 = u32; + * using Test2 = std::optional; + * optional_value_type -> u32 + * optional_value_type -> u32 + */ +template +using optional_inner_or_type = typename detail::is_optional_trait::value_type; diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 2ac231f50..df8451e28 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -106,6 +106,7 @@ void LogSettings() { log_setting("Renderer_TextureSampling", GetTextureSamplingName(values.texture_sampling.GetValue())); log_setting("Renderer_DelayGameRenderThreasUs", values.delay_game_render_thread_us.GetValue()); + log_setting("Renderer_Simulate3DSGPUTimings", values.simulate_3ds_gpu_timings.GetValue()); log_setting("Renderer_DisableRightEyeRender", values.disable_right_eye_render.GetValue()); log_setting("Stereoscopy_Render3d", values.render_3d.GetValue()); log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue()); @@ -165,6 +166,8 @@ void LogSettings() { log_setting("Debugging_InstantDebugLog", values.instant_debug_log.GetValue()); log_setting("Debugging_ToggleUniqueDataConsoleType", values.toggle_unique_data_console_type.GetValue()); + log_setting("Debugging_BreakOnUnmappedMemoryAccess", + values.break_on_unmapped_memory_access.GetValue()); } bool IsConfiguringGlobal() { @@ -215,6 +218,7 @@ void RestoreGlobalState(bool is_powered_on) { values.texture_filter.SetGlobal(true); values.texture_sampling.SetGlobal(true); values.delay_game_render_thread_us.SetGlobal(true); + values.simulate_3ds_gpu_timings.SetGlobal(true); values.layout_option.SetGlobal(true); values.portrait_layout_option.SetGlobal(true); values.secondary_display_layout.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index 90922bcff..4196557d1 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -484,6 +484,7 @@ struct Values { Setting use_virtual_sd{true, Keys::use_virtual_sd}; Setting use_custom_storage{false, Keys::use_custom_storage}; Setting compress_cia_installs{false, Keys::compress_cia_installs}; + Setting async_fs_operations{true, Keys::async_fs_operations}; // System SwitchableSetting region_value{REGION_VALUE_AUTO_SELECT, Keys::region_value}; @@ -498,8 +499,11 @@ struct Values { Setting apply_region_free_patch{true, Keys::apply_region_free_patch}; // Renderer + // clang-format off SwitchableSetting graphics_api{ -#if defined(ENABLE_OPENGL) +#if defined(ANDROID) && defined(ENABLE_VULKAN) // Prefer Vulkan on Android, OpenGL on everything else + GraphicsAPI::Vulkan, +#elif defined(ENABLE_OPENGL) GraphicsAPI::OpenGL, #elif defined(ENABLE_VULKAN) GraphicsAPI::Vulkan, @@ -510,6 +514,7 @@ struct Values { #error "At least one renderer must be enabled." #endif GraphicsAPI::Software, GraphicsAPI::Vulkan, Keys::graphics_api}; + // clang-format on SwitchableSetting physical_device{0, Keys::physical_device}; Setting use_gles{false, Keys::use_gles}; Setting renderer_debug{false, Keys::renderer_debug}; @@ -536,8 +541,9 @@ struct Values { SwitchableSetting texture_filter{TextureFilter::NoFilter, Keys::texture_filter}; SwitchableSetting texture_sampling{TextureSampling::GameControlled, Keys::texture_sampling}; - SwitchableSetting delay_game_render_thread_us{0, 0, 16000, + SwitchableSetting delay_game_render_thread_us{0, 0, 65000, Keys::delay_game_render_thread_us}; + SwitchableSetting simulate_3ds_gpu_timings{true, Keys::simulate_3ds_gpu_timings}; SwitchableSetting layout_option{LayoutOption::Default, Keys::layout_option}; SwitchableSetting swap_screen{false, Keys::swap_screen}; @@ -623,6 +629,7 @@ struct Values { Setting output_device{"Auto", Keys::output_device}; Setting input_type{AudioCore::InputType::Auto, Keys::input_type}; Setting input_device{"Auto", Keys::input_device}; + SwitchableSetting simulate_headphones_plugged{false, Keys::simulate_headphones_plugged}; // Camera std::array camera_name; @@ -638,6 +645,7 @@ struct Values { Setting instant_debug_log{false, Keys::instant_debug_log}; Setting enable_rpc_server{false, Keys::enable_rpc_server}; Setting toggle_unique_data_console_type{false, Keys::toggle_unique_data_console_type}; + Setting break_on_unmapped_memory_access{false, Keys::break_on_unmapped_memory_access}; // Miscellaneous Setting log_filter{"*:Info", Keys::log_filter}; diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index 1637c01f1..64effc9e2 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -20,6 +24,10 @@ #include #endif +#if defined(__APPLE__) +#include +#endif + namespace Common { /// Make a char lowercase @@ -114,8 +122,7 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P _CompleteFilename += _Filename; } -std::vector SplitString(const std::string& str, const char delim) { - std::istringstream iss(str); +static std::vector SplitString(std::istringstream& iss, const char delim) { std::vector output(1); while (std::getline(iss, *output.rbegin(), delim)) { @@ -126,6 +133,16 @@ std::vector SplitString(const std::string& str, const char delim) { return output; } +std::vector SplitString(std::string_view str, const char delim) { + std::istringstream iss{std::string(str)}; + return SplitString(iss, delim); +} + +std::vector SplitString(const std::string& str, const char delim) { + std::istringstream iss(str); + return SplitString(iss, delim); +} + std::string TabsToSpaces(int tab_size, std::string in) { std::size_t i = 0; @@ -164,6 +181,52 @@ std::u16string UTF8ToUTF16(std::string_view input) { return boost::locale::conv::utf_to_utf(input.data(), input.data() + input.size()); } +#if defined(__APPLE__) +// macOS filesystems may expose decomposed Unicode names through directory listings. +// Normalize to NFC before passing names to guest APIs that expect stable text. +std::string NormalizeNFDToNFC(std::string_view input) { + const std::string fallback(input); + + // Core Foundation string + CFStringRef source = + CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast(input.data()), + static_cast(input.size()), kCFStringEncodingUTF8, false); + + if (source == nullptr) { + return fallback; + } + + // Mutable copy of the source string + CFMutableStringRef normalized = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, source); + CFRelease(source); + + if (normalized == nullptr) { + return fallback; + } + // Normalize the string to NFC form + CFStringNormalize(normalized, kCFStringNormalizationFormC); + + const CFIndex max_size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(normalized), + kCFStringEncodingUTF8) + + 1; // +1 for null terminator + + std::string output(static_cast(max_size), '\0'); + + // Convert the normalized string back to UTF-8 + const bool converted = + CFStringGetCString(normalized, &output[0], max_size, kCFStringEncodingUTF8); + + CFRelease(normalized); + + if (!converted) { + return fallback; + } + + output.resize(std::strlen(output.c_str())); + return output; +} +#endif + #ifdef _WIN32 static std::wstring CPToUTF16(u32 code_page, const std::string& input) { const auto size = diff --git a/src/common/string_util.h b/src/common/string_util.h index 342434928..5c6d0bdac 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -36,6 +36,7 @@ namespace Common { [[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending); +[[nodiscard]] std::vector SplitString(std::string_view str, const char delim); [[nodiscard]] std::vector SplitString(const std::string& str, const char delim); // "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe" @@ -49,6 +50,10 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P [[nodiscard]] std::string UTF16ToUTF8(std::u16string_view input); [[nodiscard]] std::u16string UTF8ToUTF16(std::string_view input); +// Returns UTF-8 normalized to NFC on platforms that need explicit Unicode normalization. +#if defined(__APPLE__) +[[nodiscard]] std::string NormalizeNFDToNFC(std::string_view input); +#endif #ifdef _WIN32 [[nodiscard]] std::string UTF16ToUTF8(const std::wstring& input); diff --git a/src/common/thread_queue_list.h b/src/common/thread_queue_list.h index 5995d76f2..16abe0451 100644 --- a/src/common/thread_queue_list.h +++ b/src/common/thread_queue_list.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project / PPSSPP Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include "common/common_types.h" @@ -52,33 +53,35 @@ struct ThreadQueueList { return T(); } - T pop_first() { + std::pair pop_first() { Queue* cur = first; while (cur != nullptr) { if (!cur->data.empty()) { auto tmp = std::move(cur->data.front()); cur->data.pop_front(); - return tmp; + Priority prio = static_cast(ToIndex(cur)); + return {prio, tmp}; } cur = cur->next_nonempty; } - return T(); + return {Priority(), T()}; } - T pop_first_better(Priority priority) { + std::pair pop_first_better(Priority priority) { Queue* cur = first; Queue* stop = &queues[priority]; while (cur < stop) { if (!cur->data.empty()) { auto tmp = std::move(cur->data.front()); cur->data.pop_front(); - return tmp; + Priority prio = static_cast(ToIndex(cur)); + return {prio, tmp}; } cur = cur->next_nonempty; } - return T(); + return {Priority(), T()}; } void push_front(Priority priority, const T& thread_id) { diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fd8212dcf..dfcb20a27 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(citra_core STATIC +add_library(citra_core STATIC EXCLUDE_FROM_ALL 3ds.h arm/arm_interface.h arm/dyncom/arm_dyncom.cpp @@ -126,10 +126,6 @@ add_library(citra_core STATIC frontend/image_interface.cpp frontend/image_interface.h frontend/input.h - gdbstub/gdbstub.cpp - gdbstub/gdbstub.h - gdbstub/hio.cpp - gdbstub/hio.h hle/applets/applet.cpp hle/applets/applet.h hle/applets/erreula.cpp @@ -350,6 +346,8 @@ add_library(citra_core STATIC hle/service/ldr_ro/ldr_ro.h hle/service/mcu/mcu_hwc.cpp hle/service/mcu/mcu_hwc.h + hle/service/mcu/mcu_rtc.cpp + hle/service/mcu/mcu_rtc.h hle/service/mcu/mcu.cpp hle/service/mcu/mcu.h hle/service/mic/mic_u.cpp @@ -462,7 +460,6 @@ add_library(citra_core STATIC hw/aes/key.h hw/ecc.cpp hw/ecc.h - hw/default_keys.h hw/rsa/rsa.cpp hw/rsa/rsa.h hw/unique_data.cpp @@ -500,6 +497,10 @@ add_library(citra_core STATIC tracer/recorder.h ) +if (ENABLE_BUILTIN_KEYBLOB) + target_sources(citra_core PRIVATE hw/default_keys.h) +endif() + create_target_directory_groups(citra_core) target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network video_core) @@ -524,6 +525,16 @@ if (ENABLE_SCRIPTING) ) endif() +if (ENABLE_GDBSTUB) + target_compile_definitions(citra_core PUBLIC -DENABLE_GDBSTUB) + target_sources(citra_core PRIVATE + gdbstub/gdbstub.cpp + gdbstub/gdbstub.h + gdbstub/hio.cpp + gdbstub/hio.h + ) +endif() + if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE) target_sources(citra_core PRIVATE arm/dynarmic/arm_dynarmic.cpp diff --git a/src/core/arm/arm_interface.h b/src/core/arm/arm_interface.h index 1f6ddc9e9..ceb945148 100644 --- a/src/core/arm/arm_interface.h +++ b/src/core/arm/arm_interface.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -186,6 +186,13 @@ public: /// Prepare core for thread reschedule (if needed to correctly handle state) virtual void PrepareReschedule() = 0; + /** + * Whether the backend allows to break with single instruction accuracy + * when Run() is used. If false is returned, the user should expect + * innaccuracies with memory watchpoints access exceptions. + */ + virtual bool HasSingleInstructionBreakAccuracy() = 0; + Core::Timing::Timer& GetTimer() { return *timer; } @@ -198,12 +205,28 @@ public: return id; } + /** + * Sets the core to not being runnable until its break condition is handled. + */ + void SetBreakFlag() { + break_flag = true; + } + + /* + * Sets the core being runnable after its break condition was handled. + */ + void ClearBreakFlag() { + break_flag = false; + } + protected: // This us used for serialization. Returning nullptr is valid if page tables are not used. virtual std::shared_ptr GetPageTable() const = 0; std::shared_ptr timer; + bool break_flag{}; + private: u32 id; @@ -213,6 +236,7 @@ private: void save(Archive& ar, const unsigned int file_version) const { ar << timer; ar << id; + ar << break_flag; const auto page_table = GetPageTable(); ar << page_table; for (int i = 0; i < 15; i++) { @@ -258,6 +282,7 @@ private: ClearInstructionCache(); ar >> timer; ar >> id; + ar >> break_flag; std::shared_ptr page_table{}; ar >> page_table; SetPageTable(page_table); diff --git a/src/core/arm/dynarmic/arm_dynarmic.cpp b/src/core/arm/dynarmic/arm_dynarmic.cpp index 2cdc9c119..64b740569 100644 --- a/src/core/arm/dynarmic/arm_dynarmic.cpp +++ b/src/core/arm/dynarmic/arm_dynarmic.cpp @@ -1,7 +1,8 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include @@ -13,10 +14,20 @@ #include "core/arm/dynarmic/arm_tick_counts.h" #include "core/core.h" #include "core/core_timing.h" +#ifdef ENABLE_GDBSTUB #include "core/gdbstub/gdbstub.h" +#endif #include "core/hle/kernel/svc.h" #include "core/memory.h" +#ifndef SIGILL +constexpr u32 SIGILL = 4; +#endif + +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + namespace Core { class DynarmicUserCallbacks final : public Dynarmic::A32::UserCallbacks { @@ -25,6 +36,10 @@ public: : parent(parent), svc_context(parent.system), memory(parent.memory) {} ~DynarmicUserCallbacks() = default; + std::optional MemoryReadCode(VAddr vaddr) override { + return memory.Read32OrNullopt(vaddr); + } + std::uint8_t MemoryRead8(VAddr vaddr) override { return memory.Read8(vaddr); } @@ -82,12 +97,13 @@ public: case Dynarmic::A32::Exception::NoExecuteFault: break; case Dynarmic::A32::Exception::Breakpoint: +#ifdef ENABLE_GDBSTUB if (GDBStub::IsConnected()) { - parent.jit->HaltExecution(); parent.SetPC(pc); - parent.ServeBreak(); + parent.ServeBreak(SIGTRAP); return; } +#endif break; case Dynarmic::A32::Exception::SendEvent: case Dynarmic::A32::Exception::SendEventLocal: @@ -99,11 +115,40 @@ public: case Dynarmic::A32::Exception::PreloadInstruction: return; } - for (int i = 0; i < 16; i++) { - LOG_CRITICAL(Debug, "r{:02d} = {:08X}", i, parent.GetReg(i)); + + static constexpr auto ExceptionToString = [](Dynarmic::A32::Exception e) -> std::string { + switch (e) { + case Dynarmic::A32::Exception::UndefinedInstruction: + return "UndefinedInstruction"; + case Dynarmic::A32::Exception::UnpredictableInstruction: + return "UnpredictableInstruction"; + case Dynarmic::A32::Exception::DecodeError: + return "DecodeError"; + case Dynarmic::A32::Exception::NoExecuteFault: + return "NoExecuteFault"; + case Dynarmic::A32::Exception::Breakpoint: + return "Breakpoint"; + default: + return fmt::format("Unknown({})", e); + } + }; + + parent.SetPC(pc); +#ifdef ENABLE_GDBSTUB + if (GDBStub::IsConnected()) { + parent.ServeBreak(SIGILL); + } else +#endif + { + std::string error; + for (int i = 0; i < 16; i++) { + error += fmt::format("r{:02d} = {:08X}\n", i, parent.GetReg(i)); + } + error += fmt::format("ExceptionRaised(exception = {}, pc = {:08X})", + ExceptionToString(exception), pc); + parent.system.SetStatus(Core::System::ResultStatus::ErrorCoreExceptionRaised, + error.c_str()); } - ASSERT_MSG(false, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X})", exception, - pc, MemoryReadCode(pc).value()); } void AddTicks(std::uint64_t ticks) override { @@ -138,16 +183,19 @@ MICROPROFILE_DEFINE(ARM_Jit, "ARM JIT", "ARM JIT", MP_RGB(255, 64, 64)); void ARM_Dynarmic::Run() { ASSERT(memory.GetCurrentPageTable() == current_page_table); MICROPROFILE_SCOPE(ARM_Jit); + if (break_flag) [[unlikely]] { + return; + } jit->Run(); } void ARM_Dynarmic::Step() { - jit->Step(); - - if (GDBStub::IsConnected()) { - ServeBreak(); + if (break_flag) [[unlikely]] { + return; } + + jit->Step(); } void ARM_Dynarmic::SetPC(u32 pc) { @@ -294,11 +342,10 @@ void ARM_Dynarmic::SetPageTable(const std::shared_ptr& page_t jits.emplace(current_page_table, std::move(new_jit)); } -void ARM_Dynarmic::ServeBreak() { - Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread(); - SaveContext(thread->context); - GDBStub::Break(); - GDBStub::SendTrap(thread, 5); +void ARM_Dynarmic::ServeBreak([[maybe_unused]] int signal) { +#ifdef ENABLE_GDBSTUB + GDBStub::Break(signal); +#endif } std::unique_ptr ARM_Dynarmic::MakeJit() { diff --git a/src/core/arm/dynarmic/arm_dynarmic.h b/src/core/arm/dynarmic/arm_dynarmic.h index ec0c13390..713bd99db 100644 --- a/src/core/arm/dynarmic/arm_dynarmic.h +++ b/src/core/arm/dynarmic/arm_dynarmic.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -56,11 +56,15 @@ public: void ClearExclusiveState() override; void SetPageTable(const std::shared_ptr& page_table) override; + bool HasSingleInstructionBreakAccuracy() override { + return false; + } + protected: std::shared_ptr GetPageTable() const override; private: - void ServeBreak(); + void ServeBreak(int signal); friend class DynarmicUserCallbacks; Core::System& system; diff --git a/src/core/arm/dyncom/arm_dyncom.cpp b/src/core/arm/dyncom/arm_dyncom.cpp index c9bb66e38..63db2f9c4 100644 --- a/src/core/arm/dyncom/arm_dyncom.cpp +++ b/src/core/arm/dyncom/arm_dyncom.cpp @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -24,10 +24,16 @@ ARM_DynCom::ARM_DynCom(Core::System& system_, Memory::MemorySystem& memory, ARM_DynCom::~ARM_DynCom() {} void ARM_DynCom::Run() { + if (break_flag) [[unlikely]] { + return; + } ExecuteInstructions(std::max(timer->GetDowncount(), 0)); } void ARM_DynCom::Step() { + if (break_flag) [[unlikely]] { + return; + } ExecuteInstructions(1); } diff --git a/src/core/arm/dyncom/arm_dyncom.h b/src/core/arm/dyncom/arm_dyncom.h index 3fd37989a..cfe1d3e92 100644 --- a/src/core/arm/dyncom/arm_dyncom.h +++ b/src/core/arm/dyncom/arm_dyncom.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -51,6 +51,10 @@ public: void SetPageTable(const std::shared_ptr& page_table) override; void PrepareReschedule() override; + bool HasSingleInstructionBreakAccuracy() override { + return true; + } + protected: std::shared_ptr GetPageTable() const override; diff --git a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp index 862c422bc..af30fdad3 100644 --- a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp +++ b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2012 Michael Kang, 2014 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -19,7 +23,9 @@ #include "core/arm/skyeye_common/vfp/vfp.h" #include "core/core.h" #include "core/core_timing.h" +#ifdef ENABLE_GDBSTUB #include "core/gdbstub/gdbstub.h" +#endif #include "core/hle/kernel/svc.h" #include "core/memory.h" @@ -918,9 +924,11 @@ MICROPROFILE_DEFINE(DynCom_Execute, "DynCom", "Execute", MP_RGB(255, 0, 0)); unsigned InterpreterMainLoop(ARMul_State* cpu) { MICROPROFILE_SCOPE(DynCom_Execute); +#ifdef ENABLE_GDBSTUB /// Nearest upcoming GDB code execution breakpoint, relative to the last dispatch's address. GDBStub::BreakpointAddress breakpoint_data; breakpoint_data.type = GDBStub::BreakpointType::None; +#endif #undef RM #undef RS @@ -948,17 +956,15 @@ unsigned InterpreterMainLoop(ARMul_State* cpu) { #define INC_PC(l) ptr += sizeof(arm_inst) + l #define INC_PC_STUB ptr += sizeof(arm_inst) -#ifdef ANDROID +#ifndef ENABLE_GDBSTUB #define GDB_BP_CHECK #else #define GDB_BP_CHECK \ cpu->Cpsr &= ~(1 << 5); \ cpu->Cpsr |= cpu->TFlag << 5; \ - if (GDBStub::IsServerEnabled()) { \ - if (GDBStub::IsMemoryBreak()) { \ - goto END; \ - } else if (breakpoint_data.type != GDBStub::BreakpointType::None && \ - PC == breakpoint_data.address) { \ + if (GDBStub::IsServerEnabled()) [[unlikely]] { \ + if (breakpoint_data.type != GDBStub::BreakpointType::None && \ + PC == breakpoint_data.address) { \ cpu->RecordBreak(breakpoint_data); \ goto END; \ } \ @@ -1651,7 +1657,7 @@ DISPATCH: { goto END; } -#ifndef ANDROID +#ifdef ENABLE_GDBSTUB // Find breakpoint if one exists within the block if (GDBStub::IsConnected()) { breakpoint_data = diff --git a/src/core/arm/skyeye_common/armstate.cpp b/src/core/arm/skyeye_common/armstate.cpp index 9de68dc11..9ff89c5a7 100644 --- a/src/core/arm/skyeye_common/armstate.cpp +++ b/src/core/arm/skyeye_common/armstate.cpp @@ -1,15 +1,23 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include +#include #include "common/logging/log.h" #include "common/swap.h" #include "core/arm/skyeye_common/armstate.h" #include "core/arm/skyeye_common/vfp/vfp.h" #include "core/core.h" +#ifdef ENABLE_GDBSTUB +#include "core/gdbstub/gdbstub.h" +#endif #include "core/memory.h" +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + ARMul_State::ARMul_State(Core::System& system_, Memory::MemorySystem& memory_, PrivilegeMode initial_mode) : system{system_}, memory{memory_} { @@ -182,26 +190,12 @@ void ARMul_State::ResetMPCoreCP15Registers() { CP15[CP15_MAIN_TLB_LOCKDOWN_ATTRIBUTE] = 0x00000000; CP15[CP15_TLB_DEBUG_CONTROL] = 0x00000000; } -#ifdef ANDROID -static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) {} -#else -static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) { - if (GDBStub::IsServerEnabled() && GDBStub::CheckBreakpoint(address, type)) { - LOG_DEBUG(Debug, "Found memory breakpoint @ {:08x}", address); - GDBStub::Break(true); - } -} -#endif u8 ARMul_State::ReadMemory8(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - return memory.Read8(address); } u16 ARMul_State::ReadMemory16(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - u16 data = memory.Read16(address); if (InBigEndianMode()) @@ -211,8 +205,6 @@ u16 ARMul_State::ReadMemory16(u32 address) const { } u32 ARMul_State::ReadMemory32(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - u32 data = memory.Read32(address); if (InBigEndianMode()) @@ -222,8 +214,6 @@ u32 ARMul_State::ReadMemory32(u32 address) const { } u64 ARMul_State::ReadMemory64(u32 address) const { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read); - u64 data = memory.Read64(address); if (InBigEndianMode()) @@ -233,14 +223,10 @@ u64 ARMul_State::ReadMemory64(u32 address) const { } void ARMul_State::WriteMemory8(u32 address, u8 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - memory.Write8(address, data); } void ARMul_State::WriteMemory16(u32 address, u16 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - if (InBigEndianMode()) data = Common::swap16(data); @@ -248,8 +234,6 @@ void ARMul_State::WriteMemory16(u32 address, u16 data) { } void ARMul_State::WriteMemory32(u32 address, u32 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - if (InBigEndianMode()) data = Common::swap32(data); @@ -257,8 +241,6 @@ void ARMul_State::WriteMemory32(u32 address, u32 data) { } void ARMul_State::WriteMemory64(u32 address, u64 data) { - CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write); - if (InBigEndianMode()) data = Common::swap64(data); @@ -601,6 +583,7 @@ void ARMul_State::WriteCP15Register(u32 value, u32 crn, u32 opcode_1, u32 crm, u } void ARMul_State::ServeBreak() { +#ifdef ENABLE_GDBSTUB if (!GDBStub::IsServerEnabled()) { return; } @@ -609,12 +592,9 @@ void ARMul_State::ServeBreak() { DEBUG_ASSERT(Reg[15] == last_bkpt.address); } - Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread(); - system.GetRunningCore().SaveContext(thread->context); - - if (last_bkpt_hit || GDBStub::IsMemoryBreak() || GDBStub::GetCpuStepFlag()) { + if (last_bkpt_hit) { last_bkpt_hit = false; - GDBStub::Break(); - GDBStub::SendTrap(thread, 5); + GDBStub::Break(SIGTRAP); } +#endif } diff --git a/src/core/arm/skyeye_common/armstate.h b/src/core/arm/skyeye_common/armstate.h index f36c46c07..925881304 100644 --- a/src/core/arm/skyeye_common/armstate.h +++ b/src/core/arm/skyeye_common/armstate.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + /* armdefs.h -- ARMulator common definitions: ARM6 Instruction Emulator. Copyright (C) 1994 Advanced RISC Machines Ltd. @@ -21,7 +25,9 @@ #include #include "common/common_types.h" #include "core/arm/skyeye_common/arm_regformat.h" +#ifdef ENABLE_GDBSTUB #include "core/gdbstub/gdbstub.h" +#endif namespace Core { class System; @@ -199,10 +205,12 @@ public: return TFlag ? 2 : 4; } +#ifdef ENABLE_GDBSTUB void RecordBreak(GDBStub::BreakpointAddress bkpt) { last_bkpt = bkpt; last_bkpt_hit = true; } +#endif void ServeBreak(); @@ -267,6 +275,8 @@ private: u32 exclusive_tag; // The address for which the local monitor is in exclusive access mode bool exclusive_state; +#ifdef ENABLE_GDBSTUB GDBStub::BreakpointAddress last_bkpt{}; bool last_bkpt_hit = false; +#endif }; diff --git a/src/core/core.cpp b/src/core/core.cpp index e7b5868f8..f9dfe1534 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -26,7 +26,9 @@ #include "core/dumping/backend.h" #include "core/file_sys/ncch_container.h" #include "core/frontend/image_interface.h" +#ifdef ENABLE_GDBSTUB #include "core/gdbstub/gdbstub.h" +#endif #include "core/global.h" #include "core/hle/kernel/ipc_debugger/recorder.h" #include "core/hle/kernel/kernel.h" @@ -83,23 +85,17 @@ System::ResultStatus System::RunLoop(bool tight_loop) { return ResultStatus::ErrorNotInitialized; } +#ifdef ENABLE_GDBSTUB if (GDBStub::IsServerEnabled()) { - Kernel::Thread* thread = kernel->GetCurrentThreadManager().GetCurrentThread(); - if (thread && running_core) { - running_core->SaveContext(thread->context); + // The break flag is only set if GDB is connected, + // we can do clearing here safely. If it is ever + // used outside, move the clearing outside the if. + for (auto& cpu_core : cpu_cores) { + cpu_core->ClearBreakFlag(); } GDBStub::HandlePacket(*this); - - // If the loop is halted and we want to step, use a tiny (1) number of instructions to - // execute. Otherwise, get out of the loop function. - if (GDBStub::GetCpuHaltFlag()) { - if (GDBStub::GetCpuStepFlag()) { - tight_loop = false; - } else { - return ResultStatus::Success; - } - } } +#endif Signal signal{Signal::None}; u32 param{}; @@ -259,6 +255,8 @@ System::ResultStatus System::RunLoop(bool tight_loop) { cpu_core->GetTimer().Idle(); PrepareReschedule(); } else { + // In the rare case the break flag is set (due to exception thrown) + // there is probably no need to adjust the timer accordingly. if (tight_loop) { cpu_core->Run(); } else { @@ -269,10 +267,6 @@ System::ResultStatus System::RunLoop(bool tight_loop) { } } - if (GDBStub::IsServerEnabled()) { - GDBStub::SetCpuStepFlag(false); - } - Reschedule(); return status; @@ -303,7 +297,7 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st app_loader = Loader::GetLoader(filepath); } if (!app_loader) { - LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath); + LOG_CRITICAL(Core, "Failed to obtain loader for {}", filepath); return ResultStatus::ErrorGetLoader; } @@ -440,6 +434,10 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st return ResultStatus::ErrorLoader_ErrorInvalidFormat; case Loader::ResultStatus::ErrorGbaTitle: return ResultStatus::ErrorLoader_ErrorGbaTitle; + case Loader::ResultStatus::ErrorPatches: + return ResultStatus::ErrorLoader_ErrorPatches; + case Loader::ResultStatus::ErrorPatchesInvalidTitle: + return ResultStatus::ErrorLoader_ErrorPatchesInvalidTitle; case Loader::ResultStatus::ErrorArtic: return ResultStatus::ErrorArticDisconnected; default: @@ -571,7 +569,9 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, app_loader->ReadProgramId(loading_title_id); HW::AES::InitKeys(); Service::Init(*this, loading_title_id, lle_modules, !app_loader->DoingInitialSetup()); +#ifdef ENABLE_GDBSTUB GDBStub::DeferStart(); +#endif if (!registered_image_interface) { registered_image_interface = std::make_shared(); @@ -581,8 +581,9 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, auto gsp = service_manager->GetService("gsp::Gpu"); gpu = std::make_unique(*this, emu_window, secondary_window); - gpu->SetInterruptHandler( - [gsp](Service::GSP::InterruptId interrupt_id) { gsp->SignalInterrupt(interrupt_id); }); + gpu->SetInterruptHandler([gsp](Service::GSP::InterruptId interrupt_id, u64 wait_delay_ns) { + gsp->SignalInterrupt(interrupt_id, wait_delay_ns); + }); auto plg_ldr = Service::PLGLDR::GetService(*this); if (plg_ldr) { @@ -590,6 +591,8 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, plg_ldr->SetAllowGameChangeState(Settings::values.allow_plugin_loader.GetValue()); } + SetInfoLEDColor({}); + LOG_DEBUG(Core, "Initialized OK"); is_powered_on = true; @@ -693,7 +696,9 @@ void System::Shutdown(bool is_deserializing) { gpu.reset(); if (!is_deserializing) { lle_modules.clear(); +#ifdef ENABLE_GDBSTUB GDBStub::Shutdown(); +#endif perf_stats.reset(); app_loader.reset(); } @@ -720,6 +725,8 @@ void System::Shutdown(bool is_deserializing) { memory.reset(); + SetInfoLEDColor({}); + LOG_DEBUG(Core, "Shutdown OK"); } @@ -754,8 +761,10 @@ void System::Reset() { } void System::ApplySettings() { +#ifdef ENABLE_GDBSTUB GDBStub::SetServerPort(Settings::values.gdbstub_port.GetValue()); GDBStub::ToggleServer(Settings::values.use_gdbstub.GetValue()); +#endif if (gpu) { #ifndef ANDROID @@ -898,8 +907,9 @@ void System::serialize(Archive& ar, const unsigned int file_version) { // Re-register gpu callback, because gsp service changed after service_manager got // serialized auto gsp = service_manager->GetService("gsp::Gpu"); - gpu->SetInterruptHandler( - [gsp](Service::GSP::InterruptId interrupt_id) { gsp->SignalInterrupt(interrupt_id); }); + gpu->SetInterruptHandler([gsp](Service::GSP::InterruptId interrupt_id, u64 wait_delay_ns) { + gsp->SignalInterrupt(interrupt_id, wait_delay_ns); + }); // Apply per program settings and switch the shader cache to the title running when the // savestate was created. diff --git a/src/core/core.h b/src/core/core.h index ca61e8262..c46a196b2 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -12,6 +12,7 @@ #include #include #include "common/common_types.h" +#include "common/vector_math.h" #include "core/arm/arm_interface.h" #include "core/cheats/cheats.h" #include "core/hle/service/apt/applet_manager.h" @@ -98,12 +99,16 @@ public: /// invalid format ErrorLoader_ErrorGbaTitle, ///< Error loading the specified application as it is GBA Virtual ///< Console - ErrorSystemFiles, ///< Error in finding system files - ErrorSavestate, ///< Error saving or loading - ErrorArticDisconnected, ///< Error when artic base disconnects - ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode - ShutdownRequested, ///< Emulated program requested a system shutdown - ErrorUnknown ///< Any other error + ErrorLoader_ErrorPatches, ///< Generic error while loading patches for an application + ErrorLoader_ErrorPatchesInvalidTitle, ///< A patch was loaded for the incorrect application + ErrorSystemFiles, ///< Error in finding system files + ErrorSavestate, ///< Error saving or loading + ErrorArticDisconnected, ///< Error when artic base disconnects + ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode + ErrorCoreExceptionRaised, ///< The CPU emulation raised an exception + ErrorMemoryExceptionRaised, ///< Unmmaped memory was accessed + ShutdownRequested, ///< Emulated program requested a system shutdown + ErrorUnknown ///< Any other error }; explicit System(); @@ -381,6 +386,39 @@ public: bool IsInitialSetup(); + // This returns the 3DS notification LED RGB value. + // Keep in mind this is used as a PWM duty cycle on real HW, + // so the percieved LED brightness is not linear. + const Common::Vec3& GetInfoLEDColor() const { + return info_led_color; + } + + void SetInfoLEDColor(const Common::Vec3& color) { + if (color == info_led_color) + return; + + info_led_color = color; + if (info_led_color_changed) { + info_led_color_changed(); + } + } + + void RegisterInfoLEDColorChanged(const std::function& func) { + info_led_color_changed = func; + } + + void SetDebugNextProcessFlag() { + debug_next_process = true; + } + + bool GetDebugNextProcessFlag() { + return debug_next_process; + } + + void ClearDebugNextProcessFlag() { + debug_next_process = false; + } + private: /** * Initialize the emulated system. @@ -487,6 +525,11 @@ private: std::vector lle_modules; + Common::Vec3 info_led_color; + std::function info_led_color_changed; + + bool debug_next_process; + friend class boost::serialization::access; template void serialize(Archive& ar, const unsigned int file_version); diff --git a/src/core/file_sys/disk_archive.cpp b/src/core/file_sys/disk_archive.cpp index a7ae5e92e..db0290df6 100644 --- a/src/core/file_sys/disk_archive.cpp +++ b/src/core/file_sys/disk_archive.cpp @@ -1,13 +1,15 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include +#include #include #include "common/archives.h" #include "common/common_types.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "common/string_util.h" #include "core/file_sys/disk_archive.h" #include "core/file_sys/errors.h" @@ -62,22 +64,29 @@ u32 DiskDirectory::Read(const u32 count, Entry* entries) { while (entries_read < count && children_iterator != directory.children.cend()) { const FileUtil::FSTEntry& file = *children_iterator; + // Directory entries are exposed to the guest as UTF-16. Normalize host UTF-8 names first + // so host Unicode normalization differences do not leak into guest-visible SDMC paths. +#if defined(__APPLE__) + const std::string filename = Common::NormalizeNFDToNFC(file.virtualName); +#else const std::string& filename = file.virtualName; +#endif + const std::u16string filename_utf16 = Common::UTF8ToUTF16(filename); Entry& entry = entries[entries_read]; LOG_TRACE(Service_FS, "File {}: size={} dir={}", filename, file.size, file.isDirectory); - // TODO(Link Mauve): use a proper conversion to UTF-16. - for (std::size_t j = 0; j < FILENAME_LENGTH; ++j) { - entry.filename[j] = filename[j]; - if (!filename[j]) - break; + std::fill(std::begin(entry.filename), std::end(entry.filename), u'\0'); + + const std::size_t copy_length = std::min(filename_utf16.size(), FILENAME_LENGTH - 1); + for (std::size_t j = 0; j < copy_length; ++j) { + entry.filename[j] = filename_utf16[j]; } FileUtil::SplitFilename83(filename, entry.short_name, entry.extension); entry.is_directory = file.isDirectory; - entry.is_hidden = (filename[0] == '.'); + entry.is_hidden = (!filename.empty() && filename[0] == '.'); entry.is_read_only = 0; entry.file_size = file.size; @@ -92,5 +101,4 @@ u32 DiskDirectory::Read(const u32 count, Entry* entries) { } return entries_read; } - } // namespace FileSys diff --git a/src/core/file_sys/layered_fs.cpp b/src/core/file_sys/layered_fs.cpp index 1cac97be4..93ce3e854 100644 --- a/src/core/file_sys/layered_fs.cpp +++ b/src/core/file_sys/layered_fs.cpp @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -247,14 +247,14 @@ void LayeredFS::LoadExtRelocations() { std::vector buffer(file.relocation.size); // Original size romfs->ReadFile(file.relocation.original_offset, buffer.size(), buffer.data()); - bool ret = false; + Loader::ResultStatus ret{}; if (extension == ".ips") { ret = Patch::ApplyIpsPatch(patch, buffer); } else { ret = Patch::ApplyBpsPatch(patch, buffer); } - if (ret) { + if (ret == Loader::ResultStatus::Success) { LOG_INFO(Service_FS, "LayeredFS patched file {}", file_path); file.relocation.type = 2; diff --git a/src/core/file_sys/layered_fs.h b/src/core/file_sys/layered_fs.h index 69f6e5939..e51bd8f1c 100644 --- a/src/core/file_sys/layered_fs.h +++ b/src/core/file_sys/layered_fs.h @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 382c8943e..c51de40a9 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -518,7 +518,7 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector& code) const { struct PatchLocation { std::string path; - bool (*patch_fn)(const std::vector& patch, std::vector& code); + Loader::ResultStatus (*patch_fn)(const std::vector& patch, std::vector& code); }; const auto mods_path = @@ -555,11 +555,12 @@ Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector& code) const std::vector patch(patch_file.GetSize()); if (patch_file.ReadBytes(patch.data(), patch.size()) != patch.size()) - return Loader::ResultStatus::Error; + return Loader::ResultStatus::ErrorPatches; LOG_INFO(Service_FS, "File {} patching code.bin", info.path); - if (!info.patch_fn(patch, code)) - return Loader::ResultStatus::Error; + auto patch_result = info.patch_fn(patch, code); + if (patch_result != Loader::ResultStatus::Success) + return patch_result; return Loader::ResultStatus::Success; } diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h index c7984d338..02f3bb0b1 100644 --- a/src/core/file_sys/ncch_container.h +++ b/src/core/file_sys/ncch_container.h @@ -182,7 +182,14 @@ struct ExHeader_ARM11_SystemLocalCaps { BitField<4, 4, u8> system_mode; }; u8 priority; - u8 resource_limit_descriptor[0x10][2]; + union { + u16 core1_schedule_flags; + BitField<0, 7, u16> max_cpu; + // Schedule mode flag, 0 -> "single", 1 -> "multi" + BitField<7, 1, u16> schedule_mode; + BitField<8, 8, u16> unknown; + }; + u8 resource_limit_descriptor[0xF][2]; // Always 0 (unused?) ExHeader_StorageInfo storage_info; u8 service_access_control[0x20][8]; u8 ex_service_access_control[0x2][8]; diff --git a/src/core/file_sys/patch.cpp b/src/core/file_sys/patch.cpp index fecc7fe05..83d3989eb 100644 --- a/src/core/file_sys/patch.cpp +++ b/src/core/file_sys/patch.cpp @@ -1,4 +1,4 @@ -// Copyright 2019 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -14,21 +14,21 @@ namespace FileSys::Patch { -bool ApplyIpsPatch(const std::vector& ips, std::vector& buffer) { +Loader::ResultStatus ApplyIpsPatch(const std::vector& ips, std::vector& buffer) { std::size_t cursor = 5; std::size_t patch_length = ips.size() - 3; std::string ips_header(ips.begin(), ips.begin() + 5); if (ips_header != "PATCH") { LOG_INFO(Service_FS, "Attempted to load invalid IPS"); - return false; + return Loader::ResultStatus::ErrorPatches; } while (cursor < patch_length) { std::string eof_check(ips.begin() + cursor, ips.begin() + cursor + 3); if (eof_check == "EOF") - return false; + break; std::size_t offset = ips[cursor] << 16 | ips[cursor + 1] << 8 | ips[cursor + 2]; std::size_t length = ips[cursor + 3] << 8 | ips[cursor + 4]; @@ -38,7 +38,7 @@ bool ApplyIpsPatch(const std::vector& ips, std::vector& buffer) { length = ips[cursor + 5] << 8 | ips[cursor + 6]; if (buffer.size() < offset + length) - return false; + return Loader::ResultStatus::ErrorPatches; for (u32 i = 0; i < length; ++i) buffer[offset + i] = ips[cursor + 7]; @@ -49,12 +49,12 @@ bool ApplyIpsPatch(const std::vector& ips, std::vector& buffer) { } if (buffer.size() < offset + length) - return false; + return Loader::ResultStatus::ErrorPatches; std::memcpy(&buffer[offset], &ips[cursor + 5], length); cursor += length + 5; } - return true; + return Loader::ResultStatus::Success; } namespace Bps { @@ -149,11 +149,11 @@ public: PatchApplier(Stream source, Stream target, Stream patch) : m_source{source}, m_target{target}, m_patch{patch} {} - bool Apply() { + Loader::ResultStatus Apply() { const auto magic = *m_patch.Read>(); if (std::string_view(magic.data(), magic.size()) != "BPS1") { LOG_ERROR(Service_FS, "Invalid BPS magic"); - return false; + return Loader::ResultStatus::ErrorPatches; } const Bps::Number source_size = m_patch.ReadNumber(); @@ -161,7 +161,7 @@ public: const Bps::Number metadata_size = m_patch.ReadNumber(); if (source_size > m_source.size() || target_size > m_target.size() || metadata_size != 0) { LOG_ERROR(Service_FS, "Invalid sizes"); - return false; + return Loader::ResultStatus::ErrorPatches; } const std::size_t command_start_offset = m_patch.Tell(); @@ -173,22 +173,22 @@ public: if (crc32(m_source.data(), source_size) != source_crc32) { LOG_ERROR(Service_FS, "Unexpected source hash"); - return false; + return Loader::ResultStatus::ErrorPatchesInvalidTitle; } // Process all patch commands. std::memset(m_target.data(), 0, m_target.size()); while (m_patch.Tell() < command_end_offset) { if (!HandleCommand()) - return false; + return Loader::ResultStatus::ErrorPatches; } if (crc32(m_target.data(), target_size) != target_crc32) { LOG_ERROR(Service_FS, "Unexpected target hash"); - return false; + return Loader::ResultStatus::ErrorPatches; } - return true; + return Loader::ResultStatus::Success; } private: @@ -257,7 +257,7 @@ private: } // namespace Bps -bool ApplyBpsPatch(const std::vector& patch, std::vector& buffer) { +Loader::ResultStatus ApplyBpsPatch(const std::vector& patch, std::vector& buffer) { Bps::Stream patch_stream{patch.data(), patch.size()}; // Move the offset past the file format marker (i.e. "BPS1") diff --git a/src/core/file_sys/patch.h b/src/core/file_sys/patch.h index 9a8118475..6f8993384 100644 --- a/src/core/file_sys/patch.h +++ b/src/core/file_sys/patch.h @@ -1,4 +1,4 @@ -// Copyright 2019 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -7,11 +7,12 @@ #include #include "common/common_types.h" +#include "core/loader/loader.h" namespace FileSys::Patch { -bool ApplyIpsPatch(const std::vector& patch, std::vector& buffer); +Loader::ResultStatus ApplyIpsPatch(const std::vector& patch, std::vector& buffer); -bool ApplyBpsPatch(const std::vector& patch, std::vector& buffer); +Loader::ResultStatus ApplyBpsPatch(const std::vector& patch, std::vector& buffer); } // namespace FileSys::Patch diff --git a/src/core/gdbstub/gdbstub.cpp b/src/core/gdbstub/gdbstub.cpp index 660168bc1..017677606 100644 --- a/src/core/gdbstub/gdbstub.cpp +++ b/src/core/gdbstub/gdbstub.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. @@ -36,8 +40,16 @@ #include "core/gdbstub/gdbstub.h" #include "core/gdbstub/hio.h" #include "core/hle/kernel/process.h" +#include "core/loader/loader.h" #include "core/memory.h" +#ifndef ENABLE_GDBSTUB +#error "File was compiled with GDB stub support disabled" +#endif + +// Uncomment to log all GDB traffic +// #define PRINT_GDB_TRAFFIC + namespace GDBStub { namespace { constexpr int GDB_BUFFER_SIZE = 10000; @@ -59,20 +71,38 @@ constexpr u32 SIGTERM = 15; constexpr u32 MSG_WAITALL = 8; #endif +#ifndef SIGSEGV +constexpr u32 SIGSEGV = 11; +#endif + +#ifdef _WIN32 +using SOCKET = UINT_PTR; +#else +using SOCKET = int; +#define closesocket(x) close(x) +#endif // _WIN32 + +#define INVALID_SOCKET ((SOCKET)(~0)) + constexpr u32 SP_REGISTER = 13; constexpr u32 LR_REGISTER = 14; constexpr u32 PC_REGISTER = 15; constexpr u32 CPSR_REGISTER = 25; constexpr u32 D0_REGISTER = 26; constexpr u32 FPSCR_REGISTER = 42; +constexpr u32 FPEXC_REGISTER = 43; // For sample XML files see the GDB source /gdb/features // GDB also wants the l character at the start // This XML defines what the registers are for this specific ARM device +// The CPSR is register 25, rather than register 16, because the FPA registers historically were +// placed between the PC and the CPSR in the "g" packet. constexpr char target_xml[] = R"(l + arm + 3DS @@ -90,11 +120,6 @@ constexpr char target_xml[] = - - - @@ -115,32 +140,41 @@ constexpr char target_xml[] = + )"; -int gdbserver_socket = -1; +SOCKET gdbserver_socket = INVALID_SOCKET; bool defer_start = false; u8 command_buffer[GDB_BUFFER_SIZE]; -u32 command_length; +u32 recv_command_length; +u32 send_command_length; u32 latest_signal = 0; -bool memory_break = false; static Kernel::Thread* current_thread = nullptr; +static Kernel::Process* current_process = nullptr; // Binding to a port within the reserved ports range (0-1023) requires root permissions, // so default to a port outside of that range. u16 gdbstub_port = 24689; -bool halt_loop = true; -bool step_loop = false; -bool send_trap = false; +constexpr bool supports_extended_mode = true; +bool is_extended_mode = false; + +bool is_running = false; +bool current_process_finished = false; // If set to false, the server will never be started and no // gdbstub-related functions will be executed. std::atomic server_enabled(false); +SOCKET accept_socket = INVALID_SOCKET; +int continue_thread = -1; + +static Kernel::Thread* break_thread = nullptr; +static int break_signal = 0; #ifdef _WIN32 WSADATA InitData; @@ -159,17 +193,47 @@ BreakpointMap breakpoints_read; BreakpointMap breakpoints_write; } // Anonymous namespace +static void ResetState() { + gdbserver_socket = INVALID_SOCKET; + defer_start = false; + + memset(command_buffer, 0, GDB_BUFFER_SIZE); + recv_command_length = 0; + send_command_length = 0; + + latest_signal = 0; + + current_thread = nullptr; + current_process = nullptr; + + is_extended_mode = false; + + is_running = false; + current_process_finished = false; + + accept_socket = INVALID_SOCKET; + continue_thread = -1; + + break_thread = nullptr; + break_signal = 0; + + breakpoints_execute.clear(); + breakpoints_read.clear(); + breakpoints_write.clear(); +} + static Kernel::Thread* FindThreadById(int id) { - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (auto& thread : threads) { - if (thread->GetThreadId() == static_cast(id)) { - return thread.get(); - } + if (!current_process) { + return nullptr; + } + + auto thread_list = current_process->GetThreadList(); + for (auto& thread : thread_list) { + if (thread->GetThreadId() == static_cast(id)) { + return thread.get(); } } + return nullptr; } @@ -210,6 +274,8 @@ static u64 FpuRead(std::size_t id, Kernel::Thread* thread = nullptr) { return ret; } else if (id == FPSCR_REGISTER) { return thread->context.fpscr; + } else if (id == FPEXC_REGISTER) { + return thread->context.fpexc; } else { return 0; } @@ -225,6 +291,15 @@ static void FpuWrite(std::size_t id, u64 val, Kernel::Thread* thread = nullptr) thread->context.fpu_registers[2 * (id - D0_REGISTER) + 1] = static_cast(val >> 32); } else if (id == FPSCR_REGISTER) { thread->context.fpscr = static_cast(val); + } else if (id == FPEXC_REGISTER) { + thread->context.fpexc = static_cast(val); + } +} + +// Clear instruction cache for all cores. +static void ClearAllInstructionCache() { + for (int i = 0; i < Core::GetNumCores(); i++) { + Core::GetCore(i).ClearInstructionCache(); } } @@ -356,13 +431,42 @@ static u64 GdbHexToLong(const u8* src) { return output; } +static int GetErrno() { +#ifdef _WIN32 + return WSAGetLastError(); +#else + return errno; +#endif +} + +static bool SetNonBlock(SOCKET socket, bool nonblock) { +#ifdef _WIN32 + unsigned long nonblocking = nonblock ? 1 : 0; + int ret = ioctlsocket(socket, FIONBIO, &nonblocking); + if (ret < 0) { + LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket"); + return false; + } +#else + int flags = nonblock ? O_NONBLOCK : 0; + + const int ret = ::fcntl(socket, F_SETFL, flags); + if (ret < 0) { + LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket"); + return false; + } +#endif + return true; +} + /// Read a byte from the gdb client. static u8 ReadByte() { - u8 c; + u8 c{}; std::size_t received_size = recv(gdbserver_socket, reinterpret_cast(&c), 1, MSG_WAITALL); if (received_size != 1) { - LOG_ERROR(Debug_GDBStub, "recv failed : {}", received_size); - Shutdown(); + LOG_ERROR(Debug_GDBStub, "recv failed : {}", GetErrno()); + ToggleServer(false); + ToggleServer(true); } return c; @@ -409,17 +513,34 @@ static void RemoveBreakpoint(BreakpointType type, VAddr addr) { bp->second.len, bp->second.addr, type); if (type == BreakpointType::Execute) { - Core::System::GetInstance().Memory().WriteBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), bp->second.addr, - bp->second.inst.data(), bp->second.inst.size()); + Core::System::GetInstance().Memory().WriteBlock(*current_process, bp->second.addr, + bp->second.inst.data(), bp->second.len); u32 num_cores = Core::GetNumCores(); for (u32 i = 0; i < num_cores; ++i) { Core::GetCore(i).ClearInstructionCache(); } + } else { + Core::System::GetInstance().Memory().UnregisterWatchpoint(*current_process, bp->second.addr, + bp->second.len); } p.erase(addr); } +void RemoveAllBreakpoints() { + std::vector> trash_bin; + + for (auto type : {BreakpointType::Execute, BreakpointType::Read, BreakpointType::Write}) { + auto& map = GetBreakpointMap(type); + for (auto b : map) { + trash_bin.push_back({type, b.first}); + } + } + + for (auto& p : trash_bin) { + RemoveBreakpoint(p.first, p.second); + } +} + BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, BreakpointType type) { const BreakpointMap& p = GetBreakpointMap(type); const auto next_breakpoint = p.lower_bound(addr); @@ -436,38 +557,65 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, BreakpointType type) return breakpoint; } -bool CheckBreakpoint(VAddr addr, BreakpointType type) { +bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type) { if (!IsConnected()) { return false; } const BreakpointMap& p = GetBreakpointMap(type); - const auto bp = p.find(addr); - if (bp == p.end()) { - return false; - } + // Access range: [addr, access_end) + const VAddr access_end = addr + access_len; - u32 len = bp->second.len; + for (const auto& [base_addr, bp] : p) { + if (!bp.active) { + continue; + } - // IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints - // no matter if it's a 4-byte or 2-byte instruction. When you execute a - // Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on - // two instructions instead of the single instruction you placed the breakpoint - // on. So, as a way to make sure that execution breakpoints are only breaking - // on the instruction that was specified, set the length of an execution - // breakpoint to 1. This should be fine since the CPU should never begin executing - // an instruction anywhere except the beginning of the instruction. - if (type == BreakpointType::Execute) { - len = 1; - } + u32 bp_len = bp.len; - if (bp->second.active && (addr >= bp->second.addr && addr < bp->second.addr + len)) { - LOG_DEBUG(Debug_GDBStub, - "Found breakpoint type {} @ {:08x}, range: {:08x}" - " - {:08x} ({:x} bytes)", - type, addr, bp->second.addr, bp->second.addr + len, len); - return true; + // IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints + // no matter if it's a 4-byte or 2-byte instruction. When you execute a + // Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on + // two instructions instead of the single instruction you placed the breakpoint + // on. So, as a way to make sure that execution breakpoints are only breaking + // on the instruction that was specified, set the length of an execution + // breakpoint to 1. This should be fine since the CPU should never begin executing + // an instruction anywhere except the beginning of the instruction. + if (type == BreakpointType::Execute) { + bp_len = 1; + } + + // Breakpoint/watchpoint range: [bp.addr, bp_end) + const VAddr bp_end = bp.addr + bp_len; + + bool hit = false; + + if (type == BreakpointType::Execute) { + // Execute breakpoints should only trigger on exact PC match. + hit = (addr == bp.addr); + } else { + // Range overlap test: + // [addr, access_end) overlaps [bp.addr, bp_end) + hit = (addr < bp_end) && (bp.addr < access_end); + } + + if (hit) { + LOG_DEBUG(Debug_GDBStub, + "Found breakpoint type {}, " + "access range: {:08x} - {:08x}, " + "breakpoint range: {:08x} - {:08x}", + type, addr, addr, access_end, bp.addr, bp_end); + + if (type != BreakpointType::Execute && + !Core::GetCore(0).HasSingleInstructionBreakAccuracy()) { + LOG_WARNING(Debug_GDBStub, + "The current CPU backend does not support accurate watchpoints and " + "memory exceptions. Disable CPU JIT for more accuracy."); + } + + return true; + } } return false; @@ -492,28 +640,35 @@ void SendReply(const char* reply) { std::memset(command_buffer, 0, sizeof(command_buffer)); - command_length = static_cast(strlen(reply)); - if (command_length + 4 > sizeof(command_buffer)) { + send_command_length = static_cast(strlen(reply)); + +#ifdef PRINT_GDB_TRAFFIC + LOG_INFO(Debug_GDBStub, "Res: {}", reply); +#endif + + if (send_command_length + 4 > sizeof(command_buffer)) { LOG_ERROR(Debug_GDBStub, "command_buffer overflow in SendReply"); return; } - std::memcpy(command_buffer + 1, reply, command_length); + std::memcpy(command_buffer + 1, reply, send_command_length); - u8 checksum = CalculateChecksum(command_buffer, command_length + 1); + u8 checksum = CalculateChecksum(command_buffer, send_command_length + 1); command_buffer[0] = GDB_STUB_START; - command_buffer[command_length + 1] = GDB_STUB_END; - command_buffer[command_length + 2] = NibbleToHex(checksum >> 4); - command_buffer[command_length + 3] = NibbleToHex(checksum); + command_buffer[send_command_length + 1] = GDB_STUB_END; + command_buffer[send_command_length + 2] = NibbleToHex(checksum >> 4); + command_buffer[send_command_length + 3] = NibbleToHex(checksum); u8* ptr = command_buffer; - u32 left = command_length + 4; + u32 left = send_command_length + 4; while (left > 0) { s32 sent_size = static_cast(send(gdbserver_socket, reinterpret_cast(ptr), left, 0)); if (sent_size < 0) { LOG_ERROR(Debug_GDBStub, "gdb: send failed"); - return Shutdown(); + ToggleServer(false); + ToggleServer(true); + return; } left -= sent_size; @@ -528,60 +683,103 @@ static void HandleQuery() { if (strcmp(query, "TStatus") == 0) { SendReply("T0"); + } else if (strncmp(query, "Attached", strlen("Attached")) == 0) { + SendReply("1"); } else if (strncmp(query, "Supported", strlen("Supported")) == 0) { // PacketSize needs to be large enough for target xml - SendReply("PacketSize=2000;qXfer:features:read+;qXfer:threads:read+"); + SendReply("PacketSize=9800;qXfer:features:read+;qXfer:osdata:read+;qXfer:threads:read+;" + "vContSupported+"); } else if (strncmp(query, "Xfer:features:read:target.xml:", strlen("Xfer:features:read:target.xml:")) == 0) { SendReply(target_xml); } else if (strncmp(query, "fThreadInfo", strlen("fThreadInfo")) == 0) { - std::string val = "m"; - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (const auto& thread : threads) { - val += fmt::format("{:x},", thread->GetThreadId()); - } + if (!current_process) { + SendReply("E01"); + return; } + + auto thread_list = current_process->GetThreadList(); + std::string val = "m"; + for (const auto& thread : thread_list) { + val += fmt::format("{:x},", thread->GetThreadId()); + } + val.pop_back(); SendReply(val.c_str()); } else if (strncmp(query, "sThreadInfo", strlen("sThreadInfo")) == 0) { SendReply("l"); + } else if (strncmp(query, "Xfer:osdata:read:processes", strlen("Xfer:osdata:read:processes")) == + 0) { + std::string buffer; + buffer += "l"; + auto process_list = Core::System::GetInstance().Kernel().GetProcessList(); + for (const auto& p : process_list) { + buffer += fmt::format("" + "{}" + "{}" + "", + p->process_id, p->codeset->name); + } + buffer += ""; + SendReply(buffer.c_str()); } else if (strncmp(query, "Xfer:threads:read", strlen("Xfer:threads:read")) == 0) { + if (!current_process) { + SendReply("E01"); + return; + } + std::string buffer; buffer += "l"; buffer += ""; - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (const auto& thread : threads) { - buffer += fmt::format(R"*()*", - thread->GetThreadId(), thread->GetThreadId()); - } + auto thread_list = current_process->GetThreadList(); + for (const auto& thread : thread_list) { + buffer += fmt::format(R"*()*", + thread->GetThreadId(), thread->GetThreadId()); } buffer += ""; + SendReply(buffer.c_str()); } else { SendReply(""); } } - -/// Handle set thread command from gdb client. -static void HandleSetThread() { - int thread_id = -1; - if (command_buffer[2] != '-') { - thread_id = static_cast(HexToInt(command_buffer + 2, command_length - 2)); +static bool SetThread(int thread_id) { + if (!current_process) { + // The process has not been selected yet + return false; } + if (thread_id >= 1) { current_thread = FindThreadById(thread_id); } if (!current_thread) { - thread_id = 1; - current_thread = FindThreadById(thread_id); + auto thread_list = current_process->GetThreadList(); + if (thread_list.size() > 0) { + // Select the lowest thread ID, which is the main thread + std::sort(thread_list.begin(), thread_list.end(), + [](const std::shared_ptr& a, + const std::shared_ptr& b) { + return a->thread_id < b->thread_id; + }); + current_thread = thread_list[0].get(); + } } - if (current_thread) { + return current_thread != nullptr; +} +/// Handle set thread command from gdb client. +static void HandleSetThread() { + int thread_id = -1; + if (command_buffer[2] != '-') { + thread_id = static_cast(HexToInt(command_buffer + 2, recv_command_length - 2)); + } + + if (command_buffer[1] == 'c') { + continue_thread = thread_id; + SendReply("OK"); + return; + } + + if (SetThread(thread_id)) { SendReply("OK"); return; } @@ -590,7 +788,12 @@ static void HandleSetThread() { /// Handle thread alive command from gdb client. static void HandleThreadAlive() { - int thread_id = static_cast(HexToInt(command_buffer + 1, command_length - 1)); + if (!current_process) { + SendReply("E01"); + return; + } + + int thread_id = static_cast(HexToInt(command_buffer + 1, recv_command_length - 1)); if (thread_id == 0) { thread_id = 1; } @@ -601,13 +804,27 @@ static void HandleThreadAlive() { SendReply("E01"); } +static void HandleExtendedMode() { + if (supports_extended_mode) { + is_extended_mode = true; + SendReply("OK"); + } else { + SendReply(""); + } +} + /** * Send signal packet to client. * * @param signal Signal to be sent to client. */ -static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { - if (gdbserver_socket == -1) { +static void SendStopReply(Kernel::Thread* thread, u32 signal, bool full = true) { + if (gdbserver_socket == INVALID_SOCKET) { + return; + } + + if (current_process_finished) { + SendReply("W00"); return; } @@ -619,11 +836,16 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { std::string buffer; if (full) { - + Core::ARM_Interface::ThreadContext ctx{}; + if (thread) { + ctx = thread->context; + } else { + Core::GetRunningCore().SaveContext(ctx); + } buffer = fmt::format("T{:02x}{:02x}:{:08x};{:02x}:{:08x};{:02x}:{:08x}", latest_signal, - PC_REGISTER, htonl(Core::GetRunningCore().GetPC()), SP_REGISTER, - htonl(Core::GetRunningCore().GetReg(SP_REGISTER)), LR_REGISTER, - htonl(Core::GetRunningCore().GetReg(LR_REGISTER))); + PC_REGISTER, htonl(ctx.cpu_registers[PC_REGISTER]), SP_REGISTER, + htonl(ctx.cpu_registers[SP_REGISTER]), LR_REGISTER, + htonl(ctx.cpu_registers[LR_REGISTER])); } else { buffer = fmt::format("T{:02x}", latest_signal); } @@ -636,9 +858,59 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { SendReply(buffer.c_str()); } +static void HandleGetStopReason() { + if (is_extended_mode) { + // In extended mode, tell the debugger that there is no selected process yet. + // That way the debugger can ask for the process list and attach to the right one. + if (!current_process) { + // The process has not been selected yet. + SendReply("W00"); + } else { + SendStopReply(current_thread, latest_signal); + } + } else { + // In non extended mode, select the process corresponding to the "main" + // launched application. + u64 program_id = 0; + Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); + auto process_list = Core::System::GetInstance().Kernel().GetProcessList(); + for (const auto& process : process_list) { + if (process->codeset->program_id == program_id) { + current_process = process.get(); + current_process->SetDebugBreak(true); + is_running = false; + if (SetThread(0)) { + SendStopReply(current_thread, 0); + } else { + // Should never happen + SendReply("W00"); + } + return; + } + } + + // No process, should never happen + SendReply("W00"); + } +} + +static void BreakImpl(int signal) { + if (signal == SIGSEGV && !Core::GetCore(0).HasSingleInstructionBreakAccuracy()) { + LOG_WARNING(Debug_GDBStub, "The current CPU backend does not support accurate watchpoints " + "and memory exceptions. Disable CPU JIT for more accuracy."); + } + + current_process->SetDebugBreak(true); + is_running = false; + + latest_signal = signal; + + SendStopReply(current_thread, signal); +} + /// Read command from gdb client. static void ReadCommand() { - command_length = 0; + recv_command_length = 0; std::memset(command_buffer, 0, sizeof(command_buffer)); u8 c = ReadByte(); @@ -647,8 +919,7 @@ static void ReadCommand() { return; } else if (c == 0x03) { LOG_INFO(Debug_GDBStub, "gdb: found break command\n"); - halt_loop = true; - SendSignal(current_thread, SIGTRAP); + BreakImpl(SIGTRAP); return; } else if (c != GDB_STUB_START) { LOG_DEBUG(Debug_GDBStub, "gdb: read invalid byte {:02x}\n", c); @@ -656,27 +927,27 @@ static void ReadCommand() { } while ((c = ReadByte()) != GDB_STUB_END) { - if (command_length >= sizeof(command_buffer)) { + if (recv_command_length >= sizeof(command_buffer)) { LOG_ERROR(Debug_GDBStub, "gdb: command_buffer overflow\n"); SendPacket(GDB_STUB_NACK); return; } - command_buffer[command_length++] = c; + command_buffer[recv_command_length++] = c; } u8 checksum_received = HexCharToValue(ReadByte()) << 4; checksum_received |= HexCharToValue(ReadByte()); - u8 checksum_calculated = CalculateChecksum(command_buffer, command_length); + u8 checksum_calculated = CalculateChecksum(command_buffer, recv_command_length); if (checksum_received != checksum_calculated) { LOG_ERROR( Debug_GDBStub, "gdb: invalid checksum: calculated {:02x} and read {:02x} for ${}# (length: {})\n", checksum_calculated, checksum_received, reinterpret_cast(command_buffer), - command_length); + recv_command_length); - command_length = 0; + recv_command_length = 0; SendPacket(GDB_STUB_NACK); return; @@ -694,7 +965,7 @@ static bool IsDataAvailable() { fd_set fd_socket; FD_ZERO(&fd_socket); - FD_SET(static_cast(gdbserver_socket), &fd_socket); + FD_SET(gdbserver_socket, &fd_socket); struct timeval t; t.tv_sec = 0; @@ -710,6 +981,11 @@ static bool IsDataAvailable() { /// Send requested register to gdb client. static void ReadRegister() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + static u8 reply[64]; std::memset(reply, 0, sizeof(reply)); @@ -727,6 +1003,8 @@ static void ReadRegister() { LongToGdbHex(reply, FpuRead(id, current_thread)); } else if (id == FPSCR_REGISTER) { IntToGdbHex(reply, static_cast(FpuRead(id, current_thread))); + } else if (id == FPEXC_REGISTER) { + IntToGdbHex(reply, static_cast(FpuRead(id, current_thread))); } else { return SendReply("E01"); } @@ -736,6 +1014,11 @@ static void ReadRegister() { /// Send all registers to the gdb client. static void ReadRegisters() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + static u8 buffer[GDB_BUFFER_SIZE - 4]; std::memset(buffer, 0, sizeof(buffer)); @@ -759,11 +1042,31 @@ static void ReadRegisters() { IntToGdbHex(bufptr, static_cast(FpuRead(FPSCR_REGISTER, current_thread))); + bufptr += 8; + + IntToGdbHex(bufptr, static_cast(FpuRead(FPEXC_REGISTER, current_thread))); + SendReply(reinterpret_cast(buffer)); } +static void UpdateCPUThreadContext() { + u32 core_id = current_thread->core_id; + auto& system = Core::System::GetInstance(); + auto& thread_manager = system.Kernel().GetThreadManager(core_id); + if (thread_manager.GetCurrentThread() == current_thread) { + // Only update CPU context if current thread is active, + // otherwise it will be updated when the thread is selected + Core::GetCore(current_thread->core_id).LoadContext(current_thread->context); + } +} + /// Modify data of register specified by gdb client. static void WriteRegister() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + const u8* buffer_ptr = command_buffer + 3; u32 id = HexCharToValue(command_buffer[1]); @@ -781,71 +1084,84 @@ static void WriteRegister() { FpuWrite(id, GdbHexToLong(buffer_ptr), current_thread); } else if (id == FPSCR_REGISTER) { FpuWrite(id, GdbHexToInt(buffer_ptr), current_thread); + } else if (id == FPEXC_REGISTER) { + FpuWrite(id, GdbHexToInt(buffer_ptr), current_thread); } else { return SendReply("E01"); } - Core::GetRunningCore().LoadContext(current_thread->context); + UpdateCPUThreadContext(); SendReply("OK"); } /// Modify all registers with data received from the client. static void WriteRegisters() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + const u8* buffer_ptr = command_buffer + 1; if (command_buffer[0] != 'G') return SendReply("E01"); - for (u32 i = 0, reg = 0; reg <= FPSCR_REGISTER; i++, reg++) { + for (u32 i = 0, reg = 0; reg <= FPEXC_REGISTER; i++, reg++) { if (reg <= PC_REGISTER) { - RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8)); + RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); } else if (reg == CPSR_REGISTER) { - RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8)); + RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); } else if (reg == CPSR_REGISTER - 1) { // Dummy FPA register, ignore } else if (reg < CPSR_REGISTER) { // Dummy FPA registers, ignore i += 2; } else if (reg >= D0_REGISTER && reg < FPSCR_REGISTER) { - FpuWrite(reg, GdbHexToLong(buffer_ptr + i * 16)); + FpuWrite(reg, GdbHexToLong(buffer_ptr + i * 16), current_thread); i++; // Skip padding } else if (reg == FPSCR_REGISTER) { - FpuWrite(reg, GdbHexToInt(buffer_ptr + i * 8)); + FpuWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); + } else if (reg == FPEXC_REGISTER) { + FpuWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); } } - Core::GetRunningCore().LoadContext(current_thread->context); + UpdateCPUThreadContext(); SendReply("OK"); } /// Read location in memory specified by gdb client. static void ReadMemory() { + if (!current_process) { + SendReply(""); + return; + } + static u8 reply[GDB_BUFFER_SIZE - 4]; auto start_offset = command_buffer + 1; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); start_offset = addr_pos + 1; - u32 len = - HexToInt(start_offset, static_cast((command_buffer + command_length) - start_offset)); + u32 len = HexToInt(start_offset, + static_cast((command_buffer + recv_command_length) - start_offset)); LOG_DEBUG(Debug_GDBStub, "ReadMemory addr: {:08x} len: {:08x}", addr, len); if (len * 2 > sizeof(reply)) { - SendReply("E01"); + SendReply(""); } auto& memory = Core::System::GetInstance().Memory(); - if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(), - addr)) { - return SendReply("E00"); + if (!memory.IsValidVirtualAddress(*current_process, addr)) { + return SendReply(""); } std::vector data(len); - memory.ReadBlock(addr, data.data(), len); + memory.ReadBlock(*current_process, addr, data.data(), len); MemToGdbHex(reply, data.data(), len); reply[len * 2] = '\0'; @@ -858,60 +1174,84 @@ static void ReadMemory() { /// Modify location in memory with data received from the gdb client. static void WriteMemory() { + if (!current_process) { + SendReply("E01"); + return; + } + auto start_offset = command_buffer + 1; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); start_offset = addr_pos + 1; - auto len_pos = std::find(start_offset, command_buffer + command_length, ':'); + auto len_pos = std::find(start_offset, command_buffer + recv_command_length, ':'); u32 len = HexToInt(start_offset, static_cast(len_pos - start_offset)); auto& memory = Core::System::GetInstance().Memory(); - if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(), - addr)) { - return SendReply("E00"); + if (!memory.IsValidVirtualAddress(*current_process, addr)) { + return SendReply("E0E"); } std::vector data(len); GdbHexToMem(data.data(), len_pos + 1, len); - memory.WriteBlock(addr, data.data(), len); - Core::GetRunningCore().ClearInstructionCache(); + memory.WriteBlock(*current_process, addr, data.data(), len); + ClearAllInstructionCache(); SendReply("OK"); } -void Break(bool is_memory_break) { - send_trap = true; - - memory_break = is_memory_break; -} - -/// Tell the CPU that it should perform a single step. -static void Step() { - if (command_length > 1) { - RegWrite(PC_REGISTER, GdbHexToInt(command_buffer + 1), current_thread); - Core::GetRunningCore().LoadContext(current_thread->context); +void Break(int signal) { + if (!IsConnected() || !current_process || + Core::System::GetInstance().Kernel().GetCurrentProcess().get() != current_process) { + LOG_ERROR(Debug_GDBStub, "Got signal for un-attached process, ignoring..."); + return; } - step_loop = true; - halt_loop = true; - send_trap = true; + + if (break_thread) { + LOG_ERROR(Debug_GDBStub, + "Got multiple break signals in quick succession, latest may be lost"); + return; + } + + break_thread = + Core::System::GetInstance().Kernel().GetCurrentThreadManager().GetCurrentThread(); + if (break_thread) { + break_signal = signal; + } + + // Try to break CPU asap + Core::GetRunningCore().SetBreakFlag(); Core::GetRunningCore().ClearInstructionCache(); -} - -bool IsMemoryBreak() { - if (!IsConnected()) { - return false; - } - - return memory_break; + Core::GetRunningCore().PrepareReschedule(); } /// Tell the CPU to continue executing. static void Continue() { - memory_break = false; - step_loop = false; - halt_loop = false; - Core::GetRunningCore().ClearInstructionCache(); + if (!current_process) { + return; + } + + u32 thread_id = -1; + if (continue_thread == 0) { + thread_id = current_process->GetThreadList()[0]->thread_id; + } else { + thread_id = continue_thread; + } + + // There is no documentation anywhere if continue should + // reset the continue thread value. Luma3DS implementation + // does this, and looks like IDA Pro expects it that way too. + continue_thread = -1; + + std::vector continue_list{}; + if (thread_id != -1) { + continue_list.push_back(thread_id); + } + + current_process->SetDebugBreak(false, continue_list); + is_running = true; + + ClearAllInstructionCache(); } /** @@ -924,20 +1264,26 @@ static void Continue() { static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) { BreakpointMap& p = GetBreakpointMap(type); + if (type == BreakpointType::Execute && len != 2 && len != 4) { + return false; + } + Breakpoint breakpoint; breakpoint.active = true; breakpoint.addr = addr; breakpoint.len = len; - Core::System::GetInstance().Memory().ReadBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, breakpoint.inst.data(), - breakpoint.inst.size()); + Core::System::GetInstance().Memory().ReadBlock(*current_process, addr, breakpoint.inst.data(), + len); static constexpr std::array btrap{0x70, 0x00, 0x20, 0xe1}; + static constexpr std::array btrap_thumb{0x00, 0xBE}; + if (type == BreakpointType::Execute) { Core::System::GetInstance().Memory().WriteBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, btrap.data(), - btrap.size()); - Core::GetRunningCore().ClearInstructionCache(); + *current_process, addr, (len == 2) ? btrap_thumb.data() : btrap.data(), len); + ClearAllInstructionCache(); + } else { + Core::System::GetInstance().Memory().RegisterWatchpoint(*current_process, addr, len); } p.insert({addr, breakpoint}); @@ -949,6 +1295,11 @@ static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) { /// Handle add breakpoint command from gdb client. static void AddBreakpoint() { + if (!current_process) { + SendReply("E01"); + return; + } + BreakpointType type; u8 type_id = HexCharToValue(command_buffer[1]); @@ -971,12 +1322,12 @@ static void AddBreakpoint() { } auto start_offset = command_buffer + 3; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); start_offset = addr_pos + 1; - u32 len = - HexToInt(start_offset, static_cast((command_buffer + command_length) - start_offset)); + u32 len = HexToInt(start_offset, + static_cast((command_buffer + recv_command_length) - start_offset)); if (type == BreakpointType::Access) { // Access is made up of Read and Write types, so add both breakpoints @@ -998,6 +1349,11 @@ static void AddBreakpoint() { /// Handle remove breakpoint command from gdb client. static void RemoveBreakpoint() { + if (!current_process) { + SendReply("E01"); + return; + } + BreakpointType type; u8 type_id = HexCharToValue(command_buffer[1]); @@ -1020,7 +1376,7 @@ static void RemoveBreakpoint() { } auto start_offset = command_buffer + 3; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); if (type == BreakpointType::Access) { @@ -1035,11 +1391,146 @@ static void RemoveBreakpoint() { SendReply("OK"); } +void HandleVCommand() { + std::string_view cmd_view((const char*)command_buffer, recv_command_length); + + if (cmd_view.size() <= 1) { + SendReply("E01"); + return; + } + size_t delimiter_pos = cmd_view.find(';'); + std::string_view command = + cmd_view.substr(1, delimiter_pos == cmd_view.npos ? cmd_view.npos : delimiter_pos); + if (command == "Attach;") { + if (!is_extended_mode) { + SendReply("E01"); + return; + } + + std::string_view arg = cmd_view.substr(delimiter_pos + 1); + u32 pid = HexToInt(reinterpret_cast(arg.data()), arg.size()); + auto process = Core::System::GetInstance().Kernel().GetProcessById(pid); + if (!process) { + SendReply("E02"); + } else { + current_process = process.get(); + current_process->SetDebugBreak(true); + is_running = false; + if (SetThread(0)) { + SendStopReply(current_thread, 0); + } else { + // Should never happen + SendReply("W00"); + } + } + return; + } else if (command == "Cont?") { + SendReply("vCont;c;C"); + } else if (command == "Cont;") { + if (!current_process) { + SendReply("E01"); + return; + } + + std::string_view arg = cmd_view.substr(delimiter_pos + 1); + auto actions = Common::SplitString(arg, ';'); + + if (actions.empty()) { + SendReply("E01"); + return; + } + for (auto& action : actions) { + auto threads = Common::SplitString(action, ':'); + if (threads.empty()) { + SendReply("E01"); + return; + } + char action_type = threads[0][0]; + if (action_type != 'c' && action_type != 'C') { + SendReply("E01"); + return; + } + std::vector thread_ids; + for (size_t i = 1; i < threads.size(); i++) { + thread_ids.push_back( + HexToInt(reinterpret_cast(threads[i].c_str()), threads[i].size())); + } + + current_process->SetDebugBreak(false, thread_ids); + is_running = true; + } + } else { + SendReply(""); + } +} + +void OnProcessExit(u32 process_id) { + if (!GDBStub::IsConnected || !current_process || current_process->process_id != process_id) { + return; + } + + current_process_finished = true; + if (is_running) { + SendStopReply(nullptr, 0); + } + current_process = nullptr; + current_thread = nullptr; +} + +void OnThreadExit(u32 thread_id) { + if (!GDBStub::IsConnected || !current_thread || current_thread->thread_id != thread_id) { + return; + } + + current_thread = nullptr; +} + void HandlePacket(Core::System& system) { + if (!IsConnected()) { if (defer_start) { ToggleServer(true); + defer_start = false; } + + // Handle accept new GDB connection + if (accept_socket != INVALID_SOCKET) { + sockaddr_in saddr_client; + sockaddr* client_addr = reinterpret_cast(&saddr_client); + socklen_t client_addrlen = sizeof(saddr_client); + gdbserver_socket = + static_cast(accept(accept_socket, client_addr, &client_addrlen)); + if (gdbserver_socket == INVALID_SOCKET) { +#ifdef _WIN32 + if (GetErrno() == WSAEWOULDBLOCK) { + // Nothing connected yet + return; + } +#else + if (GetErrno() == EAGAIN || GetErrno() == EWOULDBLOCK) { + // Nothing connected yet + return; + } +#endif + LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client"); + } else { + LOG_INFO(Debug_GDBStub, "Client connected.\n"); + SetNonBlock(gdbserver_socket, false); + } + + shutdown(accept_socket, SHUT_RDWR); + closesocket(accept_socket); + accept_socket = INVALID_SOCKET; + } + return; + } + + if (break_thread) { + current_thread = break_thread; + break_thread = nullptr; + int signal = break_signal; + break_signal = 0; + BreakImpl(signal); return; } @@ -1053,10 +1544,15 @@ void HandlePacket(Core::System& system) { } ReadCommand(); - if (command_length == 0) { + if (recv_command_length == 0) { return; } +#ifdef PRINT_GDB_TRAFFIC + std::string cmd_str(command_buffer + 1, command_buffer + recv_command_length); + LOG_INFO(Debug_GDBStub, "Req: {:c} {}", command_buffer[0], cmd_str); +#endif + LOG_DEBUG(Debug_GDBStub, "Packet: {0:d} ('{0:c}')", command_buffer[0]); switch (command_buffer[0]) { @@ -1067,16 +1563,29 @@ void HandlePacket(Core::System& system) { HandleSetThread(); break; case '?': - SendSignal(current_thread, latest_signal); + HandleGetStopReason(); + break; + case '!': + HandleExtendedMode(); + break; + case 'D': + SendReply("OK"); + ToggleServer(false); + ToggleServer(true); + // Continue execution + continue_thread = -1; + Continue(); break; case 'k': LOG_INFO(Debug_GDBStub, "killed by gdb"); ToggleServer(false); - // Continue execution so we don't hang forever after shutting down the server + // Continue execution and stop emulation + continue_thread = -1; Continue(); + system.RequestShutdown(); return; case 'F': - HandleHioReply(system, command_buffer, command_length); + HandleHioReply(system, current_process, command_buffer, recv_command_length); break; case 'g': ReadRegisters(); @@ -1097,7 +1606,8 @@ void HandlePacket(Core::System& system) { WriteMemory(); break; case 's': - Step(); + // Single step not supported, return ENOTSUP + SendReply("E5F"); return; case 'C': case 'c': @@ -1112,6 +1622,9 @@ void HandlePacket(Core::System& system) { case 'T': HandleThreadAlive(); break; + case 'v': + HandleVCommand(); + break; default: SendReply(""); break; @@ -1127,14 +1640,12 @@ void ToggleServer(bool status) { server_enabled = status; // Start server - if (!IsConnected() && Core::System::GetInstance().IsPoweredOn()) { + if (!IsInitialized() && Core::System::GetInstance().IsPoweredOn()) { Init(); } } else { // Stop server - if (IsConnected()) { - Shutdown(); - } + Shutdown(); server_enabled = status; } @@ -1146,21 +1657,9 @@ void DeferStart() { static void Init(u16 port) { if (!server_enabled) { - // Set the halt loop to false in case the user enabled the gdbstub mid-execution. - // This way the CPU can still execute normally. - halt_loop = false; - step_loop = false; return; } - // Setup initial gdbstub status - halt_loop = true; - step_loop = false; - - breakpoints_execute.clear(); - breakpoints_read.clear(); - breakpoints_write.clear(); - // Start gdb server LOG_INFO(Debug_GDBStub, "Starting GDB server on port {}...", port); @@ -1173,50 +1672,36 @@ static void Init(u16 port) { WSAStartup(MAKEWORD(2, 2), &InitData); #endif - int tmpsock = static_cast(socket(PF_INET, SOCK_STREAM, 0)); - if (tmpsock == -1) { + accept_socket = static_cast(socket(PF_INET, SOCK_STREAM, 0)); + if (accept_socket == INVALID_SOCKET) { LOG_ERROR(Debug_GDBStub, "Failed to create gdb socket"); + return; } // Set socket to SO_REUSEADDR so it can always bind on the same port int reuse_enabled = 1; - if (setsockopt(tmpsock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled, + if (setsockopt(accept_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled, sizeof(reuse_enabled)) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to set gdb socket option"); + Shutdown(); + return; } const sockaddr* server_addr = reinterpret_cast(&saddr_server); socklen_t server_addrlen = sizeof(saddr_server); - if (bind(tmpsock, server_addr, server_addrlen) < 0) { + if (bind(accept_socket, server_addr, server_addrlen) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to bind gdb socket"); + Shutdown(); + return; } - if (listen(tmpsock, 1) < 0) { + if (listen(accept_socket, 1) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to listen to gdb socket"); + Shutdown(); + return; } - // Wait for gdb to connect - LOG_INFO(Debug_GDBStub, "Waiting for gdb to connect...\n"); - sockaddr_in saddr_client; - sockaddr* client_addr = reinterpret_cast(&saddr_client); - socklen_t client_addrlen = sizeof(saddr_client); - gdbserver_socket = static_cast(accept(tmpsock, client_addr, &client_addrlen)); - if (gdbserver_socket < 0) { - // In the case that we couldn't start the server for whatever reason, just start CPU - // execution like normal. - halt_loop = false; - step_loop = false; - - LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client"); - } else { - LOG_INFO(Debug_GDBStub, "Client connected.\n"); - saddr_client.sin_addr.s_addr = ntohl(saddr_client.sin_addr.s_addr); - } - - // Clean up temporary socket if it's still alive at this point. - if (tmpsock != -1) { - shutdown(tmpsock, SHUT_RDWR); - } + SetNonBlock(accept_socket, true); } void Init() { @@ -1227,18 +1712,28 @@ void Shutdown() { if (!server_enabled) { return; } - defer_start = false; + + RemoveAllBreakpoints(); LOG_INFO(Debug_GDBStub, "Stopping GDB ..."); - if (gdbserver_socket != -1) { + if (gdbserver_socket != INVALID_SOCKET) { shutdown(gdbserver_socket, SHUT_RDWR); - gdbserver_socket = -1; + closesocket(gdbserver_socket); + gdbserver_socket = INVALID_SOCKET; + } + + if (accept_socket != INVALID_SOCKET) { + shutdown(accept_socket, SHUT_RDWR); + closesocket(accept_socket); + accept_socket = INVALID_SOCKET; } #ifdef _WIN32 WSACleanup(); #endif + ResetState(); + LOG_INFO(Debug_GDBStub, "GDB stopped."); } @@ -1246,36 +1741,12 @@ bool IsServerEnabled() { return server_enabled; } +bool IsInitialized() { + return IsServerEnabled() && + (accept_socket != INVALID_SOCKET || gdbserver_socket != INVALID_SOCKET); +} + bool IsConnected() { - return IsServerEnabled() && gdbserver_socket != -1; -} - -bool GetCpuHaltFlag() { - return halt_loop; -} - -void SetCpuHaltFlag(bool halt) { - halt_loop = halt; -} - -bool GetCpuStepFlag() { - return step_loop; -} - -void SetCpuStepFlag(bool is_step) { - step_loop = is_step; -} - -void SendTrap(Kernel::Thread* thread, int trap) { - if (!send_trap) { - return; - } - - current_thread = thread; - - SendSignal(thread, trap); - - halt_loop = true; - send_trap = false; + return IsServerEnabled() && gdbserver_socket != INVALID_SOCKET; } }; // namespace GDBStub diff --git a/src/core/gdbstub/gdbstub.h b/src/core/gdbstub/gdbstub.h index fc0c7df23..0fd3c7c00 100644 --- a/src/core/gdbstub/gdbstub.h +++ b/src/core/gdbstub/gdbstub.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. @@ -10,6 +14,10 @@ #include "common/common_types.h" #include "core/hle/kernel/thread.h" +#ifndef ENABLE_GDBSTUB +#error "File was included with GDB stub support disabled" +#endif + namespace Core { class System; } @@ -60,18 +68,28 @@ void Shutdown(); /// Checks if the gdbstub server is enabled. bool IsServerEnabled(); +/// Returns true if the GDB server is initialized +bool IsInitialized(); + /// Returns true if there is an active socket connection. bool IsConnected(); /** * Signal to the gdbstub server that it should halt CPU execution. * - * @param is_memory_break If true, the break resulted from a memory breakpoint. + * @param signal Signal that produced the break (SIGTRAP by default) */ -void Break(bool is_memory_break = false); +void Break(int signal); -/// Determine if there was a memory breakpoint. -bool IsMemoryBreak(); +/** + * Signal to the GDB stub that the specified process ID is exiting + */ +void OnProcessExit(u32 process_id); + +/** + * Signal to the GDB stub that the specified thread ID is exiting + */ +void OnThreadExit(u32 thread_id); /// Read and handle packet from gdb client. void HandlePacket(Core::System& system); @@ -88,37 +106,10 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, GDBStub::BreakpointTy * Check if a breakpoint of the specified type exists at the given address. * * @param addr Address of breakpoint. + * @param access_len Access size in bytes. * @param type Type of breakpoint. */ -bool CheckBreakpoint(VAddr addr, GDBStub::BreakpointType type); - -// If set to true, the CPU will halt at the beginning of the next CPU loop. -bool GetCpuHaltFlag(); - -/** - * If set to true, the CPU will halt at the beginning of the next CPU loop. - * - * @param halt whether to halt on the next loop - */ -void SetCpuHaltFlag(bool halt); - -// If set to true and the CPU is halted, the CPU will step one instruction. -bool GetCpuStepFlag(); - -/** - * When set to true, the CPU will step one instruction when the CPU is halted next. - * - * @param is_step - */ -void SetCpuStepFlag(bool is_step); - -/** - * Send trap signal from thread back to the gdbstub server. - * - * @param thread Sending thread. - * @param trap Trap no. - */ -void SendTrap(Kernel::Thread* thread, int trap); +bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type); /** * Send reply to gdb client. diff --git a/src/core/gdbstub/hio.cpp b/src/core/gdbstub/hio.cpp index 00eab9061..8a8a7adc6 100644 --- a/src/core/gdbstub/hio.cpp +++ b/src/core/gdbstub/hio.cpp @@ -1,13 +1,22 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include "common/string_util.h" #include "core/core.h" #include "core/gdbstub/gdbstub.h" #include "core/gdbstub/hio.h" +#ifndef ENABLE_GDBSTUB +#error "File was compiled with GDB stub support disabled" +#endif + +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + namespace GDBStub { namespace { @@ -23,9 +32,6 @@ enum class Status { static std::atomic request_status{Status::NoRequest}; -static std::atomic was_halted = false; -static std::atomic was_stepping = false; - } // namespace /** @@ -54,7 +60,7 @@ static void SendErrorReply(int error_code, int retval = -1) { SendReply(packet.data()); } -void SetHioRequest(Core::System& system, const VAddr addr) { +void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr addr) { if (!IsServerEnabled()) { LOG_WARNING(Debug_GDBStub, "HIO requested but GDB stub is not running"); return; @@ -70,14 +76,13 @@ void SetHioRequest(Core::System& system, const VAddr addr) { } auto& memory = system.Memory(); - const auto process = system.Kernel().GetCurrentProcess(); if (!memory.IsValidVirtualAddress(*process, addr)) { LOG_WARNING(Debug_GDBStub, "Invalid address for HIO request"); return; } - memory.ReadBlock(addr, ¤t_hio_request, sizeof(PackedGdbHioRequest)); + memory.ReadBlock(*process, addr, ¤t_hio_request, sizeof(PackedGdbHioRequest)); if (current_hio_request.magic != std::array{'G', 'D', 'B', '\0'}) { std::string_view bad_magic{ @@ -97,18 +102,13 @@ void SetHioRequest(Core::System& system, const VAddr addr) { current_hio_request_addr = addr; request_status = Status::NotSent; - was_halted = GetCpuHaltFlag(); - was_stepping = GetCpuStepFlag(); - // Now halt, so that no further instructions are executed until the request // is processed by the client. We will continue after the reply comes back - Break(); - SetCpuHaltFlag(true); - SetCpuStepFlag(false); + Break(SIGTRAP); system.GetRunningCore().ClearInstructionCache(); } -void HandleHioReply(Core::System& system, const u8* const command_buffer, +void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer, const u32 command_length) { if (!IsWaitingForHioReply()) { LOG_WARNING(Debug_GDBStub, "Got HIO reply but never sent a request"); @@ -177,7 +177,6 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer, current_hio_request.retval, current_hio_request.gdb_errno, current_hio_request.ctrl_c); - const auto process = system.Kernel().GetCurrentProcess(); auto& memory = system.Memory(); // should have been checked when we first initialized the request, @@ -188,15 +187,14 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer, return; } - memory.WriteBlock(current_hio_request_addr, ¤t_hio_request, sizeof(PackedGdbHioRequest)); + memory.WriteBlock(*process, current_hio_request_addr, ¤t_hio_request, + sizeof(PackedGdbHioRequest)); current_hio_request = {}; current_hio_request_addr = 0; request_status = Status::NoRequest; // Restore state from before the request came in - SetCpuStepFlag(was_stepping); - SetCpuHaltFlag(was_halted); system.GetRunningCore().ClearInstructionCache(); } diff --git a/src/core/gdbstub/hio.h b/src/core/gdbstub/hio.h index 827521841..066bef55a 100644 --- a/src/core/gdbstub/hio.h +++ b/src/core/gdbstub/hio.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -6,10 +6,18 @@ #include "common/common_types.h" +#ifndef ENABLE_GDBSTUB +#error "File was included with GDB stub support disabled" +#endif + namespace Core { class System; } +namespace Kernel { +class Process; +} + namespace GDBStub { /** @@ -51,7 +59,7 @@ static_assert(sizeof(PackedGdbHioRequest) == 152, * * @param address The memory address of the \ref PackedGdbHioRequest. */ -void SetHioRequest(Core::System& system, const VAddr address); +void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr address); /** * If there is a pending HIO request, send it to the client. @@ -63,6 +71,7 @@ bool HandlePendingHioRequestPacket(); /** * Process an HIO reply from the client. */ -void HandleHioReply(Core::System& system, const u8* const command_buffer, const u32 command_length); +void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer, + const u32 command_length); } // namespace GDBStub diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index 1b3b8c93e..bdd7877fc 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -17,6 +17,7 @@ #include "common/serialization/boost_small_vector.hpp" #include "common/settings.h" #include "common/swap.h" +#include "common/thread_worker.h" #include "core/hle/ipc.h" #include "core/hle/kernel/object.h" #include "core/hle/kernel/server_session.h" @@ -327,6 +328,55 @@ public: } } + /** + * Same as RunAsync, but runs the async operation on a specific thread worker provided by the + * caller. + * @param worker The thread worker where the operation will be run. + * @param async_section Callable that takes Kernel::HLERequestContext& as argument + * and returns the amount of nanoseconds to wait before calling result_function. + * This callable is ran asynchronously. + * @param result_function Callable that takes Kernel::HLERequestContext& as argument + * and doesn't return anything. This callable is ran from the emulator thread + * and can be used to set the IPC result. + * @param really_async If set to false, it will call both async_section and result_function + * from the emulator thread. + */ + template + void RunOnThreadWorker(Common::ThreadWorker& worker, AsyncFunctor async_section, + ResultFunctor result_function, bool really_async = true) { + + if (!Settings::values.deterministic_async_operations && really_async) { + kernel.ReportAsyncState(true); + + // We use packaged_task so we can retrieve a std::future to pass to AsyncWakeUpCallback + auto task = std::make_shared>([this, async_section] { + s64 sleep_for = async_section(*this); + this->thread->WakeAfterDelay(sleep_for, true); + }); + + auto future = task->get_future(); + + worker.QueueWork([task]() { (*task)(); }); + + this->SleepClientThread("RunOnThread", std::chrono::nanoseconds(-1), + std::make_shared>( + kernel, result_function, std::move(future))); + + } else { + // Synchronous fallback (same logic as original) + s64 sleep_for = async_section(*this); + if (sleep_for > 0) { + kernel.ReportAsyncState(true); + auto parallel_wakeup = std::make_shared>( + kernel, result_function, std::move(std::future())); + this->SleepClientThread("RunOnThread", std::chrono::nanoseconds(sleep_for), + parallel_wakeup); + } else { + result_function(*this); + } + } + } + /** * Resolves a object id from the request command buffer into a pointer to an object. See the * "HLE handle protocol" section in the class documentation for more details. diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index 21ca655db..5c9e5b6ab 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -202,6 +202,14 @@ void KernelSystem::RestoreMemoryState(u64 title_id) { } } +void KernelSystem::SetCore1ScheduleMode(Core1ScheduleMode mode) { + GetThreadManager(1).SetScheduleMode(mode); +} + +void KernelSystem::UpdateCore1AppCpuLimit() { + GetThreadManager(1).UpdateAppCpuLimit(); +} + template void KernelSystem::serialize(Archive& ar, const unsigned int) { ar & memory_regions; @@ -246,4 +254,10 @@ void New3dsHwCapabilities::serialize(Archive& ar, const unsigned int) { } SERIALIZE_IMPL(New3dsHwCapabilities) +template +void Core1CpuTime::serialize(Archive& ar, const unsigned int) { + ar & raw; +} +SERIALIZE_IMPL(Core1CpuTime) + } // namespace Kernel diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index 53d88cddf..4637d1de0 100644 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h @@ -136,6 +136,43 @@ private: friend class boost::serialization::access; }; +enum class Core1ScheduleMode : u32 { + // https://github.com/LumaTeam/Luma3DS/blob/e35972ea/sysmodules/pm/source/reslimit.c#L144 + // Starting by "sysmodule" threads, alternatively allow (if preemptible) only sysmodule + // threads, and then only application threads to run. This happens at a rate of 2ms * + // (cpuTime/100). + Multi, + // This divides the core1 time into slices of 25ms. Only one application thread is allowed to be + // created on core1. The "application" thread is given cpuTime% of the slice. The "sysmodules" + // threads are given a total of (90 - cpuTime)% of the slice. 10% remain for other threads. This + // mode is half-broken due to a kernel bug. + Single, +}; + +class Core1CpuTime { +public: + Core1CpuTime() = default; + constexpr Core1CpuTime(s32 value) : raw(value) {} + + static const Core1CpuTime PREEMPTION_DISABLED; + static const Core1CpuTime PREEMPTION_SYSMODULE; + static const Core1CpuTime PREEMPTION_EXCEMPTED; + + operator s32() const { + return raw; + } + +private: + s32 raw{}; + + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; +inline constexpr Core1CpuTime Core1CpuTime::PREEMPTION_DISABLED{0}; +inline constexpr Core1CpuTime Core1CpuTime::PREEMPTION_SYSMODULE{1000}; +inline constexpr Core1CpuTime Core1CpuTime::PREEMPTION_EXCEMPTED{10000}; + class KernelSystem { public: explicit KernelSystem(Memory::MemorySystem& memory, Core::Timing& timing, @@ -378,6 +415,10 @@ public: void RestoreMemoryState(u64 title_id); + void SetCore1ScheduleMode(Core1ScheduleMode mode); + + void UpdateCore1AppCpuLimit(); + private: void MemoryInit(MemoryMode memory_mode, u64 override_init_time); @@ -447,3 +488,4 @@ private: } // namespace Kernel BOOST_CLASS_EXPORT_KEY(Kernel::New3dsHwCapabilities) +BOOST_CLASS_EXPORT_KEY(Kernel::Core1CpuTime) diff --git a/src/core/hle/kernel/process.cpp b/src/core/hle/kernel/process.cpp index 5e0d3810f..b895a1f13 100644 --- a/src/core/hle/kernel/process.cpp +++ b/src/core/hle/kernel/process.cpp @@ -16,6 +16,9 @@ #include "common/logging/log.h" #include "common/serialization/boost_vector.hpp" #include "core/core.h" +#ifdef ENABLE_GDBSTUB +#include "core/gdbstub/gdbstub.h" +#endif #include "core/hle/kernel/errors.h" #include "core/hle/kernel/memory.h" #include "core/hle/kernel/process.h" @@ -260,9 +263,25 @@ void Process::Run(s32 main_thread_priority, u32 stack_size) { vm_manager.LogLayout(Common::Log::Level::Debug); Kernel::SetupMainThread(kernel, codeset->entrypoint, main_thread_priority, SharedFrom(this)); + + // Pause process at start if flag enabled and we are not a sysmodule + if (Core::System::GetInstance().GetDebugNextProcessFlag() && + resource_limit->GetCategory() != Kernel::ResourceLimitCategory::Other) { +#ifdef ENABLE_GDBSTUB + if (GDBStub::IsServerEnabled()) { + LOG_INFO(Loader, "Pausing process {} at start", process_id); + SetDebugBreak(true); + } +#endif + Core::System::GetInstance().ClearDebugNextProcessFlag(); + } } void Process::Exit() { +#ifdef ENABLE_GDBSTUB + GDBStub::OnProcessExit(process_id); +#endif + auto plgldr = Service::PLGLDR::GetService(Core::System::GetInstance()); if (plgldr) { plgldr->OnProcessExit(*this, kernel); @@ -592,6 +611,42 @@ Result Process::Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms, return ResultSuccess; } +std::vector> Kernel::Process::GetThreadList() { + std::vector> ret; + for (u32 core = 0; core < Core::GetNumCores(); core++) { + auto thread_list = kernel.GetThreadManager(core).GetThreadList(); + for (auto& thread : thread_list) { + if (thread->owner_process.lock().get() == this) { + ret.push_back(thread); + } + } + } + return ret; +} + +void Kernel::Process::SetDebugBreak(bool debug_break, std::vector thread_ids) { + auto thread_list = GetThreadList(); + bool needs_reschedule = false; + for (auto& t : thread_list) { + + if (!thread_ids.empty()) { + u32 thread_id = t->thread_id; + if (std::find(thread_ids.begin(), thread_ids.end(), thread_id) == thread_ids.end()) { + continue; + } + } + + needs_reschedule |= t->SetDebugBreak(debug_break); + } + + if (needs_reschedule) { + for (u32 i = 0; i < Core::GetNumCores(); i++) { + Core::GetCore(i).PrepareReschedule(); + kernel.GetThreadManager(i).Reschedule(); + } + } +} + void Process::FreeAllMemory() { if (memory_region == nullptr || resource_limit == nullptr) { return; diff --git a/src/core/hle/kernel/process.h b/src/core/hle/kernel/process.h index 7ec1adfbb..4067839cd 100644 --- a/src/core/hle/kernel/process.h +++ b/src/core/hle/kernel/process.h @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -226,6 +226,10 @@ public: Result Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms, bool privileged = false); + std::vector> GetThreadList(); + + void SetDebugBreak(bool debug_break, std::vector thread_ids = {}); + private: void FreeAllMemory(); diff --git a/src/core/hle/kernel/resource_limit.cpp b/src/core/hle/kernel/resource_limit.cpp index e4b0dba0a..6cc309897 100644 --- a/src/core/hle/kernel/resource_limit.cpp +++ b/src/core/hle/kernel/resource_limit.cpp @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -9,19 +9,24 @@ #include "common/archives.h" #include "common/assert.h" #include "common/settings.h" +#include "core/hle/kernel/process.h" #include "core/hle/kernel/resource_limit.h" +#include "core/hle/kernel/thread.h" SERIALIZE_EXPORT_IMPL(Kernel::ResourceLimit) SERIALIZE_EXPORT_IMPL(Kernel::ResourceLimitList) namespace Kernel { -ResourceLimit::ResourceLimit(KernelSystem& kernel) : Object(kernel) {} +ResourceLimit::ResourceLimit(KernelSystem& _kernel) : Object(_kernel), kernel(_kernel) {} ResourceLimit::~ResourceLimit() = default; -std::shared_ptr ResourceLimit::Create(KernelSystem& kernel, std::string name) { +std::shared_ptr ResourceLimit::Create(KernelSystem& kernel, + ResourceLimitCategory category, + std::string name) { auto resource_limit = std::make_shared(kernel); + resource_limit->m_category = category; resource_limit->m_name = std::move(name); return resource_limit; } @@ -36,6 +41,11 @@ s32 ResourceLimit::GetLimitValue(ResourceLimitType type) const { return m_limit_values[index]; } +void ResourceLimit::SetCurrentValue(ResourceLimitType type, s32 value) { + const auto index = static_cast(type); + m_current_values[index] = value; +} + void ResourceLimit::SetLimitValue(ResourceLimitType type, s32 value) { const auto index = static_cast(type); m_limit_values[index] = value; @@ -66,9 +76,89 @@ bool ResourceLimit::Release(ResourceLimitType type, s32 amount) { return true; } +void ResourceLimit::ApplyAppMaxCPUSetting(std::shared_ptr& process, u8 exh_mode, + u8 exh_cpu_limit) { + + // List of 1.0 titles with max CPU time overrides. This is not a "hack" list, + // these values actually exist in the 3DS kernel. + static constexpr std::array, 16> cpu_time_overrides = {{ + // 3DS sound (JPN, USA, EUR) + {0x205, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x215, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x225, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Star Fox 64 3D (JPN) + {0x304, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Super Monkey Ball 3D (JPN) + {0x32E, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // The Legend of Zelda: Ocarina of Time 3D (JPN, USA, EUR) + {0x334, 30}, + {0x335, 30}, + {0x336, 30}, + + // Doctor Lautrec to Boukyaku no Kishidan (JPN, USA, EUR) + {0x348, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x368, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x562, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Pro Yakyuu Spirits 2011 (JPN) + {0x349, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Super Monkey Ball 3D (USA, EUR) + {0x370, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x389, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + // Star Fox 64 3D (USA, EUR) + {0x490, Core1CpuTime::PREEMPTION_EXCEMPTED}, + {0x491, Core1CpuTime::PREEMPTION_EXCEMPTED}, + + }}; + + Core1ScheduleMode final_mode{}; + Core1CpuTime limit_cpu_time{}; + Core1CpuTime set_cpu_time = Core1CpuTime::PREEMPTION_DISABLED; + + if (exh_mode == 0 && exh_cpu_limit == 0) { + // This is a 1.0 app with the CPU time value not set. Use default or hardcoded value. + + final_mode = Core1ScheduleMode::Single; + limit_cpu_time = 80; + set_cpu_time = 25; + + u32 uID = static_cast(((process->codeset->program_id >> 8) & 0xFFFFF)); + + auto it = std::find_if(cpu_time_overrides.begin(), cpu_time_overrides.end(), + [uID](auto& v) { return v.first == uID; }); + + if (it != cpu_time_overrides.end()) { + const Core1CpuTime& time = it->second; + if (static_cast(time) > 100 && static_cast(time) < 200) { + // This code path is actually unused + final_mode = Core1ScheduleMode::Multi; + set_cpu_time = limit_cpu_time = static_cast(time) - 100; + } else { + final_mode = Core1ScheduleMode::Single; + set_cpu_time = limit_cpu_time = time; + } + } + } else { + final_mode = (exh_mode == 0) ? Core1ScheduleMode::Single : Core1ScheduleMode::Multi; + limit_cpu_time = exh_cpu_limit; + } + + // Normally done by PM through svcSetKernelState and svcSetResourceLimitValues. + SetLimitValue(ResourceLimitType::CpuTime, limit_cpu_time); + SetCurrentValue(ResourceLimitType::CpuTime, set_cpu_time); + kernel.SetCore1ScheduleMode(final_mode); + kernel.UpdateCore1AppCpuLimit(); +} + template void ResourceLimit::serialize(Archive& ar, const unsigned int) { ar& boost::serialization::base_object(*this); + ar & m_category; ar & m_name; ar & m_limit_values; ar & m_current_values; @@ -82,7 +172,8 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { const auto& appmemalloc = kernel.GetMemoryRegion(MemoryRegion::APPLICATION); // Create the Application resource limit - auto resource_limit = ResourceLimit::Create(kernel, "Applications"); + auto resource_limit = + ResourceLimit::Create(kernel, ResourceLimitCategory::Application, "Applications"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x18); resource_limit->SetLimitValue(ResourceLimitType::Commit, appmemalloc->size); resource_limit->SetLimitValue(ResourceLimitType::Thread, 0x20); @@ -92,11 +183,12 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, 0x8); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, 0x10); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, 0x2); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x0); - resource_limits[static_cast(ResourceLimitCategory::Application)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_DISABLED); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; // Create the SysApplet resource limit - resource_limit = ResourceLimit::Create(kernel, "System Applets"); + resource_limit = + ResourceLimit::Create(kernel, ResourceLimitCategory::SysApplet, "System Applets"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x4); resource_limit->SetLimitValue(ResourceLimitType::Commit, is_new_3ds ? 0x5E06000 : 0x2606000); resource_limit->SetLimitValue(ResourceLimitType::Thread, is_new_3ds ? 0x1D : 0xE); @@ -106,11 +198,12 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, 0x4); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, 0x8); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, 0x3); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x2710); - resource_limits[static_cast(ResourceLimitCategory::SysApplet)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_EXCEMPTED); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; // Create the LibApplet resource limit - resource_limit = ResourceLimit::Create(kernel, "Library Applets"); + resource_limit = + ResourceLimit::Create(kernel, ResourceLimitCategory::LibApplet, "Library Applets"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x4); resource_limit->SetLimitValue(ResourceLimitType::Commit, 0x602000); resource_limit->SetLimitValue(ResourceLimitType::Thread, 0xE); @@ -120,11 +213,11 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, 0x4); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, 0x8); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, 0x1); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x2710); - resource_limits[static_cast(ResourceLimitCategory::LibApplet)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_EXCEMPTED); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; // Create the Other resource limit - resource_limit = ResourceLimit::Create(kernel, "Others"); + resource_limit = ResourceLimit::Create(kernel, ResourceLimitCategory::Other, "Others"); resource_limit->SetLimitValue(ResourceLimitType::Priority, 0x4); resource_limit->SetLimitValue(ResourceLimitType::Commit, is_new_3ds ? 0x2182000 : 0x1682000); resource_limit->SetLimitValue(ResourceLimitType::Thread, is_new_3ds ? 0xE1 : 0xCA); @@ -134,8 +227,8 @@ ResourceLimitList::ResourceLimitList(KernelSystem& kernel) { resource_limit->SetLimitValue(ResourceLimitType::Timer, is_new_3ds ? 0x2C : 0x2B); resource_limit->SetLimitValue(ResourceLimitType::SharedMemory, is_new_3ds ? 0x1F : 0x1E); resource_limit->SetLimitValue(ResourceLimitType::AddressArbiter, is_new_3ds ? 0x2D : 0x2B); - resource_limit->SetLimitValue(ResourceLimitType::CpuTime, 0x3E8); - resource_limits[static_cast(ResourceLimitCategory::Other)] = resource_limit; + resource_limit->SetLimitValue(ResourceLimitType::CpuTime, Core1CpuTime::PREEMPTION_SYSMODULE); + resource_limits[static_cast(resource_limit->GetCategory())] = resource_limit; } ResourceLimitList::~ResourceLimitList() = default; diff --git a/src/core/hle/kernel/resource_limit.h b/src/core/hle/kernel/resource_limit.h index 48235411e..63694a83c 100644 --- a/src/core/hle/kernel/resource_limit.h +++ b/src/core/hle/kernel/resource_limit.h @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -42,6 +42,7 @@ public: * Creates a resource limit object. */ static std::shared_ptr Create(KernelSystem& kernel, + ResourceLimitCategory category, std::string name = "Unknown"); std::string GetTypeName() const override { @@ -56,15 +57,25 @@ public: return HANDLE_TYPE; } + ResourceLimitCategory GetCategory() const { + return m_category; + } + s32 GetCurrentValue(ResourceLimitType type) const; s32 GetLimitValue(ResourceLimitType type) const; + void SetCurrentValue(ResourceLimitType name, s32 value); void SetLimitValue(ResourceLimitType name, s32 value); bool Reserve(ResourceLimitType type, s32 amount); bool Release(ResourceLimitType type, s32 amount); + void ApplyAppMaxCPUSetting(std::shared_ptr& process, u8 exh_mode, + u8 exh_cpu_limit); + private: + Kernel::KernelSystem& kernel; + ResourceLimitCategory m_category; using ResourceArray = std::array(ResourceLimitType::Max)>; ResourceArray m_limit_values{}; ResourceArray m_current_values{}; diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 77fdf422a..715aedd6a 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -14,7 +14,9 @@ #include "core/arm/arm_interface.h" #include "core/core.h" #include "core/core_timing.h" +#ifdef ENABLE_GDBSTUB #include "core/gdbstub/hio.h" +#endif #include "core/hle/kernel/address_arbiter.h" #include "core/hle/kernel/client_port.h" #include "core/hle/kernel/client_session.h" @@ -1171,7 +1173,9 @@ void SVC::OutputDebugString(VAddr address, s32 len) { } if (len == 0) { - GDBStub::SetHioRequest(system, address); +#ifdef ENABLE_GDBSTUB + GDBStub::SetHioRequest(system, kernel.GetCurrentProcess().get(), address); +#endif return; } @@ -2051,12 +2055,16 @@ Result SVC::GetProcessList(s32* process_count, VAddr out_process_array, } Result SVC::InvalidateInstructionCacheRange(u32 addr, u32 size) { - system.GetRunningCore().InvalidateCacheRange(addr, size); + for (size_t i = 0; i < system.GetNumCores(); i++) { + system.GetCore(i).InvalidateCacheRange(addr, size); + } return ResultSuccess; } Result SVC::InvalidateEntireInstructionCache() { - system.GetRunningCore().ClearInstructionCache(); + for (size_t i = 0; i < system.GetNumCores(); i++) { + system.GetCore(i).ClearInstructionCache(); + } return ResultSuccess; } @@ -2218,67 +2226,69 @@ Result SVC::ControlProcess(Handle process_handle, u32 process_OP, u32 varg2, u32 // SVCs that couldn't be profiled. const std::array SVC::SVC_Table{{ {0x00, nullptr, "Unknown", 0}, - {0x01, &SVC::Wrap<&SVC::ControlMemory>, "ControlMemory", 1000}, - {0x02, &SVC::Wrap<&SVC::QueryMemory>, "QueryMemory", 1000}, + {0x01, &SVC::Wrap<&SVC::ControlMemory, 0x01>, "ControlMemory", 1000}, + {0x02, &SVC::Wrap<&SVC::QueryMemory, 0x02>, "QueryMemory", 1000}, {0x03, &SVC::ExitProcess, "ExitProcess", 1000}, {0x04, nullptr, "GetProcessAffinityMask", 1000}, {0x05, nullptr, "SetProcessAffinityMask", 1000}, {0x06, nullptr, "GetProcessIdealProcessor", 1000}, {0x07, nullptr, "SetProcessIdealProcessor", 1000}, - {0x08, &SVC::Wrap<&SVC::CreateThread>, "CreateThread", 5214}, + {0x08, &SVC::Wrap<&SVC::CreateThread, 0x08>, "CreateThread", 5214}, {0x09, &SVC::ExitThread, "ExitThread", 1000}, - {0x0A, &SVC::Wrap<&SVC::SleepThread>, "SleepThread", 946}, - {0x0B, &SVC::Wrap<&SVC::GetThreadPriority>, "GetThreadPriority", 616}, - {0x0C, &SVC::Wrap<&SVC::SetThreadPriority>, "SetThreadPriority", 1812}, + {0x0A, &SVC::Wrap<&SVC::SleepThread, 0x0A>, "SleepThread", 946}, + {0x0B, &SVC::Wrap<&SVC::GetThreadPriority, 0x0B>, "GetThreadPriority", 616}, + {0x0C, &SVC::Wrap<&SVC::SetThreadPriority, 0x0C>, "SetThreadPriority", 1812}, {0x0D, nullptr, "GetThreadAffinityMask", 1000}, {0x0E, nullptr, "SetThreadAffinityMask", 1000}, {0x0F, nullptr, "GetThreadIdealProcessor", 1000}, {0x10, nullptr, "SetThreadIdealProcessor", 1000}, {0x11, nullptr, "GetCurrentProcessorNumber", 1000}, {0x12, nullptr, "Run", 1000}, - {0x13, &SVC::Wrap<&SVC::CreateMutex>, "CreateMutex", 1000}, - {0x14, &SVC::Wrap<&SVC::ReleaseMutex>, "ReleaseMutex", 1324}, - {0x15, &SVC::Wrap<&SVC::CreateSemaphore>, "CreateSemaphore", 1000}, - {0x16, &SVC::Wrap<&SVC::ReleaseSemaphore>, "ReleaseSemaphore", 2713}, - {0x17, &SVC::Wrap<&SVC::CreateEvent>, "CreateEvent", 4329}, - {0x18, &SVC::Wrap<&SVC::SignalEvent>, "SignalEvent", 3285}, - {0x19, &SVC::Wrap<&SVC::ClearEvent>, "ClearEvent", 1389}, - {0x1A, &SVC::Wrap<&SVC::CreateTimer>, "CreateTimer", 1000}, - {0x1B, &SVC::Wrap<&SVC::SetTimer>, "SetTimer", 5163}, - {0x1C, &SVC::Wrap<&SVC::CancelTimer>, "CancelTimer", 1000}, - {0x1D, &SVC::Wrap<&SVC::ClearTimer>, "ClearTimer", 1000}, - {0x1E, &SVC::Wrap<&SVC::CreateMemoryBlock>, "CreateMemoryBlock", 1000}, - {0x1F, &SVC::Wrap<&SVC::MapMemoryBlock>, "MapMemoryBlock", 1000}, - {0x20, &SVC::Wrap<&SVC::UnmapMemoryBlock>, "UnmapMemoryBlock", 1000}, - {0x21, &SVC::Wrap<&SVC::CreateAddressArbiter>, "CreateAddressArbiter", 1000}, - {0x22, &SVC::Wrap<&SVC::ArbitrateAddress>, "ArbitrateAddress", 5664}, - {0x23, &SVC::Wrap<&SVC::CloseHandle>, "CloseHandle", 2937}, - {0x24, &SVC::Wrap<&SVC::WaitSynchronization1>, "WaitSynchronization1", 4005}, - {0x25, &SVC::Wrap<&SVC::WaitSynchronizationN>, "WaitSynchronizationN", 6918}, + {0x13, &SVC::Wrap<&SVC::CreateMutex, 0x13>, "CreateMutex", 1000}, + {0x14, &SVC::Wrap<&SVC::ReleaseMutex, 0x14>, "ReleaseMutex", 1324}, + {0x15, &SVC::Wrap<&SVC::CreateSemaphore, 0x15>, "CreateSemaphore", 1000}, + {0x16, &SVC::Wrap<&SVC::ReleaseSemaphore, 0x16>, "ReleaseSemaphore", 2713}, + {0x17, &SVC::Wrap<&SVC::CreateEvent, 0x17>, "CreateEvent", 4329}, + {0x18, &SVC::Wrap<&SVC::SignalEvent, 0x18>, "SignalEvent", 3285}, + {0x19, &SVC::Wrap<&SVC::ClearEvent, 0x19>, "ClearEvent", 1389}, + {0x1A, &SVC::Wrap<&SVC::CreateTimer, 0x1A>, "CreateTimer", 1000}, + {0x1B, &SVC::Wrap<&SVC::SetTimer, 0x1B>, "SetTimer", 5163}, + {0x1C, &SVC::Wrap<&SVC::CancelTimer, 0x1C>, "CancelTimer", 1000}, + {0x1D, &SVC::Wrap<&SVC::ClearTimer, 0x1D>, "ClearTimer", 1000}, + {0x1E, &SVC::Wrap<&SVC::CreateMemoryBlock, 0x1E>, "CreateMemoryBlock", 1000}, + {0x1F, &SVC::Wrap<&SVC::MapMemoryBlock, 0x1F>, "MapMemoryBlock", 1000}, + {0x20, &SVC::Wrap<&SVC::UnmapMemoryBlock, 0x20>, "UnmapMemoryBlock", 1000}, + {0x21, &SVC::Wrap<&SVC::CreateAddressArbiter, 0x21>, "CreateAddressArbiter", 1000}, + {0x22, &SVC::Wrap<&SVC::ArbitrateAddress, 0x22>, "ArbitrateAddress", 5664}, + {0x23, &SVC::Wrap<&SVC::CloseHandle, 0x23>, "CloseHandle", 2937}, + {0x24, &SVC::Wrap<&SVC::WaitSynchronization1, 0x24>, "WaitSynchronization1", 4005}, + {0x25, &SVC::Wrap<&SVC::WaitSynchronizationN, 0x25>, "WaitSynchronizationN", 6918}, {0x26, nullptr, "SignalAndWait", 1000}, - {0x27, &SVC::Wrap<&SVC::DuplicateHandle>, "DuplicateHandle", 1000}, - {0x28, &SVC::Wrap<&SVC::GetSystemTick>, "GetSystemTick", 340}, - {0x29, &SVC::Wrap<&SVC::GetHandleInfo>, "GetHandleInfo", 1000}, - {0x2A, &SVC::Wrap<&SVC::GetSystemInfo>, "GetSystemInfo", 1000}, - {0x2B, &SVC::Wrap<&SVC::GetProcessInfo>, "GetProcessInfo", 1510}, - {0x2C, &SVC::Wrap<&SVC::GetThreadInfo>, "GetThreadInfo", 1000}, - {0x2D, &SVC::Wrap<&SVC::ConnectToPort>, "ConnectToPort", 1000}, + {0x27, &SVC::Wrap<&SVC::DuplicateHandle, 0x27>, "DuplicateHandle", 1000}, + {0x28, &SVC::Wrap<&SVC::GetSystemTick, 0x28>, "GetSystemTick", 340}, + {0x29, &SVC::Wrap<&SVC::GetHandleInfo, 0x29>, "GetHandleInfo", 1000}, + {0x2A, &SVC::Wrap<&SVC::GetSystemInfo, 0x2A>, "GetSystemInfo", 1000}, + {0x2B, &SVC::Wrap<&SVC::GetProcessInfo, 0x2B>, "GetProcessInfo", 1510}, + {0x2C, &SVC::Wrap<&SVC::GetThreadInfo, 0x2C>, "GetThreadInfo", 1000}, + {0x2D, &SVC::Wrap<&SVC::ConnectToPort, 0x2D>, "ConnectToPort", 1000}, {0x2E, nullptr, "SendSyncRequest1", 1000}, {0x2F, nullptr, "SendSyncRequest2", 1000}, {0x30, nullptr, "SendSyncRequest3", 1000}, {0x31, nullptr, "SendSyncRequest4", 1000}, - {0x32, &SVC::Wrap<&SVC::SendSyncRequest>, "SendSyncRequest", 5825}, - {0x33, &SVC::Wrap<&SVC::OpenProcess>, "OpenProcess", 1000}, - {0x34, &SVC::Wrap<&SVC::OpenThread>, "OpenThread", 1000}, - {0x35, &SVC::Wrap<&SVC::GetProcessId>, "GetProcessId", 1000}, - {0x36, &SVC::Wrap<&SVC::GetProcessIdOfThread>, "GetProcessIdOfThread", 1000}, - {0x37, &SVC::Wrap<&SVC::GetThreadId>, "GetThreadId", 677}, - {0x38, &SVC::Wrap<&SVC::GetResourceLimit>, "GetResourceLimit", 1000}, - {0x39, &SVC::Wrap<&SVC::GetResourceLimitLimitValues>, "GetResourceLimitLimitValues", 1000}, - {0x3A, &SVC::Wrap<&SVC::GetResourceLimitCurrentValues>, "GetResourceLimitCurrentValues", 1000}, + {0x32, &SVC::Wrap<&SVC::SendSyncRequest, 0x32>, "SendSyncRequest", 5825}, + {0x33, &SVC::Wrap<&SVC::OpenProcess, 0x33>, "OpenProcess", 1000}, + {0x34, &SVC::Wrap<&SVC::OpenThread, 0x34>, "OpenThread", 1000}, + {0x35, &SVC::Wrap<&SVC::GetProcessId, 0x35>, "GetProcessId", 1000}, + {0x36, &SVC::Wrap<&SVC::GetProcessIdOfThread, 0x36>, "GetProcessIdOfThread", 1000}, + {0x37, &SVC::Wrap<&SVC::GetThreadId, 0x37>, "GetThreadId", 677}, + {0x38, &SVC::Wrap<&SVC::GetResourceLimit, 0x38>, "GetResourceLimit", 1000}, + {0x39, &SVC::Wrap<&SVC::GetResourceLimitLimitValues, 0x39>, "GetResourceLimitLimitValues", + 1000}, + {0x3A, &SVC::Wrap<&SVC::GetResourceLimitCurrentValues, 0x3A>, "GetResourceLimitCurrentValues", + 1000}, {0x3B, nullptr, "GetThreadContext", 1000}, - {0x3C, &SVC::Wrap<&SVC::Break>, "Break", 1000}, - {0x3D, &SVC::Wrap<&SVC::OutputDebugString>, "OutputDebugString", 1000}, + {0x3C, &SVC::Wrap<&SVC::Break, 0x3C>, "Break", 1000}, + {0x3D, &SVC::Wrap<&SVC::OutputDebugString, 0x3D>, "OutputDebugString", 1000}, {0x3E, nullptr, "ControlPerformanceCounter", 1000}, {0x3F, nullptr, "Unknown", 1000}, {0x40, nullptr, "Unknown", 1000}, @@ -2288,20 +2298,20 @@ const std::array SVC::SVC_Table{{ {0x44, nullptr, "Unknown", 1000}, {0x45, nullptr, "Unknown", 1000}, {0x46, nullptr, "Unknown", 1000}, - {0x47, &SVC::Wrap<&SVC::CreatePort>, "CreatePort", 1000}, - {0x48, &SVC::Wrap<&SVC::CreateSessionToPort>, "CreateSessionToPort", 5122}, - {0x49, &SVC::Wrap<&SVC::CreateSession>, "CreateSession", 3492}, - {0x4A, &SVC::Wrap<&SVC::AcceptSession>, "AcceptSession", 1842}, + {0x47, &SVC::Wrap<&SVC::CreatePort, 0x47>, "CreatePort", 1000}, + {0x48, &SVC::Wrap<&SVC::CreateSessionToPort, 0x48>, "CreateSessionToPort", 5122}, + {0x49, &SVC::Wrap<&SVC::CreateSession, 0x49>, "CreateSession", 3492}, + {0x4A, &SVC::Wrap<&SVC::AcceptSession, 0x4A>, "AcceptSession", 1842}, {0x4B, nullptr, "ReplyAndReceive1", 1000}, {0x4C, nullptr, "ReplyAndReceive2", 1000}, {0x4D, nullptr, "ReplyAndReceive3", 1000}, {0x4E, nullptr, "ReplyAndReceive4", 1000}, - {0x4F, &SVC::Wrap<&SVC::ReplyAndReceive>, "ReplyAndReceive", 8762}, + {0x4F, &SVC::Wrap<&SVC::ReplyAndReceive, 0x4F>, "ReplyAndReceive", 8762}, {0x50, nullptr, "BindInterrupt", 1000}, {0x51, nullptr, "UnbindInterrupt", 1000}, - {0x52, &SVC::Wrap<&SVC::InvalidateProcessDataCache>, "InvalidateProcessDataCache", 9609}, - {0x53, &SVC::Wrap<&SVC::StoreProcessDataCache>, "StoreProcessDataCache", 7174}, - {0x54, &SVC::Wrap<&SVC::FlushProcessDataCache>, "FlushProcessDataCache", 9084}, + {0x52, &SVC::Wrap<&SVC::InvalidateProcessDataCache, 0x52>, "InvalidateProcessDataCache", 9609}, + {0x53, &SVC::Wrap<&SVC::StoreProcessDataCache, 0x53>, "StoreProcessDataCache", 7174}, + {0x54, &SVC::Wrap<&SVC::FlushProcessDataCache, 0x54>, "FlushProcessDataCache", 9084}, {0x55, nullptr, "StartInterProcessDma", 9146}, {0x56, nullptr, "StopDma", 1163}, {0x57, nullptr, "GetDmaState", 2222}, @@ -2318,7 +2328,7 @@ const std::array SVC::SVC_Table{{ {0x62, nullptr, "TerminateDebugProcess", 1000}, {0x63, nullptr, "GetProcessDebugEvent", 1000}, {0x64, nullptr, "ContinueDebugEvent", 1000}, - {0x65, &SVC::Wrap<&SVC::GetProcessList>, "GetProcessList", 1000}, + {0x65, &SVC::Wrap<&SVC::GetProcessList, 0x65>, "GetProcessList", 1000}, {0x66, nullptr, "GetThreadList", 1000}, {0x67, nullptr, "GetDebugThreadContext", 1000}, {0x68, nullptr, "SetDebugThreadContext", 1000}, @@ -2335,14 +2345,15 @@ const std::array SVC::SVC_Table{{ {0x73, nullptr, "CreateCodeSet", 1000}, {0x74, nullptr, "RandomStub", 1000}, {0x75, nullptr, "CreateProcess", 1000}, - {0x76, &SVC::Wrap<&SVC::TerminateProcess>, "TerminateProcess", 1000}, + {0x76, &SVC::Wrap<&SVC::TerminateProcess, 0x76>, "TerminateProcess", 1000}, {0x77, nullptr, "SetProcessResourceLimits", 1000}, {0x78, nullptr, "CreateResourceLimit", 1000}, - {0x79, &SVC::Wrap<&SVC::SetResourceLimitLimitValues>, "SetResourceLimitLimitValues", 1000}, + {0x79, &SVC::Wrap<&SVC::SetResourceLimitLimitValues, 0x79>, "SetResourceLimitLimitValues", + 1000}, {0x7A, nullptr, "AddCodeSegment", 1000}, {0x7B, nullptr, "Backdoor", 1000}, - {0x7C, &SVC::Wrap<&SVC::KernelSetState>, "KernelSetState", 1000}, - {0x7D, &SVC::Wrap<&SVC::QueryProcessMemory>, "QueryProcessMemory", 1000}, + {0x7C, &SVC::Wrap<&SVC::KernelSetState, 0x7C>, "KernelSetState", 1000}, + {0x7D, &SVC::Wrap<&SVC::QueryProcessMemory, 0x7D>, "QueryProcessMemory", 1000}, // Custom SVCs {0x7E, nullptr, "Unused", 1000}, {0x7F, nullptr, "Unused", 1000}, @@ -2362,13 +2373,13 @@ const std::array SVC::SVC_Table{{ {0x8D, nullptr, "Unused", 1000}, {0x8E, nullptr, "Unused", 1000}, {0x8F, nullptr, "Unused", 1000}, - {0x90, &SVC::Wrap<&SVC::ConvertVaToPa>, "ConvertVaToPa", 1000}, + {0x90, &SVC::Wrap<&SVC::ConvertVaToPa, 0x90>, "ConvertVaToPa", 1000}, {0x91, nullptr, "FlushDataCacheRange", 1000}, {0x92, nullptr, "FlushEntireDataCache", 1000}, - {0x93, &SVC::Wrap<&SVC::InvalidateInstructionCacheRange>, "InvalidateInstructionCacheRange", - 1000}, - {0x94, &SVC::Wrap<&SVC::InvalidateEntireInstructionCache>, "InvalidateEntireInstructionCache", - 1000}, + {0x93, &SVC::Wrap<&SVC::InvalidateInstructionCacheRange, 0x93>, + "InvalidateInstructionCacheRange", 1000}, + {0x94, &SVC::Wrap<&SVC::InvalidateEntireInstructionCache, 0x94>, + "InvalidateEntireInstructionCache", 1000}, {0x95, nullptr, "Unused", 1000}, {0x96, nullptr, "Unused", 1000}, {0x97, nullptr, "Unused", 1000}, @@ -2380,8 +2391,8 @@ const std::array SVC::SVC_Table{{ {0x9D, nullptr, "Unused", 1000}, {0x9E, nullptr, "Unused", 1000}, {0x9F, nullptr, "Unused", 1000}, - {0xA0, &SVC::Wrap<&SVC::MapProcessMemoryEx>, "MapProcessMemoryEx", 1000}, - {0xA1, &SVC::Wrap<&SVC::UnmapProcessMemoryEx>, "UnmapProcessMemoryEx", 1000}, + {0xA0, &SVC::Wrap<&SVC::MapProcessMemoryEx, 0xA0>, "MapProcessMemoryEx", 1000}, + {0xA1, &SVC::Wrap<&SVC::UnmapProcessMemoryEx, 0xA1>, "UnmapProcessMemoryEx", 1000}, {0xA2, nullptr, "ControlMemoryEx", 1000}, {0xA3, nullptr, "ControlMemoryUnsafe", 1000}, {0xA4, nullptr, "Unused", 1000}, @@ -2399,7 +2410,7 @@ const std::array SVC::SVC_Table{{ {0xB0, nullptr, "ControlService", 1000}, {0xB1, nullptr, "CopyHandle", 1000}, {0xB2, nullptr, "TranslateHandle", 1000}, - {0xB3, &SVC::Wrap<&SVC::ControlProcess>, "ControlProcess", 1000}, + {0xB3, &SVC::Wrap<&SVC::ControlProcess, 0xB3>, "ControlProcess", 1000}, }}; const SVC::FunctionDef* SVC::GetSVCInfo(u32 func_num) { diff --git a/src/core/hle/kernel/svc_wrapper.h b/src/core/hle/kernel/svc_wrapper.h index 50c7adb46..783a29cbe 100644 --- a/src/core/hle/kernel/svc_wrapper.h +++ b/src/core/hle/kernel/svc_wrapper.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -21,9 +21,9 @@ namespace Kernel { template class SVCWrapper { protected: - template + template void Wrap() { - WrapHelper::Call(*static_cast(this), F); + WrapHelper::Call(*static_cast(this), F, ID); }; private: @@ -249,18 +249,18 @@ private: // T is the current param to do I/O. // Ts are params whose I/O is not handled yet. template - static void Call(Context& context, SVCT svc, Us... u) { + static void Call(Context& context, SVCT svc, int id, Us... u) { static_assert(std::is_same_v); constexpr std::size_t current_param_index = sizeof...(Us); if constexpr (std::is_pointer_v) { using OutputT = std::remove_pointer_t; OutputT output; - WrapPass::Call(context, svc, u..., &output); + WrapPass::Call(context, svc, id, u..., &output); SetParam(context, output); } else { T input; GetParam(context, input); - WrapPass::Call(context, svc, u..., input); + WrapPass::Call(context, svc, id, u..., input); } } }; @@ -269,7 +269,7 @@ private: struct WrapPass { // Call function R(Context::svc)(Us...) and transfer the return value to registers template - static void Call(Context& context, SVCT svc, Us... u) { + static void Call(Context& context, SVCT svc, [[maybe_unused]] int id, Us... u) { static_assert(std::is_same_v); if constexpr (std::is_void_v) { (context.*svc)(u...); @@ -284,16 +284,18 @@ private: struct WrapPass { // Call function R(Context::svc)(Us...) and transfer the return value to registers template - static void Call(Context& context, SVCT svc, Us... u) { + static void Call(Context& context, SVCT svc, int id, Us... u) { static_assert(std::is_same_v); if constexpr (std::is_void_v) { (context.*svc)(u...); } else { Result r = (context.*svc)(u...); if (r.IsError()) { - LOG_ERROR(Kernel_SVC, "level={} summary={} module={} description={}", - r.level.ExtractValue(r.raw), r.summary.ExtractValue(r.raw), - r.module.ExtractValue(r.raw), r.description.ExtractValue(r.raw)); + LOG_ERROR( + Kernel_SVC, + "svc=0x{:02X} raw=0x{:08X} level={} summary={} module={} description={}", + id, r.raw, r.level.ExtractValue(r.raw), r.summary.ExtractValue(r.raw), + r.module.ExtractValue(r.raw), r.description.ExtractValue(r.raw)); } SetParam(context, r); } @@ -305,8 +307,8 @@ private: template struct WrapHelper { - static void Call(Context& context, R (Context::*svc)(T...)) { - WrapPass::Call(context, svc /*Empty for Us*/); + static void Call(Context& context, R (Context::*svc)(T...), int id) { + WrapPass::Call(context, svc, id /*Empty for Us*/); } }; }; diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index 7c491309d..5f982c9e6 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -17,6 +17,9 @@ #include "core/arm/arm_interface.h" #include "core/arm/skyeye_common/armstate.h" #include "core/core.h" +#ifdef ENABLE_GDBSTUB +#include "core/gdbstub/gdbstub.h" +#endif #include "core/hle/kernel/errors.h" #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/mutex.h" @@ -37,6 +40,9 @@ void ThreadManager::serialize(Archive& ar, const unsigned int) { ar & ready_queue; ar & wakeup_callback_table; ar & thread_list; + ar & current_schedule_mode; + ar & single_time_limiter; + ar & multi_time_limiter; } SERIALIZE_IMPL(ThreadManager) @@ -56,6 +62,7 @@ void Thread::serialize(Archive& ar, const unsigned int file_version) { ar & held_mutexes; ar & pending_mutexes; ar & owner_process; + ar & resource_limit_category; ar & wait_objects; ar & wait_address; ar & name; @@ -73,6 +80,7 @@ void Thread::serialize(Archive& ar, const unsigned int file_version) { } } ar & wakeup_callback; + ar & debug_break; } SERIALIZE_IMPL(Thread) @@ -129,6 +137,10 @@ void Thread::Stop() { process->tls_slots[tls_page].reset(tls_slot); process->resource_limit->Release(ResourceLimitType::Thread, 1); } + +#ifdef ENABLE_GDBSTUB + GDBStub::OnThreadExit(thread_id); +#endif } void ThreadManager::SwitchContext(Thread* new_thread) { @@ -179,33 +191,49 @@ void ThreadManager::SwitchContext(Thread* new_thread) { } Thread* ThreadManager::PopNextReadyThread() { - Thread* next = nullptr; + Thread* next; Thread* thread = GetCurrentThread(); - if (thread && thread->status == ThreadStatus::Running) { - do { - // We have to do better than the current thread. - // This call returns null when that's not possible. - next = ready_queue.pop_first_better(thread->current_priority); - if (!next) { - // Otherwise just keep going with the current thread - next = thread; - break; - } else if (!next->can_schedule) - unscheduled_ready_queue.push_back(next); - } while (!next->can_schedule); - } else { - do { - next = ready_queue.pop_first(); - if (next && !next->can_schedule) - unscheduled_ready_queue.push_back(next); - } while (next && !next->can_schedule); - } + while (true) { + std::vector> skipped; + u32 next_priority{}; + next = nullptr; - while (!unscheduled_ready_queue.empty()) { - auto t = std::move(unscheduled_ready_queue.back()); - ready_queue.push_back(t->current_priority, t); - unscheduled_ready_queue.pop_back(); + if (thread && thread->status == ThreadStatus::Running && thread->CanSchedule()) { + do { + // We have to do better than the current thread. + // This call returns null when that's not possible. + std::tie(next_priority, next) = + ready_queue.pop_first_better(thread->current_priority); + if (!next) { + // Otherwise just keep going with the current thread + next = thread; + break; + } else if (!next->CanSchedule()) { + skipped.push_back({next_priority, next}); + } + + } while (!next->CanSchedule()); + } else { + do { + std::tie(next_priority, next) = ready_queue.pop_first(); + if (next && !next->CanSchedule()) { + skipped.push_back({next_priority, next}); + } + } while (next && !next->CanSchedule()); + } + + for (auto it = skipped.rbegin(); it != skipped.rend(); it++) { + ready_queue.push_front(it->first, it->second); + } + + // Try to time limit the selected thread on core 1 + if (core_id == 1 && next && GetCpuLimiter()->DoTimeLimit(next)) { + // If the thread is time limited, select the next one + continue; + } + + break; } return next; @@ -392,6 +420,7 @@ ResultVal> KernelSystem::CreateThread( thread->name = std::move(name); thread_managers[processor_id]->wakeup_callback_table[thread->thread_id] = thread.get(); thread->owner_process = owner_process; + thread->resource_limit_category = owner_process->resource_limit->GetCategory(); CASCADE_RESULT(thread->tls_address, owner_process->AllocateThreadLocalStorage()); // TODO(peachum): move to ScheduleThread() when scheduler is added so selected core is used @@ -516,10 +545,166 @@ VAddr Thread::GetCommandBufferAddress() const { return GetTLSAddress() + command_header_offset; } -ThreadManager::ThreadManager(Kernel::KernelSystem& kernel, u32 core_id) : kernel(kernel) { +bool Thread::SetDebugBreak(bool _debug_break) { + if (debug_break == _debug_break) { + return false; + } + debug_break = _debug_break; + return true; +} + +CpuLimiter::~CpuLimiter() {} + +CpuLimiterMulti::CpuLimiterMulti(Kernel::KernelSystem& _kernel) : kernel(_kernel) {} + +void CpuLimiterMulti::Initialize(bool is_single) { + // TODO(PabloMK7): The is_single variable is needed to prevent + // registering an event twice with the same name. Once CpuLimiterSingle + // is implemented we can remove it. + tick_event = kernel.timing.RegisterEvent( + fmt::format("Kernel::{}::tick_event", is_single ? "CpuLimiterSingle" : "CpuLimiterMulti"), + [this](u64, s64 cycles_late) { this->OnTick(cycles_late); }); +} + +void CpuLimiterMulti::Start() { + if (ready) { + return; + } + ready = true; + active = false; + curr_state = SchedState::APP; // So that ChangeState starts with SYS + app_cpu_time = Core1CpuTime::PREEMPTION_DISABLED; +} + +void CpuLimiterMulti::End() { + if (!ready) { + return; + } + ready = false; + active = false; + kernel.timing.UnscheduleEvent(tick_event, 0); + WakeupSleepingThreads(); +} + +void CpuLimiterMulti::UpdateAppCpuLimit() { + if (!ready) { + return; + } + + app_cpu_time = static_cast(kernel.ResourceLimit() + .GetForCategory(Kernel::ResourceLimitCategory::Application) + ->GetCurrentValue(Kernel::ResourceLimitType::CpuTime)); + if (app_cpu_time == Core1CpuTime::PREEMPTION_DISABLED) { + // No preemption, disable event + active = false; + kernel.timing.UnscheduleEvent(tick_event, 0); + WakeupSleepingThreads(); + } else { + // Start preempting, enable event + if (active) { + // If we were active already, unschedule first + // so that the event is not scheduled twice. + // We could just not call ChangeState instead, + // but this allows adjusting the timing of the + // event sooner. + kernel.timing.UnscheduleEvent(tick_event, 0); + } + active = true; + ChangeState(0); + } +} + +bool CpuLimiterMulti::DoTimeLimit(Thread* thread) { + if (!ready || !active) { + // Preemption is not active, don't do anything. + return false; + } + if (kernel.ResourceLimit() + .GetForCategory(thread->resource_limit_category) + ->GetLimitValue(ResourceLimitType::CpuTime) == Core1CpuTime::PREEMPTION_EXCEMPTED) { + // The thread is excempted from preemption + return false; + } + + // On real hardware, the kernel uses a KPreemptionTimer to determine if a + // thread needs to be time limited. This properly uses the resource limit + // value to check if it is a sysmodule or not. We can do this instead and + // it should be good enough. TODO(PabloMK7): fix? + if (thread->resource_limit_category == ResourceLimitCategory::Application && + curr_state == SchedState::SYS || + thread->resource_limit_category == ResourceLimitCategory::Other && + curr_state == SchedState::APP) { + // Block thread as not in the current mode + thread->status = ThreadStatus::WaitSleep; + sleeping_thread_ids.push(thread->thread_id); + return true; + } + return false; +} + +void CpuLimiterMulti::OnTick(s64 cycles_late) { + WakeupSleepingThreads(); + ChangeState(cycles_late); +} + +void CpuLimiterMulti::ChangeState(s64 cycles_late) { + curr_state = (curr_state == SchedState::SYS) ? SchedState::APP : SchedState::SYS; + + s64 next_timer = base_tick_interval * (app_cpu_time / 100.f); + if (curr_state == SchedState::SYS) { + next_timer = base_tick_interval - next_timer; + } + if (next_timer > cycles_late) { + next_timer -= cycles_late; + } + kernel.timing.ScheduleEvent(next_timer, tick_event, 0, 1); +} + +void CpuLimiterMulti::WakeupSleepingThreads() { + while (!sleeping_thread_ids.empty()) { + u32 curr_id = sleeping_thread_ids.front(); + + auto thread = kernel.GetThreadManager(1).GetThreadByID(curr_id); + if (thread && thread->status == ThreadStatus::WaitSleep) { + thread->ResumeFromWait(); + } + + sleeping_thread_ids.pop(); + } +} + +template +void CpuLimiterMulti::serialize(Archive& ar, const unsigned int) { + ar & ready; + ar & active; + ar & app_cpu_time; + ar & curr_state; + std::vector v; + if (Archive::is_loading::value) { + ar & v; + for (auto it : v) { + sleeping_thread_ids.push(it); + } + } else { + std::queue temp = sleeping_thread_ids; + while (!temp.empty()) { + v.push_back(temp.front()); + temp.pop(); + } + ar & v; + } +} + +ThreadManager::ThreadManager(Kernel::KernelSystem& kernel, u32 core_id) + : kernel(kernel), core_id(core_id), current_schedule_mode(Core1ScheduleMode::Multi), + single_time_limiter(kernel), multi_time_limiter(kernel) { ThreadWakeupEventType = kernel.timing.RegisterEvent( "ThreadWakeupCallback_" + std::to_string(core_id), [this](u64 thread_id, s64 cycle_late) { ThreadWakeupCallback(thread_id, cycle_late); }); + if (core_id == 1) { + single_time_limiter.Initialize(true); + multi_time_limiter.Initialize(false); + } } ThreadManager::~ThreadManager() { @@ -532,13 +717,33 @@ std::span> ThreadManager::GetThreadList() const { return thread_list; } +std::shared_ptr ThreadManager::GetThreadByID(u32 thread_id) const { + for (auto& thread : thread_list) { + if (thread->thread_id == thread_id) { + return thread; + } + } + return nullptr; +} + +void ThreadManager::SetScheduleMode(Core1ScheduleMode mode) { + GetCpuLimiter()->End(); + current_schedule_mode = mode; + if (mode == Core1ScheduleMode::Single) { + LOG_WARNING(Kernel, "Unimplemented \"Single\" schedule mode."); + } + GetCpuLimiter()->Start(); +} + +void ThreadManager::UpdateAppCpuLimit() { + GetCpuLimiter()->UpdateAppCpuLimit(); +} + std::shared_ptr KernelSystem::GetThreadByID(u32 thread_id) const { for (u32 core_id = 0; core_id < Core::System::GetInstance().GetNumCores(); core_id++) { - const auto thread_list = GetThreadManager(core_id).GetThreadList(); - for (auto& thread : thread_list) { - if (thread->thread_id == thread_id) { - return thread; - } + auto ret = GetThreadManager(core_id).GetThreadByID(thread_id); + if (ret) { + return ret; } } return nullptr; diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h index 8853384f4..49891b31a 100644 --- a/src/core/hle/kernel/thread.h +++ b/src/core/hle/kernel/thread.h @@ -15,11 +15,13 @@ #include #include #include +#include #include "common/common_types.h" #include "common/thread_queue_list.h" #include "core/arm/arm_interface.h" #include "core/core_timing.h" #include "core/hle/kernel/object.h" +#include "core/hle/kernel/resource_limit.h" #include "core/hle/kernel/wait_object.h" #include "core/hle/result.h" @@ -49,7 +51,7 @@ enum class ThreadStatus { Running, ///< Currently running Ready, ///< Ready to run WaitArb, ///< Waiting on an address arbiter - WaitSleep, ///< Waiting due to a SleepThread SVC + WaitSleep, ///< Waiting due to a SleepThread SVC or time limited WaitIPC, ///< Waiting for the reply from an IPC request WaitSynchAny, ///< Waiting due to WaitSynch1 or WaitSynchN with wait_all = false WaitSynchAll, ///< Waiting due to WaitSynchronizationN with wait_all = true @@ -81,6 +83,64 @@ private: friend class boost::serialization::access; }; +class CpuLimiter { +public: + CpuLimiter() = default; + virtual ~CpuLimiter() = 0; + + virtual void Start() = 0; + virtual void End() = 0; + + virtual void UpdateAppCpuLimit() = 0; + + virtual bool DoTimeLimit(Thread* thread) = 0; +}; + +class CpuLimiterMulti : public CpuLimiter { +public: + CpuLimiterMulti(Kernel::KernelSystem& kernel); + ~CpuLimiterMulti() override = default; + + void Initialize(bool is_single); + + void Start() override; + void End() override; + + void UpdateAppCpuLimit() override; + + bool DoTimeLimit(Thread* thread) override; + +private: + enum class SchedState : u32 { + APP, + SYS, + }; + + void OnTick(s64 cycles_late); + + void ChangeState(s64 cycles_late); + + void WakeupSleepingThreads(); + + static constexpr u64 base_tick_interval = nsToCycles(2'000'000); // 2ms + + Kernel::KernelSystem& kernel; + Core::TimingEventType* tick_event{}; + + bool ready = false; + bool active = false; + Core1CpuTime app_cpu_time = Core1CpuTime::PREEMPTION_DISABLED; + SchedState curr_state{}; + std::queue sleeping_thread_ids; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int); +}; + +// TODO(PabloMK7): Replace with proper implementation +using CpuLimiterSingle = CpuLimiterMulti; + class ThreadManager { public: explicit ThreadManager(Kernel::KernelSystem& kernel, u32 core_id); @@ -126,10 +186,21 @@ public: */ std::span> GetThreadList() const; + std::shared_ptr GetThreadByID(u32 thread_id) const; + void SetCPU(Core::ARM_Interface& cpu_) { cpu = &cpu_; } + void SetScheduleMode(Core1ScheduleMode mode); + + void UpdateAppCpuLimit(); + + CpuLimiter* GetCpuLimiter() { + return (current_schedule_mode == Core1ScheduleMode::Single) ? &single_time_limiter + : &multi_time_limiter; + } + private: /** * Switches the CPU's active thread context to that of the specified thread @@ -151,11 +222,11 @@ private: void ThreadWakeupCallback(u64 thread_id, s64 cycles_late); Kernel::KernelSystem& kernel; - Core::ARM_Interface* cpu; + u32 core_id; + Core::ARM_Interface* cpu{}; std::shared_ptr current_thread; Common::ThreadQueueList ready_queue; - std::deque unscheduled_ready_queue; std::unordered_map wakeup_callback_table; /// Event type for the thread wake up event @@ -164,6 +235,10 @@ private: // Lists all threadsthat aren't deleted. std::vector> thread_list; + Core1ScheduleMode current_schedule_mode{}; + CpuLimiterSingle single_time_limiter; + CpuLimiterMulti multi_time_limiter; + friend class Thread; friend class KernelSystem; @@ -290,6 +365,15 @@ public: return status == ThreadStatus::WaitSynchAll; } + bool CanSchedule() { + // TODO(PabloMK7): This may not be the proper way + // threads are marked as non-schedulable when they + // are in debug break. Figure out and fix. + return can_schedule && !debug_break; + } + + bool SetDebugBreak(bool debug_break); + Core::ARM_Interface::ThreadContext context{}; u32 thread_id; @@ -316,6 +400,8 @@ public: std::weak_ptr owner_process{}; ///< Process that owns this thread + ResourceLimitCategory resource_limit_category{}; + /// Objects that the thread is waiting on, in the same order as they were /// passed to WaitSynchronization1/N. std::vector> wait_objects{}; @@ -333,6 +419,7 @@ public: private: ThreadManager& thread_manager; + bool debug_break{}; friend class boost::serialization::access; template diff --git a/src/core/hle/service/apt/apt.cpp b/src/core/hle/service/apt/apt.cpp index 6d0754c6c..355f98bab 100644 --- a/src/core/hle/service/apt/apt.cpp +++ b/src/core/hle/service/apt/apt.cpp @@ -29,6 +29,7 @@ #include "core/hle/service/cfg/cfg.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/fs/fs_user.h" +#include "core/hle/service/pm/pm_app.h" #include "core/hle/service/ptm/ptm.h" #include "core/hle/service/service.h" #include "core/hw/aes/ccm.h" @@ -47,7 +48,6 @@ void Module::serialize(Archive& ar, const unsigned int file_version) { ar & shared_font_mem; ar & shared_font_loaded; ar & shared_font_relocated; - ar & cpu_percent; ar & screen_capture_post_permission; ar & applet_manager; ar & wireless_reboot_info; @@ -731,29 +731,39 @@ void Module::APTInterface::SetAppCpuTimeLimit(Kernel::HLERequestContext& ctx) { const auto must_be_one = rp.Pop(); const auto value = rp.Pop(); - LOG_WARNING(Service_APT, "(STUBBED) called, must_be_one={}, value={}", must_be_one, value); if (must_be_one != 1) { LOG_ERROR(Service_APT, "This value should be one, but is actually {}!", must_be_one); } - apt->cpu_percent = value; - + auto pm_app = Service::PM::GetServiceAPP(apt->system); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); // No error + if (pm_app) { + rb.Push(pm_app->UpdateResourceLimit(Kernel::ResourceLimitType::CpuTime, value)); + } else { + LOG_ERROR(Service_APT, "Failed to get PM:APP module"); + rb.Push(ResultUnknown); + } } void Module::APTInterface::GetAppCpuTimeLimit(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto must_be_one = rp.Pop(); - LOG_WARNING(Service_APT, "(STUBBED) called, must_be_one={}", must_be_one); if (must_be_one != 1) { LOG_ERROR(Service_APT, "This value should be one, but is actually {}!", must_be_one); } + auto pm_app = Service::PM::GetServiceAPP(apt->system); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); - rb.Push(ResultSuccess); // No error - rb.Push(apt->cpu_percent); + if (pm_app) { + auto res = pm_app->GetResourceLimit(Kernel::ResourceLimitType::CpuTime); + rb.Push(res.Code()); + rb.Push(res.ValueOr(u32{})); + } else { + LOG_ERROR(Service_APT, "Failed to get PM:APP module"); + rb.Push(ResultUnknown); + rb.Push(u32{}); + } } void Module::APTInterface::PrepareToStartLibraryApplet(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/apt/apt.h b/src/core/hle/service/apt/apt.h index 85b92bb60..3eaf5fa65 100644 --- a/src/core/hle/service/apt/apt.h +++ b/src/core/hle/service/apt/apt.h @@ -1089,8 +1089,6 @@ private: bool shared_font_loaded = false; bool shared_font_relocated = false; - u32 cpu_percent = 0; ///< CPU time available to the running application - ScreencapPostPermission screen_capture_post_permission = ScreencapPostPermission::CleanThePermission; // TODO(JamePeng): verify the initial value diff --git a/src/core/hle/service/dsp/dsp_dsp.cpp b/src/core/hle/service/dsp/dsp_dsp.cpp index 9b4a9c27b..ae936ba94 100644 --- a/src/core/hle/service/dsp/dsp_dsp.cpp +++ b/src/core/hle/service/dsp/dsp_dsp.cpp @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -290,9 +290,8 @@ void DSP_DSP::GetHeadphoneStatus(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); - rb.Push(false); /// u8, 0 = not inserted, 1 = inserted - - LOG_DEBUG(Service_DSP, "called"); + rb.Push(Settings::values.simulate_headphones_plugged + .GetValue()); /// u8, 0 = not inserted, 1 = inserted } void DSP_DSP::ForceHeadphoneOut(Kernel::HLERequestContext& ctx) { diff --git a/src/core/hle/service/fs/directory.cpp b/src/core/hle/service/fs/directory.cpp index 0a862f097..13f011b5c 100644 --- a/src/core/hle/service/fs/directory.cpp +++ b/src/core/hle/service/fs/directory.cpp @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -42,27 +42,54 @@ Directory::~Directory() {} void Directory::Read(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - u32 count = rp.Pop(); - auto& buffer = rp.PopMappedBuffer(); - std::vector entries(count); - LOG_TRACE(Service_FS, "Read {}: count={}", GetName(), count); - // Number of entries actually read - u32 read = backend->Read(static_cast(entries.size()), entries.data()); - buffer.Write(entries.data(), 0, read * sizeof(FileSys::Entry)); - IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); - rb.Push(ResultSuccess); - rb.Push(read); - rb.PushMappedBuffer(buffer); + struct AsyncData { + // Input + u32 count; + + // Output + Result ret{0}; + u32 read; + Kernel::MappedBuffer* buffer; + }; + + auto async_data = std::make_shared(); + async_data->count = rp.Pop(); + async_data->buffer = &rp.PopMappedBuffer(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + std::vector entries(async_data->count); + LOG_TRACE(Service_FS, "Read {}: count={}", GetName(), count); + // Number of entries actually read + async_data->read = backend->Read(static_cast(entries.size()), entries.data()); + async_data->buffer->Write(entries.data(), 0, async_data->read * sizeof(FileSys::Entry)); + return 0; + }, + [async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 2, 2); + + rb.Push(ResultSuccess); + rb.Push(async_data->read); + rb.PushMappedBuffer(*async_data->buffer); + }, + Settings::values.async_fs_operations.GetValue()); } void Directory::Close(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); LOG_TRACE(Service_FS, "Close {}", GetName()); - backend->Close(); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + ctx.RunAsync( + [this](Kernel::HLERequestContext& ctx) { + backend->Close(); + return 0; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + Settings::values.async_fs_operations.GetValue()); } } // namespace Service::FS diff --git a/src/core/hle/service/fs/file.cpp b/src/core/hle/service/fs/file.cpp index 7ba13f935..6ba8f2b77 100644 --- a/src/core/hle/service/fs/file.cpp +++ b/src/core/hle/service/fs/file.cpp @@ -75,8 +75,13 @@ void File::Read(Kernel::HLERequestContext& ctx) { offset, length, backend->GetSize()); } + const bool allows_cache_reads = backend->AllowsCachedReads(); + // Conventional reading if the backend does not support cache. - if (!backend->AllowsCachedReads()) { + // Do not use asynchronous operations on file reads, as in most cases + // there are many of them with small sizes. This causes a lot of delay + // due to thread communication overhead. + if (!allows_cache_reads) { auto& buffer = rp.PopMappedBuffer(); IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); std::unique_ptr data = std::make_unique_for_overwrite(length); @@ -115,7 +120,8 @@ void File::Read(Kernel::HLERequestContext& ctx) { async_data->length = length; async_data->offset = offset; async_data->cache_ready = backend->CacheReady(offset, length); - if (!async_data->cache_ready) { + const bool really_async = !async_data->cache_ready; + if (really_async) { async_data->pre_timer = std::chrono::steady_clock::now(); } @@ -161,7 +167,7 @@ void File::Read(Kernel::HLERequestContext& ctx) { } rb.PushMappedBuffer(*async_data->buffer); }, - !async_data->cache_ready); + really_async); } void File::Write(Kernel::HLERequestContext& ctx) { @@ -186,6 +192,7 @@ void File::Write(Kernel::HLERequestContext& ctx) { } bool flush = (flags & 0xFF) != 0, update_timestamp = (flags & 0xFF00) != 0; + // Do not use asynchronous fs operations here for the same reason as File::Read. if (!backend->AllowsCachedReads()) { std::vector data(length); buffer.Read(data.data(), 0, data.size()); @@ -274,7 +281,7 @@ void File::SetSize(Kernel::HLERequestContext& ctx) { return; } - if (!backend->AllowsCachedReads()) { + if (!backend->AllowsCachedReads() && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); file->size = size; backend->SetSize(size); @@ -303,7 +310,7 @@ void File::Close(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_FS, "Closing File backend but {} clients still connected", connected_sessions.size()); - if (!backend->AllowsCachedReads()) { + if (!backend->AllowsCachedReads() && !Settings::values.async_fs_operations) { backend->Close(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); @@ -334,7 +341,7 @@ void File::Flush(Kernel::HLERequestContext& ctx) { return; } - if (!backend->AllowsCachedReads()) { + if (!backend->AllowsCachedReads() && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); backend->Flush(); rb.Push(ResultSuccess); diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index c9641a518..fdbdfbc58 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -67,7 +67,7 @@ void FS_USER::OpenFile(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "path={}, mode={} attrs={}", file_path.DebugStr(), mode.hex, attributes); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { const auto [file_res, open_timeout_ns] = archives.OpenFileFromArchive(archive_handle, file_path, mode, attributes); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); @@ -100,7 +100,8 @@ void FS_USER::OpenFile(Kernel::HLERequestContext& ctx) { async_data->attributes = attributes; async_data->pre_timer = std::chrono::steady_clock::now(); - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->file = archives.OpenFileFromArchive(async_data->archive_handle, async_data->file_path, @@ -151,7 +152,7 @@ void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) { u64 program_id = GetSessionData(ctx.Session())->program_id; - if (!archives.ArchiveIsSlow(archive_id)) { + if (!archives.ArchiveIsSlow(archive_id) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); ResultVal archive_handle = @@ -203,7 +204,8 @@ void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) { async_data->attributes = attributes; async_data->pre_timer = std::chrono::steady_clock::now(); - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->archive_handle = archives.OpenArchive( async_data->archive_id, async_data->archive_path, async_data->program_id); @@ -259,7 +261,7 @@ void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", filename_type, filename_size, file_path.DebugStr()); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.DeleteFileFromArchive(archive_handle, file_path)); return; @@ -275,7 +277,8 @@ void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) { async_data->archive_handle = archive_handle; async_data->file_path = file_path; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.DeleteFileFromArchive(async_data->archive_handle, async_data->file_path); @@ -312,7 +315,7 @@ void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) { dest_filename_size, dest_file_path.DebugStr()); if (!archives.ArchiveIsSlow(src_archive_handle) && - !archives.ArchiveIsSlow(dest_archive_handle)) { + !archives.ArchiveIsSlow(dest_archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.RenameFileBetweenArchives(src_archive_handle, src_file_path, dest_archive_handle, dest_file_path)); @@ -333,7 +336,8 @@ void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) { async_data->dest_archive_handle = dest_archive_handle; async_data->dest_file_path = dest_file_path; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.RenameFileBetweenArchives( async_data->src_archive_handle, async_data->src_file_path, @@ -362,7 +366,7 @@ void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.DeleteDirectoryFromArchive(archive_handle, dir_path)); return; @@ -378,7 +382,8 @@ void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) { async_data->archive_handle = archive_handle; async_data->dir_path = dir_path; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.DeleteDirectoryFromArchive(async_data->archive_handle, async_data->dir_path); @@ -406,7 +411,7 @@ void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.DeleteDirectoryRecursivelyFromArchive(archive_handle, dir_path)); return; @@ -422,7 +427,8 @@ void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) { async_data->archive_handle = archive_handle; async_data->dir_path = dir_path; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.DeleteDirectoryRecursivelyFromArchive( async_data->archive_handle, async_data->dir_path); @@ -452,7 +458,7 @@ void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} attributes={} size={:x} data={}", filename_type, attributes, file_size, file_path.DebugStr()); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.CreateFileInArchive(archive_handle, file_path, file_size, attributes)); return; @@ -472,7 +478,8 @@ void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) { async_data->file_size = file_size; async_data->attributes = attributes; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.CreateFileInArchive(async_data->archive_handle, async_data->file_path, @@ -500,7 +507,7 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.CreateDirectoryFromArchive(archive_handle, dir_path, attributes)); return; @@ -518,7 +525,8 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) { async_data->dir_path = dir_path; async_data->attributes = attributes; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.CreateDirectoryFromArchive( async_data->archive_handle, async_data->dir_path, async_data->attributes); @@ -554,7 +562,7 @@ void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) { dest_dirname_size, dest_dir_path.DebugStr()); if (!archives.ArchiveIsSlow(src_archive_handle) && - !archives.ArchiveIsSlow(dest_archive_handle)) { + !archives.ArchiveIsSlow(dest_archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.RenameDirectoryBetweenArchives(src_archive_handle, src_dir_path, dest_archive_handle, dest_dir_path)); @@ -575,7 +583,8 @@ void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) { async_data->dest_archive_handle = dest_archive_handle; async_data->dest_dir_path = dest_dir_path; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.RenameDirectoryBetweenArchives( async_data->src_archive_handle, async_data->src_dir_path, @@ -602,7 +611,7 @@ void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size, dir_path.DebugStr()); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); ResultVal> dir_res = archives.OpenDirectoryFromArchive(archive_handle, dir_path); @@ -630,7 +639,8 @@ void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) { async_data->archive_handle = archive_handle; async_data->dir_path = dir_path; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->dir_res = archives.OpenDirectoryFromArchive(async_data->archive_handle, async_data->dir_path); @@ -669,7 +679,7 @@ void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) { u64 program_id = slot->program_id; // Conventional opening - if (!archives.ArchiveIsSlow(archive_id)) { + if (!archives.ArchiveIsSlow(archive_id) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); const ResultVal handle = archives.OpenArchive(archive_id, archive_path, program_id); @@ -699,7 +709,8 @@ void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) { async_data->archive_path = archive_path; async_data->program_id = program_id; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->handle = archives.OpenArchive( async_data->archive_id, async_data->archive_path, async_data->program_id); @@ -727,7 +738,7 @@ void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) { const auto input_size = rp.Pop(); const auto output_size = rp.Pop(); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { auto input = rp.PopMappedBuffer(); auto output = rp.PopMappedBuffer(); std::vector in_data(input_size); @@ -766,7 +777,8 @@ void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) { async_data->in_buffer = &rp.PopMappedBuffer(); async_data->out_buffer = &rp.PopMappedBuffer(); - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { std::vector in_data(async_data->in_size); async_data->in_buffer->Read(in_data.data(), 0, in_data.size()); @@ -792,7 +804,7 @@ void FS_USER::CloseArchive(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto archive_handle = rp.PopRaw(); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.CloseArchive(archive_handle)); return; @@ -806,7 +818,8 @@ void FS_USER::CloseArchive(Kernel::HLERequestContext& ctx) { auto async_data = std::make_shared(); async_data->handle = archive_handle; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.CloseArchive(async_data->handle); return 0; @@ -1395,7 +1408,7 @@ void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 unique_id = rp.Pop(); const u8 title_variation = rp.Pop(); - if (!secure_value_backend->BackendIsSlow()) { + if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(secure_value_backend->ObsoletedSetSaveDataSecureValue(unique_id, title_variation, secure_value_slot, value)); @@ -1416,7 +1429,8 @@ void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { async_data->unique_id = unique_id; async_data->title_variation = title_variation; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = secure_value_backend->ObsoletedSetSaveDataSecureValue( async_data->unique_id, async_data->title_variation, async_data->secure_value_slot, @@ -1436,7 +1450,7 @@ void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 unique_id = rp.Pop(); const u8 title_variation = rp.Pop(); - if (!secure_value_backend->BackendIsSlow()) { + if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) { auto res = secure_value_backend->ObsoletedGetSaveDataSecureValue(unique_id, title_variation, secure_value_slot); if (res.Failed()) { @@ -1463,7 +1477,8 @@ void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { async_data->unique_id = unique_id; async_data->title_variation = title_variation; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = secure_value_backend->ObsoletedGetSaveDataSecureValue( async_data->unique_id, async_data->title_variation, async_data->secure_value_slot); @@ -1511,7 +1526,7 @@ void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u32 secure_value_slot = rp.Pop(); const u64 value = rp.Pop(); - if (!secure_value_backend->BackendIsSlow()) { + if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(secure_value_backend->SetThisSaveDataSecureValue(secure_value_slot, value)); return; @@ -1527,7 +1542,8 @@ void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { async_data->value = value; async_data->secure_value_slot = secure_value_slot; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = secure_value_backend->SetThisSaveDataSecureValue( async_data->secure_value_slot, async_data->value); @@ -1544,7 +1560,7 @@ void FS_USER::GetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 secure_value_slot = rp.Pop(); - if (!secure_value_backend->BackendIsSlow()) { + if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) { auto res = secure_value_backend->GetThisSaveDataSecureValue(secure_value_slot); if (res.Failed()) { @@ -1569,7 +1585,8 @@ void FS_USER::GetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) { auto async_data = std::make_shared(); async_data->secure_value_slot = secure_value_slot; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = secure_value_backend->GetThisSaveDataSecureValue(async_data->secure_value_slot); @@ -1599,7 +1616,7 @@ void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const u64 value = rp.Pop(); const bool flush = rp.Pop(); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(archives.SetSaveDataSecureValue(archive_handle, secure_value_slot, value, flush)); @@ -1620,7 +1637,8 @@ void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { async_data->secure_value_slot = secure_value_slot; async_data->flush = flush; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.SetSaveDataSecureValue(async_data->archive_handle, async_data->secure_value_slot, @@ -1639,7 +1657,7 @@ void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { const auto archive_handle = rp.PopRaw(); const u32 secure_value_slot = rp.Pop(); - if (!archives.ArchiveIsSlow(archive_handle)) { + if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) { auto res = archives.GetSaveDataSecureValue(archive_handle, secure_value_slot); if (res.Failed()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); @@ -1665,7 +1683,8 @@ void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { async_data->archive_handle = archive_handle; async_data->secure_value_slot = secure_value_slot; - ctx.RunAsync( + ctx.RunOnThreadWorker( + fs_async_worker, [this, async_data](Kernel::HLERequestContext& ctx) { async_data->res = archives.GetSaveDataSecureValue(async_data->archive_handle, async_data->secure_value_slot); @@ -1900,6 +1919,9 @@ FS_USER::FS_USER(Core::System& system) template void Service::FS::FS_USER::serialize(Archive& ar, const unsigned int) { DEBUG_SERIALIZATION_POINT; + if (!Archive::is_loading::value) { + fs_async_worker.WaitForRequests(); + } ar& boost::serialization::base_object(*this); ar & priority; ar & secure_value_backend; diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index f94f318f2..9dd021bdc 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -49,6 +49,9 @@ private: class FS_USER final : public ServiceFramework { public: explicit FS_USER(Core::System& system); + ~FS_USER() { + fs_async_worker.WaitForRequests(); + } // On real HW this is part of FSReg (FSReg:Register). But since that module is only used by // loader and pm, which we HLEed, we can just directly use it here @@ -756,6 +759,8 @@ private: std::shared_ptr secure_value_backend; + Common::ThreadWorker fs_async_worker{1, "FSUSER_Worker"}; + template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; diff --git a/src/core/hle/service/gsp/gsp_gpu.cpp b/src/core/hle/service/gsp/gsp_gpu.cpp index 39d10f2d3..853dd8d3c 100644 --- a/src/core/hle/service/gsp/gsp_gpu.cpp +++ b/src/core/hle/service/gsp/gsp_gpu.cpp @@ -12,6 +12,7 @@ #include "common/hacks/hack_manager.h" #include "common/settings.h" #include "core/core.h" +#include "core/core_timing.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/shared_memory.h" #include "core/hle/kernel/shared_page.h" @@ -295,6 +296,23 @@ void GSP_GPU::SetAxiConfigQoSMode(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_GSP, "(STUBBED) called mode=0x{:08X}", mode); } +void GSP_GPU::SetPerfLogMode(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + bool enabled = rp.Pop() != 0; + + perf_recorder.SetEnabled(enabled); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void GSP_GPU::GetPerfLog(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(15, 0); + rb.PushRaw(perf_recorder.GetResults()); +} + void GSP_GPU::RegisterInterruptRelayQueue(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u32 flags = rp.Pop(); @@ -337,7 +355,124 @@ void GSP_GPU::UnregisterInterruptRelayQueue(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_GSP, "called"); } -void GSP_GPU::SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id) { +// Uncomment the following line to display the average delay calculated for every frame. +// #define SHOW_AVERAGE_TIME_PER_FRAME + +void GSP_GPU::SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id, u64 wait_delay_ns) { + + // Every gsp request takes a constant amount of time to be + // processed and control returned to the application. This + // time is estimated below. + static constexpr u64 sync_delay_nanoseconds = 300 * 1000; + + // For a reason not yet understood, Super Mario 3D Land hangs on a white screen after the title + // screen when any of the save slots have the completion star icons. This is in some way related + // to the timings of texture copy commands, and gets fixed if we increase the amount of time + // those take. This issue may be resolved as timings become more accurate in the future. + static constexpr u64 sync_delay_nanoseconds_delayed_texcopy = 1000 * 1000; + +#ifdef SHOW_AVERAGE_TIME_PER_FRAME + auto track_average = [&](bool is_vsync) { + using clock = std::chrono::steady_clock; + + static uint64_t total_ns = 0; + static uint64_t sample_count = 0; + static auto last_print = clock::now(); + + if (!is_vsync) { + total_ns += wait_delay_ns; + ++sample_count; + } + + auto now = clock::now(); + + if (now - last_print >= std::chrono::milliseconds(250)) { + double average_ns = + (sample_count > 0) + ? (static_cast(total_ns) / static_cast(sample_count)) + : 0; + + LOG_INFO(Service_GSP, "Average delay milliseconds per frame: {}", + average_ns / 1000000.f); + + total_ns = 0; + sample_count = 0; + last_print = now; + } + }; +#endif + + // Signal VBlank interrupt immediately, this interrupt is signaled from + // an scheduler event so it already has the proper timing. + if (interrupt_id == InterruptId::PDC0 || interrupt_id == InterruptId::PDC1) { + +#ifdef SHOW_AVERAGE_TIME_PER_FRAME + track_average(true); +#endif + + if (perf_recorder.IsEnabled()) { + constexpr u64 nanoseconds_per_frame = static_cast( + ((static_cast(VideoCore::FRAME_TICKS) / BASE_CLOCK_RATE_ARM11) * 1e9)); + + perf_recorder.UpdateTime(interrupt_id, nanoseconds_per_frame); + } + + ProcessPendingInterruptImpl(interrupt_id, thread_id); + return; + } + + if (perf_recorder.IsEnabled()) { + perf_recorder.UpdateTime(interrupt_id, wait_delay_ns); + } + + if (Settings::values.simulate_3ds_gpu_timings.GetValue()) { + + if (delay_texture_copy_completion) { + wait_delay_ns += (interrupt_id == InterruptId::PPF) + ? sync_delay_nanoseconds_delayed_texcopy + : sync_delay_nanoseconds; + } else { + wait_delay_ns += sync_delay_nanoseconds; + } + } else { + if (delay_texture_copy_completion && interrupt_id == InterruptId::PPF) { + wait_delay_ns += sync_delay_nanoseconds_delayed_texcopy; + } else { + wait_delay_ns = 0; + } + } + +#ifdef SHOW_AVERAGE_TIME_PER_FRAME + track_average(false); +#endif + + if (wait_delay_ns) { + size_t pending_interrupt_id = + pending_interrupts.Push(std::make_pair(interrupt_id, thread_id)); + if (pending_interrupt_id == std::numeric_limits::max()) { + LOG_ERROR(Service_GSP, "Pending interrupts queue is full"); + ProcessPendingInterruptImpl(interrupt_id, thread_id); + } else { + system.Kernel().timing.ScheduleEvent(nsToCycles(wait_delay_ns), + SignalInterruptEventType, + static_cast(pending_interrupt_id)); + } + } else { + ProcessPendingInterruptImpl(interrupt_id, thread_id); + } +} + +void Service::GSP::GSP_GPU::ProcessPendingInterrupt(size_t pending_interrupt_id) { + auto pending_interrupt = pending_interrupts.Pop(pending_interrupt_id); + if (!pending_interrupt.has_value()) { + return; + } + const auto& [interrupt_id, thread_id] = *pending_interrupt; + + ProcessPendingInterruptImpl(interrupt_id, thread_id); +} + +void Service::GSP::GSP_GPU::ProcessPendingInterruptImpl(InterruptId interrupt_id, u32 thread_id) { SessionData* session_data = FindRegisteredThreadData(thread_id); if (!session_data) { return; @@ -349,32 +484,55 @@ void GSP_GPU::SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id) return; } + const bool is_pdc = interrupt_id == InterruptId::PDC0 || interrupt_id == InterruptId::PDC1; auto* interrupt_relay_queue = GetInterruptRelayQueue(thread_id); - u8 next = interrupt_relay_queue->index; - next += interrupt_relay_queue->number_interrupts; - next = next % 0x34; // 0x34 is the number of interrupt slots - interrupt_relay_queue->number_interrupts += 1; + auto queue_interrupt = [&]() { + if (interrupt_relay_queue->number_interrupts >= InterruptRelayQueue::max_slots) { + interrupt_relay_queue->error_code = InterruptRelayQueue::queue_full_error; + } else { + u8 next = interrupt_relay_queue->index; + next += interrupt_relay_queue->number_interrupts; + next %= InterruptRelayQueue::max_slots; - interrupt_relay_queue->slot[next] = interrupt_id; - interrupt_relay_queue->error_code = 0x0; // No error + interrupt_relay_queue->number_interrupts += 1; + + interrupt_relay_queue->slot[next] = interrupt_id; + + interrupt_event->Signal(); + } + }; + + if (is_pdc) { + if (!interrupt_relay_queue->ignore_pdc.Value()) { + + if (interrupt_relay_queue->number_interrupts >= + InterruptRelayQueue::stop_queuing_pdc_threeshold) { + if (interrupt_id == InterruptId::PDC0) { + interrupt_relay_queue->missed_PDC0++; + } else { + interrupt_relay_queue->missed_PDC1++; + } + } else { + queue_interrupt(); + } + } + + // Update framebuffer information if requested + const s32 screen_id = (interrupt_id == InterruptId::PDC0) ? 0 : 1; - // Update framebuffer information if requested - const s32 screen_id = (interrupt_id == InterruptId::PDC0) ? 0 - : (interrupt_id == InterruptId::PDC1) ? 1 - : -1; - if (screen_id != -1) { auto* info = GetFrameBufferInfo(thread_id, screen_id); if (info->is_dirty) { system.GPU().SetBufferSwap(screen_id, info->framebuffer_info[info->index]); info->is_dirty.Assign(false); } - } - interrupt_event->Signal(); + } else { + queue_interrupt(); + } } -void GSP_GPU::SignalInterrupt(InterruptId interrupt_id) { +void GSP_GPU::SignalInterrupt(InterruptId interrupt_id, u64 wait_delay_ns) { if (nullptr == shared_memory) { LOG_WARNING(Service_GSP, "cannot synchronize until GSP shared memory has been created!"); return; @@ -385,7 +543,7 @@ void GSP_GPU::SignalInterrupt(InterruptId interrupt_id) { // right), but the PDC0/1 interrupts are signaled for every registered thread. if (interrupt_id == InterruptId::PDC0 || interrupt_id == InterruptId::PDC1) { for (u32 thread_id = 0; thread_id < MaxGSPThreads; ++thread_id) { - SignalInterruptForThread(interrupt_id, thread_id); + SignalInterruptForThread(interrupt_id, thread_id, wait_delay_ns); } return; } @@ -395,7 +553,7 @@ void GSP_GPU::SignalInterrupt(InterruptId interrupt_id) { return; } - SignalInterruptForThread(interrupt_id, active_thread_id); + SignalInterruptForThread(interrupt_id, active_thread_id, wait_delay_ns); } void GSP_GPU::SetLcdForceBlack(Kernel::HLERequestContext& ctx) { @@ -692,6 +850,11 @@ Result GSP_GPU::AcquireGpuRight(const Kernel::HLERequestContext& ctx, Common::Hacks::HackType::REQUIRES_SHADER_FIXUP, process->codeset->program_id, Common::Hacks::HackAllowMode::DISALLOW) != Common::Hacks::HackAllowMode::DISALLOW; + delay_texture_copy_completion = + Common::Hacks::hack_manager.GetHackAllowMode( + Common::Hacks::HackType::DELAY_TEXTURE_COPY_COMPLETION, process->codeset->program_id, + Common::Hacks::HackAllowMode::DISALLOW) != Common::Hacks::HackAllowMode::DISALLOW; + auto& gpu = system.GPU(); gpu.ApplyPerProgramSettings(process->codeset->program_id); gpu.GetRightEyeDisabler().SetEnabled(right_eye_disable_allow); @@ -818,6 +981,9 @@ void GSP_GPU::serialize(Archive& ar, const unsigned int) { ar & first_initialization; ar & used_thread_ids; ar & saved_vram; + ar & delay_texture_copy_completion; + ar & pending_interrupts; + ar & perf_recorder; } SERIALIZE_IMPL(GSP_GPU) @@ -840,8 +1006,8 @@ GSP_GPU::GSP_GPU(Core::System& system) : ServiceFramework("gsp::Gpu", 4), system {0x000E, nullptr, "SetTextureCopy"}, {0x000F, nullptr, "SetMemoryFill"}, {0x0010, &GSP_GPU::SetAxiConfigQoSMode, "SetAxiConfigQoSMode"}, - {0x0011, nullptr, "SetPerfLogMode"}, - {0x0012, nullptr, "GetPerfLog"}, + {0x0011, &GSP_GPU::SetPerfLogMode, "SetPerfLogMode"}, + {0x0012, &GSP_GPU::GetPerfLog, "GetPerfLog"}, {0x0013, &GSP_GPU::RegisterInterruptRelayQueue, "RegisterInterruptRelayQueue"}, {0x0014, &GSP_GPU::UnregisterInterruptRelayQueue, "UnregisterInterruptRelayQueue"}, {0x0015, &GSP_GPU::TryAcquireRight, "TryAcquireRight"}, @@ -866,6 +1032,11 @@ GSP_GPU::GSP_GPU(Core::System& system) : ServiceFramework("gsp::Gpu", 4), system Kernel::MemoryRegion::BASE, "GSP:SharedMemory") .Unwrap(); + SignalInterruptEventType = system.Kernel().timing.RegisterEvent( + "GSPPendingInterrupt", [this](uintptr_t arg, s64 cycle_late) { + ProcessPendingInterrupt(static_cast(arg)); + }); + first_initialization = true; }; diff --git a/src/core/hle/service/gsp/gsp_gpu.h b/src/core/hle/service/gsp/gsp_gpu.h index 0864e37de..3afbb0916 100644 --- a/src/core/hle/service/gsp/gsp_gpu.h +++ b/src/core/hle/service/gsp/gsp_gpu.h @@ -4,8 +4,10 @@ #pragma once +#include #include #include +#include #include #include #include @@ -104,7 +106,7 @@ public: * Signals that the specified interrupt type has occurred to userland code * @param interrupt_id ID of interrupt that is being signalled */ - void SignalInterrupt(InterruptId interrupt_id); + void SignalInterrupt(InterruptId interrupt_id, u64 wait_delay_ns); /** * Retrieves the framebuffer info stored in the GSP shared memory for the @@ -143,7 +145,11 @@ private: * @param interrupt_id ID of interrupt that is being signalled. * @param thread_id GSP thread that will receive the interrupt. */ - void SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id); + void SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id, u64 wait_delay_ns); + + void ProcessPendingInterrupt(size_t pending_interrupt_id); + + void ProcessPendingInterruptImpl(InterruptId interrupt_id, u32 thread_id); /** * GSP_GPU::WriteHWRegs service function @@ -240,6 +246,10 @@ private: */ void SetAxiConfigQoSMode(Kernel::HLERequestContext& ctx); + void SetPerfLogMode(Kernel::HLERequestContext& ctx); + + void GetPerfLog(Kernel::HLERequestContext& ctx); + /** * GSP_GPU::RegisterInterruptRelayQueue service function * Inputs: @@ -405,6 +415,118 @@ private: /// Thread ids currently in use by the sessions connected to the GSPGPU service. std::array used_thread_ids{}; + /// The current thread needs a longer emulated texture copy completion + bool delay_texture_copy_completion{}; + + class PendingInterruptArray { + public: + PendingInterruptArray() { + for (size_t i = 0; i < array_size; i++) { + elements[i].first = InterruptId::COUNT; + } + } + + size_t Push(const std::pair elem) { + if (elements[head].first != InterruptId::COUNT) { + // If the head position is occupied, the queue is full + return std::numeric_limits::max(); + } + + elements[head] = elem; + size_t index = head; + head = (head + 1) % array_size; + return index; + } + + std::optional> Pop(size_t at) { + if (at >= array_size || elements[at].first == InterruptId::COUNT) { + // Invalid index or already free + return std::nullopt; + } + + std::pair value = elements[at]; + elements[at].first = InterruptId::COUNT; + + return value; + } + + private: + static constexpr size_t array_size = 512; + size_t head = 0; + + std::array, array_size> elements; + + template + void serialize(Archive& ar, const unsigned int) { + ar & elements; + ar & head; + } + friend class boost::serialization::access; + }; + + class PerformanceRecorder { + public: + struct PerformanceEntry { + u32 delta_time{}; + u32 sum_time{}; + + template + void serialize(Archive& ar, const unsigned int) { + ar & delta_time; + ar & sum_time; + } + friend class boost::serialization::access; + }; + + using PerfArray = std::array(InterruptId::COUNT)>; + + PerformanceRecorder() = default; + + void Reset() { + entries.fill({}); + } + + bool IsEnabled() { + return enabled; + } + + void SetEnabled(bool _enabled) { + enabled = _enabled; + if (enabled) { + Reset(); + } + } + + void UpdateTime(InterruptId id, u64 nanoseconds) { + // These counters may overflow, which is normal. + entries[static_cast(id)].delta_time = static_cast(nanoseconds); + entries[static_cast(id)].sum_time += static_cast(nanoseconds); + } + + const PerfArray& GetResults() { + return entries; + } + + private: + PerfArray entries{}; + bool enabled{}; + + template + void serialize(Archive& ar, const unsigned int) { + ar & entries; + ar & enabled; + } + friend class boost::serialization::access; + }; + + // This array is only needed to keep track of delayed notifications and simulate the GPU + // taking some time to finish the work, it doesn't exist on real hardware. + PendingInterruptArray pending_interrupts; + + PerformanceRecorder perf_recorder; + + Core::TimingEventType* SignalInterruptEventType = nullptr; + friend class SessionData; template diff --git a/src/core/hle/service/gsp/gsp_interrupt.h b/src/core/hle/service/gsp/gsp_interrupt.h index db2b584a6..efd46b39e 100644 --- a/src/core/hle/service/gsp/gsp_interrupt.h +++ b/src/core/hle/service/gsp/gsp_interrupt.h @@ -1,10 +1,11 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include +#include "common/bit_field.h" #include "common/common_types.h" namespace Service::GSP { @@ -18,25 +19,35 @@ enum class InterruptId : u8 { PPF = 0x04, P3D = 0x05, DMA = 0x06, + + COUNT, }; /// GSP thread interrupt relay queue struct InterruptRelayQueue { + static constexpr size_t max_slots = 0x34; + static constexpr size_t stop_queuing_pdc_threeshold = 0x20; + static constexpr u8 queue_full_error = 0x1; + // Index of last interrupt in the queue u8 index; // Number of interrupts remaining to be processed by the userland code u8 number_interrupts; // Error code - zero on success, otherwise an error has occurred u8 error_code; - u8 padding1; + + union { + u8 config; + BitField<0, 1, u8> ignore_pdc; + }; u32 missed_PDC0; u32 missed_PDC1; - InterruptId slot[0x34]; ///< Interrupt ID slots + InterruptId slot[max_slots]; ///< Interrupt ID slots }; static_assert(sizeof(InterruptRelayQueue) == 0x40, "InterruptRelayQueue struct has incorrect size"); -using InterruptHandler = std::function; +using InterruptHandler = std::function; } // namespace Service::GSP diff --git a/src/core/hle/service/mcu/mcu.cpp b/src/core/hle/service/mcu/mcu.cpp index dff4ee3e9..83b0742b6 100644 --- a/src/core/hle/service/mcu/mcu.cpp +++ b/src/core/hle/service/mcu/mcu.cpp @@ -1,16 +1,18 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "core/core.h" #include "core/hle/service/mcu/mcu.h" #include "core/hle/service/mcu/mcu_hwc.h" +#include "core/hle/service/mcu/mcu_rtc.h" namespace Service::MCU { void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); - std::make_shared()->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); } } // namespace Service::MCU diff --git a/src/core/hle/service/mcu/mcu_hwc.cpp b/src/core/hle/service/mcu/mcu_hwc.cpp index c2a5d7514..dbdf6b2af 100644 --- a/src/core/hle/service/mcu/mcu_hwc.cpp +++ b/src/core/hle/service/mcu/mcu_hwc.cpp @@ -1,15 +1,18 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" +#include "core/hle/ipc_helpers.h" #include "core/hle/service/mcu/mcu_hwc.h" +#include "core/hle/service/mcu/mcu_rtc.h" +SERVICE_CONSTRUCT_IMPL(Service::MCU::HWC) SERIALIZE_EXPORT_IMPL(Service::MCU::HWC) namespace Service::MCU { -HWC::HWC() : ServiceFramework("mcu::HWC", 1) { +HWC::HWC(Core::System& _system) : ServiceFramework("mcu::HWC", 1), system(_system) { static const FunctionInfo functions[] = { // clang-format off {0x0001, nullptr, "ReadRegister"}, @@ -21,7 +24,7 @@ HWC::HWC() : ServiceFramework("mcu::HWC", 1) { {0x0007, nullptr, "SetWifiLEDState"}, {0x0008, nullptr, "SetCameraLEDPattern"}, {0x0009, nullptr, "Set3DLEDState"}, - {0x000A, nullptr, "SetInfoLEDPattern"}, + {0x000A, &HWC::SetInfoLEDPattern, "SetInfoLEDPattern"}, {0x000B, nullptr, "GetSoundVolume"}, {0x000C, nullptr, "SetTopScreenFlicker"}, {0x000D, nullptr, "SetBottomScreenFlicker"}, @@ -33,4 +36,19 @@ HWC::HWC() : ServiceFramework("mcu::HWC", 1) { RegisterHandlers(functions); } +void HWC::SetInfoLEDPattern(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto pat = rp.PopRaw(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto mcu_rtc = MCU::RTC::GetService(system); + if (mcu_rtc) { + mcu_rtc->UpdateInfoLEDPattern(pat); + rb.Push(ResultSuccess); + } else { + rb.Push(ResultUnknown); + } +} + } // namespace Service::MCU diff --git a/src/core/hle/service/mcu/mcu_hwc.h b/src/core/hle/service/mcu/mcu_hwc.h index fdcd7bf2f..8e117e9f9 100644 --- a/src/core/hle/service/mcu/mcu_hwc.h +++ b/src/core/hle/service/mcu/mcu_hwc.h @@ -1,4 +1,4 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,12 +10,17 @@ namespace Service::MCU { class HWC final : public ServiceFramework { public: - explicit HWC(); + explicit HWC(Core::System& _system); private: + Core::System& system; + + void SetInfoLEDPattern(Kernel::HLERequestContext& ctx); + SERVICE_SERIALIZATION_SIMPLE }; } // namespace Service::MCU +SERVICE_CONSTRUCT(Service::MCU::HWC) BOOST_CLASS_EXPORT_KEY(Service::MCU::HWC) diff --git a/src/core/hle/service/mcu/mcu_rtc.cpp b/src/core/hle/service/mcu/mcu_rtc.cpp new file mode 100644 index 000000000..294f4c41f --- /dev/null +++ b/src/core/hle/service/mcu/mcu_rtc.cpp @@ -0,0 +1,293 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "common/archives.h" +#include "common/vector_math.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/mcu/mcu.h" +#include "core/hle/service/mcu/mcu_rtc.h" + +SERVICE_CONSTRUCT_IMPL(Service::MCU::RTC) +SERIALIZE_EXPORT_IMPL(Service::MCU::RTC) + +namespace Service::MCU { + +class InfoLedHandler { +public: + InfoLedHandler() = default; + ~InfoLedHandler() = default; + + static constexpr s64 CALLBACK_PERIOD_NS = 1'000'000'000ll / 60; // 60Hz (~16ms) + static constexpr s64 MCU_TICK_PERIOD_NS = 1'000'000'000ll / 512; // 512Hz (~2ms) + + void SetPattern(const InfoLedPattern& p) { + current_pattern = p; + pattern_changed = true; + } + + void SetHeader(const InfoLedPattern::Header& header) { + current_pattern.header = header; + pattern_changed = true; + } + + // The MCU led code is updated with a frequency of 512Hz on real hardware. However + // it is not a very relevant feature for emulation, so to prevent slicing the core + // timing too much let's update it every frame instead (60Hz) and adjust for it. + void Tick(s64 cycles_late) { + + const s64 late_ns = cyclesToNs(cycles_late); + + // Accumulate elapsed time. + arm_time_ns += CALLBACK_PERIOD_NS + late_ns; + if (arm_time_ns < 0) + arm_time_ns = 0; + + // Sync the MCU state up to the current ARM time + while (arm_time_ns >= MCU_TICK_PERIOD_NS) { + arm_time_ns -= MCU_TICK_PERIOD_NS; + TickMCULed(); + } + } + + Common::Vec3 Color() const { + return result_color; + } + + // To save CPU time, do not tick if all smooth state has finished + // and the pattern is all zero. + bool NeedsTicking() { + auto patAllZero = [this]() -> bool { + u32* data = reinterpret_cast(¤t_pattern); + for (size_t i = 0; i < sizeof(InfoLedPattern) / sizeof(u32); i++) { + if (data[i]) + return false; + } + return true; + }; + + return !patAllZero() || !state_r.Finished() || !state_g.Finished() || !state_b.Finished(); + } + + bool Status() const { + return status_finished; + } + +private: + struct LedSmoothState { + s16 target = 0; + s16 increment = 0; + s16 current = 0; + + bool Finished() { + return current == target; + } + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int) { + ar & target; + ar & increment; + ar & current; + } + }; + + // Decompilation of MCU function at address 0x2f44 + void setSmoothState(LedSmoothState& state, u8 color) { + // Looks like the color is multiplied for better precision + state.target = static_cast(color) * 128; + + // Real HW makes sure ticks_to_progress is not 0 when the led pattern + // is set through I2C. We check for it here instead as it's equivalent. + const u8 ticks = std::max(current_pattern.header.ticks_to_progress, 1); + state.increment = (state.target - state.current) / ticks; + } + + // Decompilation of MCU function at address 0x2dc0 + static u8 updateSmoothState(LedSmoothState& status) { + if (!status.Finished()) { + if (std::abs(status.target - status.current) > std::abs(status.increment)) { + status.current += status.increment; + } else { + status.current = status.target; + } + } + + return static_cast(status.current / 128); + } + + // Decompilation of MCU function at address 0x2f6b + // This function is called every 1/512 seconds + void TickMCULed() { + + // Here, a few things happen. + // If a global variable is set to 2 (0xff904), the led state is cleared. + // If a global variable bit 0 is set (0xffe98), this function does not run at all. + // If a global variable bit 7 is set (0xffe97), this function takes another path which + // runs function 0x2f1d instead of setSmoothState() to set the LED smooth status. + // This function seems to setup smooth to fade to off state. + // TODO(PabloMK7): Figure out what those mean. Maybe power on/off related + + if (pattern_changed) { + pattern_changed = false; + status_finished = false; + ticks_to_next_index = 0; + index = 0; + } else { + if (ticks_to_next_index == 0) { + ticks_to_next_index = current_pattern.header.ticks_per_index; + + if (index < InfoLedPattern::PATTERN_INDEX_COUNT - 1) { + status_finished = false; + index = (index + 1) % InfoLedPattern::PATTERN_INDEX_COUNT; + last_index_repeat_times = 0; + } else { + status_finished = true; + if (current_pattern.header.last_index_repeat_times != 0xFF) { + last_index_repeat_times++; + if (last_index_repeat_times > + current_pattern.header.last_index_repeat_times) { + index = 0; + } + } + } + + // Set smooth for the next index + setSmoothState(state_r, current_pattern.r[index]); + setSmoothState(state_g, current_pattern.g[index]); + setSmoothState(state_b, current_pattern.b[index]); + } + ticks_to_next_index--; + } + + // Update smooth state + result_color.r() = updateSmoothState(state_r); + result_color.g() = updateSmoothState(state_g); + result_color.b() = updateSmoothState(state_b); + } + +private: + InfoLedPattern current_pattern{}; + + bool pattern_changed = false; + bool status_finished = false; + + u8 ticks_to_next_index = 0; + u8 index = 0; + u8 last_index_repeat_times = 0; + + LedSmoothState state_r{}; + LedSmoothState state_g{}; + LedSmoothState state_b{}; + + Common::Vec3 result_color{}; + + s64 arm_time_ns = 0; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int) { + ar & current_pattern; + ar & pattern_changed; + ar & status_finished; + ar & ticks_to_next_index; + ar & index; + ar & last_index_repeat_times; + ar & state_r; + ar & state_g; + ar & state_b; + ar & result_color; + ar & arm_time_ns; + } +}; + +RTC::RTC(Core::System& _system) : ServiceFramework("mcu::RTC", 1), system(_system) { + static const FunctionInfo functions[] = { + // clang-format off + {0x003B, &RTC::SetInfoLEDPattern, "SetInfoLEDPattern"}, + {0x003C, &RTC::SetInfoLEDPatternHeader, "SetInfoLEDPattern"}, + {0x003D, &RTC::GetInfoLEDStatus, "SetInfoLEDPattern"}, + // clang-format on + }; + RegisterHandlers(functions); + + info_led = std::make_unique(); + info_led_tick_event = + system.Kernel().timing.RegisterEvent("MCUTickInfoLED", [this](u64, s64 cycles_late) { + info_led->Tick(cycles_late); + system.SetInfoLEDColor(info_led->Color()); + if (info_led->NeedsTicking()) { + system.Kernel().timing.ScheduleEvent(nsToCycles(InfoLedHandler::CALLBACK_PERIOD_NS), + info_led_tick_event, 0, 1); + } else { + info_led_ticking = false; + } + }); +} + +RTC::~RTC() {} + +void RTC::UpdateInfoLEDPattern(const InfoLedPattern& pat) { + info_led->SetPattern(pat); + if (!info_led_ticking) { + system.Kernel().timing.ScheduleEvent(0, info_led_tick_event, 0, 1); + info_led_ticking = true; + } +} + +void RTC::UpdateInfoLEDHeader(const InfoLedPattern::Header& header) { + info_led->SetHeader(header); + if (!info_led_ticking) { + system.Kernel().timing.ScheduleEvent(0, info_led_tick_event, 0, 1); + info_led_ticking = true; + } +} + +bool RTC::GetInfoLEDStatusFinished() { + return info_led->Status(); +} + +std::shared_ptr RTC::GetService(Core::System& system) { + return system.ServiceManager().GetService("mcu::RTC"); +} + +void RTC::SetInfoLEDPattern(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto pat = rp.PopRaw(); + + UpdateInfoLEDPattern(pat); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void RTC::SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto head = rp.PopRaw(); + + UpdateInfoLEDHeader(head); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void RTC::GetInfoLEDStatus(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(static_cast(GetInfoLEDStatusFinished())); +} + +template +void RTC::serialize(Archive& ar, const unsigned int) { + DEBUG_SERIALIZATION_POINT; + ar& boost::serialization::base_object(*this); + ar & info_led; + ar & info_led_ticking; +} + +} // namespace Service::MCU diff --git a/src/core/hle/service/mcu/mcu_rtc.h b/src/core/hle/service/mcu/mcu_rtc.h new file mode 100644 index 000000000..a5a3cd692 --- /dev/null +++ b/src/core/hle/service/mcu/mcu_rtc.h @@ -0,0 +1,83 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/core_timing.h" +#include "core/hle/service/service.h" + +namespace Service::MCU { +class InfoLedHandler; + +struct InfoLedPattern { + static constexpr size_t PATTERN_INDEX_COUNT = 32; + + struct Header { + u8 ticks_per_index{}; // Amount of ticks to stay in the current index (1 tick == 1/512 s) + u8 ticks_to_progress{}; // Amount of ticks to go from the previous value to the current + // index value. Normally, this only makes sense to be set to 0 to + // disable interpolation, or equal to "ticks_per_index" for linear + // interpolation. Any other value breaks the interpolation math. + u8 last_index_repeat_times{}; // Amount of times to repeat the last index, as if the color + // array had "last_index_repeat_times" more elements equal to + // the last array value. (0xFF means repeat forever) + u8 padding{}; + } header; + + // RGB color elements, corresponding to the LED PWM duty cycle. + // (0x0 -> fully off, 0xFF -> fully on) + std::array r{}; + std::array g{}; + std::array b{}; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int) { + ar & header.ticks_per_index; + ar & header.ticks_to_progress; + ar & header.last_index_repeat_times; + ar & header.padding; + + ar & r; + ar & g; + ar & b; + } +}; +static_assert(sizeof(InfoLedPattern) == 0x64); + +class RTC final : public ServiceFramework { +public: + explicit RTC(Core::System& _system); + ~RTC(); + + void UpdateInfoLEDPattern(const InfoLedPattern& pat); + + void UpdateInfoLEDHeader(const InfoLedPattern::Header& header); + + bool GetInfoLEDStatusFinished(); + + static std::shared_ptr GetService(Core::System& system); + +private: + void SetInfoLEDPattern(Kernel::HLERequestContext& ctx); + + void SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx); + + void GetInfoLEDStatus(Kernel::HLERequestContext& ctx); + + Core::System& system; + + std::unique_ptr info_led; + Core::TimingEventType* info_led_tick_event{}; + bool info_led_ticking{}; + + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; + +} // namespace Service::MCU + +SERVICE_CONSTRUCT(Service::MCU::RTC) +BOOST_CLASS_EXPORT_KEY(Service::MCU::RTC) diff --git a/src/core/hle/service/pm/pm.cpp b/src/core/hle/service/pm/pm.cpp index 6c9ddc7d9..ccb69ef2e 100644 --- a/src/core/hle/service/pm/pm.cpp +++ b/src/core/hle/service/pm/pm.cpp @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -11,7 +11,7 @@ namespace Service::PM { void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); - std::make_shared()->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); std::make_shared()->InstallAsService(service_manager); } diff --git a/src/core/hle/service/pm/pm_app.cpp b/src/core/hle/service/pm/pm_app.cpp index ba14d1bdc..dc360c469 100644 --- a/src/core/hle/service/pm/pm_app.cpp +++ b/src/core/hle/service/pm/pm_app.cpp @@ -1,16 +1,19 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" +#include "core/core.h" #include "core/hle/ipc_helpers.h" +#include "core/hle/kernel/kernel.h" #include "core/hle/service/pm/pm_app.h" +SERVICE_CONSTRUCT_IMPL(Service::PM::PM_APP) SERIALIZE_EXPORT_IMPL(Service::PM::PM_APP) namespace Service::PM { -PM_APP::PM_APP() : ServiceFramework("pm:app", 3) { +PM_APP::PM_APP(Core::System& _system) : ServiceFramework("pm:app", 3), system(_system) { static const FunctionInfo functions[] = { // clang-format off {0x0001, nullptr, "LaunchTitle"}, @@ -22,8 +25,8 @@ PM_APP::PM_APP() : ServiceFramework("pm:app", 3) { {0x0007, nullptr, "GetFIRMLaunchParams"}, {0x0008, nullptr, "GetTitleExheaderFlags"}, {0x0009, nullptr, "SetFIRMLaunchParams"}, - {0x000A, nullptr, "SetAppResourceLimit"}, - {0x000B, nullptr, "GetAppResourceLimit"}, + {0x000A, &PM_APP::SetAppResourceLimit, "SetAppResourceLimit"}, + {0x000B, &PM_APP::GetAppResourceLimit, "GetAppResourceLimit"}, {0x000C, nullptr, "UnregisterProcess"}, {0x000D, nullptr, "LaunchTitleUpdate"}, // clang-format on @@ -32,4 +35,65 @@ PM_APP::PM_APP() : ServiceFramework("pm:app", 3) { RegisterHandlers(functions); } +Result PM_APP::UpdateResourceLimit(Kernel::ResourceLimitType type, u32 value) { + auto res_limit = + system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + + if (type != Kernel::ResourceLimitType::CpuTime) { + return Result{ErrorDescription::NotImplemented, ErrorModule::PM, + ErrorSummary::InvalidArgument, ErrorLevel::Permanent}; + } + + if (value <= res_limit->GetLimitValue(Kernel::ResourceLimitType::CpuTime)) { + res_limit->SetCurrentValue(Kernel::ResourceLimitType::CpuTime, value); + system.Kernel().UpdateCore1AppCpuLimit(); + } + + return ResultSuccess; +} + +ResultVal PM_APP::GetResourceLimit(Kernel::ResourceLimitType type) { + auto res_limit = + system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + + if (type != Kernel::ResourceLimitType::CpuTime) { + return Result{ErrorDescription::NotImplemented, ErrorModule::PM, + ErrorSummary::InvalidArgument, ErrorLevel::Permanent}; + } + + return static_cast(res_limit->GetCurrentValue(Kernel::ResourceLimitType::CpuTime)); +} + +void PM_APP::SetAppResourceLimit(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + rp.Skip(1, false); + auto type = static_cast(rp.Pop()); + u32 value = rp.Pop(); + rp.Skip(2, false); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(UpdateResourceLimit(type, value)); +} + +void PM_APP::GetAppResourceLimit(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + rp.Skip(1, false); + auto type = static_cast(rp.Pop()); + rp.Skip(3, false); + + u64 res_value = 0; + auto res = GetResourceLimit(type); + if (res.Succeeded()) { + res_value = static_cast(res.Unwrap()); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + rb.Push(res.Code()); + rb.Push(res_value); +} + +std::shared_ptr GetServiceAPP(Core::System& system) { + return system.ServiceManager().GetService("pm:app"); +} + } // namespace Service::PM diff --git a/src/core/hle/service/pm/pm_app.h b/src/core/hle/service/pm/pm_app.h index 9aefb0cee..decec0919 100644 --- a/src/core/hle/service/pm/pm_app.h +++ b/src/core/hle/service/pm/pm_app.h @@ -1,22 +1,36 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include "core/hle/kernel/resource_limit.h" #include "core/hle/service/service.h" namespace Service::PM { class PM_APP final : public ServiceFramework { public: - PM_APP(); + explicit PM_APP(Core::System& system); ~PM_APP() = default; + Result UpdateResourceLimit(Kernel::ResourceLimitType type, u32 value); + + ResultVal GetResourceLimit(Kernel::ResourceLimitType type); + private: + Core::System& system; + + void SetAppResourceLimit(Kernel::HLERequestContext& ctx); + + void GetAppResourceLimit(Kernel::HLERequestContext& ctx); + SERVICE_SERIALIZATION_SIMPLE }; +std::shared_ptr GetServiceAPP(Core::System& system); + } // namespace Service::PM +SERVICE_CONSTRUCT(Service::PM::PM_APP) BOOST_CLASS_EXPORT_KEY(Service::PM::PM_APP) diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 1cdb64c88..f0e5769e1 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -12,6 +12,7 @@ #include "core/file_sys/errors.h" #include "core/file_sys/file_backend.h" #include "core/hle/kernel/shared_page.h" +#include "core/hle/service/mcu/mcu_rtc.h" #include "core/hle/service/ptm/ptm.h" #include "core/hle/service/ptm/ptm_gets.h" #include "core/hle/service/ptm/ptm_play.h" @@ -133,6 +134,51 @@ void Module::Interface::CheckNew3DS(Kernel::HLERequestContext& ctx) { Service::PTM::CheckNew3DS(rb); } +void Module::Interface::SetInfoLEDPattern(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto pat = rp.PopRaw(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto mcu_rtc = MCU::RTC::GetService(ptm->system); + if (mcu_rtc) { + mcu_rtc->UpdateInfoLEDPattern(pat); + rb.Push(ResultSuccess); + } else { + rb.Push(ResultUnknown); + } +} + +void Module::Interface::SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto head = rp.PopRaw(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto mcu_rtc = MCU::RTC::GetService(ptm->system); + if (mcu_rtc) { + mcu_rtc->UpdateInfoLEDHeader(head); + rb.Push(ResultSuccess); + } else { + rb.Push(ResultUnknown); + } +} + +void Module::Interface::GetInfoLEDStatus(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + + auto mcu_rtc = MCU::RTC::GetService(ptm->system); + if (mcu_rtc) { + rb.Push(ResultSuccess); + rb.Push(static_cast(mcu_rtc->GetInfoLEDStatusFinished())); + } else { + rb.Push(ResultUnknown); + rb.Push(u8{}); + } +} + void Module::Interface::GetSystemTime(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); diff --git a/src/core/hle/service/ptm/ptm.h b/src/core/hle/service/ptm/ptm.h index 2e3e5af55..95863350a 100644 --- a/src/core/hle/service/ptm/ptm.h +++ b/src/core/hle/service/ptm/ptm.h @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -139,6 +139,12 @@ public: */ void CheckNew3DS(Kernel::HLERequestContext& ctx); + void SetInfoLEDPattern(Kernel::HLERequestContext& ctx); + + void SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx); + + void GetInfoLEDStatus(Kernel::HLERequestContext& ctx); + /** * PTM::GetSystemTime service function * Outputs: diff --git a/src/core/hle/service/ptm/ptm_sysm.cpp b/src/core/hle/service/ptm/ptm_sysm.cpp index f827517d3..aced50518 100644 --- a/src/core/hle/service/ptm/ptm_sysm.cpp +++ b/src/core/hle/service/ptm/ptm_sysm.cpp @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -41,9 +41,9 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr ptm, const char* name) {0x0408, nullptr, "Awake"}, {0x0409, nullptr, "RebootAsync"}, {0x040A, &PTM_S_Common::CheckNew3DS, "CheckNew3DS"}, - {0x0801, nullptr, "SetInfoLEDPattern"}, - {0x0802, nullptr, "SetInfoLEDPatternHeader"}, - {0x0803, nullptr, "GetInfoLEDStatus"}, + {0x0801, &PTM_S_Common::SetInfoLEDPattern, "SetInfoLEDPattern"}, + {0x0802, &PTM_S_Common::SetInfoLEDPatternHeader, "SetInfoLEDPatternHeader"}, + {0x0803, &PTM_S_Common::GetInfoLEDStatus, "GetInfoLEDStatus"}, {0x0804, nullptr, "SetBatteryEmptyLEDPattern"}, {0x0805, nullptr, "ClearStepHistory"}, {0x0806, nullptr, "SetStepHistory"}, diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp index 8436f2be5..59971588c 100644 --- a/src/core/hle/service/service.cpp +++ b/src/core/hle/service/service.cpp @@ -69,7 +69,7 @@ const std::array service_module_map{ {"AC", 0x00040130'00002402, AC::InstallInterfaces, false}, {"ACT", 0x00040130'00003802, ACT::InstallInterfaces, true}, {"AM", 0x00040130'00001502, AM::InstallInterfaces, false}, - {"BOSS", 0x00040130'00003402, BOSS::InstallInterfaces, false}, + {"BOSS", 0x00040130'00003402, BOSS::InstallInterfaces, true}, {"CAM", 0x00040130'00001602, [](Core::System& system) { CAM::InstallInterfaces(system); diff --git a/src/core/hle/service/soc/soc_u.cpp b/src/core/hle/service/soc/soc_u.cpp index d4ae261bf..8971a19a5 100644 --- a/src/core/hle/service/soc/soc_u.cpp +++ b/src/core/hle/service/soc/soc_u.cpp @@ -262,6 +262,14 @@ static const std::unordered_map error_map = {{ {ERRNO(ETIMEDOUT), 76}, }}; +static const std::unordered_map gai_error_map = {{ + {EAI_AGAIN, 302}, + {EAI_FAMILY, 303}, + {EAI_MEMORY, 304}, + {EAI_NONAME, 305}, + {EAI_SOCKTYPE, 307}, +}}; + /// Converts a network error from platform-specific to 3ds-specific static int TranslateError(int error) { const auto& found = error_map.find(error); @@ -271,6 +279,15 @@ static int TranslateError(int error) { return error; } +/// Converts a getaddrinfo/getnameinfo error from platform-specific to 3ds-specific +static int TranslateGaiError(int gai_error) { + if (const auto& known_soc_errno = gai_error_map.find(gai_error); + known_soc_errno != gai_error_map.end()) { + return -known_soc_errno->second; + } + return gai_error; +} + struct CTRLinger { u32_le l_onoff; u32_le l_linger; @@ -1169,9 +1186,10 @@ void SOC_U::SendToOther(Kernel::HLERequestContext& ctx) { LOG_SEND_RECV(Service_SOC, "called, fd={}, ret={}", socket_handle, static_cast(ret)); - IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); rb.Push(ResultSuccess); rb.Push(ret); + rb.PushMappedBuffer(input_mapped_buff); } s32 SOC_U::SendToImpl(SocketHolder& holder, u32 len, u32 flags, u32 addr_len, @@ -2049,14 +2067,18 @@ void SOC_U::GetAddrInfoImpl(Kernel::HLERequestContext& ctx) { std::vector out_buff(out_size); u32 count = 0; - if (ret == SOCKET_ERROR_VALUE) { - ret = TranslateError(GET_ERRNO); + if (ret != 0) { +#ifdef _WIN32 + ret = TranslateGaiError(ret); +#else + ret = ret == EAI_SYSTEM ? TranslateError(GET_ERRNO) : TranslateGaiError(ret); +#endif out_buff.resize(0); } else { std::size_t pos = 0; addrinfo* cur = out; while (cur != nullptr) { - if (pos <= out_size - sizeof(CTRAddrInfo)) { + if (sizeof(CTRAddrInfo) <= out_size - pos) { // According to 3dbrew, this function fills whatever it can and does not error even // if the buffer is not big enough. However the count returned is always correct. CTRAddrInfo ctr_addr = CTRAddrInfo::FromPlatform(*cur); @@ -2096,8 +2118,12 @@ void SOC_U::GetNameInfoImpl(Kernel::HLERequestContext& ctx) { s32 ret = getnameinfo(reinterpret_cast(&sa), sa_len, host_data, hostlen, serv_data, servlen, flags); - if (ret == SOCKET_ERROR_VALUE) { - ret = TranslateError(GET_ERRNO); + if (ret != 0) { +#ifdef _WIN32 + ret = TranslateGaiError(ret); +#else + ret = ret == EAI_SYSTEM ? TranslateError(GET_ERRNO) : TranslateGaiError(ret); +#endif } IPC::RequestBuilder rb = rp.MakeBuilder(2, 4); diff --git a/src/core/hw/aes/key.cpp b/src/core/hw/aes/key.cpp index 2e46de838..80aa84d36 100644 --- a/src/core/hw/aes/key.cpp +++ b/src/core/hw/aes/key.cpp @@ -18,7 +18,9 @@ #include "core/hle/service/fs/archive.h" #include "core/hw/aes/arithmetic128.h" #include "core/hw/aes/key.h" +#ifdef ENABLE_BUILTIN_KEYBLOB #include "core/hw/default_keys.h" +#endif // ENABLE_BUILTIN_KEYBLOB #include "core/hw/rsa/rsa.h" #include "core/loader/loader.h" @@ -130,8 +132,8 @@ std::array, NumDlpNfcKeyYs> dlp_nfc_key_y_slots; std::array nfc_secrets; AESIV nfc_iv; -AESKey otp_key; -AESIV otp_iv; +AESKey otp_key{}; +AESIV otp_iv{}; // gets xor'd with the mac address to produce the final iv AESIV dlp_checksum_mod_iv; @@ -297,6 +299,7 @@ std::istringstream GetKeysStream() { if (file.is_open()) { return std::istringstream(std::string(std::istreambuf_iterator(file), {})); } else { +#ifdef ENABLE_BUILTIN_KEYBLOB // The key data is encrypted in the source to prevent easy access to it for unintended // purposes. std::vector kiv(16); @@ -304,6 +307,9 @@ std::istringstream GetKeysStream() { CryptoPP::CBC_Mode::Decryption(kiv.data(), kiv.size(), kiv.data()) .ProcessData(reinterpret_cast(s.data()), default_keys_enc, s.size()); return std::istringstream(s); +#else + return std::istringstream(""); +#endif // ENABLE_BUILTIN_KEYBLOB } } diff --git a/src/core/hw/default_keys.h b/src/core/hw/default_keys.h index 554b74ae1..22fdcdc64 100644 --- a/src/core/hw/default_keys.h +++ b/src/core/hw/default_keys.h @@ -2,7 +2,13 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -unsigned char default_keys_enc[] = { +#pragma once + +#ifndef ENABLE_BUILTIN_KEYBLOB +#error Attempting to include default_keys.h, but ENABLE_BUILTIN_KEYBLOB is disabled. +#endif + +constexpr unsigned char default_keys_enc[] = { 0x4E, 0x81, 0xE9, 0x54, 0xCC, 0xDE, 0xFD, 0x56, 0x7D, 0xD2, 0x72, 0xE6, 0xD9, 0xCD, 0x8E, 0x11, 0xE1, 0x7F, 0x74, 0xF4, 0xFC, 0x54, 0xA6, 0xA4, 0x27, 0xC2, 0xD7, 0x50, 0xEA, 0xE7, 0xBE, 0xC9, 0xA7, 0x5E, 0xE0, 0x2E, 0x4A, 0xBE, 0xF5, 0xD5, 0x0D, 0x22, 0x76, 0x2E, 0xB6, 0x80, 0xD8, 0x54, @@ -468,4 +474,4 @@ unsigned char default_keys_enc[] = { 0x14, 0x79, 0xD0, 0xA8, 0x3C, 0xB3, 0x46, 0xC3, 0xDA, 0x6C, 0x0C, 0xEC, 0x2A, 0xB2, 0x9B, 0x21, 0xB2, 0xAD, 0x8C, 0x0C, 0x85, 0x9A, 0x8D, 0x7C, 0x10, 0xEA, 0x51, 0x1D, 0x2D, 0xDE, 0x7D, 0x8F}; -const long int default_keys_enc_size = sizeof(default_keys_enc); +constexpr long int default_keys_enc_size = sizeof(default_keys_enc); diff --git a/src/core/hw/rsa/rsa.h b/src/core/hw/rsa/rsa.h index b6dd2c1ca..54e944caa 100644 --- a/src/core/hw/rsa/rsa.h +++ b/src/core/hw/rsa/rsa.h @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -14,7 +14,8 @@ class RsaSlot { public: RsaSlot() = default; RsaSlot(std::vector exponent, std::vector modulus) - : init(true), exponent(std::move(exponent)), modulus(std::move(modulus)) {} + : init_exponent(true), init_modulus(true), exponent(std::move(exponent)), + modulus(std::move(modulus)) {} std::vector ModularExponentiation(std::span message, int out_size_bytes = -1) const; @@ -25,11 +26,12 @@ public: explicit operator bool() const { // TODO(B3N30): Maybe check if exponent and modulus are vailid - return init; + return init_exponent && init_modulus; } void SetExponent(const std::vector& e) { exponent = e; + init_exponent = true; } const std::vector& GetExponent() const { @@ -38,6 +40,7 @@ public: void SetModulus(const std::vector& m) { modulus = m; + init_modulus = true; } const std::vector& GetModulus() const { @@ -46,6 +49,7 @@ public: void SetPrivateD(const std::vector& d) { private_d = d; + init_private_d = true; } const std::vector& GetPrivateD() const { @@ -53,7 +57,9 @@ public: } private: - bool init = false; + bool init_exponent = false; + bool init_modulus = false; + bool init_private_d = false; std::vector exponent; std::vector modulus; std::vector private_d; diff --git a/src/core/hw/unique_data.cpp b/src/core/hw/unique_data.cpp index 8d7ac9035..4ebc2091a 100644 --- a/src/core/hw/unique_data.cpp +++ b/src/core/hw/unique_data.cpp @@ -27,13 +27,17 @@ static MovableSedFull movable; static bool movable_signature_valid = false; bool SecureInfoA::VerifySignature() const { - return HW::RSA::GetSecureInfoSlot().Verify( - std::span(reinterpret_cast(&body), sizeof(body)), signature); + auto sec_info_slot = HW::RSA::GetSecureInfoSlot(); + return sec_info_slot && + sec_info_slot.Verify( + std::span(reinterpret_cast(&body), sizeof(body)), signature); } bool LocalFriendCodeSeedB::VerifySignature() const { - return HW::RSA::GetLocalFriendCodeSeedSlot().Verify( - std::span(reinterpret_cast(&body), sizeof(body)), signature); + auto lfcs_slot = HW::RSA::GetLocalFriendCodeSeedSlot(); + return lfcs_slot && + HW::RSA::GetLocalFriendCodeSeedSlot().Verify( + std::span(reinterpret_cast(&body), sizeof(body)), signature); } bool MovableSed::VerifySignature() const { @@ -42,6 +46,9 @@ bool MovableSed::VerifySignature() const { SecureDataLoadStatus LoadSecureInfoA() { if (secure_info_a.IsValid()) { + if (!HW::RSA::GetSecureInfoSlot()) { + return SecureDataLoadStatus::CannotValidateSignature; + } return secure_info_a_signature_valid ? SecureDataLoadStatus::Loaded : (secure_info_a_region_changed ? SecureDataLoadStatus::RegionChanged @@ -63,8 +70,11 @@ SecureDataLoadStatus LoadSecureInfoA() { return SecureDataLoadStatus::IOError; } - HW::AES::InitKeys(); secure_info_a_region_changed = false; + HW::AES::InitKeys(); + if (!HW::RSA::GetSecureInfoSlot()) { + return SecureDataLoadStatus::CannotValidateSignature; + } secure_info_a_signature_valid = secure_info_a.VerifySignature(); if (!secure_info_a_signature_valid) { // Check if the file has been region changed @@ -93,6 +103,9 @@ SecureDataLoadStatus LoadSecureInfoA() { SecureDataLoadStatus LoadLocalFriendCodeSeedB() { if (local_friend_code_seed_b.IsValid()) { + if (!HW::RSA::GetLocalFriendCodeSeedSlot()) { + return SecureDataLoadStatus::CannotValidateSignature; + } return local_friend_code_seed_b_signature_valid ? SecureDataLoadStatus::Loaded : SecureDataLoadStatus::InvalidSignature; } @@ -114,6 +127,9 @@ SecureDataLoadStatus LoadLocalFriendCodeSeedB() { } HW::AES::InitKeys(); + if (!HW::RSA::GetLocalFriendCodeSeedSlot()) { + return SecureDataLoadStatus::CannotValidateSignature; + } local_friend_code_seed_b_signature_valid = local_friend_code_seed_b.VerifySignature(); if (!local_friend_code_seed_b_signature_valid) { LOG_WARNING(HW, "LocalFriendCodeSeed_B signature check failed"); @@ -128,10 +144,17 @@ SecureDataLoadStatus LoadOTP() { return SecureDataLoadStatus::Loaded; } + auto is_all_zero = [](const auto& arr) { + return std::all_of(arr.begin(), arr.end(), [](auto x) { return x == 0; }); + }; + const std::string filepath = GetOTPPath(); HW::AES::InitKeys(); auto otp_keyiv = HW::AES::GetOTPKeyIV(); + if (is_all_zero(otp_keyiv.first) || is_all_zero(otp_keyiv.second)) { + return SecureDataLoadStatus::NoCryptoKeys; + } auto loader_status = otp.Load(filepath, otp_keyiv.first, otp_keyiv.second); if (loader_status != Loader::ResultStatus::Success) { @@ -169,6 +192,9 @@ SecureDataLoadStatus LoadOTP() { SecureDataLoadStatus LoadMovable() { if (movable.IsValid()) { + if (!HW::RSA::GetLocalFriendCodeSeedSlot()) { + return SecureDataLoadStatus::CannotValidateSignature; + } return movable_signature_valid ? SecureDataLoadStatus::Loaded : SecureDataLoadStatus::InvalidSignature; } @@ -193,6 +219,9 @@ SecureDataLoadStatus LoadMovable() { } HW::AES::InitKeys(); + if (!HW::RSA::GetLocalFriendCodeSeedSlot()) { + return SecureDataLoadStatus::CannotValidateSignature; + } movable_signature_valid = movable.VerifySignature(); if (!movable_signature_valid) { LOG_WARNING(HW, "movable.sed signature check failed"); diff --git a/src/core/hw/unique_data.h b/src/core/hw/unique_data.h index dd3a4bb8b..3b4f8efbe 100644 --- a/src/core/hw/unique_data.h +++ b/src/core/hw/unique_data.h @@ -136,10 +136,12 @@ enum class SecureDataLoadStatus { Loaded = 0, InvalidSignature = 1, RegionChanged = 2, + CannotValidateSignature = 3, NotFound = -1, Invalid = -2, IOError = -3, + NoCryptoKeys = -4, }; SecureDataLoadStatus LoadSecureInfoA(); diff --git a/src/core/loader/3dsx.cpp b/src/core/loader/3dsx.cpp index 83a41a5e7..163a7d472 100644 --- a/src/core/loader/3dsx.cpp +++ b/src/core/loader/3dsx.cpp @@ -261,15 +261,15 @@ AppLoader_THREEDSX::AppLoader_THREEDSX(Core::System& system_, FileUtil::IOFile&& } FileType AppLoader_THREEDSX::IdentifyType(FileUtil::IOFile* file) { - u32 magic; - file->Seek(0, SEEK_SET); - if (1 != file->ReadArray(&magic, 1)) - return FileType::Error; + u32 magic{}; - if (MakeMagic('3', 'D', 'S', 'X') == magic || - (MakeMagic('Z', '3', 'D', 'S') == magic && - FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file) == MakeMagic('3', 'D', 'S', 'X'))) - return FileType::THREEDSX; + if (file->Seek(0, SEEK_SET) && 1 == file->ReadArray(&magic, 1)) { + if (MakeMagic('3', 'D', 'S', 'X') == magic || + (MakeMagic('Z', '3', 'D', 'S') == magic && + FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file) == + MakeMagic('3', 'D', 'S', 'X'))) + return FileType::THREEDSX; + } return FileType::Error; } @@ -293,6 +293,8 @@ ResultStatus AppLoader_THREEDSX::Load(std::shared_ptr& process) process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + process->resource_limit->ApplyAppMaxCPUSetting(process, 1, 89); + // On real HW this is done with FS:Reg, but we can be lazy auto fs_user = system.ServiceManager().GetService("fs:USER"); fs_user->RegisterProgramInfo(process->GetObjectId(), process->codeset->program_id, filepath); diff --git a/src/core/loader/artic.cpp b/src/core/loader/artic.cpp index 9c7aefbeb..fbcbe2376 100644 --- a/src/core/loader/artic.cpp +++ b/src/core/loader/artic.cpp @@ -203,6 +203,14 @@ ResultStatus Apploader_Artic::LoadExecImpl(std::shared_ptr& pro exheader.arm11_system_local_caps.resource_limit_category); process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(category); + // Update application max cpu setting. PM module uses the launch flags to determine + // this, but using the resource limit category is close enough. + if (category == Kernel::ResourceLimitCategory::Application) { + process->resource_limit->ApplyAppMaxCPUSetting( + process, exheader.arm11_system_local_caps.schedule_mode, + exheader.arm11_system_local_caps.max_cpu); + } + // When running N3DS-unaware titles pm will lie about the amount of memory available. // This means RESLIMIT_COMMIT = APPMEMALLOC doesn't correspond to the actual size of // APPLICATION. See: diff --git a/src/core/loader/elf.cpp b/src/core/loader/elf.cpp index c03ef3267..916ec2fc9 100644 --- a/src/core/loader/elf.cpp +++ b/src/core/loader/elf.cpp @@ -357,13 +357,12 @@ SectionID ElfReader::GetSectionByName(const char* name, int firstSection) const namespace Loader { FileType AppLoader_ELF::IdentifyType(FileUtil::IOFile* file) { - u32 magic; - file->Seek(0, SEEK_SET); - if (1 != file->ReadArray(&magic, 1)) - return FileType::Error; + u32 magic{}; - if (MakeMagic('\x7f', 'E', 'L', 'F') == magic) - return FileType::ELF; + if (file->Seek(0, SEEK_SET) && 1 == file->ReadArray(&magic, 1)) { + if (MakeMagic('\x7f', 'E', 'L', 'F') == magic) + return FileType::ELF; + } return FileType::Error; } @@ -394,6 +393,8 @@ ResultStatus AppLoader_ELF::Load(std::shared_ptr& process) { process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(Kernel::ResourceLimitCategory::Application); + process->resource_limit->ApplyAppMaxCPUSetting(process, 1, 89); + process->Run(48, Kernel::DEFAULT_STACK_SIZE); is_loaded = true; diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 9e4c76122..678b48bf7 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -76,6 +76,8 @@ enum class ResultStatus { ErrorGbaTitle, ErrorArtic, ErrorNotFound, + ErrorPatches, + ErrorPatchesInvalidTitle, }; constexpr u32 MakeMagic(char a, char b, char c, char d) { diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index d988dbb83..2641b0c93 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -37,35 +37,17 @@ static constexpr u64 UPDATE_TID_HIGH = 0x0004000e00000000; static constexpr u64 DLP_CHILD_TID_HIGH = 0x0004000100000000; FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile* file) { - u32 magic; - file->Seek(0x100, SEEK_SET); - if (1 != file->ReadArray(&magic, 1)) - return FileType::Error; - - if (MakeMagic('N', 'C', 'S', 'D') == magic) - return FileType::CCI; - - if (MakeMagic('N', 'C', 'C', 'H') == magic) - return FileType::CXI; + u32 magic{}; std::unique_ptr file_crypto = HW::UniqueData::OpenUniqueCryptoFile( file->Filename(), "rb", HW::UniqueData::UniqueCryptoFileID::NCCH); - file_crypto->Seek(0x100, SEEK_SET); - if (1 != file_crypto->ReadArray(&magic, 1)) - return FileType::Error; - - if (MakeMagic('N', 'C', 'S', 'D') == magic) - return FileType::CCI; - - if (MakeMagic('N', 'C', 'C', 'H') == magic) - return FileType::CXI; - + // Check compressed NCCH file std::optional magic_zstd = FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file); if (!magic_zstd.has_value()) { + // Handle compressed and crypto NCCH file magic_zstd = FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file_crypto.get()); } - if (magic_zstd.has_value()) { if (MakeMagic('N', 'C', 'S', 'D') == magic_zstd) return FileType::CCI; @@ -74,6 +56,24 @@ FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile* file) { return FileType::CXI; } + // Check normal NCCH file + if (file->Seek(0x100, SEEK_SET) && 1 == file->ReadArray(&magic, 1)) { + if (MakeMagic('N', 'C', 'S', 'D') == magic) + return FileType::CCI; + + if (MakeMagic('N', 'C', 'C', 'H') == magic) + return FileType::CXI; + } + + // Check crypto NCCH file + if (file_crypto->Seek(0x100, SEEK_SET) && 1 == file_crypto->ReadArray(&magic, 1)) { + if (MakeMagic('N', 'C', 'S', 'D') == magic) + return FileType::CCI; + + if (MakeMagic('N', 'C', 'C', 'H') == magic) + return FileType::CXI; + } + return FileType::Error; } @@ -202,6 +202,14 @@ ResultStatus AppLoader_NCCH::LoadExec(std::shared_ptr& process) overlay_ncch->exheader_header.arm11_system_local_caps.resource_limit_category); process->resource_limit = system.Kernel().ResourceLimit().GetForCategory(category); + // Update application max cpu setting. PM module uses the launch flags to determine + // this, but using the resource limit category is close enough. + if (category == Kernel::ResourceLimitCategory::Application) { + process->resource_limit->ApplyAppMaxCPUSetting( + process, overlay_ncch->exheader_header.arm11_system_local_caps.schedule_mode, + overlay_ncch->exheader_header.arm11_system_local_caps.max_cpu); + } + // When running N3DS-unaware titles pm will lie about the amount of memory available. // This means RESLIMIT_COMMIT = APPMEMALLOC doesn't correspond to the actual size of // APPLICATION. See: diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 41ad049e5..ee446eb6d 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -12,10 +13,14 @@ #include "common/atomic_ops.h" #include "common/common_types.h" #include "common/logging/log.h" +#include "common/optional_helper.h" #include "common/settings.h" #include "common/swap.h" #include "core/arm/arm_interface.h" #include "core/core.h" +#ifdef ENABLE_GDBSTUB +#include "core/gdbstub/gdbstub.h" +#endif #include "core/global.h" #include "core/hle/kernel/process.h" #include "core/hle/service/plgldr/plgldr.h" @@ -28,6 +33,14 @@ SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl) SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl) +#ifndef SIGTRAP +constexpr u32 SIGTRAP = 5; +#endif + +#ifndef SIGSEGV +constexpr u32 SIGSEGV = 11; +#endif + namespace Memory { void PageTable::Clear() { @@ -187,7 +200,17 @@ public: std::memcpy(dest_buffer, src_ptr, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + const u8* src_ptr = it->second.memory.GetPtr() + page_offset; + std::memcpy(dest_buffer, src_ptr, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { if constexpr (!UNSAFE) { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Flush); @@ -235,7 +258,17 @@ public: std::memcpy(dest_ptr, src_buffer, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + u8* dest_ptr = it->second.memory.GetPtr() + page_offset; + std::memcpy(dest_ptr, src_buffer, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { if constexpr (!UNSAFE) { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Invalidate); @@ -392,6 +425,88 @@ PAddr& Memory::MemorySystem::Plugin3GXFramebufferAddress() { return impl->plugin_fb_address; } +void MemorySystem::RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) { + auto& page_table = *process.vm_manager.page_table; + + VAddr current = addr; + VAddr end = addr + size; + + while (current < end) { + const VAddr page_base = (current & ~CITRA_PAGE_MASK); + const VAddr page_index = page_base >> CITRA_PAGE_BITS; + + auto it = page_table.watchpoint_pages_map.find(page_index); + if (it != page_table.watchpoint_pages_map.end()) { + // Nothing to do, only increment count. + it->second.watchpoint_count++; + } else { + MemoryRef mem; + PageType& type = page_table.attributes[page_index]; + + switch (type) { + case PageType::Memory: + mem = page_table.pointers.Ref(page_index); + type = PageType::MemoryWatchpoint; + page_table.pointers[page_index] = nullptr; + break; + case PageType::RasterizerCachedMemory: + mem = GetPointerForRasterizerCache(page_base); + type = PageType::RasterizerCachedMemoryWatchpoint; + break; + default: + LOG_ERROR(HW_Memory, "Cannot get pointer to register watchpoint for page 0x{:08X}", + page_base); + continue; + } + + page_table.watchpoint_pages_map.insert( + {page_index, + PageTable::WatchpointPageInfo{.watchpoint_count = 1, .memory = std::move(mem)}}); + } + + current = page_base + CITRA_PAGE_SIZE; + } +} + +void MemorySystem::UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) { + auto& page_table = *process.vm_manager.page_table; + + VAddr current = addr; + VAddr end = addr + size; + + while (current < end) { + const VAddr page_base = (current & ~CITRA_PAGE_MASK); + const VAddr page_index = page_base >> CITRA_PAGE_BITS; + + auto it = page_table.watchpoint_pages_map.find(page_index); + if (it != page_table.watchpoint_pages_map.end()) { + if (--it->second.watchpoint_count == 0) { + + PageType& type = page_table.attributes[page_index]; + + switch (type) { + case PageType::MemoryWatchpoint: + type = PageType::Memory; + page_table.pointers[page_index] = it->second.memory; + break; + case PageType::RasterizerCachedMemoryWatchpoint: + type = PageType::RasterizerCachedMemory; + break; + default: + LOG_ERROR(HW_Memory, "Invalid watchpoint page type for page 0x{:08X}: {}", + page_base, static_cast(type)); + } + + page_table.watchpoint_pages_map.erase(page_index); + } + } else { + LOG_ERROR(HW_Memory, "No watchpoint found on page 0x{:08X}", page_base); + } + + current = page_base + CITRA_PAGE_SIZE; + } +} + void MemorySystem::MapPages(PageTable& page_table, u32 base, u32 size, MemoryRef memory, PageType type) { LOG_DEBUG(HW_Memory, "Mapping {} onto {:08X}-{:08X}", (void*)memory.GetPtr(), @@ -449,13 +564,37 @@ void MemorySystem::UnregisterPageTable(std::shared_ptr page_table) { } } +template +void MemorySystem::UnmappedAccess(const VAddr vaddr, const T value, bool read) { + const std::string mode = (read ? "Read" : "Write"); + const std::string value_str = read ? std::string("") : fmt::format(" 0x{:08X}", value); + const std::string message = fmt::format("unmapped {}{}{} @ 0x{:08X} at PC 0x{:08X}", mode, + sizeof(T) * 8, value_str, vaddr, impl->GetPC()); +#ifdef ENABLE_GDBSTUB + if (GDBStub::IsConnected()) { + GDBStub::Break(SIGSEGV); + } else +#endif + if (Settings::values.break_on_unmapped_memory_access) { + impl->system.SetStatus(Core::System::ResultStatus::ErrorMemoryExceptionRaised, + message.c_str()); + } + + LOG_ERROR(HW_Memory, "{}", message); +} + template T MemorySystem::Read(const std::shared_ptr& page_table, const VAddr vaddr) { + constexpr bool is_optional = is_optional_type; + using ReadType = optional_inner_or_type; + + constexpr size_t read_size = sizeof(ReadType); + const u8* page_pointer = page_table->pointers[vaddr >> CITRA_PAGE_BITS]; if (page_pointer) { // NOTE: Avoid adding any extra logic to this fast-path block - T value; - std::memcpy(&value, &page_pointer[vaddr & CITRA_PAGE_MASK], sizeof(T)); + ReadType value; + std::memcpy(&value, &page_pointer[vaddr & CITRA_PAGE_MASK], read_size); return value; } @@ -464,37 +603,77 @@ T MemorySystem::Read(const std::shared_ptr& page_table, const VAddr v if (vaddr & (1 << 31)) { PAddr paddr = (vaddr & ~(1 << 31)); if ((paddr & 0xF0000000) == Memory::FCRAM_PADDR) { // Check FCRAM region - T value; - std::memcpy(&value, GetFCRAMPointer(paddr - Memory::FCRAM_PADDR), sizeof(T)); + ReadType value; + std::memcpy(&value, GetFCRAMPointer(paddr - Memory::FCRAM_PADDR), read_size); return value; } else if ((paddr & 0xF0000000) == 0x10000000 && paddr >= Memory::IO_AREA_PADDR) { // Check MMIO region - return impl->system.GPU().ReadReg(static_cast(paddr) - Memory::IO_AREA_PADDR + - 0x1EC00000); + return static_cast(impl->system.GPU().ReadReg( + static_cast(paddr) - Memory::IO_AREA_PADDR + 0x1EC00000)); } } PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS]; switch (type) { - case PageType::Unmapped: - LOG_ERROR(HW_Memory, "unmapped Read{} @ 0x{:08X} at PC 0x{:08X}", sizeof(T) * 8, vaddr, - impl->GetPC()); - return 0; + case PageType::Unmapped: { + + UnmappedAccess(vaddr, 0, true); + + if constexpr (is_optional) { + return std::nullopt; + } else { + return T{}; + } + } case PageType::Memory: ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr); break; - case PageType::RasterizerCachedMemory: { - RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Flush); + case PageType::MemoryWatchpoint: { + auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS); + ASSERT_MSG(it != page_table->watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + ReadType value; + std::memcpy(&value, it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), read_size); + +#ifdef ENABLE_GDBSTUB + if (GDBStub::CheckBreakpoint(vaddr, read_size, GDBStub::BreakpointType::Read)) { + GDBStub::Break(SIGTRAP); + } +#endif + + return value; + } + [[likely]] case PageType::RasterizerCachedMemory: { + RasterizerFlushVirtualRegion(vaddr, read_size, FlushMode::Flush); + + ReadType value; + std::memcpy(&value, GetPointerForRasterizerCache(vaddr), read_size); + return value; + } + case PageType::RasterizerCachedMemoryWatchpoint: { + RasterizerFlushVirtualRegion(vaddr, read_size, FlushMode::Flush); + + ReadType value; + std::memcpy(&value, GetPointerForRasterizerCache(vaddr), read_size); + +#ifdef ENABLE_GDBSTUB + if (GDBStub::CheckBreakpoint(vaddr, read_size, GDBStub::BreakpointType::Read)) { + GDBStub::Break(SIGTRAP); + } +#endif - T value; - std::memcpy(&value, GetPointerForRasterizerCache(vaddr), sizeof(T)); return value; } default: UNREACHABLE(); } - return T{}; + if constexpr (is_optional) { + return std::nullopt; + } else { + return T{}; + } } template @@ -527,17 +706,43 @@ void MemorySystem::Write(const std::shared_ptr& page_table, const VAd PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS]; switch (type) { case PageType::Unmapped: - LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}", - sizeof(data) * 8, (u32)data, vaddr, impl->GetPC()); + (void)UnmappedAccess(vaddr, data, false); return; case PageType::Memory: ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr); break; - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS); + ASSERT_MSG(it != page_table->watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + std::memcpy(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), &data, sizeof(T)); + +#ifdef ENABLE_GDBSTUB + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } +#endif + + break; + } + [[likely]] case PageType::RasterizerCachedMemory: { RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T)); break; } + case PageType::RasterizerCachedMemoryWatchpoint: { + RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); + std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T)); + +#ifdef ENABLE_GDBSTUB + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } +#endif + + break; + } default: UNREACHABLE(); } @@ -556,18 +761,48 @@ bool MemorySystem::WriteExclusive(const VAddr vaddr, const T data, const T expec PageType type = impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS]; switch (type) { case PageType::Unmapped: - LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}", - sizeof(data) * 8, static_cast(data), vaddr, impl->GetPC()); + (void)UnmappedAccess(vaddr, data, false); return true; case PageType::Memory: ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr); return true; - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = impl->current_page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS); + ASSERT_MSG(it != impl->current_page_table->watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + const auto volatile_pointer = + reinterpret_cast(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK)); + + bool ret = Common::AtomicCompareAndSwap(volatile_pointer, data, expected); + +#ifdef ENABLE_GDBSTUB + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } +#endif + + return ret; + } + [[likely]] case PageType::RasterizerCachedMemory: { RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); const auto volatile_pointer = reinterpret_cast(GetPointerForRasterizerCache(vaddr).GetPtr()); return Common::AtomicCompareAndSwap(volatile_pointer, data, expected); } + case PageType::RasterizerCachedMemoryWatchpoint: { + RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate); + const auto volatile_pointer = + reinterpret_cast(GetPointerForRasterizerCache(vaddr).GetPtr()); + +#ifdef ENABLE_GDBSTUB + if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) { + GDBStub::Break(SIGTRAP); + } +#endif + + return Common::AtomicCompareAndSwap(volatile_pointer, data, expected); + } default: UNREACHABLE(); } @@ -582,7 +817,7 @@ bool MemorySystem::IsValidVirtualAddress(const Kernel::Process& process, const V return true; } - if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] == PageType::RasterizerCachedMemory) { + if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] != PageType::Unmapped) { return true; } @@ -600,7 +835,9 @@ u8* MemorySystem::GetPointer(const VAddr vaddr) { } if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == - PageType::RasterizerCachedMemory) { + PageType::RasterizerCachedMemory || + impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == + PageType::RasterizerCachedMemoryWatchpoint) { return GetPointerForRasterizerCache(vaddr); } @@ -615,7 +852,9 @@ const u8* MemorySystem::GetPointer(const VAddr vaddr) const { } if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == - PageType::RasterizerCachedMemory) { + PageType::RasterizerCachedMemory || + impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == + PageType::RasterizerCachedMemoryWatchpoint) { return GetPointerForRasterizerCache(vaddr); } @@ -755,7 +994,10 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached // address space, for example, a system module need not have a VRAM mapping. break; case PageType::Memory: - page_type = PageType::RasterizerCachedMemory; + case PageType::MemoryWatchpoint: + page_type = (page_type == PageType::Memory) + ? PageType::RasterizerCachedMemory + : PageType::RasterizerCachedMemoryWatchpoint; page_table->pointers[vaddr >> CITRA_PAGE_BITS] = nullptr; break; default: @@ -768,10 +1010,16 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached // It is not necessary for a process to have this region mapped into its // address space, for example, a system module need not have a VRAM mapping. break; - case PageType::RasterizerCachedMemory: { - page_type = PageType::Memory; - page_table->pointers[vaddr >> CITRA_PAGE_BITS] = - GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK); + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { + page_type = (page_type == PageType::RasterizerCachedMemory) + ? PageType::Memory + : PageType::MemoryWatchpoint; + + if (page_type == PageType::Memory) { + page_table->pointers[vaddr >> CITRA_PAGE_BITS] = + GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK); + } break; } default: @@ -815,6 +1063,14 @@ u64 MemorySystem::Read64(const Kernel::Process& process, VAddr addr) { return Read(process.vm_manager.page_table, addr); } +std::optional MemorySystem::Read32OrNullopt(VAddr addr) { + return Read>(impl->current_page_table, addr); +} + +std::optional MemorySystem::Read32OrNullopt(const Kernel::Process& process, VAddr addr) { + return Read>(process.vm_manager.page_table, addr); +} + void MemorySystem::ReadBlock(const Kernel::Process& process, const VAddr src_addr, void* dest_buffer, const std::size_t size) { return impl->ReadBlockImpl(process, src_addr, dest_buffer, size); @@ -911,7 +1167,17 @@ void MemorySystem::ZeroBlock(const Kernel::Process& process, const VAddr dest_ad std::memset(dest_ptr, 0, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + u8* dest_ptr = it->second.memory.GetPtr() + page_offset; + std::memset(dest_ptr, 0, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Invalidate); std::memset(GetPointerForRasterizerCache(current_vaddr), 0, copy_amount); @@ -960,7 +1226,17 @@ void MemorySystem::CopyBlock(const Kernel::Process& dest_process, WriteBlock(dest_process, dest_addr, src_ptr, copy_amount); break; } - case PageType::RasterizerCachedMemory: { + case PageType::MemoryWatchpoint: { + auto it = page_table.watchpoint_pages_map.find(page_index); + ASSERT_MSG(it != page_table.watchpoint_pages_map.end(), + "Missing memory for watchpoint page"); + + const u8* src_ptr = it->second.memory.GetPtr() + page_offset; + WriteBlock(dest_process, dest_addr, src_ptr, copy_amount); + break; + } + case PageType::RasterizerCachedMemory: + case PageType::RasterizerCachedMemoryWatchpoint: { RasterizerFlushVirtualRegion(current_vaddr, static_cast(copy_amount), FlushMode::Flush); WriteBlock(dest_process, dest_addr, GetPointerForRasterizerCache(current_vaddr), diff --git a/src/core/memory.h b/src/core/memory.h index 5c215b3f2..d4f09fa9e 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -5,11 +5,13 @@ #pragma once #include #include +#include #include #include #include #include "common/common_types.h" #include "common/memory_ref.h" +#include "common/swap.h" namespace Kernel { class Process; @@ -34,7 +36,7 @@ constexpr u32 CITRA_PAGE_MASK = CITRA_PAGE_SIZE - 1; constexpr int CITRA_PAGE_BITS = 12; constexpr std::size_t PAGE_TABLE_NUM_ENTRIES = 1 << (32 - CITRA_PAGE_BITS); -enum class PageType { +enum class PageType : u8 { /// Page is unmapped and should cause an access error. Unmapped, /// Page is mapped to regular memory. This is the only type you can get pointers to. @@ -42,6 +44,12 @@ enum class PageType { /// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and /// invalidation RasterizerCachedMemory, + /// Page is mapped to regular memory. Furthermore a debug watchpoint is set to an address within + /// the page. + MemoryWatchpoint, + /// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and + /// invalidation. Furthermore a debug watchpoint is set to an address within the page. + RasterizerCachedMemoryWatchpoint, }; /** @@ -82,6 +90,10 @@ struct PageTable { return Entry(*this, static_cast(idx)); } + const MemoryRef& Ref(std::size_t idx) { + return refs[idx]; + } + private: std::array raw; std::array refs; @@ -100,6 +112,22 @@ struct PageTable { return pointers.raw; } + struct WatchpointPageInfo { + u32 watchpoint_count{}; + MemoryRef memory; + + template + void serialize(Archive& ar, const unsigned int) { + ar & watchpoint_count; + ar & memory; + } + }; + + // Map holding pages that are marked to contain watchpoints. We don't need + // any fancy performance tricks here, as watchpoints are only used rarely + // while debugging and performance is not a priority in such cases. + std::unordered_map watchpoint_pages_map{}; + void Clear(); private: @@ -107,6 +135,7 @@ private: void serialize(Archive& ar, const unsigned int) { ar & pointers.refs; ar & attributes; + ar & watchpoint_pages_map; for (std::size_t i = 0; i < PAGE_TABLE_NUM_ENTRIES; i++) { pointers.raw[i] = pointers.refs[i].GetPtr(); } @@ -360,6 +389,29 @@ public: */ u64 Read64(const Kernel::Process& process, VAddr addr); + /** + * Reads a 32-bit unsigned value from the current process' address space + * at the given virtual address. If the address is invalid std::nullopt + * is returned instead. + * + * @param addr The virtual address to read the 32-bit value from. + * + * @returns the read 32-bit unsigned value or std::nullopt. + */ + std::optional Read32OrNullopt(VAddr addr); + + /** + * Reads a 32-bit unsigned value from the process' address space + * at the given virtual address. If the address is invalid std::nullopt + * is returned instead. + * + * @param process The process to read from. + * @param addr The virtual address to read the 32-bit value from. + * + * @returns the read 32-bit unsigned value or std::nullopt. + */ + std::optional Read32OrNullopt(const Kernel::Process& process, VAddr addr); + /** * Writes an 8-bit unsigned integer to the given virtual address in * the current process' address space. @@ -649,7 +701,14 @@ public: /// Returns a reference to the framebuffer address of the currently loaded 3GX plugin. PAddr& Plugin3GXFramebufferAddress(); + void RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size); + + void UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size); + private: + template + void UnmappedAccess(const VAddr vaddr, const T value, bool read); + template T Read(const std::shared_ptr& page_table, const VAddr vaddr); diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 70d36cbfd..2226f7931 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(input_common STATIC +add_library(input_common STATIC EXCLUDE_FROM_ALL analog_from_button.cpp analog_from_button.h keyboard.cpp diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 3feb6f479..365bd6877 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(network STATIC +add_library(network STATIC EXCLUDE_FROM_ALL announce_multiplayer_session.cpp announce_multiplayer_session.h artic_base/artic_base_client.cpp diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index f248ece4b..cf99e004d 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -23,6 +23,11 @@ add_executable(tests create_target_directory_groups(tests) +if (BSD STREQUAL "NetBSD") + include(DisablePaxMprotect) + disable_pax_mprotect(tests) +endif() + target_link_libraries(tests PRIVATE citra_common citra_core video_core audio_core) target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch2 nihstro-headers Threads::Threads) @@ -31,6 +36,10 @@ if (ENABLE_LIBRETRO) endif() add_test(NAME tests COMMAND tests) +if(NOT ANDROID AND (CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)) + catch_discover_tests(tests) +endif() + if (CITRA_USE_PRECOMPILED_HEADERS) target_precompile_headers(tests PRIVATE precompiled_headers.h) diff --git a/src/tests/common/file_util.cpp b/src/tests/common/file_util.cpp index bd7fcbdd9..72734e0a9 100644 --- a/src/tests/common/file_util.cpp +++ b/src/tests/common/file_util.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -24,3 +24,13 @@ TEST_CASE("SplitFilename83 Sanity", "[common]") { REQUIRE(std::memcmp(short_name.data(), expected_short_name.data(), short_name.size()) == 0); REQUIRE(std::memcmp(extension.data(), expected_extension.data(), extension.size()) == 0); } + +#if defined(__APPLE__) + +TEST_CASE("NormalizeNFDToNFC Sanity", "[common]") { + const std::string decomposed = "i\xCC\x81"; + const std::string composed = "\xC3\xAD"; + + REQUIRE(Common::NormalizeNFDToNFC(decomposed) == composed); +} +#endif \ No newline at end of file diff --git a/src/tests/video_core/shader.cpp b/src/tests/video_core/shader.cpp index db868a260..4ea976668 100644 --- a/src/tests/video_core/shader.cpp +++ b/src/tests/video_core/shader.cpp @@ -481,6 +481,39 @@ SHADER_TEST_CASE("RSQ", "[video_core][shader]") { REQUIRE(shader.Run({0.0625f}).x == Catch::Approx(4.0f).margin(0.004f)); } +SHADER_TEST_CASE("SETEMIT", "[video_core][shader]") { + Pica::GeometryEmitter geometry_emitter; + + for (u8 winding = 0; winding <= 1; ++winding) { + for (u8 prim_emit = 0; prim_emit <= 1; ++prim_emit) { + for (u8 vertex_id = 0; vertex_id <= 3; ++vertex_id) { + auto shader_setup = CompileShaderSetup({ + {OpCode::Id::NOP}, // setemit + {OpCode::Id::END}, + }); + + // nihstro does not support the SETEMIT instructions, so the instruction-binary must + // be manually + // inserted here: + nihstro::Instruction SETEMIT = {}; + SETEMIT.opcode = nihstro::OpCode(nihstro::OpCode::Id::SETEMIT); + SETEMIT.setemit.winding.Assign(winding); + SETEMIT.setemit.prim_emit.Assign(prim_emit); + SETEMIT.setemit.vertex_id.Assign(vertex_id); + shader_setup->UpdateProgramCode(0, SETEMIT.hex); + + auto shader = TestType(std::move(shader_setup)); + Pica::ShaderUnit shader_unit(&geometry_emitter); + shader.Run(shader_unit, 1.0f); + + REQUIRE(geometry_emitter.emit_state.winding == winding); + REQUIRE(geometry_emitter.emit_state.prim_emit == prim_emit); + REQUIRE(geometry_emitter.emit_state.vertex_id == vertex_id); + } + } + } +} + SHADER_TEST_CASE("Uniform Read", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_c0 = SourceRegister::MakeFloat(0); diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index a8ade344d..528072c9a 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -1,6 +1,6 @@ add_subdirectory(host_shaders) -add_library(video_core STATIC +add_library(video_core STATIC EXCLUDE_FROM_ALL custom_textures/custom_format.cpp custom_textures/custom_format.h custom_textures/custom_tex_manager.cpp diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 9a76f4859..735144f42 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -26,6 +26,63 @@ namespace VideoCore { constexpr VAddr VADDR_LCD = 0x1ED02000; constexpr VAddr VADDR_GPU = 0x1EF00000; +class DelayGenerator { +private: + DelayGenerator() = default; + + // Average transfer speed based on measurements taken from real + // hardware. 4 different modes have been taken into consideration: + // RAM -> RAM, RAM -> VRAM, VRAM -> RAM and VRAM -> VRAM. + // Furthermore, measurements are split into DMA transfers and tex + // copies. For simplicity, we will assume fills are as fast as + // texture copies. + + static constexpr double mibps_to_ns_per_byte(double mib_per_sec) { + return 1'000'000'000.0 / (mib_per_sec * 1024.0 * 1024.0); + } + + static constexpr std::array, 2> speed_mibps = { + {{ + 190.0, // DMA RAMTORAM + 310.0, // DMA RAMTOVRAM + 380.0, // DMA VRAMTORAM + 380.0, // DMA VRAMTOVRAM + }, + { + 450.0, // TEX RAMTORAM + 3100.0, // TEX RAMTOVRAM + 5400.0, // TEX VRAMTORAM + 5400.0, // TEX VRAMTOVRAM + }}}; + +public: + enum class CopyMode { + RAMTORAM, + RAMTOVRAM, + VRAMTORAM, + VRAMTOVRAM, + }; + + static CopyMode GetCopyMode(bool input_vram, bool output_vram) { + if (!input_vram && !output_vram) { + return CopyMode::RAMTORAM; + } else if (!input_vram && output_vram) { + return CopyMode::RAMTOVRAM; + } else if (input_vram && !output_vram) { + return CopyMode::VRAMTORAM; + } else { + return CopyMode::VRAMTOVRAM; + } + } + + static u64 CalculateDelayNanoseconds(CopyMode mode, bool is_textre, size_t size) { + double base_ns_per_byte = + mibps_to_ns_per_byte(speed_mibps[is_textre][static_cast(mode)]); + + return static_cast(size * base_ns_per_byte); + } +}; + MICROPROFILE_DEFINE(GPU_DisplayTransfer, "GPU", "DisplayTransfer", MP_RGB(100, 100, 255)); MICROPROFILE_DEFINE(GPU_CmdlistProcessing, "GPU", "Cmdlist Processing", MP_RGB(100, 255, 100)); @@ -102,7 +159,17 @@ void GPU::Execute(const Service::GSP::Command& command) { const auto process = impl->system.Kernel().GetCurrentProcess(); impl->memory.CopyBlock(*process, command.dma_request.dest_address, command.dma_request.source_address, command.dma_request.size); - impl->signal_interrupt(Service::GSP::InterruptId::DMA); + + auto is_vram = [&](u32 addr) { + return addr >= Memory::VRAM_VADDR && addr <= Memory::VRAM_VADDR_END; + }; + + u64 delay = DelayGenerator::CalculateDelayNanoseconds( + DelayGenerator::GetCopyMode(is_vram(command.dma_request.source_address), + is_vram(command.dma_request.dest_address)), + false, command.dma_request.size); + + impl->signal_interrupt(Service::GSP::InterruptId::DMA, delay); break; } case CommandId::SubmitCmdList: { @@ -361,13 +428,18 @@ void GPU::MemoryFill(u32 index, u32 intr_index) { impl->sw_blitter->MemoryFill(config); } + // Treat fill as texture transfer from VRAM + u64 delay = DelayGenerator::CalculateDelayNanoseconds( + DelayGenerator::GetCopyMode(true, config.IsVRAM()), true, + config.GetEndAddress() - config.GetStartAddress()); + // It seems that it won't signal interrupt if "address_start" is zero. // TODO: hwtest this if (config.GetStartAddress() != 0) { if (intr_index == 0) { - impl->signal_interrupt(Service::GSP::InterruptId::PSC0); + impl->signal_interrupt(Service::GSP::InterruptId::PSC0, delay); } else if (intr_index == 1) { - impl->signal_interrupt(Service::GSP::InterruptId::PSC1); + impl->signal_interrupt(Service::GSP::InterruptId::PSC1, delay); } } @@ -391,11 +463,15 @@ void GPU::MemoryTransfer() { impl->debug_context->OnEvent(Pica::DebugContext::Event::IncomingDisplayTransfer, nullptr); } + u64 delay{}; // Perform memory transfer if (config.is_texture_copy) { if (!impl->rasterizer->AccelerateTextureCopy(config)) { impl->sw_blitter->TextureCopy(config); } + delay = DelayGenerator::CalculateDelayNanoseconds( + DelayGenerator::GetCopyMode(config.IsInputVRAM(), config.IsOutputVRAM()), true, + config.texture_copy.size); } else { if (right_eye_disabler->ShouldAllowDisplayTransfer(config.GetPhysicalInputAddress(), config.input_height)) { @@ -403,11 +479,14 @@ void GPU::MemoryTransfer() { impl->sw_blitter->DisplayTransfer(config); } } + delay = DelayGenerator::CalculateDelayNanoseconds( + DelayGenerator::GetCopyMode(config.IsInputVRAM(), config.IsOutputVRAM()), true, + config.input_width * config.input_height * BytesPerPixel(config.input_format)); } // Complete transfer. config.trigger.Assign(0); - impl->signal_interrupt(Service::GSP::InterruptId::PPF); + impl->signal_interrupt(Service::GSP::InterruptId::PPF, delay); } void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) { @@ -415,8 +494,8 @@ void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) { impl->renderer->SwapBuffers(); // Signal to GSP that GPU interrupt has occurred - impl->signal_interrupt(Service::GSP::InterruptId::PDC0); - impl->signal_interrupt(Service::GSP::InterruptId::PDC1); + impl->signal_interrupt(Service::GSP::InterruptId::PDC0, 0); + impl->signal_interrupt(Service::GSP::InterruptId::PDC1, 0); // Reschedule recurrent event impl->timing.ScheduleEvent(FRAME_TICKS - cycles_late, impl->vblank_event); diff --git a/src/video_core/pica/packed_attribute.h b/src/video_core/pica/packed_attribute.h index 7c8028e38..137c3fb25 100644 --- a/src/video_core/pica/packed_attribute.h +++ b/src/video_core/pica/packed_attribute.h @@ -1,9 +1,10 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include #include #include "common/vector_math.h" diff --git a/src/video_core/pica/pica_core.cpp b/src/video_core/pica/pica_core.cpp index 9f19e9572..d264c35b5 100644 --- a/src/video_core/pica/pica_core.cpp +++ b/src/video_core/pica/pica_core.cpp @@ -98,7 +98,7 @@ void PicaCore::SetInterruptHandler(Service::GSP::InterruptHandler& signal_interr void PicaCore::ProcessCmdList(PAddr list, u32 size, bool ignore_list) { if (ignore_list) { - signal_interrupt(Service::GSP::InterruptId::P3D); + signal_interrupt(Service::GSP::InterruptId::P3D, delay_generator.CalculateAndResetDelay()); return; } // Initialize command list tracking. @@ -148,6 +148,8 @@ void PicaCore::WriteInternalReg(u32 id, u32 value, u32 mask, bool& stop_requeste return; } + delay_generator.AddCommands(1); + // Expand a 4-bit mask to 4-byte mask, e.g. 0b0101 -> 0x00FF00FF constexpr std::array ExpandBitsToBytes = { 0x00000000, 0x000000ff, 0x0000ff00, 0x0000ffff, 0x00ff0000, 0x00ff00ff, @@ -174,7 +176,8 @@ void PicaCore::WriteInternalReg(u32 id, u32 value, u32 mask, bool& stop_requeste // TODO(PabloMK7): This logic is not fully accurate, but close enough: // https://problemkaputt.de/gbatek-3ds-gpu-internal-registers-finalize-interrupt-registers.htm if (any_byte_match(regs.internal.reg_array[id], regs.internal.irq_compare)) [[likely]] { - signal_interrupt(Service::GSP::InterruptId::P3D); + signal_interrupt(Service::GSP::InterruptId::P3D, + delay_generator.CalculateAndResetDelay()); if (regs.internal.irq_autostop) [[likely]] { stop_requested = true; } @@ -551,6 +554,10 @@ void PicaCore::DrawArrays(bool is_indexed) { return accelerate_draw; }(); + // Add vertices to the delay generator. + delay_generator.AddVertices(regs.internal.pipeline.num_vertices, + regs.internal.pipeline.triangle_topology); + // Attempt to use hardware vertex shaders if possible. if (accelerate_draw && rasterizer->AccelerateDrawBatch(is_indexed)) { return; diff --git a/src/video_core/pica/pica_core.h b/src/video_core/pica/pica_core.h index 61d8e75c2..426a3ca8e 100644 --- a/src/video_core/pica/pica_core.h +++ b/src/video_core/pica/pica_core.h @@ -29,6 +29,86 @@ namespace Pica { class DebugContext; class ShaderEngine; +class DelayGenerator { +private: + // A GPU is a very complex system, the timings resulting from + // a 3D draw depend on many factors, including triangle counts, + // texture sizes and format, shader complexity, cache + // and memory layout, etc. At this point in time, we don't + // have enough information nor implemented hw emulation + // capabilities to achieve a proper timing estimate. + // + // Instead, we will try to measure how complex a scene is based + // on the amount of geometry that is drawn, the amount of GPU + // commands and the shader complexity. We will ignore all + // the other factors for now. + + // Using Mario Kart 7 as the reference, it is understood that on + // average the console can handle around 20k triangles per frame. + // This game uses standard GPU features, with no fancy stuff, + // so we can consider it an average. To prevent hurting performance, + // we will also assume the GPU is twice as powerful. Afterall we only + // want timing accuracy to fix bugs at this point. + // This average already takes into account shader complexity averages. + static constexpr float nanoseconds_per_triangle = 800.f / 2; + + // Of the total amount of submitted triangles, many of them will be culled. + // This heavily depends on the specific scene, so we will assume 35% of the + // triangles being culled. Furthermore, the culled triangles will take way less + // processing time as they will skip most of the pipeline processing, so we + // can assume that a culled triangle will only take about 20% of the time. + static constexpr float culled_triangle_threshold = 0.35f; // 35% + static constexpr float culled_triangle_time_cost = 0.20f; // 20% + + // We will assume that each command will take around 6 cycles @ 268MHz + // There are no real measurements to support this claim, but it sounds + // reasonable. TODO: Measure on real HW. + static constexpr float nanoseconds_per_command = 22.4f; + +public: + inline void AddCommands(size_t commands) { + command_count += commands; + } + + inline void AddVertices(size_t vertices, PipelineRegs::TriangleTopology topology) { + size_t triangles{}; + if (topology == PipelineRegs::TriangleTopology::Fan || + topology == PipelineRegs::TriangleTopology::Strip) { + triangles = (vertices >= 3) ? (vertices - 2) : 1; + } else { + // Geometry shaders produce more vertices per given vertex, + // but they are not that relevant for timing emulation. + triangles = vertices / 3; + } + + triangle_count += triangles; + } + + u64 CalculateAndResetDelay() { + float result = command_count * nanoseconds_per_command; + + result += (1.f - culled_triangle_threshold) * triangle_count * nanoseconds_per_triangle; + result += culled_triangle_threshold * triangle_count * + (nanoseconds_per_triangle * culled_triangle_time_cost); + + triangle_count = 0; + command_count = 0; + + return static_cast(result); + } + +private: + size_t triangle_count{}; + size_t command_count{}; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const u32 file_version) { + ar & triangle_count; + ar & command_count; + } +}; + class PicaCore { public: explicit PicaCore(Memory::MemorySystem& memory, std::shared_ptr debug_context_); @@ -277,6 +357,8 @@ public: AttributeBuffer input_default_attributes{}; ImmediateModeState immediate{}; + DelayGenerator delay_generator{}; + private: friend class boost::serialization::access; template @@ -291,6 +373,7 @@ private: ar & fog; ar & input_default_attributes; ar & immediate; + ar & delay_generator; ar & geometry_pipeline; ar & primitive_assembler; ar & cmd_list; diff --git a/src/video_core/pica/regs_external.h b/src/video_core/pica/regs_external.h index a629a11a7..4961ee23f 100644 --- a/src/video_core/pica/regs_external.h +++ b/src/video_core/pica/regs_external.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,6 +10,7 @@ #include "common/assert.h" #include "common/bit_field.h" #include "common/common_funcs.h" +#include "core/memory.h" namespace Pica { @@ -85,6 +86,11 @@ struct MemoryFillConfig { return DecodeAddressRegister(address_end); } + bool IsVRAM() const { + u32 addr = GetStartAddress(); + return !(addr >= Memory::FCRAM_PADDR && addr < Memory::FCRAM_PADDR_END); + } + inline std::string DebugName() const { return fmt::format("from {:#X} to {:#X} with {}-bit value {:#X}", GetStartAddress(), GetEndAddress(), fill_32bit ? "32" : (fill_24bit ? "24" : "16"), @@ -155,6 +161,16 @@ struct DisplayTransferConfig { input_width.Value(), output_width.Value()); } + bool IsInputVRAM() { + u32 addr = GetPhysicalInputAddress(); + return !(addr >= Memory::FCRAM_PADDR && addr < Memory::FCRAM_PADDR_END); + } + + bool IsOutputVRAM() { + u32 addr = GetPhysicalOutputAddress(); + return !(addr >= Memory::FCRAM_PADDR && addr < Memory::FCRAM_PADDR_END); + } + union { u32 output_size; diff --git a/src/video_core/pica/shader_unit.cpp b/src/video_core/pica/shader_unit.cpp index 5d81f857a..725dd9ebb 100644 --- a/src/video_core/pica/shader_unit.cpp +++ b/src/video_core/pica/shader_unit.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -29,15 +29,15 @@ void ShaderUnit::WriteOutput(const ShaderRegs& config, AttributeBuffer& buffer) } void GeometryEmitter::Emit(std::span, 16> output_regs) { - ASSERT(vertex_id < 3); + ASSERT(emit_state.vertex_id < 3); u32 output_index{}; for (u32 reg : Common::BitSet(output_mask)) { - buffer[vertex_id][output_index++] = output_regs[reg]; + buffer[emit_state.vertex_id][output_index++] = output_regs[reg]; } - if (prim_emit) { - if (winding) { + if (emit_state.prim_emit) { + if (emit_state.winding) { handlers->winding_setter(); } for (std::size_t i = 0; i < buffer.size(); ++i) { diff --git a/src/video_core/pica/shader_unit.h b/src/video_core/pica/shader_unit.h index 2f9c1843f..80eea8d23 100644 --- a/src/video_core/pica/shader_unit.h +++ b/src/video_core/pica/shader_unit.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -75,11 +75,18 @@ struct GeometryEmitter { void Emit(std::span, 16> output_regs); public: - std::array buffer; - u8 vertex_id; - bool prim_emit; - bool winding; + union EmitState { + struct { + bool winding : 1; + bool prim_emit : 1; + u8 vertex_id : 2; + }; + u8 raw; + } emit_state; + static_assert(sizeof(emit_state) == 1); + u32 output_mask; + std::array buffer; Handlers* handlers; private: @@ -87,9 +94,7 @@ private: template void serialize(Archive& ar, const u32 file_version) { ar & buffer; - ar & vertex_id; - ar & prim_emit; - ar & winding; + ar & emit_state.raw; ar & output_mask; } }; diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index 09d936020..26156cb56 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -460,7 +460,8 @@ void RasterizerCache::CopySurface(Surface& src_surface, Surface& dst_surface, template SurfaceId RasterizerCache::GetSurface(const SurfaceParams& params, ScaleMatch match_res_scale, - bool load_if_create) { + bool load_if_create, + const SurfaceFlagBits& create_initial_flags) { if (params.addr == 0 || params.height * params.width == 0) { return {}; } @@ -472,7 +473,7 @@ SurfaceId RasterizerCache::GetSurface(const SurfaceParams& params, ScaleMatch SurfaceId surface_id = FindMatch(params, match_res_scale); if (!surface_id) { - surface_id = CreateSurface(params); + surface_id = CreateSurface(params, create_initial_flags); RegisterSurface(surface_id); } @@ -485,7 +486,8 @@ SurfaceId RasterizerCache::GetSurface(const SurfaceParams& params, ScaleMatch template typename RasterizerCache::SurfaceRect_Tuple RasterizerCache::GetSurfaceSubRect( - const SurfaceParams& params, ScaleMatch match_res_scale, bool load_if_create) { + const SurfaceParams& params, ScaleMatch match_res_scale, bool load_if_create, + const SurfaceFlagBits& create_initial_flags) { if (params.addr == 0 || params.height * params.width == 0) { return std::make_pair(SurfaceId{}, Common::Rectangle{}); } @@ -501,7 +503,7 @@ typename RasterizerCache::SurfaceRect_Tuple RasterizerCache::GetSurfaceSub SurfaceParams new_params = slot_surfaces[surface_id]; new_params.res_scale = params.res_scale; - surface_id = CreateSurface(new_params); + surface_id = CreateSurface(new_params, create_initial_flags); RegisterSurface(surface_id); } } @@ -521,7 +523,7 @@ typename RasterizerCache::SurfaceRect_Tuple RasterizerCache::GetSurfaceSub new_params.width = aligned_params.stride; new_params.UpdateParams(); // GetSurface will create the new surface and possibly adjust res_scale if necessary - surface_id = GetSurface(new_params, match_res_scale, load_if_create); + surface_id = GetSurface(new_params, match_res_scale, load_if_create, create_initial_flags); } else if (load_if_create) { ValidateSurface(surface_id, aligned_params.addr, aligned_params.size); } @@ -560,6 +562,10 @@ SurfaceId RasterizerCache::GetTextureSurface(const Pica::Texture::TextureInfo params.is_tiled = true; params.pixel_format = PixelFormatFromTextureFormat(info.format); params.res_scale = filter != Settings::TextureFilter::NoFilter ? resolution_scale_factor : 1; + SurfaceFlagBits initial_flags{}; + if (info.is_shadow_source) { + initial_flags |= SurfaceFlagBits::ShadowSource; + } params.UpdateParams(); const u32 min_width = info.width >> max_level; @@ -570,11 +576,12 @@ SurfaceId RasterizerCache::GetTextureSurface(const Pica::Texture::TextureInfo min_height); return NULL_SURFACE_ID; } - const auto [src_surface_id, rect] = GetSurfaceSubRect(params, ScaleMatch::Ignore, true); + const auto [src_surface_id, rect] = + GetSurfaceSubRect(params, ScaleMatch::Ignore, true, initial_flags); Surface& src_surface = slot_surfaces[src_surface_id]; params.res_scale = src_surface.res_scale; - SurfaceId tmp_surface_id = CreateSurface(params); + SurfaceId tmp_surface_id = CreateSurface(params, initial_flags); Surface& tmp_surface = slot_surfaces[tmp_surface_id]; sentenced.emplace_back(tmp_surface_id, frame_tick); @@ -593,7 +600,7 @@ SurfaceId RasterizerCache::GetTextureSurface(const Pica::Texture::TextureInfo return NULL_SURFACE_ID; } - SurfaceId surface_id = GetSurface(params, ScaleMatch::Ignore, true); + SurfaceId surface_id = GetSurface(params, ScaleMatch::Ignore, true, initial_flags); return surface_id ? surface_id : NULL_SURFACE_ID; } @@ -1026,7 +1033,7 @@ void RasterizerCache::UploadSurface(Surface& surface, SurfaceInterval interva const auto upload_data = source_ptr.GetWriteBytes(load_info.end - load_info.addr); DecodeTexture(load_info, load_info.addr, load_info.end, upload_data, staging.mapped, - runtime.NeedsConversion(surface.pixel_format)); + runtime.NeedsConversion(surface)); const bool should_dump = False(surface.flags & SurfaceFlagBits::Custom) && False(surface.flags & SurfaceFlagBits::RenderTarget); @@ -1135,7 +1142,7 @@ void RasterizerCache::DownloadSurface(Surface& surface, SurfaceInterval inter const auto download_dest = dest_ptr.GetWriteBytes(flush_end - flush_start); EncodeTexture(flush_info, flush_start, flush_end, staging.mapped, download_dest, - runtime.NeedsConversion(surface.pixel_format)); + runtime.NeedsConversion(surface)); } template @@ -1336,13 +1343,14 @@ void RasterizerCache::InvalidateRegion(PAddr addr, u32 size, SurfaceId region } template -SurfaceId RasterizerCache::CreateSurface(const SurfaceParams& params) { +SurfaceId RasterizerCache::CreateSurface(const SurfaceParams& params, + const SurfaceFlagBits& initial_flags) { const SurfaceId surface_id = [&] { const auto it = std::find_if(sentenced.begin(), sentenced.end(), [&](const auto& pair) { return slot_surfaces[pair.first] == params; }); if (it == sentenced.end()) { - return slot_surfaces.insert(runtime, params); + return slot_surfaces.insert(runtime, params, initial_flags); } const SurfaceId surface_id = it->first; sentenced.erase(it); diff --git a/src/video_core/rasterizer_cache/rasterizer_cache_base.h b/src/video_core/rasterizer_cache/rasterizer_cache_base.h index afd3625be..406c374dc 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache_base.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache_base.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -56,6 +56,7 @@ DECLARE_ENUM_FLAG_OPERATORS(MatchFlags); class CustomTexManager; class RendererBase; +enum class SurfaceFlagBits : u32; template class RasterizerCache { @@ -104,12 +105,13 @@ public: /// Load a texture from 3DS memory to OpenGL and cache it (if not already cached) SurfaceId GetSurface(const SurfaceParams& params, ScaleMatch match_res_scale, - bool load_if_create); + bool load_if_create, const SurfaceFlagBits& create_initial_flags = {}); /// Attempt to find a subrect (resolution scaled) of a surface, otherwise loads a texture from /// 3DS memory to OpenGL and caches it (if not already cached) SurfaceRect_Tuple GetSurfaceSubRect(const SurfaceParams& params, ScaleMatch match_res_scale, - bool load_if_create); + bool load_if_create, + const SurfaceFlagBits& create_initial_flags = {}); /// Get a surface based on the texture configuration Surface& GetTextureSurface(const Pica::TexturingRegs::FullTextureConfig& config); @@ -194,7 +196,7 @@ private: const SurfaceInterval& interval); /// Create a new surface - SurfaceId CreateSurface(const SurfaceParams& params); + SurfaceId CreateSurface(const SurfaceParams& params, const SurfaceFlagBits& initial_flags = {}); /// Register surface into the cache void RegisterSurface(SurfaceId surface); diff --git a/src/video_core/rasterizer_cache/surface_base.cpp b/src/video_core/rasterizer_cache/surface_base.cpp index 8d310dfe4..c9c645db6 100644 --- a/src/video_core/rasterizer_cache/surface_base.cpp +++ b/src/video_core/rasterizer_cache/surface_base.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -9,7 +9,8 @@ namespace VideoCore { -SurfaceBase::SurfaceBase(const SurfaceParams& params) : SurfaceParams{params} {} +SurfaceBase::SurfaceBase(const SurfaceParams& params, const SurfaceFlagBits& initial_flag_bits) + : SurfaceParams{params}, flags(initial_flag_bits) {} SurfaceBase::~SurfaceBase() = default; diff --git a/src/video_core/rasterizer_cache/surface_base.h b/src/video_core/rasterizer_cache/surface_base.h index b2ca33e61..6ac0147e0 100644 --- a/src/video_core/rasterizer_cache/surface_base.h +++ b/src/video_core/rasterizer_cache/surface_base.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -19,14 +19,14 @@ enum class SurfaceFlagBits : u32 { Picked = 1 << 1, ///< Surface has been picked when searching for a match. Tracked = 1 << 2, ///< Surface is part of a texture cube and should be tracked. Custom = 1 << 3, ///< Surface texture has been replaced with a custom texture. - ShadowMap = 1 << 4, ///< Surface is used during shadow rendering. + ShadowSource = 1 << 4, ///< Surface is used as a shadow source. RenderTarget = 1 << 5, ///< Surface was a render target. }; DECLARE_ENUM_FLAG_OPERATORS(SurfaceFlagBits); class SurfaceBase : public SurfaceParams { public: - SurfaceBase(const SurfaceParams& params); + SurfaceBase(const SurfaceParams& params, const SurfaceFlagBits& initial_flag_bits); ~SurfaceBase(); /// Returns true when this surface can be used to fill the fill_interval of dest_surface @@ -88,7 +88,7 @@ public: const Material* material = nullptr; SurfaceRegions invalid_regions; u32 fill_size = 0; - std::array fill_data; + std::array fill_data{}; u64 modification_tick = 1; }; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 2c721bcf7..2f15dec9c 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -676,7 +676,7 @@ void RasterizerOpenGL::SyncTextureUnits(const Framebuffer* framebuffer) { switch (texture.config.type.Value()) { case TextureType::Shadow2D: { Surface& surface = res_cache.GetTextureSurface(texture); - surface.flags |= VideoCore::SurfaceFlagBits::ShadowMap; + surface.flags |= VideoCore::SurfaceFlagBits::ShadowSource; state.image_shadow_texture_px = surface.Handle(); continue; } @@ -724,7 +724,7 @@ void RasterizerOpenGL::BindShadowCube(const Pica::TexturingRegs::FullTextureConf VideoCore::SurfaceId surface_id = res_cache.GetTextureSurface(info); Surface& surface = res_cache.GetSurface(surface_id); - surface.flags |= VideoCore::SurfaceFlagBits::ShadowMap; + surface.flags |= VideoCore::SurfaceFlagBits::ShadowSource; state.image_shadow_texture[binding] = surface.Handle(); } } diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index f93b033ed..fc14e949f 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -127,7 +127,8 @@ u32 TextureRuntime::RemoveThreshold() { return SWAP_CHAIN_SIZE; } -bool TextureRuntime::NeedsConversion(VideoCore::PixelFormat pixel_format) const { +bool TextureRuntime::NeedsConversion(const Surface& surface) const { + const auto& pixel_format = surface.pixel_format; const bool should_convert = pixel_format == PixelFormat::RGBA8 || // Needs byteswap pixel_format == PixelFormat::RGB8; // Is converted to RGBA8 return driver.IsOpenGLES() && should_convert; @@ -290,7 +291,7 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, // Note: shadow map is treated as RGBA8 format in PICA, as well as in the rasterizer cache, but // doing linear intepolation componentwise would cause incorrect value. const GLbitfield buffer_mask = MakeBufferMask(source.type); - const bool is_shadow_map = True(source.flags & SurfaceFlagBits::ShadowMap); + const bool is_shadow_map = True(source.flags & SurfaceFlagBits::ShadowSource); const GLenum filter = buffer_mask == GL_COLOR_BUFFER_BIT && !is_shadow_map ? GL_LINEAR : GL_NEAREST; glBlitFramebuffer(blit.src_rect.left, blit.src_rect.bottom, blit.src_rect.right, @@ -316,8 +317,9 @@ void TextureRuntime::GenerateMipmaps(Surface& surface) { } } -Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& params) - : SurfaceBase{params}, driver{&runtime_.GetDriver()}, runtime{&runtime_}, +Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& params, + const VideoCore::SurfaceFlagBits& initial_flag_bits) + : SurfaceBase{params, initial_flag_bits}, driver{&runtime_.GetDriver()}, runtime{&runtime_}, tuple{runtime->GetFormatTuple(pixel_format)} { if (pixel_format == PixelFormat::Invalid) { return; @@ -334,9 +336,10 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param } } -Surface::Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface, +Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface, const VideoCore::Material* mat) - : SurfaceBase{surface}, tuple{runtime.GetFormatTuple(mat->format)} { + : SurfaceBase{surface, {}}, driver{&runtime_.GetDriver()}, runtime{&runtime_}, + tuple{runtime_.GetFormatTuple(mat->format)} { if (mat && !driver->IsCustomFormatSupported(mat->format)) { return; } diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.h b/src/video_core/renderer_opengl/gl_texture_runtime.h index 5fe7300a7..d25ba102c 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -49,7 +49,7 @@ public: void Finish() {} /// Returns true if the provided pixel format cannot be used natively by the runtime. - bool NeedsConversion(VideoCore::PixelFormat pixel_format) const; + bool NeedsConversion(const Surface& surface) const; /// Maps an internal staging buffer of the provided size of pixel uploads/downloads VideoCore::StagingData FindStaging(u32 size, bool upload); @@ -97,7 +97,8 @@ private: class Surface : public VideoCore::SurfaceBase { public: - explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceParams& params); + explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceParams& params, + const VideoCore::SurfaceFlagBits& initial_flag_bits = {}); explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface, const VideoCore::Material* material); ~Surface(); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index abab77c4e..0f997d5e9 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -140,6 +140,7 @@ RasterizerVulkan::RasterizerVulkan(Memory::MemorySystem& memory, Pica::PicaCore& RasterizerVulkan::~RasterizerVulkan() = default; void RasterizerVulkan::TickFrame() { + scheduler.WaitWorker(); res_cache.TickFrame(); } @@ -647,7 +648,7 @@ void RasterizerVulkan::SyncTextureUnits(const Framebuffer* framebuffer) { case TextureType::Shadow2D: { Surface& surface = res_cache.GetTextureSurface(texture); Sampler& sampler = res_cache.GetSampler(texture.config); - surface.flags |= VideoCore::SurfaceFlagBits::ShadowMap; + surface.flags |= VideoCore::SurfaceFlagBits::ShadowSource; update_queue.AddImageSampler(texture_set, texture_index, 0, surface.StorageView(), sampler.Handle()); continue; @@ -703,7 +704,7 @@ void RasterizerVulkan::BindShadowCube(const Pica::TexturingRegs::FullTextureConf const VideoCore::SurfaceId surface_id = res_cache.GetTextureSurface(info); Surface& surface = res_cache.GetSurface(surface_id); - surface.flags |= VideoCore::SurfaceFlagBits::ShadowMap; + surface.flags |= VideoCore::SurfaceFlagBits::ShadowSource; update_queue.AddImageSampler(texture_set, 0, binding, surface.StorageView(), sampler.Handle()); } diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 56e4cf429..3fc025a30 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -38,7 +38,14 @@ void Swapchain::Create(u32 width_, u32 height_, vk::SurfaceKHR surface_, bool lo Destroy(); SetPresentMode(); + if (needs_recreation) { + return; + } + SetSurfaceProperties(); + if (needs_recreation) { + return; + } const std::array queue_family_indices = { instance.GetGraphicsQueueFamilyIndex(), @@ -70,9 +77,13 @@ void Swapchain::Create(u32 width_, u32 height_, vk::SurfaceKHR surface_, bool lo try { swapchain = instance.GetDevice().createSwapchainKHR(swapchain_info); + } catch (vk::SurfaceLostKHRError&) { + LOG_ERROR(Render_Vulkan, "Surface lost during swapchain creation"); + needs_recreation = true; + return; } catch (vk::SystemError& err) { LOG_CRITICAL(Render_Vulkan, "{}", err.what()); - UNREACHABLE(); + throw; } SetupImages(); @@ -160,7 +171,15 @@ void Swapchain::FindPresentFormat() { } void Swapchain::SetPresentMode() { - const auto modes = instance.GetPhysicalDevice().getSurfacePresentModesKHR(surface); + std::vector modes; + try { + modes = instance.GetPhysicalDevice().getSurfacePresentModesKHR(surface); + } catch (vk::SurfaceLostKHRError&) { + LOG_ERROR(Render_Vulkan, "Surface lost during swapchain creation"); + needs_recreation = true; + return; + } + const bool use_vsync = Settings::values.use_vsync.GetValue(); const auto find_mode = [&modes](vk::PresentModeKHR requested) { const auto it = @@ -203,8 +222,14 @@ void Swapchain::SetPresentMode() { } void Swapchain::SetSurfaceProperties() { - const vk::SurfaceCapabilitiesKHR capabilities = - instance.GetPhysicalDevice().getSurfaceCapabilitiesKHR(surface); + vk::SurfaceCapabilitiesKHR capabilities; + try { + capabilities = instance.GetPhysicalDevice().getSurfaceCapabilitiesKHR(surface); + } catch (vk::SurfaceLostKHRError&) { + LOG_ERROR(Render_Vulkan, "Surface lost during swapchain creation"); + needs_recreation = true; + return; + } extent = capabilities.currentExtent; if (capabilities.currentExtent.width == std::numeric_limits::max()) { @@ -237,6 +262,7 @@ void Swapchain::Destroy() { vk::Device device = instance.GetDevice(); if (swapchain) { device.destroySwapchainKHR(swapchain); + swapchain = VK_NULL_HANDLE; } for (u32 i = 0; i < image_count; i++) { device.destroySemaphore(image_acquired[i]); diff --git a/src/video_core/renderer_vulkan/vk_swapchain.h b/src/video_core/renderer_vulkan/vk_swapchain.h index 4f885727a..9492c0743 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.h +++ b/src/video_core/renderer_vulkan/vk_swapchain.h @@ -90,7 +90,7 @@ private: private: const Instance& instance; - vk::SwapchainKHR swapchain{}; + vk::SwapchainKHR swapchain{VK_NULL_HANDLE}; vk::SurfaceKHR surface{}; vk::SurfaceFormatKHR surface_format; vk::PresentModeKHR present_mode; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 52b7c6392..c62757384 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -720,15 +720,16 @@ void TextureRuntime::GenerateMipmaps(Surface& surface) { } } -bool TextureRuntime::NeedsConversion(VideoCore::PixelFormat format) const { - const FormatTraits traits = instance.GetTraits(format); +bool TextureRuntime::NeedsConversion(const Surface& surface) const { + const FormatTraits& traits = surface.traits; return traits.needs_conversion && // DepthStencil formats are handled elsewhere due to de-interleaving. traits.aspect != (vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil); } -Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& params) - : SurfaceBase{params}, runtime{runtime_}, instance{runtime_.GetInstance()}, +Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& params, + const VideoCore::SurfaceFlagBits& initial_flag_bits) + : SurfaceBase{params, initial_flag_bits}, runtime{runtime_}, instance{runtime_.GetInstance()}, scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(pixel_format)}, handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { @@ -736,7 +737,17 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param return; } - const bool is_mutable = pixel_format == VideoCore::PixelFormat::RGBA8; + bool is_mutable = traits.native == vk::Format::eR8G8B8A8Unorm; + + if (True(flags & VideoCore::SurfaceFlagBits::ShadowSource) && + traits.native != vk::Format::eR8G8B8A8Unorm) { + // If the surface is a shadow source, it needs conversion + // to be forced as it always has to be RGBA8 + traits = instance.GetTraits(VideoCore::PixelFormat::RGBA8); + traits.needs_conversion = true; + is_mutable = true; + } + const vk::Format format = traits.native; ASSERT_MSG(format != vk::Format::eUndefined && levels >= 1, @@ -1278,7 +1289,7 @@ vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { auto aspect = traits.aspect; if (view_type == ViewType::Storage) { - ASSERT(pixel_format == PixelFormat::RGBA8); + ASSERT(traits.native == vk::Format::eR8G8B8A8Unorm); is_storage = true; } if (view_type == ViewType::Depth || view_type == ViewType::Stencil) { diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index 17ac89c91..b46479c58 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -155,7 +155,7 @@ public: void GenerateMipmaps(Surface& surface); /// Returns true if the provided pixel format needs convertion - bool NeedsConversion(VideoCore::PixelFormat format) const; + bool NeedsConversion(const Surface& surface) const; private: /// Clears a partial texture rect using a clear rectangle @@ -175,7 +175,8 @@ class Surface : public VideoCore::SurfaceBase { friend class TextureRuntime; public: - explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceParams& params); + explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceParams& params, + const VideoCore::SurfaceFlagBits& initial_flag_bits = {}); explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface, const VideoCore::Material* materal); diff --git a/src/video_core/shader/generator/glsl_fs_shader_gen.cpp b/src/video_core/shader/generator/glsl_fs_shader_gen.cpp index 7d7ae63bb..8ff5ac9d4 100644 --- a/src/video_core/shader/generator/glsl_fs_shader_gen.cpp +++ b/src/video_core/shader/generator/glsl_fs_shader_gen.cpp @@ -895,7 +895,11 @@ void FragmentModule::WriteLogicOp() { } void FragmentModule::WriteBlending() { - if (!config.EmulateBlend() || profile.is_vulkan) [[likely]] { + bool requires_rgb_minmax_emulation = + config.framebuffer.requested_rgb_blend.RequiresMinMaxEmulation(); + bool requires_alpha_minmax_emulation = + config.framebuffer.requested_alpha_blend.RequiresMinMaxEmulation(); + if (!requires_rgb_minmax_emulation && !requires_alpha_minmax_emulation) [[likely]] { return; } @@ -937,23 +941,25 @@ void FragmentModule::WriteBlending() { return "vec4(1.f)"; } }; + + // At this point, the blend equation can only be min or max. const auto get_func = [](Pica::FramebufferRegs::BlendEquation eq) { return eq == Pica::FramebufferRegs::BlendEquation::Min ? "min" : "max"; }; - if (config.framebuffer.rgb_blend.eq != Pica::FramebufferRegs::BlendEquation::Add) { + if (requires_rgb_minmax_emulation) { out += fmt::format( "combiner_output.rgb = {}(source_color.rgb * ({}).rgb, dest_color.rgb * ({}).rgb);\n", - get_func(config.framebuffer.rgb_blend.eq), - get_factor(config.framebuffer.rgb_blend.src_factor), - get_factor(config.framebuffer.rgb_blend.dst_factor)); + get_func(config.framebuffer.requested_rgb_blend.eq), + get_factor(config.framebuffer.requested_rgb_blend.src_factor), + get_factor(config.framebuffer.requested_rgb_blend.dst_factor)); } - if (config.framebuffer.alpha_blend.eq != Pica::FramebufferRegs::BlendEquation::Add) { + if (requires_alpha_minmax_emulation) { out += fmt::format("combiner_output.a = {}(source_color.a * ({}).a, dest_color.a * ({}).a);\n", - get_func(config.framebuffer.alpha_blend.eq), - get_factor(config.framebuffer.alpha_blend.src_factor), - get_factor(config.framebuffer.alpha_blend.dst_factor)); + get_func(config.framebuffer.requested_alpha_blend.eq), + get_factor(config.framebuffer.requested_alpha_blend.src_factor), + get_factor(config.framebuffer.requested_alpha_blend.dst_factor)); } } @@ -1239,7 +1245,8 @@ void FragmentModule::DefineExtensions() { use_fragment_shader_barycentric = false; } } - if (config.EmulateBlend() && !profile.is_vulkan) { + if (config.framebuffer.requested_rgb_blend.RequiresMinMaxEmulation() || + config.framebuffer.requested_alpha_blend.RequiresMinMaxEmulation()) [[unlikely]] { if (profile.has_gl_ext_framebuffer_fetch) { out += "#extension GL_EXT_shader_framebuffer_fetch : enable\n"; out += "#define destFactor color\n"; @@ -1338,7 +1345,7 @@ void FragmentModule::DefineBindingsGL() { out += "layout(binding = 6) uniform sampler2D tex_normal;\n"; } if (use_blend_fallback) { - out += "layout(location = 7) uniform sampler2D tex_color;\n"; + out += "layout(binding = 7) uniform sampler2D tex_color;\n"; } // Shadow textures diff --git a/src/video_core/shader/generator/pica_fs_config.cpp b/src/video_core/shader/generator/pica_fs_config.cpp index 0f3854ce8..c584b99a0 100644 --- a/src/video_core/shader/generator/pica_fs_config.cpp +++ b/src/video_core/shader/generator/pica_fs_config.cpp @@ -21,13 +21,13 @@ FramebufferConfig::FramebufferConfig(const Pica::RegsInternal& regs) { logic_op.Assign(Pica::FramebufferRegs::LogicOp::Copy); if (alphablend_enable) { - rgb_blend.eq = output_merger.alpha_blending.blend_equation_rgb.Value(); - rgb_blend.src_factor = output_merger.alpha_blending.factor_source_rgb; - rgb_blend.dst_factor = output_merger.alpha_blending.factor_dest_rgb; + requested_rgb_blend.eq = output_merger.alpha_blending.blend_equation_rgb.Value(); + requested_rgb_blend.src_factor = output_merger.alpha_blending.factor_source_rgb; + requested_rgb_blend.dst_factor = output_merger.alpha_blending.factor_dest_rgb; - alpha_blend.eq = output_merger.alpha_blending.blend_equation_a.Value(); - alpha_blend.src_factor = output_merger.alpha_blending.factor_source_a; - alpha_blend.dst_factor = output_merger.alpha_blending.factor_dest_a; + requested_alpha_blend.eq = output_merger.alpha_blending.blend_equation_a.Value(); + requested_alpha_blend.src_factor = output_merger.alpha_blending.factor_source_a; + requested_alpha_blend.dst_factor = output_merger.alpha_blending.factor_dest_a; } } @@ -37,17 +37,10 @@ void FramebufferConfig::ApplyProfile(const Profile& profile) { logic_op.Assign(requested_logic_op); } - // Min/max blend emulation - if (!profile.has_blend_minmax_factor && alphablend_enable) { - if (rgb_blend.eq != Pica::FramebufferRegs::BlendEquation::Min && - rgb_blend.eq != Pica::FramebufferRegs::BlendEquation::Max) { - rgb_blend = {}; - } - - if (alpha_blend.eq != Pica::FramebufferRegs::BlendEquation::Min && - alpha_blend.eq != Pica::FramebufferRegs::BlendEquation::Max) { - alpha_blend = {}; - } + // Check if we don't need blend min/max emulation. + if ((profile.has_blend_minmax_factor || profile.is_vulkan) && alphablend_enable) { + requested_rgb_blend.SetMinMaxEmulationDisabled(); + requested_alpha_blend.SetMinMaxEmulationDisabled(); } } diff --git a/src/video_core/shader/generator/pica_fs_config.h b/src/video_core/shader/generator/pica_fs_config.h index 736880a5b..f0e2fe718 100644 --- a/src/video_core/shader/generator/pica_fs_config.h +++ b/src/video_core/shader/generator/pica_fs_config.h @@ -43,6 +43,17 @@ struct BlendConfig { // fields FIELD_HASH(eq), FIELD_HASH(src_factor), FIELD_HASH(dst_factor)); } + + void SetMinMaxEmulationDisabled() { + // If we don't need min/max emulation, set the blend equation + // to "-1" as a clear marker that this config is disabled. + eq = static_cast(UINT32_MAX); + } + + bool RequiresMinMaxEmulation() { + return eq == Pica::FramebufferRegs::BlendEquation::Min || + eq == Pica::FramebufferRegs::BlendEquation::Max; + } }; static_assert(std::has_unique_object_representations_v); @@ -58,8 +69,8 @@ struct FramebufferConfig { BitField<10, 1, u32> shadow_rendering; BitField<11, 1, u32> alphablend_enable; }; - BlendConfig rgb_blend{}; - BlendConfig alpha_blend{}; + BlendConfig requested_rgb_blend{}; + BlendConfig requested_alpha_blend{}; Pica::FramebufferRegs::LogicOp requested_logic_op{}; @@ -78,7 +89,8 @@ struct FramebufferConfig { // fields FIELD_HASH(alpha_test_func), FIELD_HASH(scissor_test_mode), FIELD_HASH(depthmap_enable), FIELD_HASH(logic_op), FIELD_HASH(shadow_rendering), FIELD_HASH(alphablend_enable), - FIELD_HASH(rgb_blend), FIELD_HASH(alpha_blend), FIELD_HASH(requested_logic_op), + FIELD_HASH(requested_rgb_blend), FIELD_HASH(requested_alpha_blend), + FIELD_HASH(requested_logic_op), // nested layout BlendConfig::StructHash()); @@ -387,11 +399,6 @@ struct FSConfig { return (stage_index < 4) && ((texture.combiner_buffer_input >> 4) & (1 << stage_index)); } - [[nodiscard]] bool EmulateBlend() const { - return framebuffer.rgb_blend.eq != Pica::FramebufferRegs::BlendEquation::Add || - framebuffer.alpha_blend.eq != Pica::FramebufferRegs::BlendEquation::Add; - } - [[nodiscard]] bool UsesSpirvIncompatibleConfig() const { const auto texture0_type = texture.texture0_type.Value(); return texture0_type == Pica::TexturingRegs::TextureConfig::ShadowCube || diff --git a/src/video_core/shader/shader_interpreter.cpp b/src/video_core/shader/shader_interpreter.cpp index cb06a62bc..6d373e28f 100644 --- a/src/video_core/shader/shader_interpreter.cpp +++ b/src/video_core/shader/shader_interpreter.cpp @@ -671,9 +671,9 @@ static void RunInterpreter(const ShaderSetup& setup, ShaderUnit& state, case OpCode::Id::SETEMIT: { auto* emitter = state.emitter_ptr; ASSERT_MSG(emitter, "Execute SETEMIT on VS"); - emitter->vertex_id = instr.setemit.vertex_id; - emitter->prim_emit = instr.setemit.prim_emit != 0; - emitter->winding = instr.setemit.winding != 0; + emitter->emit_state.vertex_id = instr.setemit.vertex_id; + emitter->emit_state.prim_emit = instr.setemit.prim_emit != 0; + emitter->emit_state.winding = instr.setemit.winding != 0; break; } diff --git a/src/video_core/shader/shader_jit_a64_compiler.cpp b/src/video_core/shader/shader_jit_a64_compiler.cpp index 636d9cb57..e138857c0 100644 --- a/src/video_core/shader/shader_jit_a64_compiler.cpp +++ b/src/video_core/shader/shader_jit_a64_compiler.cpp @@ -508,6 +508,7 @@ void JitShader::Compile_DPH(Instruction instr) { void JitShader::Compile_EX2(Instruction instr) { Compile_SwizzleSrc(instr, 1, instr.common.src1, SRC1); STR(X30, SP, POST_INDEXED, -16); + exp2_used = true; BL(exp2_subroutine); LDR(X30, SP, PRE_INDEXED, 16); Compile_DestEnable(instr, SRC1); @@ -516,6 +517,7 @@ void JitShader::Compile_EX2(Instruction instr) { void JitShader::Compile_LG2(Instruction instr) { Compile_SwizzleSrc(instr, 1, instr.common.src1, SRC1); STR(X30, SP, POST_INDEXED, -16); + log2_used = true; BL(log2_subroutine); LDR(X30, SP, PRE_INDEXED, 16); Compile_DestEnable(instr, SRC1); @@ -865,12 +867,13 @@ void JitShader::Compile_SETE(Instruction instr) { l(have_emitter); - MOV(XSCRATCH1.toW(), instr.setemit.vertex_id); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, vertex_id))); - MOV(XSCRATCH1.toW(), instr.setemit.prim_emit); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, prim_emit))); - MOV(XSCRATCH1.toW(), instr.setemit.winding); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, winding))); + const GeometryEmitter::EmitState new_state{ + .winding = instr.setemit.winding != 0, + .prim_emit = instr.setemit.prim_emit != 0, + .vertex_id = static_cast(instr.setemit.vertex_id), + }; + MOV(XSCRATCH1.toW(), new_state.raw); + STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, emit_state))); l(end); } @@ -993,6 +996,14 @@ void JitShader::Compile(const std::array* program_ // Compile entire program Compile_Block(static_cast(program_code->size())); + // Compile utility functions + if (log2_used) { + Compile_Log2(log2_subroutine); + } + if (exp2_used) { + Compile_Exp2(exp2_subroutine); + } + // Free memory that's no longer needed program_code = nullptr; swizzle_data = nullptr; @@ -1020,18 +1031,9 @@ void JitShader::Compile(const std::array* program_ code_vec.shrink_to_fit(); } -JitShader::JitShader() : oaknut::VectorCodeGenerator(code_vec) { - CompilePrelude(); -} - -void JitShader::CompilePrelude() { - log2_subroutine = CompilePrelude_Log2(); - exp2_subroutine = CompilePrelude_Exp2(); -} - -Label JitShader::CompilePrelude_Log2() { - Label subroutine; +JitShader::JitShader() : oaknut::VectorCodeGenerator(code_vec) {} +void JitShader::Compile_Log2(Label subroutine) { // We perform this approximation by first performing a range reduction into the range // [1.0, 2.0). A minimax polynomial which was fit for the function log2(x) / (x - 1) is then // evaluated. We multiply the result by (x - 1) then restore the result into the appropriate @@ -1135,13 +1137,9 @@ Label JitShader::CompilePrelude_Log2() { DUP(SRC1.S4(), SRC1.Selem()[0]); RET(); - - return subroutine; } -Label JitShader::CompilePrelude_Exp2() { - Label subroutine; - +void JitShader::Compile_Exp2(Label subroutine) { // This approximation first performs a range reduction into the range [-0.5, 0.5). A minmax // polynomial which was fit for the function exp2(x) is then evaluated. We then restore the // result into the appropriate range. @@ -1240,8 +1238,6 @@ Label JitShader::CompilePrelude_Exp2() { DUP(SRC1.S4(), SRC1.Selem()[0]); RET(); - - return subroutine; } } // namespace Pica::Shader diff --git a/src/video_core/shader/shader_jit_a64_compiler.h b/src/video_core/shader/shader_jit_a64_compiler.h index 7accf66ac..becede149 100644 --- a/src/video_core/shader/shader_jit_a64_compiler.h +++ b/src/video_core/shader/shader_jit_a64_compiler.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -123,9 +123,8 @@ private: /** * Emits data and code for utility functions. */ - void CompilePrelude(); - oaknut::Label CompilePrelude_Log2(); - oaknut::Label CompilePrelude_Exp2(); + void Compile_Log2(oaknut::Label subroutine); + void Compile_Exp2(oaknut::Label subroutine); const std::array* program_code = nullptr; const std::array* swizzle_data = nullptr; @@ -146,6 +145,10 @@ private: using CompiledShader = void(const void* setup, void* state, const std::byte* start_addr); CompiledShader* program = nullptr; + /// Library functions, emitted as used + bool log2_used : 1 = false; + bool exp2_used : 1 = false; + oaknut::Label log2_subroutine; oaknut::Label exp2_subroutine; }; diff --git a/src/video_core/shader/shader_jit_x64_compiler.cpp b/src/video_core/shader/shader_jit_x64_compiler.cpp index 84cdc09bc..476a024e9 100644 --- a/src/video_core/shader/shader_jit_x64_compiler.cpp +++ b/src/video_core/shader/shader_jit_x64_compiler.cpp @@ -511,12 +511,14 @@ void JitShader::Compile_DPH(Instruction instr) { void JitShader::Compile_EX2(Instruction instr) { Compile_SwizzleSrc(instr, 1, instr.common.src1, SRC1); + exp2_used = true; call(exp2_subroutine); Compile_DestEnable(instr, SRC1); } void JitShader::Compile_LG2(Instruction instr) { Compile_SwizzleSrc(instr, 1, instr.common.src1, SRC1); + log2_used = true; call(log2_subroutine); Compile_DestEnable(instr, SRC1); } @@ -905,9 +907,12 @@ void JitShader::Compile_SETE(Instruction instr) { jmp(end); L(have_emitter); - mov(byte[rax + offsetof(GeometryEmitter, vertex_id)], instr.setemit.vertex_id); - mov(byte[rax + offsetof(GeometryEmitter, prim_emit)], instr.setemit.prim_emit); - mov(byte[rax + offsetof(GeometryEmitter, winding)], instr.setemit.winding); + const GeometryEmitter::EmitState new_state{ + .winding = instr.setemit.winding != 0, + .prim_emit = instr.setemit.prim_emit != 0, + .vertex_id = static_cast(instr.setemit.vertex_id), + }; + mov(byte[rax + offsetof(GeometryEmitter, emit_state)], new_state.raw); L(end); } @@ -1035,6 +1040,14 @@ void JitShader::Compile(const std::array* program_ // Compile entire program Compile_Block(static_cast(program_code->size())); + // Compile utility functions + if (log2_used) { + Compile_Log2(log2_subroutine); + } + if (exp2_used) { + Compile_Exp2(exp2_subroutine); + } + // Free memory that's no longer needed program_code = nullptr; swizzle_data = nullptr; @@ -1047,18 +1060,9 @@ void JitShader::Compile(const std::array* program_ LOG_DEBUG(HW_GPU, "Compiled shader size={}", getSize()); } -JitShader::JitShader() : Xbyak::CodeGenerator(MAX_SHADER_SIZE) { - CompilePrelude(); -} - -void JitShader::CompilePrelude() { - log2_subroutine = CompilePrelude_Log2(); - exp2_subroutine = CompilePrelude_Exp2(); -} - -Xbyak::Label JitShader::CompilePrelude_Log2() { - Xbyak::Label subroutine; +JitShader::JitShader() : Xbyak::CodeGenerator(MAX_SHADER_SIZE) {} +void JitShader::Compile_Log2(Xbyak::Label subroutine) { // SSE does not have a log instruction, thus we must approximate. // We perform this approximation first performaing a range reduction into the range [1.0, 2.0). // A minimax polynomial which was fit for the function log2(x) / (x - 1) is then evaluated. @@ -1160,12 +1164,9 @@ Xbyak::Label JitShader::CompilePrelude_Log2() { shufps(SRC1, SRC1, _MM_SHUFFLE(0, 0, 0, 0)); ret(); - - return subroutine; } -Xbyak::Label JitShader::CompilePrelude_Exp2() { - Xbyak::Label subroutine; +void JitShader::Compile_Exp2(Xbyak::Label subroutine) { // SSE does not have a exp instruction, thus we must approximate. // We perform this approximation first performaing a range reduction into the range [-0.5, 0.5). @@ -1268,8 +1269,6 @@ Xbyak::Label JitShader::CompilePrelude_Exp2() { shufps(SRC1, SRC1, _MM_SHUFFLE(0, 0, 0, 0)); ret(); - - return subroutine; } } // namespace Pica::Shader diff --git a/src/video_core/shader/shader_jit_x64_compiler.h b/src/video_core/shader/shader_jit_x64_compiler.h index 911183296..aff9cae7b 100644 --- a/src/video_core/shader/shader_jit_x64_compiler.h +++ b/src/video_core/shader/shader_jit_x64_compiler.h @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -115,9 +115,8 @@ private: /** * Emits data and code for utility functions. */ - void CompilePrelude(); - Xbyak::Label CompilePrelude_Log2(); - Xbyak::Label CompilePrelude_Exp2(); + void Compile_Log2(Xbyak::Label subroutine); + void Compile_Exp2(Xbyak::Label subroutine); const std::array* program_code = nullptr; const std::array* swizzle_data = nullptr; @@ -138,6 +137,10 @@ private: using CompiledShader = void(const void* setup, void* state, const u8* start_addr); CompiledShader* program = nullptr; + /// Library functions, emitted as used + bool log2_used : 1 = false; + bool exp2_used : 1 = false; + Xbyak::Label log2_subroutine; Xbyak::Label exp2_subroutine; }; diff --git a/src/video_core/texture/texture_decode.cpp b/src/video_core/texture/texture_decode.cpp index 8c5bea703..3b693e781 100644 --- a/src/video_core/texture/texture_decode.cpp +++ b/src/video_core/texture/texture_decode.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -219,6 +219,8 @@ TextureInfo TextureInfo::FromPicaRegister(const TexturingRegs::TextureConfig& co info.height = config.height; info.format = format; info.SetDefaultStride(); + info.is_shadow_source = config.type == TexturingRegs::TextureConfig::TextureType::Shadow2D || + config.type == TexturingRegs::TextureConfig::TextureType::ShadowCube; return info; } diff --git a/src/video_core/texture/texture_decode.h b/src/video_core/texture/texture_decode.h index 67ee03e5d..bbab95aca 100644 --- a/src/video_core/texture/texture_decode.h +++ b/src/video_core/texture/texture_decode.h @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -19,6 +19,7 @@ struct TextureInfo { u32 height; ptrdiff_t stride; TexturingRegs::TextureFormat format; + bool is_shadow_source; static TextureInfo FromPicaRegister(const TexturingRegs::TextureConfig& config, const TexturingRegs::TextureFormat& format); diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 7dabb7b45..d21c0eace 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(web_service STATIC +add_library(web_service STATIC EXCLUDE_FROM_ALL announce_room_json.cpp announce_room_json.h precompiled_headers.h diff --git a/tools/README.md b/tools/README.md index b4db3f866..126b8822a 100644 --- a/tools/README.md +++ b/tools/README.md @@ -7,7 +7,16 @@ The scripts in this directory assume that your current working directory is the ## Pre-release checklist - [ ] Update compatibility list -- [ ] If this is a major release (2123.1 -> major.minor), update translations +- [ ] Update translations if either of the following apply: + - This is a major release (e.g. 2125.x --> 2126.0) + - This is a minor release, but the release branch hasn't yet diverged from master + +## Post-release checklist + +- [ ] Publish to Google Play Store +- [ ] Publish files to the official Internet Archive account (incl. changelog as .md file) +- [ ] Publish to Flathub +- [ ] Force-push tagged commit to retroarch-live branch ### Note: diff --git a/tools/verify-release.sh b/tools/verify-release.sh new file mode 100755 index 000000000..7d3e7b040 --- /dev/null +++ b/tools/verify-release.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash + +# Copyright Citra Emulator Project / Azahar Emulator Project +# Licensed under GPLv2 or any later version +# Refer to the license.txt file included. + +set -euo pipefail + +# Usage: +# ./verify-release.sh +# +# Example: +# ./verify-release.sh azahar-emu/azahar 2126.0 +# +# Behavior: +# - Downloads all release assets +# - Verifies asset is published in the release +# - Verifies SPDX attestations for every asset +# - Extracts SPDX SBOMs +# +# Notes: +# - Requires installation of the GitHub CLI (gh) and jq tools. +# - Draft release support requires authentication with permission +# to view the draft release. +# - gh release verify-asset currently does NOT support draft releases. + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " + exit 1 +fi + +command -v gh >/dev/null 2>&1 || { + echo "ERROR: GitHub CLI (gh) is not installed or not in PATH" + exit 1 +} + +command -v jq >/dev/null 2>&1 || { + echo "ERROR: jq is not installed or not in PATH" + exit 1 +} + +REPO="$1" +TAG="$2" + +echo "==> Fetching release metadata" + +IS_DRAFT=$( + gh release view "$TAG" \ + --repo "$REPO" \ + --json isDraft \ + --jq '.isDraft' +) + +WORKDIR="verify/release-${TAG}" +SBOMSUBDIR="sbom" + +rm -rf "$WORKDIR" +mkdir -p "$WORKDIR" +cd "$WORKDIR" +mkdir -p "$SBOMSUBDIR" + + +echo +echo "==> Downloading release assets" + +gh release download "$TAG" \ + --repo "$REPO" + +echo +echo "==> Fetching asset list" + +ASSETS=() + +while IFS= read -r asset; do + ASSETS+=("$asset") +done < <( + gh release view "$TAG" \ + --repo "$REPO" \ + --json assets \ + --jq '.assets[].name' +) + +echo +echo "==> Release type: $( + [[ "$IS_DRAFT" == "true" ]] && echo "draft" || echo "published" +)" + +echo +echo "==> Verifying assets" + +for asset in "${ASSETS[@]}"; do + # Skip attestation files themselves + if [[ "$asset" == *.intoto.jsonl ]]; then + continue + fi + + if [[ ! -f "$asset" ]]; then + echo "ERROR: Missing downloaded asset: $asset" + exit 1 + fi + + echo + echo "========================================" + echo "Asset: $asset" + echo "========================================" + + echo "1/3 Release asset verification" + + if [[ "$IS_DRAFT" != "true" ]]; then + gh release verify-asset "$TAG" "$asset" \ + --repo "$REPO" + echo + else + echo "SKIPPED (draft releases unsupported)" + echo + fi + + echo "2/3 Attestation verification" + + if [[ "$asset" == *.sha256sum ]]; then + echo "SKIPPED (sha256sum does not need verification)" + echo "SKIPPED (no SPDX SBOM extraction)" + else + gh attestation verify "$asset" \ + --repo "$REPO" \ + --predicate-type https://spdx.dev/Document + + echo + echo "3/3 SBOM extraction" + + BASE_NAME="$(basename "$asset")" + SBOM_FILE="${SBOMSUBDIR}/${BASE_NAME}.spdx.json" + + # gh attestation download does not currently support + # specifying the output file, nor it allows piping the + # output. For that reason, we need to find the .jsonl + # in the current directory. + + # Exclude any existing .jsonl files from find + # (failsafe, should not happen) + BEFORE_JSONL="$(find . -maxdepth 1 -name '*.jsonl' -print)" + + gh attestation download "$asset" \ + --repo "$REPO" \ + >/dev/null + + ATTESTATION_FILE="" + + while IFS= read -r file; do + FOUND=false + + while IFS= read -r oldfile; do + if [[ "$file" == "$oldfile" ]]; then + FOUND=true + break + fi + done <<< "$BEFORE_JSONL" + + # Only consider new jsonl files + if [[ "$FOUND" == "false" ]]; then + ATTESTATION_FILE="$file" + break + fi + done < <(find . -maxdepth 1 -name '*.jsonl' -print) + + if [[ -z "$ATTESTATION_FILE" ]]; then + echo "ERROR: Could not locate downloaded attestation jsonl" + exit 1 + fi + + # Extract and decode the SBOM from the jsonl + jq -r ' + .dsseEnvelope.payload + ' "$ATTESTATION_FILE" | + while IFS= read -r payload; do + echo "$payload" | base64 -d + done | + jq '.predicate' \ + > "$SBOM_FILE" + + rm -f "$ATTESTATION_FILE" + + echo "Saved SBOM: $SBOM_FILE" + fi + + echo + echo "OK: $asset" +done + +echo +echo "========================================" +echo "All assets verified successfully" +echo "SBOMs saved in: $WORKDIR/$SBOMSUBDIR" +echo "========================================" \ No newline at end of file