Browse Source
Merge remote-tracking branch 'upstream/master' into nx
Merge remote-tracking branch 'upstream/master' into nx
# Conflicts: # src/core/CMakeLists.txt # src/core/arm/dynarmic/arm_dynarmic.cpp # src/core/arm/dyncom/arm_dyncom.cpp # src/core/hle/kernel/process.cpp # src/core/hle/kernel/thread.cpp # src/core/hle/kernel/thread.h # src/core/hle/kernel/vm_manager.cpp # src/core/loader/3dsx.cpp # src/core/loader/elf.cpp # src/core/loader/ncch.cpp # src/core/memory.cpp # src/core/memory.h # src/core/memory_setup.hnce_cpp
241 changed files with 20959 additions and 2734 deletions
-
4.gitignore
-
67.travis-build.sh
-
40.travis-deps.sh
-
129.travis-upload.sh
-
45.travis.yml
-
37.travis/clang-format/script.sh
-
22.travis/common/post-upload.sh
-
6.travis/common/pre-upload.sh
-
3.travis/linux/build.sh
-
3.travis/linux/deps.sh
-
17.travis/linux/docker.sh
-
14.travis/linux/upload.sh
-
12.travis/macos/build.sh
-
4.travis/macos/deps.sh
-
110.travis/macos/upload.sh
-
43CMakeLists.txt
-
18CMakeModules/DownloadExternals.cmake
-
2README.md
-
167appveyor.yml
-
BINdist/citra.icns
-
BINdist/citra.ico
-
24dist/citra.manifest
-
80dist/citra.svg
-
BINdist/doc-icon.png
-
BINdist/icons/checked.png
-
BINdist/icons/failed.png
-
6dist/icons/icons.qrc
-
23externals/CMakeLists.txt
-
2externals/cryptopp/cryptopp
-
2externals/dynarmic
-
2externals/enet
-
2externals/soundtouch
-
4src/audio_core/codec.cpp
-
4src/audio_core/codec.h
-
49src/audio_core/hle/source.cpp
-
2src/audio_core/hle/source.h
-
86src/audio_core/interpolate.cpp
-
31src/audio_core/interpolate.h
-
2src/citra/citra.cpp
-
8src/citra/citra.rc
-
11src/citra/config.cpp
-
29src/citra/default_ini.h
-
10src/citra/emu_window/emu_window_sdl2.cpp
-
4src/citra/emu_window/emu_window_sdl2.h
-
8src/citra_qt/CMakeLists.txt
-
10src/citra_qt/bootmanager.cpp
-
4src/citra_qt/bootmanager.h
-
12src/citra_qt/citra-qt.rc
-
23src/citra_qt/configuration/config.cpp
-
15src/citra_qt/configuration/configure.ui
-
1src/citra_qt/configuration/configure_dialog.cpp
-
33src/citra_qt/configuration/configure_graphics.ui
-
3src/citra_qt/configuration/configure_system.cpp
-
102src/citra_qt/configuration/configure_web.cpp
-
40src/citra_qt/configuration/configure_web.h
-
190src/citra_qt/configuration/configure_web.ui
-
4src/citra_qt/debugger/graphics/graphics_cmdlists.cpp
-
3src/citra_qt/debugger/graphics/graphics_surface.cpp
-
41src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp
-
48src/citra_qt/main.cpp
-
2src/citra_qt/main.h
-
2src/citra_qt/ui_settings.h
-
4src/common/common_types.h
-
5src/common/quaternion.h
-
2src/common/scm_rev.cpp.in
-
1src/common/scm_rev.h
-
2src/common/string_util.cpp
-
2src/common/string_util.h
-
29src/common/vector_math.h
-
20src/core/CMakeLists.txt
-
22src/core/arm/arm_interface.h
-
93src/core/arm/dynarmic/arm_dynarmic.cpp
-
17src/core/arm/dynarmic/arm_dynarmic.h
-
19src/core/arm/dyncom/arm_dyncom.cpp
-
5src/core/arm/dyncom/arm_dyncom.h
-
8src/core/arm/dyncom/arm_dyncom_interpreter.cpp
-
2src/core/arm/skyeye_common/armstate.h
-
12src/core/core.cpp
-
9src/core/core.h
-
36src/core/core_timing.cpp
-
6src/core/core_timing.h
-
2src/core/file_sys/archive_backend.cpp
-
29src/core/file_sys/archive_ncch.cpp
-
41src/core/file_sys/archive_sdmc.cpp
-
60src/core/file_sys/archive_selfncch.cpp
-
13src/core/file_sys/archive_selfncch.h
-
423src/core/file_sys/ncch_container.cpp
-
274src/core/file_sys/ncch_container.h
-
41src/core/file_sys/savedata_archive.cpp
-
212src/core/file_sys/title_metadata.cpp
-
125src/core/file_sys/title_metadata.h
-
97src/core/frontend/emu_window.cpp
-
114src/core/frontend/emu_window.h
-
36src/core/frontend/framebuffer_layout.cpp
-
11src/core/frontend/framebuffer_layout.h
-
25src/core/frontend/input.h
-
89src/core/frontend/motion_emu.cpp
-
52src/core/frontend/motion_emu.h
-
24src/core/gdbstub/gdbstub.cpp
-
4src/core/hle/applets/erreula.cpp
@ -1,67 +0,0 @@ |
|||
#!/bin/bash |
|||
|
|||
set -e |
|||
set -x |
|||
|
|||
if grep -nr '\s$' src *.yml *.txt *.md Doxyfile .gitignore .gitmodules .travis* dist/*.desktop \ |
|||
dist/*.svg dist/*.xml; then |
|||
echo Trailing whitespace found, aborting |
|||
exit 1 |
|||
fi |
|||
|
|||
# Only run clang-format on Linux because we don't have 4.0 on OS X images |
|||
if [ "$TRAVIS_OS_NAME" = "linux" ]; then |
|||
# Default clang-format points to default 3.5 version one |
|||
CLANG_FORMAT=clang-format-3.9 |
|||
$CLANG_FORMAT --version |
|||
|
|||
if [ "$TRAVIS_EVENT_TYPE" = "pull_request" ]; then |
|||
# Get list of every file modified in this pull request |
|||
files_to_lint="$(git diff --name-only --diff-filter=ACMRTUXB $TRAVIS_COMMIT_RANGE | grep '^src/[^.]*[.]\(cpp\|h\)$' || true)" |
|||
else |
|||
# Check everything for branch pushes |
|||
files_to_lint="$(find src/ -name '*.cpp' -or -name '*.h')" |
|||
fi |
|||
|
|||
# Turn off tracing for this because it's too verbose |
|||
set +x |
|||
|
|||
for f in $files_to_lint; do |
|||
d=$(diff -u "$f" <($CLANG_FORMAT "$f") || true) |
|||
if ! [ -z "$d" ]; then |
|||
echo "!!! $f not compliant to coding style, here is the fix:" |
|||
echo "$d" |
|||
fail=1 |
|||
fi |
|||
done |
|||
|
|||
set -x |
|||
|
|||
if [ "$fail" = 1 ]; then |
|||
exit 1 |
|||
fi |
|||
fi |
|||
|
|||
#if OS is linux or is not set |
|||
if [ "$TRAVIS_OS_NAME" = "linux" -o -z "$TRAVIS_OS_NAME" ]; then |
|||
export CC=gcc-6 |
|||
export CXX=g++-6 |
|||
export PKG_CONFIG_PATH=$HOME/.local/lib/pkgconfig:$PKG_CONFIG_PATH |
|||
|
|||
mkdir build && cd build |
|||
cmake .. |
|||
make -j4 |
|||
|
|||
ctest -VV -C Release |
|||
elif [ "$TRAVIS_OS_NAME" = "osx" ]; then |
|||
set -o pipefail |
|||
|
|||
export MACOSX_DEPLOYMENT_TARGET=10.9 |
|||
export Qt5_DIR=$(brew --prefix)/opt/qt5 |
|||
|
|||
mkdir build && cd build |
|||
cmake .. -GXcode |
|||
xcodebuild -configuration Release |
|||
|
|||
ctest -VV -C Release |
|||
fi |
|||
@ -1,40 +0,0 @@ |
|||
#!/bin/sh |
|||
|
|||
set -e |
|||
set -x |
|||
|
|||
#if OS is linux or is not set |
|||
if [ "$TRAVIS_OS_NAME" = "linux" -o -z "$TRAVIS_OS_NAME" ]; then |
|||
export CC=gcc-6 |
|||
export CXX=g++-6 |
|||
mkdir -p $HOME/.local |
|||
|
|||
if [ ! -e $HOME/.local/bin/cmake ]; then |
|||
echo "CMake not found in the cache, get and extract it..." |
|||
curl -L http://www.cmake.org/files/v3.6/cmake-3.6.3-Linux-x86_64.tar.gz \ |
|||
| tar -xz -C $HOME/.local --strip-components=1 |
|||
else |
|||
echo "Using cached CMake" |
|||
fi |
|||
|
|||
if [ ! -e $HOME/.local/lib/libSDL2.la ]; then |
|||
echo "SDL2 not found in cache, get and build it..." |
|||
wget http://libsdl.org/release/SDL2-2.0.5.tar.gz -O - | tar xz |
|||
cd SDL2-2.0.5 |
|||
./configure --prefix=$HOME/.local |
|||
make -j4 && make install |
|||
else |
|||
echo "Using cached SDL2" |
|||
fi |
|||
|
|||
export DEBIAN_FRONTEND=noninteractive |
|||
# Amazing placebo security |
|||
curl http://apt.llvm.org/llvm-snapshot.gpg.key | sudo -E apt-key add - |
|||
sudo -E add-apt-repository "deb http://apt.llvm.org/trusty/ llvm-toolchain-trusty-3.9 main" |
|||
sudo -E apt-get -yq update |
|||
sudo -E apt-get -yq install clang-format-3.9 |
|||
|
|||
elif [ "$TRAVIS_OS_NAME" = "osx" ]; then |
|||
brew update |
|||
brew install qt5 sdl2 dylibbundler |
|||
fi |
|||
@ -1,129 +0,0 @@ |
|||
if [ "$TRAVIS_EVENT_TYPE" = "push" ]&&[ "$TRAVIS_BRANCH" = "master" ]; then |
|||
GITDATE="`git show -s --date=short --format='%ad' | sed 's/-//g'`" |
|||
GITREV="`git show -s --format='%h'`" |
|||
mkdir -p artifacts |
|||
|
|||
if [ "$TRAVIS_OS_NAME" = "linux" -o -z "$TRAVIS_OS_NAME" ]; then |
|||
REV_NAME="citra-linux-${GITDATE}-${GITREV}" |
|||
ARCHIVE_NAME="${REV_NAME}.tar.xz" |
|||
COMPRESSION_FLAGS="-cJvf" |
|||
mkdir "$REV_NAME" |
|||
|
|||
cp build/src/citra/citra "$REV_NAME" |
|||
cp build/src/citra_qt/citra-qt "$REV_NAME" |
|||
elif [ "$TRAVIS_OS_NAME" = "osx" ]; then |
|||
REV_NAME="citra-osx-${GITDATE}-${GITREV}" |
|||
ARCHIVE_NAME="${REV_NAME}.tar.gz" |
|||
COMPRESSION_FLAGS="-czvf" |
|||
mkdir "$REV_NAME" |
|||
|
|||
cp build/src/citra/Release/citra "$REV_NAME" |
|||
cp -r build/src/citra_qt/Release/citra-qt.app "$REV_NAME" |
|||
|
|||
# move qt libs into app bundle for deployment |
|||
$(brew --prefix)/opt/qt5/bin/macdeployqt "${REV_NAME}/citra-qt.app" |
|||
|
|||
# move SDL2 libs into folder for deployment |
|||
dylibbundler -b -x "${REV_NAME}/citra" -cd -d "${REV_NAME}/libs" -p "@executable_path/libs/" |
|||
|
|||
# Make the changes to make the citra-qt app standalone (i.e. not dependent on the current brew installation). |
|||
# To do this, the absolute references to each and every QT framework must be re-written to point to the local frameworks |
|||
# (in the Contents/Frameworks folder). |
|||
# The "install_name_tool" is used to do so. |
|||
|
|||
# Coreutils is a hack to coerce Homebrew to point to the absolute Cellar path (symlink dereferenced). i.e: |
|||
# ls -l /usr/local/opt/qt5:: /usr/local/opt/qt5 -> ../Cellar/qt5/5.6.1-1 |
|||
# grealpath ../Cellar/qt5/5.6.1-1:: /usr/local/Cellar/qt5/5.6.1-1 |
|||
brew install coreutils |
|||
|
|||
REV_NAME_ALT=$REV_NAME/ |
|||
# grealpath is located in coreutils, there is no "realpath" for OS X :( |
|||
QT_BREWS_PATH=$(grealpath "$(brew --prefix qt5)") |
|||
BREW_PATH=$(brew --prefix) |
|||
QT_VERSION_NUM=5 |
|||
|
|||
$BREW_PATH/opt/qt5/bin/macdeployqt "${REV_NAME_ALT}citra-qt.app" \ |
|||
-executable="${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt" |
|||
|
|||
# These are the files that macdeployqt packed into Contents/Frameworks/ - we don't want those, so we replace them. |
|||
declare -a macos_libs=("QtCore" "QtWidgets" "QtGui" "QtOpenGL" "QtPrintSupport") |
|||
|
|||
for macos_lib in "${macos_libs[@]}" |
|||
do |
|||
SC_FRAMEWORK_PART=$macos_lib.framework/Versions/$QT_VERSION_NUM/$macos_lib |
|||
# Replace macdeployqt versions of the Frameworks with our own (from /usr/local/opt/qt5/lib/) |
|||
cp "$BREW_PATH/opt/qt5/lib/$SC_FRAMEWORK_PART" "${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$SC_FRAMEWORK_PART" |
|||
|
|||
# Replace references within the embedded Framework files with "internal" versions. |
|||
for macos_lib2 in "${macos_libs[@]}" |
|||
do |
|||
# Since brew references both the non-symlinked and symlink paths of QT5, it needs to be duplicated. |
|||
# /usr/local/Cellar/qt5/5.6.1-1/lib and /usr/local/opt/qt5/lib both resolve to the same files. |
|||
# So the two lines below are effectively duplicates when resolved as a path, but as strings, they aren't. |
|||
RM_FRAMEWORK_PART=$macos_lib2.framework/Versions/$QT_VERSION_NUM/$macos_lib2 |
|||
install_name_tool -change \ |
|||
$QT_BREWS_PATH/lib/$RM_FRAMEWORK_PART \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$SC_FRAMEWORK_PART" |
|||
install_name_tool -change \ |
|||
"$BREW_PATH/opt/qt5/lib/$RM_FRAMEWORK_PART" \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$SC_FRAMEWORK_PART" |
|||
done |
|||
done |
|||
|
|||
# Handles `This application failed to start because it could not find or load the Qt platform plugin "cocoa"` |
|||
# Which manifests itself as: |
|||
# "Exception Type: EXC_CRASH (SIGABRT) | Exception Codes: 0x0000000000000000, 0x0000000000000000 | Exception Note: EXC_CORPSE_NOTIFY" |
|||
# There may be more dylibs needed to be fixed... |
|||
declare -a macos_plugins=("Plugins/platforms/libqcocoa.dylib") |
|||
|
|||
for macos_lib in "${macos_plugins[@]}" |
|||
do |
|||
install_name_tool -id @executable_path/../$macos_lib "${REV_NAME_ALT}citra-qt.app/Contents/$macos_lib" |
|||
for macos_lib2 in "${macos_libs[@]}" |
|||
do |
|||
RM_FRAMEWORK_PART=$macos_lib2.framework/Versions/$QT_VERSION_NUM/$macos_lib2 |
|||
install_name_tool -change \ |
|||
$QT_BREWS_PATH/lib/$RM_FRAMEWORK_PART \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/$macos_lib" |
|||
install_name_tool -change \ |
|||
"$BREW_PATH/opt/qt5/lib/$RM_FRAMEWORK_PART" \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/$macos_lib" |
|||
done |
|||
done |
|||
|
|||
for macos_lib in "${macos_libs[@]}" |
|||
do |
|||
# Debugging info for Travis-CI |
|||
otool -L "${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$macos_lib.framework/Versions/$QT_VERSION_NUM/$macos_lib" |
|||
done |
|||
|
|||
# Make the citra-qt.app application launch a debugging terminal. |
|||
# Store away the actual binary |
|||
mv ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt-bin |
|||
|
|||
cat > ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt <<EOL |
|||
#!/usr/bin/env bash |
|||
cd "\`dirname "\$0"\`" |
|||
chmod +x citra-qt-bin |
|||
open citra-qt-bin --args "\$@" |
|||
EOL |
|||
# Content that will serve as the launching script for citra (within the .app folder) |
|||
|
|||
# Make the launching script executable |
|||
chmod +x ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt |
|||
|
|||
fi |
|||
|
|||
# Copy documentation |
|||
cp license.txt "$REV_NAME" |
|||
cp README.md "$REV_NAME" |
|||
|
|||
tar $COMPRESSION_FLAGS "$ARCHIVE_NAME" "$REV_NAME" |
|||
|
|||
# move the compiled archive into the artifacts directory to be uploaded by travis releases |
|||
mv "$ARCHIVE_NAME" artifacts/ |
|||
fi |
|||
@ -0,0 +1,37 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
if grep -nr '\s$' src *.yml *.txt *.md Doxyfile .gitignore .gitmodules .travis* dist/*.desktop \ |
|||
dist/*.svg dist/*.xml; then |
|||
echo Trailing whitespace found, aborting |
|||
exit 1 |
|||
fi |
|||
|
|||
# Default clang-format points to default 3.5 version one |
|||
CLANG_FORMAT=clang-format-3.9 |
|||
$CLANG_FORMAT --version |
|||
|
|||
if [ "$TRAVIS_EVENT_TYPE" = "pull_request" ]; then |
|||
# Get list of every file modified in this pull request |
|||
files_to_lint="$(git diff --name-only --diff-filter=ACMRTUXB $TRAVIS_COMMIT_RANGE | grep '^src/[^.]*[.]\(cpp\|h\)$' || true)" |
|||
else |
|||
# Check everything for branch pushes |
|||
files_to_lint="$(find src/ -name '*.cpp' -or -name '*.h')" |
|||
fi |
|||
|
|||
# Turn off tracing for this because it's too verbose |
|||
set +x |
|||
|
|||
for f in $files_to_lint; do |
|||
d=$(diff -u "$f" <($CLANG_FORMAT "$f") || true) |
|||
if ! [ -z "$d" ]; then |
|||
echo "!!! $f not compliant to coding style, here is the fix:" |
|||
echo "$d" |
|||
fail=1 |
|||
fi |
|||
done |
|||
|
|||
set -x |
|||
|
|||
if [ "$fail" = 1 ]; then |
|||
exit 1 |
|||
fi |
|||
@ -0,0 +1,22 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
# Copy documentation |
|||
cp license.txt "$REV_NAME" |
|||
cp README.md "$REV_NAME" |
|||
|
|||
tar $COMPRESSION_FLAGS "$ARCHIVE_NAME" "$REV_NAME" |
|||
|
|||
# Find out what release we are building |
|||
if [ -z $TRAVIS_TAG ]; then |
|||
RELEASE_NAME=head |
|||
else |
|||
RELEASE_NAME=$(echo $TRAVIS_TAG | cut -d- -f1) |
|||
fi |
|||
|
|||
mv "$REV_NAME" $RELEASE_NAME |
|||
|
|||
7z a "$REV_NAME.7z" $RELEASE_NAME |
|||
|
|||
# move the compiled archive into the artifacts directory to be uploaded by travis releases |
|||
mv "$ARCHIVE_NAME" artifacts/ |
|||
mv "$REV_NAME.7z" artifacts/ |
|||
@ -0,0 +1,6 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
GITDATE="`git show -s --date=short --format='%ad' | sed 's/-//g'`" |
|||
GITREV="`git show -s --format='%h'`" |
|||
|
|||
mkdir -p artifacts |
|||
@ -0,0 +1,3 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
docker run -v $(pwd):/citra ubuntu:16.04 /bin/bash /citra/.travis/linux/docker.sh |
|||
@ -0,0 +1,3 @@ |
|||
#!/bin/sh -ex |
|||
|
|||
docker pull ubuntu:16.04 |
|||
@ -0,0 +1,17 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
cd /citra |
|||
|
|||
apt-get update |
|||
apt-get install -y build-essential libsdl2-dev qtbase5-dev libqt5opengl5-dev libcurl4-openssl-dev libssl-dev wget git |
|||
|
|||
# Get a recent version of CMake |
|||
wget https://cmake.org/files/v3.9/cmake-3.9.0-Linux-x86_64.sh |
|||
echo y | sh cmake-3.9.0-Linux-x86_64.sh --prefix=cmake |
|||
export PATH=/citra/cmake/cmake-3.9.0-Linux-x86_64/bin:$PATH |
|||
|
|||
mkdir build && cd build |
|||
cmake .. -DUSE_SYSTEM_CURL=ON -DCMAKE_BUILD_TYPE=Release |
|||
make -j4 |
|||
|
|||
ctest -VV -C Release |
|||
@ -0,0 +1,14 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
. .travis/common/pre-upload.sh |
|||
|
|||
REV_NAME="citra-linux-${GITDATE}-${GITREV}" |
|||
ARCHIVE_NAME="${REV_NAME}.tar.xz" |
|||
COMPRESSION_FLAGS="-cJvf" |
|||
|
|||
mkdir "$REV_NAME" |
|||
|
|||
cp build/src/citra/citra "$REV_NAME" |
|||
cp build/src/citra_qt/citra-qt "$REV_NAME" |
|||
|
|||
. .travis/common/post-upload.sh |
|||
@ -0,0 +1,12 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
set -o pipefail |
|||
|
|||
export MACOSX_DEPLOYMENT_TARGET=10.9 |
|||
export Qt5_DIR=$(brew --prefix)/opt/qt5 |
|||
|
|||
mkdir build && cd build |
|||
cmake .. -DUSE_SYSTEM_CURL=ON -DCMAKE_OSX_ARCHITECTURES="x86_64;x86_64h" -DCMAKE_BUILD_TYPE=Release |
|||
make -j4 |
|||
|
|||
ctest -VV -C Release |
|||
@ -0,0 +1,4 @@ |
|||
#!/bin/sh -ex |
|||
|
|||
brew update |
|||
brew install qt5 sdl2 dylibbundler p7zip |
|||
@ -0,0 +1,110 @@ |
|||
#!/bin/bash -ex |
|||
|
|||
. .travis/common/pre-upload.sh |
|||
|
|||
REV_NAME="citra-osx-${GITDATE}-${GITREV}" |
|||
ARCHIVE_NAME="${REV_NAME}.tar.gz" |
|||
COMPRESSION_FLAGS="-czvf" |
|||
|
|||
mkdir "$REV_NAME" |
|||
|
|||
cp build/src/citra/citra "$REV_NAME" |
|||
cp -r build/src/citra_qt/citra-qt.app "$REV_NAME" |
|||
|
|||
# move qt libs into app bundle for deployment |
|||
$(brew --prefix)/opt/qt5/bin/macdeployqt "${REV_NAME}/citra-qt.app" |
|||
|
|||
# move SDL2 libs into folder for deployment |
|||
dylibbundler -b -x "${REV_NAME}/citra" -cd -d "${REV_NAME}/libs" -p "@executable_path/libs/" |
|||
|
|||
# Make the changes to make the citra-qt app standalone (i.e. not dependent on the current brew installation). |
|||
# To do this, the absolute references to each and every QT framework must be re-written to point to the local frameworks |
|||
# (in the Contents/Frameworks folder). |
|||
# The "install_name_tool" is used to do so. |
|||
|
|||
# Coreutils is a hack to coerce Homebrew to point to the absolute Cellar path (symlink dereferenced). i.e: |
|||
# ls -l /usr/local/opt/qt5:: /usr/local/opt/qt5 -> ../Cellar/qt5/5.6.1-1 |
|||
# grealpath ../Cellar/qt5/5.6.1-1:: /usr/local/Cellar/qt5/5.6.1-1 |
|||
brew install coreutils || brew upgrade coreutils || true |
|||
|
|||
REV_NAME_ALT=$REV_NAME/ |
|||
# grealpath is located in coreutils, there is no "realpath" for OS X :( |
|||
QT_BREWS_PATH=$(grealpath "$(brew --prefix qt5)") |
|||
BREW_PATH=$(brew --prefix) |
|||
QT_VERSION_NUM=5 |
|||
|
|||
$BREW_PATH/opt/qt5/bin/macdeployqt "${REV_NAME_ALT}citra-qt.app" \ |
|||
-executable="${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt" |
|||
|
|||
# These are the files that macdeployqt packed into Contents/Frameworks/ - we don't want those, so we replace them. |
|||
declare -a macos_libs=("QtCore" "QtWidgets" "QtGui" "QtOpenGL" "QtPrintSupport") |
|||
|
|||
for macos_lib in "${macos_libs[@]}" |
|||
do |
|||
SC_FRAMEWORK_PART=$macos_lib.framework/Versions/$QT_VERSION_NUM/$macos_lib |
|||
# Replace macdeployqt versions of the Frameworks with our own (from /usr/local/opt/qt5/lib/) |
|||
cp "$BREW_PATH/opt/qt5/lib/$SC_FRAMEWORK_PART" "${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$SC_FRAMEWORK_PART" |
|||
|
|||
# Replace references within the embedded Framework files with "internal" versions. |
|||
for macos_lib2 in "${macos_libs[@]}" |
|||
do |
|||
# Since brew references both the non-symlinked and symlink paths of QT5, it needs to be duplicated. |
|||
# /usr/local/Cellar/qt5/5.6.1-1/lib and /usr/local/opt/qt5/lib both resolve to the same files. |
|||
# So the two lines below are effectively duplicates when resolved as a path, but as strings, they aren't. |
|||
RM_FRAMEWORK_PART=$macos_lib2.framework/Versions/$QT_VERSION_NUM/$macos_lib2 |
|||
install_name_tool -change \ |
|||
$QT_BREWS_PATH/lib/$RM_FRAMEWORK_PART \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$SC_FRAMEWORK_PART" |
|||
install_name_tool -change \ |
|||
"$BREW_PATH/opt/qt5/lib/$RM_FRAMEWORK_PART" \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$SC_FRAMEWORK_PART" |
|||
done |
|||
done |
|||
|
|||
# Handles `This application failed to start because it could not find or load the Qt platform plugin "cocoa"` |
|||
# Which manifests itself as: |
|||
# "Exception Type: EXC_CRASH (SIGABRT) | Exception Codes: 0x0000000000000000, 0x0000000000000000 | Exception Note: EXC_CORPSE_NOTIFY" |
|||
# There may be more dylibs needed to be fixed... |
|||
declare -a macos_plugins=("Plugins/platforms/libqcocoa.dylib") |
|||
|
|||
for macos_lib in "${macos_plugins[@]}" |
|||
do |
|||
install_name_tool -id @executable_path/../$macos_lib "${REV_NAME_ALT}citra-qt.app/Contents/$macos_lib" |
|||
for macos_lib2 in "${macos_libs[@]}" |
|||
do |
|||
RM_FRAMEWORK_PART=$macos_lib2.framework/Versions/$QT_VERSION_NUM/$macos_lib2 |
|||
install_name_tool -change \ |
|||
$QT_BREWS_PATH/lib/$RM_FRAMEWORK_PART \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/$macos_lib" |
|||
install_name_tool -change \ |
|||
"$BREW_PATH/opt/qt5/lib/$RM_FRAMEWORK_PART" \ |
|||
@executable_path/../Frameworks/$RM_FRAMEWORK_PART \ |
|||
"${REV_NAME_ALT}citra-qt.app/Contents/$macos_lib" |
|||
done |
|||
done |
|||
|
|||
for macos_lib in "${macos_libs[@]}" |
|||
do |
|||
# Debugging info for Travis-CI |
|||
otool -L "${REV_NAME_ALT}citra-qt.app/Contents/Frameworks/$macos_lib.framework/Versions/$QT_VERSION_NUM/$macos_lib" |
|||
done |
|||
|
|||
# Make the citra-qt.app application launch a debugging terminal. |
|||
# Store away the actual binary |
|||
mv ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt-bin |
|||
|
|||
cat > ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt <<EOL |
|||
#!/usr/bin/env bash |
|||
cd "\`dirname "\$0"\`" |
|||
chmod +x citra-qt-bin |
|||
open citra-qt-bin --args "\$@" |
|||
EOL |
|||
# Content that will serve as the launching script for citra (within the .app folder) |
|||
|
|||
# Make the launching script executable |
|||
chmod +x ${REV_NAME_ALT}citra-qt.app/Contents/MacOS/citra-qt |
|||
|
|||
. .travis/common/post-upload.sh |
|||
@ -0,0 +1,18 @@ |
|||
|
|||
# This function downloads a binary library package from our external repo. |
|||
# Params: |
|||
# remote_path: path to the file to download, relative to the remote repository root |
|||
# prefix_var: name of a variable which will be set with the path to the extracted contents |
|||
function(download_bundled_external remote_path lib_name prefix_var) |
|||
set(prefix "${CMAKE_BINARY_DIR}/externals/${lib_name}") |
|||
if (NOT EXISTS "${prefix}") |
|||
message(STATUS "Downloading binaries for ${lib_name}...") |
|||
file(DOWNLOAD |
|||
https://github.com/citra-emu/ext-windows-bin/raw/master/${remote_path}${lib_name}.7z |
|||
"${CMAKE_BINARY_DIR}/externals/${lib_name}.7z" SHOW_PROGRESS) |
|||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${CMAKE_BINARY_DIR}/externals/${lib_name}.7z" |
|||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals") |
|||
endif() |
|||
message(STATUS "Using bundled binaries at ${prefix}") |
|||
set(${prefix_var} "${prefix}" PARENT_SCOPE) |
|||
endfunction() |
|||
@ -0,0 +1,24 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
|||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> |
|||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> |
|||
<security> |
|||
<requestedPrivileges> |
|||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/> |
|||
</requestedPrivileges> |
|||
</security> |
|||
</trustInfo> |
|||
<application xmlns="urn:schemas-microsoft-com:asm.v3"> |
|||
<windowsSettings> |
|||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware> |
|||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware> |
|||
</windowsSettings> |
|||
</application> |
|||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> |
|||
<application> |
|||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> |
|||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> |
|||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> |
|||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> |
|||
</application> |
|||
</compatibility> |
|||
</assembly> |
|||
80
dist/citra.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
|
Before Width: 72 | Height: 72 | Size: 8.6 KiB After Width: 72 | Height: 72 | Size: 7.6 KiB |
|
After Width: 16 | Height: 16 | Size: 451 B |
|
After Width: 16 | Height: 16 | Size: 428 B |
@ -0,0 +1,6 @@ |
|||
<RCC> |
|||
<qresource prefix="icons"> |
|||
<file>checked.png</file> |
|||
<file>failed.png</file> |
|||
</qresource> |
|||
</RCC> |
|||
@ -1 +1 @@ |
|||
Subproject commit 841c37e34765487a2968357369ab74db8b10a62d |
|||
Subproject commit 24bc2b85674254fb294e717eb5b47d9f53e786b8 |
|||
@ -1 +1 @@ |
|||
Subproject commit 8f15e3f70cb96e56705e5de6ba97b5d09423a56b |
|||
Subproject commit 69eccf826d657a6cfb1d731b00629939d230ec5f |
|||
@ -1 +1 @@ |
|||
Subproject commit a84c120eff13d2fa3eadb41ef7afe0f7819f4d6c |
|||
Subproject commit 9d9ba122d4818f7ae1aef2197933ac696edb2331 |
|||
@ -1 +1 @@ |
|||
Subproject commit 5274ec4dec498bd88ccbcd28862a0f78a3b95eff |
|||
Subproject commit 019d2089bbadf70d73ba85aa8ea51490b071262c |
|||
@ -0,0 +1,102 @@ |
|||
// Copyright 2017 Citra Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <QMessageBox>
|
|||
#include "citra_qt/configuration/configure_web.h"
|
|||
#include "core/settings.h"
|
|||
#include "core/telemetry_session.h"
|
|||
#include "ui_configure_web.h"
|
|||
|
|||
ConfigureWeb::ConfigureWeb(QWidget* parent) |
|||
: QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) { |
|||
ui->setupUi(this); |
|||
connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this, |
|||
&ConfigureWeb::RefreshTelemetryID); |
|||
connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin); |
|||
connect(this, &ConfigureWeb::LoginVerified, this, &ConfigureWeb::OnLoginVerified); |
|||
|
|||
this->setConfiguration(); |
|||
} |
|||
|
|||
ConfigureWeb::~ConfigureWeb() {} |
|||
|
|||
void ConfigureWeb::setConfiguration() { |
|||
ui->web_credentials_disclaimer->setWordWrap(true); |
|||
ui->telemetry_learn_more->setOpenExternalLinks(true); |
|||
ui->telemetry_learn_more->setText(tr("<a " |
|||
"href='https://citra-emu.org/entry/" |
|||
"telemetry-and-why-thats-a-good-thing/'>Learn more</a>")); |
|||
|
|||
ui->web_signup_link->setOpenExternalLinks(true); |
|||
ui->web_signup_link->setText(tr("<a href='https://services.citra-emu.org/'>Sign up</a>")); |
|||
ui->web_token_info_link->setOpenExternalLinks(true); |
|||
ui->web_token_info_link->setText( |
|||
tr("<a href='https://citra-emu.org/wiki/citra-web-service/'>What is my token?</a>")); |
|||
|
|||
ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry); |
|||
ui->edit_username->setText(QString::fromStdString(Settings::values.citra_username)); |
|||
ui->edit_token->setText(QString::fromStdString(Settings::values.citra_token)); |
|||
// Connect after setting the values, to avoid calling OnLoginChanged now
|
|||
connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); |
|||
connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); |
|||
ui->label_telemetry_id->setText( |
|||
tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper())); |
|||
user_verified = true; |
|||
} |
|||
|
|||
void ConfigureWeb::applyConfiguration() { |
|||
Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); |
|||
if (user_verified) { |
|||
Settings::values.citra_username = ui->edit_username->text().toStdString(); |
|||
Settings::values.citra_token = ui->edit_token->text().toStdString(); |
|||
} else { |
|||
QMessageBox::warning(this, tr("Username and token not verfied"), |
|||
tr("Username and token were not verified. The changes to your " |
|||
"username and/or token have not been saved.")); |
|||
} |
|||
Settings::Apply(); |
|||
} |
|||
|
|||
void ConfigureWeb::RefreshTelemetryID() { |
|||
const u64 new_telemetry_id{Core::RegenerateTelemetryId()}; |
|||
ui->label_telemetry_id->setText( |
|||
tr("Telemetry ID: 0x%1").arg(QString::number(new_telemetry_id, 16).toUpper())); |
|||
} |
|||
|
|||
void ConfigureWeb::OnLoginChanged() { |
|||
if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) { |
|||
user_verified = true; |
|||
ui->label_username_verified->setPixmap(QPixmap(":/icons/checked.png")); |
|||
ui->label_token_verified->setPixmap(QPixmap(":/icons/checked.png")); |
|||
} else { |
|||
user_verified = false; |
|||
ui->label_username_verified->setPixmap(QPixmap(":/icons/failed.png")); |
|||
ui->label_token_verified->setPixmap(QPixmap(":/icons/failed.png")); |
|||
} |
|||
} |
|||
|
|||
void ConfigureWeb::VerifyLogin() { |
|||
verified = |
|||
Core::VerifyLogin(ui->edit_username->text().toStdString(), |
|||
ui->edit_token->text().toStdString(), [&]() { emit LoginVerified(); }); |
|||
ui->button_verify_login->setDisabled(true); |
|||
ui->button_verify_login->setText(tr("Verifying")); |
|||
} |
|||
|
|||
void ConfigureWeb::OnLoginVerified() { |
|||
ui->button_verify_login->setEnabled(true); |
|||
ui->button_verify_login->setText(tr("Verify")); |
|||
if (verified.get()) { |
|||
user_verified = true; |
|||
ui->label_username_verified->setPixmap(QPixmap(":/icons/checked.png")); |
|||
ui->label_token_verified->setPixmap(QPixmap(":/icons/checked.png")); |
|||
} else { |
|||
ui->label_username_verified->setPixmap(QPixmap(":/icons/failed.png")); |
|||
ui->label_token_verified->setPixmap(QPixmap(":/icons/failed.png")); |
|||
QMessageBox::critical( |
|||
this, tr("Verification failed"), |
|||
tr("Verification failed. Check that you have entered your username and token " |
|||
"correctly, and that your internet connection is working.")); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// Copyright 2017 Citra Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <future> |
|||
#include <memory> |
|||
#include <QWidget> |
|||
|
|||
namespace Ui { |
|||
class ConfigureWeb; |
|||
} |
|||
|
|||
class ConfigureWeb : public QWidget { |
|||
Q_OBJECT |
|||
|
|||
public: |
|||
explicit ConfigureWeb(QWidget* parent = nullptr); |
|||
~ConfigureWeb(); |
|||
|
|||
void applyConfiguration(); |
|||
|
|||
public slots: |
|||
void RefreshTelemetryID(); |
|||
void OnLoginChanged(); |
|||
void VerifyLogin(); |
|||
void OnLoginVerified(); |
|||
|
|||
signals: |
|||
void LoginVerified(); |
|||
|
|||
private: |
|||
void setConfiguration(); |
|||
|
|||
bool user_verified = true; |
|||
std::future<bool> verified; |
|||
|
|||
std::unique_ptr<Ui::ConfigureWeb> ui; |
|||
}; |
|||
@ -0,0 +1,190 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<ui version="4.0"> |
|||
<class>ConfigureWeb</class> |
|||
<widget class="QWidget" name="ConfigureWeb"> |
|||
<property name="geometry"> |
|||
<rect> |
|||
<x>0</x> |
|||
<y>0</y> |
|||
<width>926</width> |
|||
<height>561</height> |
|||
</rect> |
|||
</property> |
|||
<property name="windowTitle"> |
|||
<string>Form</string> |
|||
</property> |
|||
<layout class="QVBoxLayout" name="verticalLayout"> |
|||
<item> |
|||
<layout class="QVBoxLayout" name="verticalLayout_3"> |
|||
<item> |
|||
<widget class="QGroupBox" name="groupBoxWebConfig"> |
|||
<property name="title"> |
|||
<string>Citra Web Service</string> |
|||
</property> |
|||
<layout class="QVBoxLayout" name="verticalLayoutCitraWebService"> |
|||
<item> |
|||
<widget class="QLabel" name="web_credentials_disclaimer"> |
|||
<property name="text"> |
|||
<string>By providing your username and token, you agree to allow Citra to collect additional usage data, which may include user identifying information.</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<layout class="QGridLayout" name="gridLayoutCitraUsername"> |
|||
<item row="2" column="3"> |
|||
<widget class="QPushButton" name="button_verify_login"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="layoutDirection"> |
|||
<enum>Qt::RightToLeft</enum> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Verify</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="2" column="0"> |
|||
<widget class="QLabel" name="web_signup_link"> |
|||
<property name="text"> |
|||
<string>Sign up</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="1" colspan="3"> |
|||
<widget class="QLineEdit" name="edit_username"> |
|||
<property name="maxLength"> |
|||
<number>36</number> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="0"> |
|||
<widget class="QLabel" name="label_token"> |
|||
<property name="text"> |
|||
<string>Token: </string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="4"> |
|||
<widget class="QLabel" name="label_token_verified"> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="0"> |
|||
<widget class="QLabel" name="label_username"> |
|||
<property name="text"> |
|||
<string>Username: </string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="4"> |
|||
<widget class="QLabel" name="label_username_verified"> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="1" colspan="3"> |
|||
<widget class="QLineEdit" name="edit_token"> |
|||
<property name="maxLength"> |
|||
<number>36</number> |
|||
</property> |
|||
<property name="echoMode"> |
|||
<enum>QLineEdit::Password</enum> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="2" column="1"> |
|||
<widget class="QLabel" name="web_token_info_link"> |
|||
<property name="text"> |
|||
<string>What is my token?</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="2" column="2"> |
|||
<spacer name="horizontalSpacer"> |
|||
<property name="orientation"> |
|||
<enum>Qt::Horizontal</enum> |
|||
</property> |
|||
<property name="sizeHint" stdset="0"> |
|||
<size> |
|||
<width>40</width> |
|||
<height>20</height> |
|||
</size> |
|||
</property> |
|||
</spacer> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QGroupBox" name="groupBox"> |
|||
<property name="title"> |
|||
<string>Telemetry</string> |
|||
</property> |
|||
<layout class="QVBoxLayout" name="verticalLayout_2"> |
|||
<item> |
|||
<widget class="QCheckBox" name="toggle_telemetry"> |
|||
<property name="text"> |
|||
<string>Share anonymous usage data with the Citra team</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="telemetry_learn_more"> |
|||
<property name="text"> |
|||
<string>Learn more</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<layout class="QGridLayout" name="gridLayoutTelemetryId"> |
|||
<item row="0" column="0"> |
|||
<widget class="QLabel" name="label_telemetry_id"> |
|||
<property name="text"> |
|||
<string>Telemetry ID:</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="1"> |
|||
<widget class="QPushButton" name="button_regenerate_telemetry_id"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="layoutDirection"> |
|||
<enum>Qt::RightToLeft</enum> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Regenerate</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
<item> |
|||
<spacer name="verticalSpacer"> |
|||
<property name="orientation"> |
|||
<enum>Qt::Vertical</enum> |
|||
</property> |
|||
<property name="sizeHint" stdset="0"> |
|||
<size> |
|||
<width>20</width> |
|||
<height>40</height> |
|||
</size> |
|||
</property> |
|||
</spacer> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
<resources/> |
|||
<connections/> |
|||
</ui> |
|||
@ -0,0 +1,423 @@ |
|||
// Copyright 2017 Citra Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <cinttypes>
|
|||
#include <cstring>
|
|||
#include <memory>
|
|||
#include "common/common_types.h"
|
|||
#include "common/logging/log.h"
|
|||
#include "core/core.h"
|
|||
#include "core/file_sys/ncch_container.h"
|
|||
#include "core/loader/loader.h"
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
// FileSys namespace
|
|||
|
|||
namespace FileSys { |
|||
|
|||
static const int kMaxSections = 8; ///< Maximum number of sections (files) in an ExeFs
|
|||
static const int kBlockSize = 0x200; ///< Size of ExeFS blocks (in bytes)
|
|||
|
|||
/**
|
|||
* Get the decompressed size of an LZSS compressed ExeFS file |
|||
* @param buffer Buffer of compressed file |
|||
* @param size Size of compressed buffer |
|||
* @return Size of decompressed buffer |
|||
*/ |
|||
static u32 LZSS_GetDecompressedSize(const u8* buffer, u32 size) { |
|||
u32 offset_size = *(u32*)(buffer + size - 4); |
|||
return offset_size + size; |
|||
} |
|||
|
|||
/**
|
|||
* Decompress ExeFS file (compressed with LZSS) |
|||
* @param compressed Compressed buffer |
|||
* @param compressed_size Size of compressed buffer |
|||
* @param decompressed Decompressed buffer |
|||
* @param decompressed_size Size of decompressed buffer |
|||
* @return True on success, otherwise false |
|||
*/ |
|||
static bool LZSS_Decompress(const u8* compressed, u32 compressed_size, u8* decompressed, |
|||
u32 decompressed_size) { |
|||
const u8* footer = compressed + compressed_size - 8; |
|||
u32 buffer_top_and_bottom = *reinterpret_cast<const u32*>(footer); |
|||
u32 out = decompressed_size; |
|||
u32 index = compressed_size - ((buffer_top_and_bottom >> 24) & 0xFF); |
|||
u32 stop_index = compressed_size - (buffer_top_and_bottom & 0xFFFFFF); |
|||
|
|||
memset(decompressed, 0, decompressed_size); |
|||
memcpy(decompressed, compressed, compressed_size); |
|||
|
|||
while (index > stop_index) { |
|||
u8 control = compressed[--index]; |
|||
|
|||
for (unsigned i = 0; i < 8; i++) { |
|||
if (index <= stop_index) |
|||
break; |
|||
if (index <= 0) |
|||
break; |
|||
if (out <= 0) |
|||
break; |
|||
|
|||
if (control & 0x80) { |
|||
// Check if compression is out of bounds
|
|||
if (index < 2) |
|||
return false; |
|||
index -= 2; |
|||
|
|||
u32 segment_offset = compressed[index] | (compressed[index + 1] << 8); |
|||
u32 segment_size = ((segment_offset >> 12) & 15) + 3; |
|||
segment_offset &= 0x0FFF; |
|||
segment_offset += 2; |
|||
|
|||
// Check if compression is out of bounds
|
|||
if (out < segment_size) |
|||
return false; |
|||
|
|||
for (unsigned j = 0; j < segment_size; j++) { |
|||
// Check if compression is out of bounds
|
|||
if (out + segment_offset >= decompressed_size) |
|||
return false; |
|||
|
|||
u8 data = decompressed[out + segment_offset]; |
|||
decompressed[--out] = data; |
|||
} |
|||
} else { |
|||
// Check if compression is out of bounds
|
|||
if (out < 1) |
|||
return false; |
|||
decompressed[--out] = compressed[--index]; |
|||
} |
|||
control <<= 1; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
NCCHContainer::NCCHContainer(const std::string& filepath) : filepath(filepath) { |
|||
file = FileUtil::IOFile(filepath, "rb"); |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::OpenFile(const std::string& filepath) { |
|||
this->filepath = filepath; |
|||
file = FileUtil::IOFile(filepath, "rb"); |
|||
|
|||
if (!file.IsOpen()) { |
|||
LOG_WARNING(Service_FS, "Failed to open %s", filepath.c_str()); |
|||
return Loader::ResultStatus::Error; |
|||
} |
|||
|
|||
LOG_DEBUG(Service_FS, "Opened %s", filepath.c_str()); |
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::Load() { |
|||
if (is_loaded) |
|||
return Loader::ResultStatus::Success; |
|||
|
|||
if (file.IsOpen()) { |
|||
// Reset read pointer in case this file has been read before.
|
|||
file.Seek(0, SEEK_SET); |
|||
|
|||
if (file.ReadBytes(&ncch_header, sizeof(NCCH_Header)) != sizeof(NCCH_Header)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
// Skip NCSD header and load first NCCH (NCSD is just a container of NCCH files)...
|
|||
if (Loader::MakeMagic('N', 'C', 'S', 'D') == ncch_header.magic) { |
|||
LOG_DEBUG(Service_FS, "Only loading the first (bootable) NCCH within the NCSD file!"); |
|||
ncch_offset = 0x4000; |
|||
file.Seek(ncch_offset, SEEK_SET); |
|||
file.ReadBytes(&ncch_header, sizeof(NCCH_Header)); |
|||
} |
|||
|
|||
// Verify we are loading the correct file type...
|
|||
if (Loader::MakeMagic('N', 'C', 'C', 'H') != ncch_header.magic) |
|||
return Loader::ResultStatus::ErrorInvalidFormat; |
|||
|
|||
has_header = true; |
|||
|
|||
// System archives and DLC don't have an extended header but have RomFS
|
|||
if (ncch_header.extended_header_size) { |
|||
if (file.ReadBytes(&exheader_header, sizeof(ExHeader_Header)) != |
|||
sizeof(ExHeader_Header)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
is_compressed = (exheader_header.codeset_info.flags.flag & 1) == 1; |
|||
u32 entry_point = exheader_header.codeset_info.text.address; |
|||
u32 code_size = exheader_header.codeset_info.text.code_size; |
|||
u32 stack_size = exheader_header.codeset_info.stack_size; |
|||
u32 bss_size = exheader_header.codeset_info.bss_size; |
|||
u32 core_version = exheader_header.arm11_system_local_caps.core_version; |
|||
u8 priority = exheader_header.arm11_system_local_caps.priority; |
|||
u8 resource_limit_category = |
|||
exheader_header.arm11_system_local_caps.resource_limit_category; |
|||
|
|||
LOG_DEBUG(Service_FS, "Name: %s", |
|||
exheader_header.codeset_info.name); |
|||
LOG_DEBUG(Service_FS, "Program ID: %016" PRIX64, |
|||
ncch_header.program_id); |
|||
LOG_DEBUG(Service_FS, "Code compressed: %s", is_compressed ? "yes" : "no"); |
|||
LOG_DEBUG(Service_FS, "Entry point: 0x%08X", entry_point); |
|||
LOG_DEBUG(Service_FS, "Code size: 0x%08X", code_size); |
|||
LOG_DEBUG(Service_FS, "Stack size: 0x%08X", stack_size); |
|||
LOG_DEBUG(Service_FS, "Bss size: 0x%08X", bss_size); |
|||
LOG_DEBUG(Service_FS, "Core version: %d", core_version); |
|||
LOG_DEBUG(Service_FS, "Thread priority: 0x%X", priority); |
|||
LOG_DEBUG(Service_FS, "Resource limit category: %d", resource_limit_category); |
|||
LOG_DEBUG(Service_FS, "System Mode: %d", |
|||
static_cast<int>(exheader_header.arm11_system_local_caps.system_mode)); |
|||
|
|||
if (exheader_header.system_info.jump_id != ncch_header.program_id) { |
|||
LOG_ERROR(Service_FS, |
|||
"ExHeader Program ID mismatch: the ROM is probably encrypted."); |
|||
return Loader::ResultStatus::ErrorEncrypted; |
|||
} |
|||
|
|||
has_exheader = true; |
|||
} |
|||
|
|||
// DLC can have an ExeFS and a RomFS but no extended header
|
|||
if (ncch_header.exefs_size) { |
|||
exefs_offset = ncch_header.exefs_offset * kBlockSize; |
|||
u32 exefs_size = ncch_header.exefs_size * kBlockSize; |
|||
|
|||
LOG_DEBUG(Service_FS, "ExeFS offset: 0x%08X", exefs_offset); |
|||
LOG_DEBUG(Service_FS, "ExeFS size: 0x%08X", exefs_size); |
|||
|
|||
file.Seek(exefs_offset + ncch_offset, SEEK_SET); |
|||
if (file.ReadBytes(&exefs_header, sizeof(ExeFs_Header)) != sizeof(ExeFs_Header)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
exefs_file = FileUtil::IOFile(filepath, "rb"); |
|||
has_exefs = true; |
|||
} |
|||
|
|||
if (ncch_header.romfs_offset != 0 && ncch_header.romfs_size != 0) |
|||
has_romfs = true; |
|||
} |
|||
|
|||
LoadOverrides(); |
|||
|
|||
// We need at least one of these or overrides, practically
|
|||
if (!(has_exefs || has_romfs || is_tainted)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
is_loaded = true; |
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::LoadOverrides() { |
|||
// Check for split-off files, mark the archive as tainted if we will use them
|
|||
std::string romfs_override = filepath + ".romfs"; |
|||
if (FileUtil::Exists(romfs_override)) { |
|||
is_tainted = true; |
|||
} |
|||
|
|||
// If we have a split-off exefs file/folder, it takes priority
|
|||
std::string exefs_override = filepath + ".exefs"; |
|||
std::string exefsdir_override = filepath + ".exefsdir/"; |
|||
if (FileUtil::Exists(exefs_override)) { |
|||
exefs_file = FileUtil::IOFile(exefs_override, "rb"); |
|||
|
|||
if (exefs_file.ReadBytes(&exefs_header, sizeof(ExeFs_Header)) == sizeof(ExeFs_Header)) { |
|||
LOG_DEBUG(Service_FS, "Loading ExeFS section from %s", exefs_override.c_str()); |
|||
exefs_offset = 0; |
|||
is_tainted = true; |
|||
has_exefs = true; |
|||
} else { |
|||
exefs_file = FileUtil::IOFile(filepath, "rb"); |
|||
} |
|||
} else if (FileUtil::Exists(exefsdir_override) && FileUtil::IsDirectory(exefsdir_override)) { |
|||
is_tainted = true; |
|||
} |
|||
|
|||
if (is_tainted) |
|||
LOG_WARNING(Service_FS, |
|||
"Loaded NCCH %s is tainted, application behavior may not be as expected!", |
|||
filepath.c_str()); |
|||
|
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vector<u8>& buffer) { |
|||
Loader::ResultStatus result = Load(); |
|||
if (result != Loader::ResultStatus::Success) |
|||
return result; |
|||
|
|||
// Check if we have files that can drop-in and replace
|
|||
result = LoadOverrideExeFSSection(name, buffer); |
|||
if (result == Loader::ResultStatus::Success || !has_exefs) |
|||
return result; |
|||
|
|||
// If we don't have any separate files, we'll need a full ExeFS
|
|||
if (!exefs_file.IsOpen()) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
LOG_DEBUG(Service_FS, "%d sections:", kMaxSections); |
|||
// Iterate through the ExeFs archive until we find a section with the specified name...
|
|||
for (unsigned section_number = 0; section_number < kMaxSections; section_number++) { |
|||
const auto& section = exefs_header.section[section_number]; |
|||
|
|||
// Load the specified section...
|
|||
if (strcmp(section.name, name) == 0) { |
|||
LOG_DEBUG(Service_FS, "%d - offset: 0x%08X, size: 0x%08X, name: %s", section_number, |
|||
section.offset, section.size, section.name); |
|||
|
|||
s64 section_offset = |
|||
(section.offset + exefs_offset + sizeof(ExeFs_Header) + ncch_offset); |
|||
exefs_file.Seek(section_offset, SEEK_SET); |
|||
|
|||
if (strcmp(section.name, ".code") == 0 && is_compressed) { |
|||
// Section is compressed, read compressed .code section...
|
|||
std::unique_ptr<u8[]> temp_buffer; |
|||
try { |
|||
temp_buffer.reset(new u8[section.size]); |
|||
} catch (std::bad_alloc&) { |
|||
return Loader::ResultStatus::ErrorMemoryAllocationFailed; |
|||
} |
|||
|
|||
if (exefs_file.ReadBytes(&temp_buffer[0], section.size) != section.size) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
// Decompress .code section...
|
|||
u32 decompressed_size = LZSS_GetDecompressedSize(&temp_buffer[0], section.size); |
|||
buffer.resize(decompressed_size); |
|||
if (!LZSS_Decompress(&temp_buffer[0], section.size, &buffer[0], decompressed_size)) |
|||
return Loader::ResultStatus::ErrorInvalidFormat; |
|||
} else { |
|||
// Section is uncompressed...
|
|||
buffer.resize(section.size); |
|||
if (exefs_file.ReadBytes(&buffer[0], section.size) != section.size) |
|||
return Loader::ResultStatus::Error; |
|||
} |
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
} |
|||
return Loader::ResultStatus::ErrorNotUsed; |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::LoadOverrideExeFSSection(const char* name, |
|||
std::vector<u8>& buffer) { |
|||
std::string override_name; |
|||
|
|||
// Map our section name to the extracted equivalent
|
|||
if (!strcmp(name, ".code")) |
|||
override_name = "code.bin"; |
|||
else if (!strcmp(name, "icon")) |
|||
override_name = "code.bin"; |
|||
else if (!strcmp(name, "banner")) |
|||
override_name = "banner.bnr"; |
|||
else if (!strcmp(name, "logo")) |
|||
override_name = "logo.bcma.lz"; |
|||
else |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
std::string section_override = filepath + ".exefsdir/" + override_name; |
|||
FileUtil::IOFile section_file(section_override, "rb"); |
|||
|
|||
if (section_file.IsOpen()) { |
|||
auto section_size = section_file.GetSize(); |
|||
buffer.resize(section_size); |
|||
|
|||
section_file.Seek(0, SEEK_SET); |
|||
if (section_file.ReadBytes(&buffer[0], section_size) == section_size) { |
|||
LOG_WARNING(Service_FS, "File %s overriding built-in ExeFS file", |
|||
section_override.c_str()); |
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
} |
|||
return Loader::ResultStatus::ErrorNotUsed; |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr<FileUtil::IOFile>& romfs_file, |
|||
u64& offset, u64& size) { |
|||
Loader::ResultStatus result = Load(); |
|||
if (result != Loader::ResultStatus::Success) |
|||
return result; |
|||
|
|||
if (ReadOverrideRomFS(romfs_file, offset, size) == Loader::ResultStatus::Success) |
|||
return Loader::ResultStatus::Success; |
|||
|
|||
if (!has_romfs) { |
|||
LOG_DEBUG(Service_FS, "RomFS requested from NCCH which has no RomFS"); |
|||
return Loader::ResultStatus::ErrorNotUsed; |
|||
} |
|||
|
|||
if (!file.IsOpen()) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
u32 romfs_offset = ncch_offset + (ncch_header.romfs_offset * kBlockSize) + 0x1000; |
|||
u32 romfs_size = (ncch_header.romfs_size * kBlockSize) - 0x1000; |
|||
|
|||
LOG_DEBUG(Service_FS, "RomFS offset: 0x%08X", romfs_offset); |
|||
LOG_DEBUG(Service_FS, "RomFS size: 0x%08X", romfs_size); |
|||
|
|||
if (file.GetSize() < romfs_offset + romfs_size) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
// We reopen the file, to allow its position to be independent from file's
|
|||
romfs_file = std::make_shared<FileUtil::IOFile>(filepath, "rb"); |
|||
if (!romfs_file->IsOpen()) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
offset = romfs_offset; |
|||
size = romfs_size; |
|||
|
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::ReadOverrideRomFS(std::shared_ptr<FileUtil::IOFile>& romfs_file, |
|||
u64& offset, u64& size) { |
|||
// Check for RomFS overrides
|
|||
std::string split_filepath = filepath + ".romfs"; |
|||
if (FileUtil::Exists(split_filepath)) { |
|||
romfs_file = std::make_shared<FileUtil::IOFile>(split_filepath, "rb"); |
|||
if (romfs_file->IsOpen()) { |
|||
LOG_WARNING(Service_FS, "File %s overriding built-in RomFS", split_filepath.c_str()); |
|||
offset = 0; |
|||
size = romfs_file->GetSize(); |
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
} |
|||
|
|||
return Loader::ResultStatus::ErrorNotUsed; |
|||
} |
|||
|
|||
Loader::ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) { |
|||
Loader::ResultStatus result = Load(); |
|||
if (result != Loader::ResultStatus::Success) |
|||
return result; |
|||
|
|||
if (!has_header) |
|||
return Loader::ResultStatus::ErrorNotUsed; |
|||
|
|||
program_id = ncch_header.program_id; |
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
|
|||
bool NCCHContainer::HasExeFS() { |
|||
Loader::ResultStatus result = Load(); |
|||
if (result != Loader::ResultStatus::Success) |
|||
return false; |
|||
|
|||
return has_exefs; |
|||
} |
|||
|
|||
bool NCCHContainer::HasRomFS() { |
|||
Loader::ResultStatus result = Load(); |
|||
if (result != Loader::ResultStatus::Success) |
|||
return false; |
|||
|
|||
return has_romfs; |
|||
} |
|||
|
|||
bool NCCHContainer::HasExHeader() { |
|||
Loader::ResultStatus result = Load(); |
|||
if (result != Loader::ResultStatus::Success) |
|||
return false; |
|||
|
|||
return has_exheader; |
|||
} |
|||
|
|||
} // namespace FileSys
|
|||
@ -0,0 +1,274 @@ |
|||
// Copyright 2017 Citra Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <cstddef> |
|||
#include <memory> |
|||
#include <string> |
|||
#include <vector> |
|||
#include "common/bit_field.h" |
|||
#include "common/common_types.h" |
|||
#include "common/file_util.h" |
|||
#include "common/swap.h" |
|||
#include "core/core.h" |
|||
|
|||
//////////////////////////////////////////////////////////////////////////////////////////////////// |
|||
/// NCCH header (Note: "NCCH" appears to be a publicly unknown acronym) |
|||
|
|||
struct NCCH_Header { |
|||
u8 signature[0x100]; |
|||
u32_le magic; |
|||
u32_le content_size; |
|||
u8 partition_id[8]; |
|||
u16_le maker_code; |
|||
u16_le version; |
|||
u8 reserved_0[4]; |
|||
u64_le program_id; |
|||
u8 reserved_1[0x10]; |
|||
u8 logo_region_hash[0x20]; |
|||
u8 product_code[0x10]; |
|||
u8 extended_header_hash[0x20]; |
|||
u32_le extended_header_size; |
|||
u8 reserved_2[4]; |
|||
u8 flags[8]; |
|||
u32_le plain_region_offset; |
|||
u32_le plain_region_size; |
|||
u32_le logo_region_offset; |
|||
u32_le logo_region_size; |
|||
u32_le exefs_offset; |
|||
u32_le exefs_size; |
|||
u32_le exefs_hash_region_size; |
|||
u8 reserved_3[4]; |
|||
u32_le romfs_offset; |
|||
u32_le romfs_size; |
|||
u32_le romfs_hash_region_size; |
|||
u8 reserved_4[4]; |
|||
u8 exefs_super_block_hash[0x20]; |
|||
u8 romfs_super_block_hash[0x20]; |
|||
}; |
|||
|
|||
static_assert(sizeof(NCCH_Header) == 0x200, "NCCH header structure size is wrong"); |
|||
|
|||
//////////////////////////////////////////////////////////////////////////////////////////////////// |
|||
// ExeFS (executable file system) headers |
|||
|
|||
struct ExeFs_SectionHeader { |
|||
char name[8]; |
|||
u32 offset; |
|||
u32 size; |
|||
}; |
|||
|
|||
struct ExeFs_Header { |
|||
ExeFs_SectionHeader section[8]; |
|||
u8 reserved[0x80]; |
|||
u8 hashes[8][0x20]; |
|||
}; |
|||
|
|||
//////////////////////////////////////////////////////////////////////////////////////////////////// |
|||
// ExHeader (executable file system header) headers |
|||
|
|||
struct ExHeader_SystemInfoFlags { |
|||
u8 reserved[5]; |
|||
u8 flag; |
|||
u8 remaster_version[2]; |
|||
}; |
|||
|
|||
struct ExHeader_CodeSegmentInfo { |
|||
u32 address; |
|||
u32 num_max_pages; |
|||
u32 code_size; |
|||
}; |
|||
|
|||
struct ExHeader_CodeSetInfo { |
|||
u8 name[8]; |
|||
ExHeader_SystemInfoFlags flags; |
|||
ExHeader_CodeSegmentInfo text; |
|||
u32 stack_size; |
|||
ExHeader_CodeSegmentInfo ro; |
|||
u8 reserved[4]; |
|||
ExHeader_CodeSegmentInfo data; |
|||
u32 bss_size; |
|||
}; |
|||
|
|||
struct ExHeader_DependencyList { |
|||
u8 program_id[0x30][8]; |
|||
}; |
|||
|
|||
struct ExHeader_SystemInfo { |
|||
u64 save_data_size; |
|||
u64_le jump_id; |
|||
u8 reserved_2[0x30]; |
|||
}; |
|||
|
|||
struct ExHeader_StorageInfo { |
|||
u8 ext_save_data_id[8]; |
|||
u8 system_save_data_id[8]; |
|||
u8 reserved[8]; |
|||
u8 access_info[7]; |
|||
u8 other_attributes; |
|||
}; |
|||
|
|||
struct ExHeader_ARM11_SystemLocalCaps { |
|||
u64_le program_id; |
|||
u32_le core_version; |
|||
u8 reserved_flags[2]; |
|||
union { |
|||
u8 flags0; |
|||
BitField<0, 2, u8> ideal_processor; |
|||
BitField<2, 2, u8> affinity_mask; |
|||
BitField<4, 4, u8> system_mode; |
|||
}; |
|||
u8 priority; |
|||
u8 resource_limit_descriptor[0x10][2]; |
|||
ExHeader_StorageInfo storage_info; |
|||
u8 service_access_control[0x20][8]; |
|||
u8 ex_service_access_control[0x2][8]; |
|||
u8 reserved[0xf]; |
|||
u8 resource_limit_category; |
|||
}; |
|||
|
|||
struct ExHeader_ARM11_KernelCaps { |
|||
u32_le descriptors[28]; |
|||
u8 reserved[0x10]; |
|||
}; |
|||
|
|||
struct ExHeader_ARM9_AccessControl { |
|||
u8 descriptors[15]; |
|||
u8 descversion; |
|||
}; |
|||
|
|||
struct ExHeader_Header { |
|||
ExHeader_CodeSetInfo codeset_info; |
|||
ExHeader_DependencyList dependency_list; |
|||
ExHeader_SystemInfo system_info; |
|||
ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; |
|||
ExHeader_ARM11_KernelCaps arm11_kernel_caps; |
|||
ExHeader_ARM9_AccessControl arm9_access_control; |
|||
struct { |
|||
u8 signature[0x100]; |
|||
u8 ncch_public_key_modulus[0x100]; |
|||
ExHeader_ARM11_SystemLocalCaps arm11_system_local_caps; |
|||
ExHeader_ARM11_KernelCaps arm11_kernel_caps; |
|||
ExHeader_ARM9_AccessControl arm9_access_control; |
|||
} access_desc; |
|||
}; |
|||
|
|||
static_assert(sizeof(ExHeader_Header) == 0x800, "ExHeader structure size is wrong"); |
|||
|
|||
//////////////////////////////////////////////////////////////////////////////////////////////////// |
|||
// FileSys namespace |
|||
|
|||
namespace FileSys { |
|||
|
|||
/** |
|||
* Helper which implements an interface to deal with NCCH containers which can |
|||
* contain ExeFS archives or RomFS archives for games or other applications. |
|||
*/ |
|||
class NCCHContainer { |
|||
public: |
|||
NCCHContainer(const std::string& filepath); |
|||
NCCHContainer() {} |
|||
|
|||
Loader::ResultStatus OpenFile(const std::string& filepath); |
|||
|
|||
/** |
|||
* Ensure ExeFS and exheader is loaded and ready for reading sections |
|||
* @return ResultStatus result of function |
|||
*/ |
|||
Loader::ResultStatus Load(); |
|||
|
|||
/** |
|||
* Attempt to find overridden sections for the NCCH and mark the container as tainted |
|||
* if any are found. |
|||
* @return ResultStatus result of function |
|||
*/ |
|||
Loader::ResultStatus LoadOverrides(); |
|||
|
|||
/** |
|||
* Reads an application ExeFS section of an NCCH file (e.g. .code, .logo, etc.) |
|||
* @param name Name of section to read out of NCCH file |
|||
* @param buffer Vector to read data into |
|||
* @return ResultStatus result of function |
|||
*/ |
|||
Loader::ResultStatus LoadSectionExeFS(const char* name, std::vector<u8>& buffer); |
|||
|
|||
/** |
|||
* Reads an application ExeFS section from external files instead of an NCCH file, |
|||
* (e.g. code.bin, logo.bcma.lz, icon.icn, banner.bnr) |
|||
* @param name Name of section to read from external files |
|||
* @param buffer Vector to read data into |
|||
* @return ResultStatus result of function |
|||
*/ |
|||
Loader::ResultStatus LoadOverrideExeFSSection(const char* name, std::vector<u8>& buffer); |
|||
|
|||
/** |
|||
* Get the RomFS of the NCCH container |
|||
* Since the RomFS can be huge, we return a file reference instead of copying to a buffer |
|||
* @param romfs_file The file containing the RomFS |
|||
* @param offset The offset the romfs begins on |
|||
* @param size The size of the romfs |
|||
* @return ResultStatus result of function |
|||
*/ |
|||
Loader::ResultStatus ReadRomFS(std::shared_ptr<FileUtil::IOFile>& romfs_file, u64& offset, |
|||
u64& size); |
|||
|
|||
/** |
|||
* Get the override RomFS of the NCCH container |
|||
* Since the RomFS can be huge, we return a file reference instead of copying to a buffer |
|||
* @param romfs_file The file containing the RomFS |
|||
* @param offset The offset the romfs begins on |
|||
* @param size The size of the romfs |
|||
* @return ResultStatus result of function |
|||
*/ |
|||
Loader::ResultStatus ReadOverrideRomFS(std::shared_ptr<FileUtil::IOFile>& romfs_file, |
|||
u64& offset, u64& size); |
|||
|
|||
/** |
|||
* Get the Program ID of the NCCH container |
|||
* @return ResultStatus result of function |
|||
*/ |
|||
Loader::ResultStatus ReadProgramId(u64_le& program_id); |
|||
|
|||
/** |
|||
* Checks whether the NCCH container contains an ExeFS |
|||
* @return bool check result |
|||
*/ |
|||
bool HasExeFS(); |
|||
|
|||
/** |
|||
* Checks whether the NCCH container contains a RomFS |
|||
* @return bool check result |
|||
*/ |
|||
bool HasRomFS(); |
|||
|
|||
/** |
|||
* Checks whether the NCCH container contains an ExHeader |
|||
* @return bool check result |
|||
*/ |
|||
bool HasExHeader(); |
|||
|
|||
NCCH_Header ncch_header; |
|||
ExeFs_Header exefs_header; |
|||
ExHeader_Header exheader_header; |
|||
|
|||
private: |
|||
bool has_header = false; |
|||
bool has_exheader = false; |
|||
bool has_exefs = false; |
|||
bool has_romfs = false; |
|||
|
|||
bool is_tainted = false; // Are there parts of this container being overridden? |
|||
bool is_loaded = false; |
|||
bool is_compressed = false; |
|||
|
|||
u32 ncch_offset = 0; // Offset to NCCH header, can be 0 or after NCSD header |
|||
u32 exefs_offset = 0; |
|||
|
|||
std::string filepath; |
|||
FileUtil::IOFile file; |
|||
FileUtil::IOFile exefs_file; |
|||
}; |
|||
|
|||
} // namespace FileSys |
|||
@ -0,0 +1,212 @@ |
|||
// Copyright 2017 Citra Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <cinttypes>
|
|||
#include <cryptopp/sha.h>
|
|||
#include "common/alignment.h"
|
|||
#include "common/file_util.h"
|
|||
#include "common/logging/log.h"
|
|||
#include "core/file_sys/title_metadata.h"
|
|||
#include "core/loader/loader.h"
|
|||
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
// FileSys namespace
|
|||
|
|||
namespace FileSys { |
|||
|
|||
static u32 GetSignatureSize(u32 signature_type) { |
|||
switch (signature_type) { |
|||
case Rsa4096Sha1: |
|||
case Rsa4096Sha256: |
|||
return 0x200; |
|||
|
|||
case Rsa2048Sha1: |
|||
case Rsa2048Sha256: |
|||
return 0x100; |
|||
|
|||
case EllipticSha1: |
|||
case EcdsaSha256: |
|||
return 0x3C; |
|||
} |
|||
} |
|||
|
|||
Loader::ResultStatus TitleMetadata::Load() { |
|||
FileUtil::IOFile file(filepath, "rb"); |
|||
if (!file.IsOpen()) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
if (!file.ReadBytes(&signature_type, sizeof(u32_be))) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
// Signature lengths are variable, and the body follows the signature
|
|||
u32 signature_size = GetSignatureSize(signature_type); |
|||
|
|||
tmd_signature.resize(signature_size); |
|||
if (!file.ReadBytes(&tmd_signature[0], signature_size)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
// The TMD body start position is rounded to the nearest 0x40 after the signature
|
|||
size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); |
|||
file.Seek(body_start, SEEK_SET); |
|||
|
|||
// Read our TMD body, then load the amount of ContentChunks specified
|
|||
if (file.ReadBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
for (u16 i = 0; i < tmd_body.content_count; i++) { |
|||
ContentChunk chunk; |
|||
if (file.ReadBytes(&chunk, sizeof(ContentChunk)) == sizeof(ContentChunk)) { |
|||
tmd_chunks.push_back(chunk); |
|||
} else { |
|||
LOG_ERROR(Service_FS, "Malformed TMD %s, failed to load content chunk index %u!", |
|||
filepath.c_str(), i); |
|||
return Loader::ResultStatus::ErrorInvalidFormat; |
|||
} |
|||
} |
|||
|
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
|
|||
Loader::ResultStatus TitleMetadata::Save() { |
|||
FileUtil::IOFile file(filepath, "wb"); |
|||
if (!file.IsOpen()) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
if (!file.WriteBytes(&signature_type, sizeof(u32_be))) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
// Signature lengths are variable, and the body follows the signature
|
|||
u32 signature_size = GetSignatureSize(signature_type); |
|||
|
|||
if (!file.WriteBytes(tmd_signature.data(), signature_size)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
// The TMD body start position is rounded to the nearest 0x40 after the signature
|
|||
size_t body_start = Common::AlignUp(signature_size + sizeof(u32), 0x40); |
|||
file.Seek(body_start, SEEK_SET); |
|||
|
|||
// Update our TMD body values and hashes
|
|||
tmd_body.content_count = static_cast<u16>(tmd_chunks.size()); |
|||
|
|||
// TODO(shinyquagsire23): Do TMDs with more than one contentinfo exist?
|
|||
// For now we'll just adjust the first index to hold all content chunks
|
|||
// and ensure that no further content info data exists.
|
|||
tmd_body.contentinfo = {}; |
|||
tmd_body.contentinfo[0].index = 0; |
|||
tmd_body.contentinfo[0].command_count = static_cast<u16>(tmd_chunks.size()); |
|||
|
|||
CryptoPP::SHA256 chunk_hash; |
|||
for (u16 i = 0; i < tmd_body.content_count; i++) { |
|||
chunk_hash.Update(reinterpret_cast<u8*>(&tmd_chunks[i]), sizeof(ContentChunk)); |
|||
} |
|||
chunk_hash.Final(tmd_body.contentinfo[0].hash.data()); |
|||
|
|||
CryptoPP::SHA256 contentinfo_hash; |
|||
for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { |
|||
chunk_hash.Update(reinterpret_cast<u8*>(&tmd_body.contentinfo[i]), sizeof(ContentInfo)); |
|||
} |
|||
chunk_hash.Final(tmd_body.contentinfo_hash.data()); |
|||
|
|||
// Write our TMD body, then write each of our ContentChunks
|
|||
if (file.WriteBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) |
|||
return Loader::ResultStatus::Error; |
|||
|
|||
for (u16 i = 0; i < tmd_body.content_count; i++) { |
|||
ContentChunk chunk = tmd_chunks[i]; |
|||
if (file.WriteBytes(&chunk, sizeof(ContentChunk)) != sizeof(ContentChunk)) |
|||
return Loader::ResultStatus::Error; |
|||
} |
|||
|
|||
return Loader::ResultStatus::Success; |
|||
} |
|||
|
|||
u64 TitleMetadata::GetTitleID() const { |
|||
return tmd_body.title_id; |
|||
} |
|||
|
|||
u32 TitleMetadata::GetTitleType() const { |
|||
return tmd_body.title_type; |
|||
} |
|||
|
|||
u16 TitleMetadata::GetTitleVersion() const { |
|||
return tmd_body.title_version; |
|||
} |
|||
|
|||
u64 TitleMetadata::GetSystemVersion() const { |
|||
return tmd_body.system_version; |
|||
} |
|||
|
|||
size_t TitleMetadata::GetContentCount() const { |
|||
return tmd_chunks.size(); |
|||
} |
|||
|
|||
u32 TitleMetadata::GetBootContentID() const { |
|||
return tmd_chunks[TMDContentIndex::Main].id; |
|||
} |
|||
|
|||
u32 TitleMetadata::GetManualContentID() const { |
|||
return tmd_chunks[TMDContentIndex::Manual].id; |
|||
} |
|||
|
|||
u32 TitleMetadata::GetDLPContentID() const { |
|||
return tmd_chunks[TMDContentIndex::DLP].id; |
|||
} |
|||
|
|||
void TitleMetadata::SetTitleID(u64 title_id) { |
|||
tmd_body.title_id = title_id; |
|||
} |
|||
|
|||
void TitleMetadata::SetTitleType(u32 type) { |
|||
tmd_body.title_type = type; |
|||
} |
|||
|
|||
void TitleMetadata::SetTitleVersion(u16 version) { |
|||
tmd_body.title_version = version; |
|||
} |
|||
|
|||
void TitleMetadata::SetSystemVersion(u64 version) { |
|||
tmd_body.system_version = version; |
|||
} |
|||
|
|||
void TitleMetadata::AddContentChunk(const ContentChunk& chunk) { |
|||
tmd_chunks.push_back(chunk); |
|||
} |
|||
|
|||
void TitleMetadata::Print() const { |
|||
LOG_DEBUG(Service_FS, "%s - %u chunks", filepath.c_str(), |
|||
static_cast<u32>(tmd_body.content_count)); |
|||
|
|||
// Content info describes ranges of content chunks
|
|||
LOG_DEBUG(Service_FS, "Content info:"); |
|||
for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { |
|||
if (tmd_body.contentinfo[i].command_count == 0) |
|||
break; |
|||
|
|||
LOG_DEBUG(Service_FS, " Index %04X, Command Count %04X", |
|||
static_cast<u32>(tmd_body.contentinfo[i].index), |
|||
static_cast<u32>(tmd_body.contentinfo[i].command_count)); |
|||
} |
|||
|
|||
// For each content info, print their content chunk range
|
|||
for (size_t i = 0; i < tmd_body.contentinfo.size(); i++) { |
|||
u16 index = static_cast<u16>(tmd_body.contentinfo[i].index); |
|||
u16 count = static_cast<u16>(tmd_body.contentinfo[i].command_count); |
|||
|
|||
if (count == 0) |
|||
continue; |
|||
|
|||
LOG_DEBUG(Service_FS, "Content chunks for content info index %zu:", i); |
|||
for (u16 j = index; j < index + count; j++) { |
|||
// Don't attempt to print content we don't have
|
|||
if (j > tmd_body.content_count) |
|||
break; |
|||
|
|||
const ContentChunk& chunk = tmd_chunks[j]; |
|||
LOG_DEBUG(Service_FS, " ID %08X, Index %04X, Type %04x, Size %016" PRIX64, |
|||
static_cast<u32>(chunk.id), static_cast<u32>(chunk.index), |
|||
static_cast<u32>(chunk.type), static_cast<u64>(chunk.size)); |
|||
} |
|||
} |
|||
} |
|||
} // namespace FileSys
|
|||
@ -0,0 +1,125 @@ |
|||
// Copyright 2017 Citra Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <string> |
|||
#include <vector> |
|||
#include "common/common_types.h" |
|||
#include "common/swap.h" |
|||
|
|||
namespace Loader { |
|||
enum class ResultStatus; |
|||
} |
|||
|
|||
//////////////////////////////////////////////////////////////////////////////////////////////////// |
|||
// FileSys namespace |
|||
|
|||
namespace FileSys { |
|||
|
|||
enum TMDSignatureType : u32 { |
|||
Rsa4096Sha1 = 0x10000, |
|||
Rsa2048Sha1 = 0x10001, |
|||
EllipticSha1 = 0x10002, |
|||
Rsa4096Sha256 = 0x10003, |
|||
Rsa2048Sha256 = 0x10004, |
|||
EcdsaSha256 = 0x10005 |
|||
}; |
|||
|
|||
enum TMDContentTypeFlag : u16 { |
|||
Encrypted = 1 << 1, |
|||
Disc = 1 << 2, |
|||
CFM = 1 << 3, |
|||
Optional = 1 << 14, |
|||
Shared = 1 << 15 |
|||
}; |
|||
|
|||
/** |
|||
* Helper which implements an interface to read and write Title Metadata (TMD) files. |
|||
* If a file path is provided and the file exists, it can be parsed and used, otherwise |
|||
* it must be created. The TMD file can then be interpreted, modified and/or saved. |
|||
*/ |
|||
class TitleMetadata { |
|||
public: |
|||
struct ContentChunk { |
|||
u32_be id; |
|||
u16_be index; |
|||
u16_be type; |
|||
u64_be size; |
|||
std::array<u8, 0x20> hash; |
|||
}; |
|||
|
|||
static_assert(sizeof(ContentChunk) == 0x30, "TMD ContentChunk structure size is wrong"); |
|||
|
|||
struct ContentInfo { |
|||
u16_be index; |
|||
u16_be command_count; |
|||
std::array<u8, 0x20> hash; |
|||
}; |
|||
|
|||
static_assert(sizeof(ContentInfo) == 0x24, "TMD ContentInfo structure size is wrong"); |
|||
|
|||
#pragma pack(push, 1) |
|||
|
|||
struct Body { |
|||
std::array<u8, 0x40> issuer; |
|||
u8 version; |
|||
u8 ca_crl_version; |
|||
u8 signer_crl_version; |
|||
u8 reserved; |
|||
u64_be system_version; |
|||
u64_be title_id; |
|||
u32_be title_type; |
|||
u16_be group_id; |
|||
u32_be savedata_size; |
|||
u32_be srl_private_savedata_size; |
|||
std::array<u8, 4> reserved_2; |
|||
u8 srl_flag; |
|||
std::array<u8, 0x31> reserved_3; |
|||
u32_be access_rights; |
|||
u16_be title_version; |
|||
u16_be content_count; |
|||
u16_be boot_content; |
|||
std::array<u8, 2> reserved_4; |
|||
std::array<u8, 0x20> contentinfo_hash; |
|||
std::array<ContentInfo, 64> contentinfo; |
|||
}; |
|||
|
|||
static_assert(sizeof(Body) == 0x9C4, "TMD body structure size is wrong"); |
|||
|
|||
#pragma pack(pop) |
|||
|
|||
explicit TitleMetadata(std::string& path) : filepath(std::move(path)) {} |
|||
Loader::ResultStatus Load(); |
|||
Loader::ResultStatus Save(); |
|||
|
|||
u64 GetTitleID() const; |
|||
u32 GetTitleType() const; |
|||
u16 GetTitleVersion() const; |
|||
u64 GetSystemVersion() const; |
|||
size_t GetContentCount() const; |
|||
u32 GetBootContentID() const; |
|||
u32 GetManualContentID() const; |
|||
u32 GetDLPContentID() const; |
|||
|
|||
void SetTitleID(u64 title_id); |
|||
void SetTitleType(u32 type); |
|||
void SetTitleVersion(u16 version); |
|||
void SetSystemVersion(u64 version); |
|||
void AddContentChunk(const ContentChunk& chunk); |
|||
|
|||
void Print() const; |
|||
|
|||
private: |
|||
enum TMDContentIndex { Main = 0, Manual = 1, DLP = 2 }; |
|||
|
|||
Body tmd_body; |
|||
u32_be signature_type; |
|||
std::vector<u8> tmd_signature; |
|||
std::vector<ContentChunk> tmd_chunks; |
|||
|
|||
std::string filepath; |
|||
}; |
|||
|
|||
} // namespace FileSys |
|||
@ -1,89 +0,0 @@ |
|||
// Copyright 2016 Citra Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include "common/math_util.h"
|
|||
#include "common/quaternion.h"
|
|||
#include "core/frontend/emu_window.h"
|
|||
#include "core/frontend/motion_emu.h"
|
|||
|
|||
namespace Motion { |
|||
|
|||
static constexpr int update_millisecond = 100; |
|||
static constexpr auto update_duration = |
|||
std::chrono::duration_cast<std::chrono::steady_clock::duration>( |
|||
std::chrono::milliseconds(update_millisecond)); |
|||
|
|||
MotionEmu::MotionEmu(EmuWindow& emu_window) |
|||
: motion_emu_thread(&MotionEmu::MotionEmuThread, this, std::ref(emu_window)) {} |
|||
|
|||
MotionEmu::~MotionEmu() { |
|||
if (motion_emu_thread.joinable()) { |
|||
shutdown_event.Set(); |
|||
motion_emu_thread.join(); |
|||
} |
|||
} |
|||
|
|||
void MotionEmu::MotionEmuThread(EmuWindow& emu_window) { |
|||
auto update_time = std::chrono::steady_clock::now(); |
|||
Math::Quaternion<float> q = MakeQuaternion(Math::Vec3<float>(), 0); |
|||
Math::Quaternion<float> old_q; |
|||
|
|||
while (!shutdown_event.WaitUntil(update_time)) { |
|||
update_time += update_duration; |
|||
old_q = q; |
|||
|
|||
{ |
|||
std::lock_guard<std::mutex> guard(tilt_mutex); |
|||
|
|||
// Find the quaternion describing current 3DS tilting
|
|||
q = MakeQuaternion(Math::MakeVec(-tilt_direction.y, 0.0f, tilt_direction.x), |
|||
tilt_angle); |
|||
} |
|||
|
|||
auto inv_q = q.Inverse(); |
|||
|
|||
// Set the gravity vector in world space
|
|||
auto gravity = Math::MakeVec(0.0f, -1.0f, 0.0f); |
|||
|
|||
// Find the angular rate vector in world space
|
|||
auto angular_rate = ((q - old_q) * inv_q).xyz * 2; |
|||
angular_rate *= 1000 / update_millisecond / MathUtil::PI * 180; |
|||
|
|||
// Transform the two vectors from world space to 3DS space
|
|||
gravity = QuaternionRotate(inv_q, gravity); |
|||
angular_rate = QuaternionRotate(inv_q, angular_rate); |
|||
|
|||
// Update the sensor state
|
|||
emu_window.AccelerometerChanged(gravity.x, gravity.y, gravity.z); |
|||
emu_window.GyroscopeChanged(angular_rate.x, angular_rate.y, angular_rate.z); |
|||
} |
|||
} |
|||
|
|||
void MotionEmu::BeginTilt(int x, int y) { |
|||
mouse_origin = Math::MakeVec(x, y); |
|||
is_tilting = true; |
|||
} |
|||
|
|||
void MotionEmu::Tilt(int x, int y) { |
|||
constexpr float SENSITIVITY = 0.01f; |
|||
auto mouse_move = Math::MakeVec(x, y) - mouse_origin; |
|||
if (is_tilting) { |
|||
std::lock_guard<std::mutex> guard(tilt_mutex); |
|||
if (mouse_move.x == 0 && mouse_move.y == 0) { |
|||
tilt_angle = 0; |
|||
} else { |
|||
tilt_direction = mouse_move.Cast<float>(); |
|||
tilt_angle = MathUtil::Clamp(tilt_direction.Normalize() * SENSITIVITY, 0.0f, |
|||
MathUtil::PI * 0.5f); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void MotionEmu::EndTilt() { |
|||
std::lock_guard<std::mutex> guard(tilt_mutex); |
|||
tilt_angle = 0; |
|||
is_tilting = false; |
|||
} |
|||
|
|||
} // namespace Motion
|
|||
@ -1,52 +0,0 @@ |
|||
// Copyright 2016 Citra Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
#include "common/thread.h" |
|||
#include "common/vector_math.h" |
|||
|
|||
class EmuWindow; |
|||
|
|||
namespace Motion { |
|||
|
|||
class MotionEmu final { |
|||
public: |
|||
MotionEmu(EmuWindow& emu_window); |
|||
~MotionEmu(); |
|||
|
|||
/** |
|||
* Signals that a motion sensor tilt has begun. |
|||
* @param x the x-coordinate of the cursor |
|||
* @param y the y-coordinate of the cursor |
|||
*/ |
|||
void BeginTilt(int x, int y); |
|||
|
|||
/** |
|||
* Signals that a motion sensor tilt is occurring. |
|||
* @param x the x-coordinate of the cursor |
|||
* @param y the y-coordinate of the cursor |
|||
*/ |
|||
void Tilt(int x, int y); |
|||
|
|||
/** |
|||
* Signals that a motion sensor tilt has ended. |
|||
*/ |
|||
void EndTilt(); |
|||
|
|||
private: |
|||
Math::Vec2<int> mouse_origin; |
|||
|
|||
std::mutex tilt_mutex; |
|||
Math::Vec2<float> tilt_direction; |
|||
float tilt_angle = 0; |
|||
|
|||
bool is_tilting = false; |
|||
|
|||
Common::Event shutdown_event; |
|||
std::thread motion_emu_thread; |
|||
|
|||
void MotionEmuThread(EmuWindow& emu_window); |
|||
}; |
|||
|
|||
} // namespace Motion |
|||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue