Compare commits
544 Commits
__refs_pul
...
__refs_pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
957ddab679 | ||
|
|
55af5bda55 | ||
|
|
1aa195a9c0 | ||
|
|
e51bd49f87 | ||
|
|
7e697ab7ff | ||
|
|
6d9dd1dc6d | ||
|
|
112351d557 | ||
|
|
a6efff8b02 | ||
|
|
9cd79c25ed | ||
|
|
2515d2433b | ||
|
|
8b08cb925b | ||
|
|
a8974f0556 | ||
|
|
460ebc8187 | ||
|
|
6ac1bd9f5d | ||
|
|
7e9b79955f | ||
|
|
564b7fdc9c | ||
|
|
c08c5d346a | ||
|
|
9382414b20 | ||
|
|
e3af341d5b | ||
|
|
3f17fe7133 | ||
|
|
a164b413fa | ||
|
|
9273c02427 | ||
|
|
b89dda2b98 | ||
|
|
9947c6ad59 | ||
|
|
9b50dca2bb | ||
|
|
009a2cc9cc | ||
|
|
6faf1b0972 | ||
|
|
820f646458 | ||
|
|
948f6c0738 | ||
|
|
ddcdbce067 | ||
|
|
8d685a29bc | ||
|
|
14230fe2af | ||
|
|
68296d9474 | ||
|
|
8f4e09ba07 | ||
|
|
56ab608044 | ||
|
|
54724fe918 | ||
|
|
b155b3ef81 | ||
|
|
a859a35ec8 | ||
|
|
fbaefc47a0 | ||
|
|
742f895f8b | ||
|
|
a781042700 | ||
|
|
77554ac773 | ||
|
|
6f09c5b128 | ||
|
|
e63b229f4a | ||
|
|
94f193af65 | ||
|
|
a6ae765410 | ||
|
|
aba988f71c | ||
|
|
7f15306f78 | ||
|
|
6bd6beee20 | ||
|
|
d3ad9469a1 | ||
|
|
c0b7ed8b58 | ||
|
|
527e362a83 | ||
|
|
50a806ea67 | ||
|
|
a1ef02c3e6 | ||
|
|
c913136eb2 | ||
|
|
7d5d781b20 | ||
|
|
23a16c1720 | ||
|
|
92e26df00f | ||
|
|
c91b60a421 | ||
|
|
cbd517d8cc | ||
|
|
2814ca3624 | ||
|
|
a6e75cd45b | ||
|
|
9664ce255d | ||
|
|
f92b3512e0 | ||
|
|
8e150c46b9 | ||
|
|
f5e03b9173 | ||
|
|
08fcb4694f | ||
|
|
97bf83bc56 | ||
|
|
8e900a301a | ||
|
|
54e7ddb93a | ||
|
|
1efe5a76b1 | ||
|
|
9951f6d054 | ||
|
|
d2caf4af7d | ||
|
|
99fbcb3bf2 | ||
|
|
faa9e066ab | ||
|
|
99a71580c4 | ||
|
|
87be4bc283 | ||
|
|
e973cceadd | ||
|
|
23d2c50479 | ||
|
|
1280061725 | ||
|
|
8974771334 | ||
|
|
e4e55d064e | ||
|
|
58473309a0 | ||
|
|
f7eaea424d | ||
|
|
d7518cf6e0 | ||
|
|
5c8aff984e | ||
|
|
93703431e2 | ||
|
|
a040929c90 | ||
|
|
b555311438 | ||
|
|
d770c60205 | ||
|
|
dda4b5e89e | ||
|
|
9a07e9f805 | ||
|
|
ed37b68fb5 | ||
|
|
26e96d16d0 | ||
|
|
8ec1e16867 | ||
|
|
5a29b358aa | ||
|
|
c156ee8eb8 | ||
|
|
6ef84f1c4c | ||
|
|
2949d9552c | ||
|
|
978f3a3282 | ||
|
|
dce624e3f1 | ||
|
|
2081ed7db2 | ||
|
|
10bc725944 | ||
|
|
a813c10e1c | ||
|
|
1c5636e690 | ||
|
|
2afe8ac4a7 | ||
|
|
04397cd185 | ||
|
|
1ff3318458 | ||
|
|
b2268f1f8d | ||
|
|
9cfe2414cb | ||
|
|
c6fd56b00f | ||
|
|
18a89931a9 | ||
|
|
a405373144 | ||
|
|
1242c1ec0a | ||
|
|
41cd766438 | ||
|
|
325f3e0693 | ||
|
|
89be49d2f3 | ||
|
|
2714d9e64c | ||
|
|
d2ade27c3f | ||
|
|
177c45e97d | ||
|
|
9c206fe94d | ||
|
|
1ccc0457d5 | ||
|
|
7a439630bb | ||
|
|
fda8f1da20 | ||
|
|
0f453488e2 | ||
|
|
a0e1fbfe14 | ||
|
|
60754b4728 | ||
|
|
2edab4e840 | ||
|
|
2bc6abb9a1 | ||
|
|
6f8ed9508d | ||
|
|
b89fc407d7 | ||
|
|
948bc87a59 | ||
|
|
ad3dca7e62 | ||
|
|
f32e28c7b8 | ||
|
|
19d0951ae6 | ||
|
|
7939ea18e8 | ||
|
|
c69dc5acf9 | ||
|
|
1c05c06e04 | ||
|
|
4a587b81b2 | ||
|
|
dc3cc0002c | ||
|
|
42588493d5 | ||
|
|
7f7eb29323 | ||
|
|
123c065086 | ||
|
|
9bc71fcc5f | ||
|
|
d647d9550c | ||
|
|
16d65182f9 | ||
|
|
06578e89b2 | ||
|
|
f08d24e9c0 | ||
|
|
6683bf50b5 | ||
|
|
e205e74e1f | ||
|
|
e2457418da | ||
|
|
5be8b7a362 | ||
|
|
d626bc8c62 | ||
|
|
26aaa86ece | ||
|
|
915ab81ec2 | ||
|
|
d6accf96ff | ||
|
|
5094dfa081 | ||
|
|
42ef40884f | ||
|
|
6e73039eb5 | ||
|
|
a6dd577d02 | ||
|
|
78653f7339 | ||
|
|
02dfbf961e | ||
|
|
a2c97de929 | ||
|
|
b1ccd88434 | ||
|
|
0cbcd6ec9a | ||
|
|
4d7e1662c8 | ||
|
|
eb4f2d5596 | ||
|
|
d8ba202070 | ||
|
|
72e4499a9e | ||
|
|
2e7dc4cac9 | ||
|
|
45fb74d262 | ||
|
|
6771a18c6c | ||
|
|
0d2435343a | ||
|
|
f1bc62bb4c | ||
|
|
4a56931703 | ||
|
|
ffe2336136 | ||
|
|
b4ac8218d0 | ||
|
|
9a6bfc55f3 | ||
|
|
a409d49bbd | ||
|
|
b55d8111e6 | ||
|
|
a0e1566dc5 | ||
|
|
382852418b | ||
|
|
2f5ed3877c | ||
|
|
90fd03015a | ||
|
|
2562fe4a16 | ||
|
|
62edc01525 | ||
|
|
5d2043598e | ||
|
|
c6024379a4 | ||
|
|
d3934d7da7 | ||
|
|
887a9c5c29 | ||
|
|
af59d4bff0 | ||
|
|
f96ded9815 | ||
|
|
8c66a5a9a5 | ||
|
|
34a447d24e | ||
|
|
8d86747514 | ||
|
|
43a2598e26 | ||
|
|
d26a46feed | ||
|
|
be2f1eabd7 | ||
|
|
23b86fd3ea | ||
|
|
f708207ae6 | ||
|
|
bfb0c87b7b | ||
|
|
81ca46dd17 | ||
|
|
b8be5524bc | ||
|
|
2fd45093f2 | ||
|
|
f170159fde | ||
|
|
e81354ae38 | ||
|
|
6426b0f551 | ||
|
|
6314a799aa | ||
|
|
43e0d865fa | ||
|
|
c65713832c | ||
|
|
1e6a209649 | ||
|
|
b6425c0511 | ||
|
|
20800f2df7 | ||
|
|
f09da5d1c9 | ||
|
|
8492ec1669 | ||
|
|
c74b7ee204 | ||
|
|
36093a3e4d | ||
|
|
ec59e4a6c5 | ||
|
|
8fd9eb71b4 | ||
|
|
018c25e123 | ||
|
|
f6f5c2e4d8 | ||
|
|
165c23c848 | ||
|
|
d1a6dd61d1 | ||
|
|
4f18c17df7 | ||
|
|
5049ca5d8c | ||
|
|
ba2972bc64 | ||
|
|
06487c2c8d | ||
|
|
67fa51ea2f | ||
|
|
78b109d195 | ||
|
|
0dce6d7008 | ||
|
|
3ed0115e36 | ||
|
|
ccfd176382 | ||
|
|
119ab308b5 | ||
|
|
a7e8d10969 | ||
|
|
42dc856ce1 | ||
|
|
61a5b56abd | ||
|
|
f26fc64cb4 | ||
|
|
cde665c565 | ||
|
|
60b7a3b904 | ||
|
|
ab44192ab0 | ||
|
|
8b52d6682a | ||
|
|
13524578b6 | ||
|
|
4112dd6b4e | ||
|
|
bf33f80fae | ||
|
|
ef3768f323 | ||
|
|
410062031b | ||
|
|
b247e0cab0 | ||
|
|
2164702cf7 | ||
|
|
c4845df3d4 | ||
|
|
10e5356e9a | ||
|
|
6dd369ab88 | ||
|
|
a9dc5a3c10 | ||
|
|
d65f079cc1 | ||
|
|
fee8bdd90c | ||
|
|
fde2017a3f | ||
|
|
ebf5768340 | ||
|
|
a4ac3bed6c | ||
|
|
da3da6be90 | ||
|
|
2a472ff54d | ||
|
|
c4ed0b16b1 | ||
|
|
c7f2fb2151 | ||
|
|
232b0d9d2a | ||
|
|
eccc77a8c8 | ||
|
|
74e08b4800 | ||
|
|
a1bdc597e9 | ||
|
|
c5ea6db02d | ||
|
|
c7c4e6dcba | ||
|
|
c5c0da41b4 | ||
|
|
f835349364 | ||
|
|
12ba80a86c | ||
|
|
1fd979f50a | ||
|
|
b2ca8089ce | ||
|
|
e70a3c5a5d | ||
|
|
d1b1c42c07 | ||
|
|
dd35b4b18a | ||
|
|
4877e6c2f6 | ||
|
|
8e8326595f | ||
|
|
8ce02d85e9 | ||
|
|
b38d67d940 | ||
|
|
cea627b0fc | ||
|
|
5abf71fe65 | ||
|
|
eef0c93643 | ||
|
|
125d7122ac | ||
|
|
ad1220e1b3 | ||
|
|
92b85fad70 | ||
|
|
cb8b371570 | ||
|
|
38517241ec | ||
|
|
15cc34b93e | ||
|
|
99fc32428a | ||
|
|
d63b1d21f1 | ||
|
|
ac68c8a605 | ||
|
|
c2695aa2eb | ||
|
|
16b83fac9b | ||
|
|
a769d8c913 | ||
|
|
a0e2bd85a5 | ||
|
|
29ac15d1b8 | ||
|
|
0057a47e41 | ||
|
|
5a53d75313 | ||
|
|
8dd9cb98ce | ||
|
|
c95c4442e9 | ||
|
|
37f2ec6fc2 | ||
|
|
624239ed6b | ||
|
|
5678ec0dd0 | ||
|
|
3f4fb4b037 | ||
|
|
bfb28c5b3f | ||
|
|
f2d5b100c2 | ||
|
|
6923ecee3a | ||
|
|
36090521ce | ||
|
|
cc71832b19 | ||
|
|
bf89a99839 | ||
|
|
79243b6fa0 | ||
|
|
b0f7713fce | ||
|
|
8c9abe1d41 | ||
|
|
ca58929eb0 | ||
|
|
523e4be02c | ||
|
|
fde3b1b6f2 | ||
|
|
477eee3993 | ||
|
|
93a4097e9d | ||
|
|
e3bddf8137 | ||
|
|
c4ce7e456a | ||
|
|
e33452f7e8 | ||
|
|
5aaee2ff8d | ||
|
|
2ae88feea7 | ||
|
|
16db8b9d9f | ||
|
|
948002635f | ||
|
|
ea99819f37 | ||
|
|
eac3cf301c | ||
|
|
fc5b489b0f | ||
|
|
19b05c3f55 | ||
|
|
2788144f46 | ||
|
|
96463d0a55 | ||
|
|
dd70ddad7e | ||
|
|
c0fb321935 | ||
|
|
34f3d58470 | ||
|
|
b5fb246a99 | ||
|
|
c6fda4c758 | ||
|
|
609cb04f3f | ||
|
|
eb88fedc5d | ||
|
|
f5b132676f | ||
|
|
0fcdf37917 | ||
|
|
350f6e0aa4 | ||
|
|
9d8f19d7bf | ||
|
|
38cd4e9c61 | ||
|
|
f9a26d468c | ||
|
|
1277556c69 | ||
|
|
dfdf4a46fe | ||
|
|
69dd37d874 | ||
|
|
f13a66b963 | ||
|
|
2b9eee4d1e | ||
|
|
b1d238bbb8 | ||
|
|
f24ab6d9e6 | ||
|
|
46ef072cf9 | ||
|
|
6bcdf37d4f | ||
|
|
bc16f7f3cc | ||
|
|
ba8ff096fd | ||
|
|
7784ce1854 | ||
|
|
e8cb6f5c9b | ||
|
|
9e9a4bb3a7 | ||
|
|
d7c68fbb12 | ||
|
|
3fe77be392 | ||
|
|
028d90eb79 | ||
|
|
296e57fa0e | ||
|
|
b20ed93884 | ||
|
|
185b35bfcd | ||
|
|
943771e703 | ||
|
|
ce4b77bd7d | ||
|
|
6ee8b15abe | ||
|
|
23d45715dc | ||
|
|
ffd60ee476 | ||
|
|
8a88110060 | ||
|
|
6cf719a4ab | ||
|
|
51ddb130c5 | ||
|
|
9b17486be6 | ||
|
|
0a1d4fbc5c | ||
|
|
f7edbcd7a3 | ||
|
|
b0eb580931 | ||
|
|
85da529f15 | ||
|
|
7fb406c3fc | ||
|
|
3ef4b3d4b4 | ||
|
|
73b937b190 | ||
|
|
656758fd81 | ||
|
|
29d4f8c2dd | ||
|
|
9baf5de90c | ||
|
|
d6cb22b0df | ||
|
|
1b92ae136f | ||
|
|
731701a2d2 | ||
|
|
706fc5d2d6 | ||
|
|
27da7bc9da | ||
|
|
9b1c49a9cf | ||
|
|
e0f66c1fbf | ||
|
|
8335b2f115 | ||
|
|
367feaefa0 | ||
|
|
71cc482bbd | ||
|
|
ff358d97e8 | ||
|
|
2e95ba2e9c | ||
|
|
6eba539f4a | ||
|
|
63dff47e22 | ||
|
|
504cff2b7a | ||
|
|
804aebf7c7 | ||
|
|
2003771789 | ||
|
|
f246fd778d | ||
|
|
1db7839f11 | ||
|
|
224be09d66 | ||
|
|
e341d868ee | ||
|
|
da7226442f | ||
|
|
727136a9c9 | ||
|
|
0d9b3e425e | ||
|
|
ce56b8e1fa | ||
|
|
cef35e7c9c | ||
|
|
928e78dced | ||
|
|
3fd78f4d24 | ||
|
|
9791f0d590 | ||
|
|
2a3d7128d1 | ||
|
|
aac807fd3a | ||
|
|
a0ce6de913 | ||
|
|
94329038b6 | ||
|
|
24a759de4a | ||
|
|
b39cd70cd4 | ||
|
|
c594ec3417 | ||
|
|
89c3d6a2a3 | ||
|
|
c00b374e78 | ||
|
|
69236e5aff | ||
|
|
1dd27aff47 | ||
|
|
cee6a7ab55 | ||
|
|
a2fa37b499 | ||
|
|
f96de510ee | ||
|
|
91140f6c0a | ||
|
|
38592a3b5e | ||
|
|
40ecdda19e | ||
|
|
5ef447cc0e | ||
|
|
11c221cc62 | ||
|
|
40f83fee6a | ||
|
|
0bca5743ab | ||
|
|
aac5792a2b | ||
|
|
5752454629 | ||
|
|
87d8a9c986 | ||
|
|
0769aa594e | ||
|
|
c8e3f98c27 | ||
|
|
b74df62959 | ||
|
|
e0b0f4eece | ||
|
|
b1148d269d | ||
|
|
c5284efd4f | ||
|
|
8599e1e4fc | ||
|
|
3aad82b1a3 | ||
|
|
2a42dea568 | ||
|
|
c8cd1785e6 | ||
|
|
991eb4824c | ||
|
|
301baaa942 | ||
|
|
3db1b8e0cd | ||
|
|
8f9c49f7ee | ||
|
|
f009ed63f3 | ||
|
|
18dfa99030 | ||
|
|
6eda9ebbdb | ||
|
|
ad7815a28d | ||
|
|
409d2e07c2 | ||
|
|
8dc4407586 | ||
|
|
96c0b81a51 | ||
|
|
25d71454d1 | ||
|
|
5e66a24423 | ||
|
|
290439a6a5 | ||
|
|
dc876fd63a | ||
|
|
d8fd3ef4fe | ||
|
|
1c31cbad72 | ||
|
|
2e715ef70d | ||
|
|
319dbc5843 | ||
|
|
60f476cd8f | ||
|
|
6d549abb4e | ||
|
|
0ce0905380 | ||
|
|
11895d54af | ||
|
|
d1520410a3 | ||
|
|
4dacb8a4b1 | ||
|
|
5b32594fbe | ||
|
|
882ce44986 | ||
|
|
bc7bfd96f0 | ||
|
|
6bfcf13187 | ||
|
|
309564abe3 | ||
|
|
b6c47b578f | ||
|
|
9d09d92c56 | ||
|
|
57d007e545 | ||
|
|
6e52f37d5b | ||
|
|
59d18ef55b | ||
|
|
46fbf6dd92 | ||
|
|
f19b4fab5f | ||
|
|
875d52a81f | ||
|
|
9bf9c71c88 | ||
|
|
45cc022ea9 | ||
|
|
eab35c8235 | ||
|
|
01d199965a | ||
|
|
4b44b8b4fb | ||
|
|
56300f2928 | ||
|
|
41b77c4e0a | ||
|
|
76fad8410d | ||
|
|
92492ee23b | ||
|
|
35e4a47be0 | ||
|
|
6b76b77400 | ||
|
|
fdf27bf390 | ||
|
|
8f06a0f898 | ||
|
|
dda8ef11c7 | ||
|
|
149bda980a | ||
|
|
893447b6b0 | ||
|
|
22bdddd6f0 | ||
|
|
62e859c6c7 | ||
|
|
f78a6e752f | ||
|
|
3b3c919e20 | ||
|
|
10812f8407 | ||
|
|
e5504a060d | ||
|
|
167bfddafa | ||
|
|
bfb945c243 | ||
|
|
b67e751ccb | ||
|
|
a91983b11c | ||
|
|
9aab787122 | ||
|
|
ab8acce645 | ||
|
|
9b0e3556ed | ||
|
|
c0257cf52f | ||
|
|
70a510bd8f | ||
|
|
95bb1067c1 | ||
|
|
5b4119fa7f | ||
|
|
42114e1df4 | ||
|
|
a27ec24c0f | ||
|
|
b70a831608 | ||
|
|
10aac376d1 | ||
|
|
10f494eefe | ||
|
|
448290bee4 | ||
|
|
2592e41301 | ||
|
|
0b6f8ba51e | ||
|
|
d0b2950434 | ||
|
|
42431d2aa6 | ||
|
|
b8e70faa2d | ||
|
|
662218e997 | ||
|
|
c3013c7c9c | ||
|
|
acff922762 | ||
|
|
dfea525cbe | ||
|
|
82fa0bcea7 | ||
|
|
4fa4d80c68 | ||
|
|
6aa8ee6943 | ||
|
|
b76ddb7647 | ||
|
|
2a3b335b15 | ||
|
|
4e1471ef21 | ||
|
|
e9978fd4f5 | ||
|
|
75169c7570 | ||
|
|
03d7faf583 | ||
|
|
6f691e71bf | ||
|
|
5f8d253ce0 | ||
|
|
ed8b2a0254 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -31,3 +31,6 @@
|
||||
[submodule "opus"]
|
||||
path = externals/opus
|
||||
url = https://github.com/ogniK5377/opus.git
|
||||
[submodule "soundtouch"]
|
||||
path = externals/soundtouch
|
||||
url = https://github.com/citra-emu/ext-soundtouch.git
|
||||
|
||||
@@ -20,6 +20,7 @@ matrix:
|
||||
install: "./.travis/linux/deps.sh"
|
||||
script: "./.travis/linux/build.sh"
|
||||
after_success: "./.travis/linux/upload.sh"
|
||||
cache: ccache
|
||||
- os: osx
|
||||
env: NAME="macos build"
|
||||
sudo: false
|
||||
@@ -27,6 +28,7 @@ matrix:
|
||||
install: "./.travis/macos/deps.sh"
|
||||
script: "./.travis/macos/build.sh"
|
||||
after_success: "./.travis/macos/upload.sh"
|
||||
cache: ccache
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
@@ -42,7 +44,3 @@ notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://api.yuzu-emu.org/code/travis/notify
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.ccache
|
||||
|
||||
12
.travis/common/travis-ci.env
Normal file
12
.travis/common/travis-ci.env
Normal file
@@ -0,0 +1,12 @@
|
||||
# List of environment variables to be shared with Docker containers
|
||||
CI
|
||||
TRAVIS
|
||||
CONTINUOUS_INTEGRATION
|
||||
TRAVIS_BRANCH
|
||||
TRAVIS_BUILD_ID
|
||||
TRAVIS_BUILD_NUMBER
|
||||
TRAVIS_COMMIT
|
||||
TRAVIS_JOB_ID
|
||||
TRAVIS_JOB_NUMBER
|
||||
TRAVIS_REPO_SLUG
|
||||
TRAVIS_TAG
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash -ex
|
||||
|
||||
docker run -e CCACHE_DIR=/ccache -v $HOME/.ccache:/ccache -v $(pwd):/yuzu ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh
|
||||
mkdir -p "$HOME/.ccache"
|
||||
docker run --env-file .travis/common/travis-ci.env -v $(pwd):/yuzu -v "$HOME/.ccache":/root/.ccache ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh
|
||||
|
||||
@@ -5,14 +5,8 @@ apt-get install --no-install-recommends -y build-essential git libqt5opengl5-dev
|
||||
|
||||
cd /yuzu
|
||||
|
||||
export PATH=/usr/lib/ccache:$PATH
|
||||
ln -sf /usr/bin/ccache /usr/lib/ccache/cc
|
||||
ln -sf /usr/bin/ccache /usr/lib/ccache/c++
|
||||
mkdir build && cd build
|
||||
ccache --show-stats > ccache_before
|
||||
cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -G Ninja
|
||||
cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -G Ninja
|
||||
ninja
|
||||
ccache --show-stats > ccache_after
|
||||
diff -U100 ccache_before ccache_after || true
|
||||
|
||||
ctest -VV -C Release
|
||||
|
||||
@@ -5,14 +5,11 @@ set -o pipefail
|
||||
export MACOSX_DEPLOYMENT_TARGET=10.12
|
||||
export Qt5_DIR=$(brew --prefix)/opt/qt5
|
||||
export UNICORNDIR=$(pwd)/externals/unicorn
|
||||
export PATH="/usr/local/opt/ccache/libexec:$PATH"
|
||||
|
||||
mkdir build && cd build
|
||||
export PATH=/usr/local/opt/ccache/libexec:$PATH
|
||||
ccache --show-stats > ccache_before
|
||||
cmake --version
|
||||
cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release
|
||||
cmake .. -DYUZU_BUILD_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON
|
||||
make -j4
|
||||
ccache --show-stats > ccache_after
|
||||
diff -U100 ccache_before ccache_after || true
|
||||
|
||||
ctest -VV -C Release
|
||||
|
||||
@@ -41,6 +41,19 @@ function(check_submodules_present)
|
||||
endfunction()
|
||||
check_submodules_present()
|
||||
|
||||
configure_file(${CMAKE_SOURCE_DIR}/dist/compatibility_list/compatibility_list.qrc
|
||||
${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc
|
||||
COPYONLY)
|
||||
if (ENABLE_COMPATIBILITY_LIST_DOWNLOAD AND NOT EXISTS ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)
|
||||
message(STATUS "Downloading compatibility list for yuzu...")
|
||||
file(DOWNLOAD
|
||||
https://api.yuzu-emu.org/gamedb/
|
||||
"${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json" SHOW_PROGRESS)
|
||||
endif()
|
||||
if (NOT EXISTS ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)
|
||||
file(WRITE ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json "")
|
||||
endif()
|
||||
|
||||
# Detect current compilation architecture and create standard definitions
|
||||
# =======================================================================
|
||||
|
||||
@@ -66,10 +79,12 @@ if (NOT ENABLE_GENERIC)
|
||||
detect_architecture("_M_AMD64" x86_64)
|
||||
detect_architecture("_M_IX86" x86)
|
||||
detect_architecture("_M_ARM" ARM)
|
||||
detect_architecture("_M_ARM64" ARM64)
|
||||
else()
|
||||
detect_architecture("__x86_64__" x86_64)
|
||||
detect_architecture("__i386__" x86)
|
||||
detect_architecture("__arm__" ARM)
|
||||
detect_architecture("__aarch64__" ARM64)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ before_build:
|
||||
- ps: |
|
||||
if ($env:BUILD_TYPE -eq 'msvc') {
|
||||
# redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning
|
||||
cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 .. 2>&1 && exit 0'
|
||||
cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1 && exit 0'
|
||||
} else {
|
||||
C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release .. 2>&1"
|
||||
C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1"
|
||||
}
|
||||
- cd ..
|
||||
|
||||
@@ -162,10 +162,6 @@ artifacts:
|
||||
- path: $(BUILD_ZIP)
|
||||
name: build
|
||||
type: zip
|
||||
- path: $(BUILD_SYMBOLS)
|
||||
name: debugsymbols
|
||||
- path: $(BUILD_UPDATE)
|
||||
name: update
|
||||
|
||||
deploy:
|
||||
provider: GitHub
|
||||
|
||||
5
dist/compatibility_list/compatibility_list.qrc
vendored
Normal file
5
dist/compatibility_list/compatibility_list.qrc
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="compatibility_list">
|
||||
<file>compatibility_list.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
3
externals/CMakeLists.txt
vendored
3
externals/CMakeLists.txt
vendored
@@ -47,6 +47,9 @@ target_include_directories(microprofile INTERFACE ./microprofile)
|
||||
add_library(unicorn-headers INTERFACE)
|
||||
target_include_directories(unicorn-headers INTERFACE ./unicorn/include)
|
||||
|
||||
# SoundTouch
|
||||
add_subdirectory(soundtouch)
|
||||
|
||||
# Xbyak
|
||||
if (ARCHITECTURE_x86_64)
|
||||
# Defined before "dynarmic" above
|
||||
|
||||
2
externals/boost
vendored
2
externals/boost
vendored
Submodule externals/boost updated: d80e506e17...0b920df1c9
2
externals/dynarmic
vendored
2
externals/dynarmic
vendored
Submodule externals/dynarmic updated: 0118ee04f9...959446573f
2
externals/fmt
vendored
2
externals/fmt
vendored
Submodule externals/fmt updated: c2ce7e4f07...62010520ed
1
externals/soundtouch
vendored
Submodule
1
externals/soundtouch
vendored
Submodule
Submodule externals/soundtouch added at 060181eaf2
@@ -1,4 +1,8 @@
|
||||
add_library(audio_core STATIC
|
||||
algorithm/filter.cpp
|
||||
algorithm/filter.h
|
||||
algorithm/interpolate.cpp
|
||||
algorithm/interpolate.h
|
||||
audio_out.cpp
|
||||
audio_out.h
|
||||
audio_renderer.cpp
|
||||
@@ -7,12 +11,14 @@ add_library(audio_core STATIC
|
||||
codec.cpp
|
||||
codec.h
|
||||
null_sink.h
|
||||
stream.cpp
|
||||
stream.h
|
||||
sink.h
|
||||
sink_details.cpp
|
||||
sink_details.h
|
||||
sink_stream.h
|
||||
stream.cpp
|
||||
stream.h
|
||||
time_stretch.cpp
|
||||
time_stretch.h
|
||||
|
||||
$<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h>
|
||||
)
|
||||
@@ -20,6 +26,7 @@ add_library(audio_core STATIC
|
||||
create_target_directory_groups(audio_core)
|
||||
|
||||
target_link_libraries(audio_core PUBLIC common core)
|
||||
target_link_libraries(audio_core PRIVATE SoundTouch)
|
||||
|
||||
if(ENABLE_CUBEB)
|
||||
target_link_libraries(audio_core PRIVATE cubeb)
|
||||
|
||||
79
src/audio_core/algorithm/filter.cpp
Normal file
79
src/audio_core/algorithm/filter.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#define _USE_MATH_DEFINES
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include "audio_core/algorithm/filter.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
Filter Filter::LowPass(double cutoff, double Q) {
|
||||
const double w0 = 2.0 * M_PI * cutoff;
|
||||
const double sin_w0 = std::sin(w0);
|
||||
const double cos_w0 = std::cos(w0);
|
||||
const double alpha = sin_w0 / (2 * Q);
|
||||
|
||||
const double a0 = 1 + alpha;
|
||||
const double a1 = -2.0 * cos_w0;
|
||||
const double a2 = 1 - alpha;
|
||||
const double b0 = 0.5 * (1 - cos_w0);
|
||||
const double b1 = 1.0 * (1 - cos_w0);
|
||||
const double b2 = 0.5 * (1 - cos_w0);
|
||||
|
||||
return {a0, a1, a2, b0, b1, b2};
|
||||
}
|
||||
|
||||
Filter::Filter() : Filter(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) {}
|
||||
|
||||
Filter::Filter(double a0, double a1, double a2, double b0, double b1, double b2)
|
||||
: a1(a1 / a0), a2(a2 / a0), b0(b0 / a0), b1(b1 / a0), b2(b2 / a0) {}
|
||||
|
||||
void Filter::Process(std::vector<s16>& signal) {
|
||||
const size_t num_frames = signal.size() / 2;
|
||||
for (size_t i = 0; i < num_frames; i++) {
|
||||
std::rotate(in.begin(), in.end() - 1, in.end());
|
||||
std::rotate(out.begin(), out.end() - 1, out.end());
|
||||
|
||||
for (size_t ch = 0; ch < channel_count; ch++) {
|
||||
in[0][ch] = signal[i * channel_count + ch];
|
||||
|
||||
out[0][ch] = b0 * in[0][ch] + b1 * in[1][ch] + b2 * in[2][ch] - a1 * out[1][ch] -
|
||||
a2 * out[2][ch];
|
||||
|
||||
signal[i * 2 + ch] = static_cast<s16>(std::clamp(out[0][ch], -32768.0, 32767.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the appropriate Q for each biquad in a cascading filter.
|
||||
/// @param total_count The total number of biquads to be cascaded.
|
||||
/// @param index 0-index of the biquad to calculate the Q value for.
|
||||
static double CascadingBiquadQ(size_t total_count, size_t index) {
|
||||
const double pole = M_PI * (2 * index + 1) / (4.0 * total_count);
|
||||
return 1.0 / (2.0 * std::cos(pole));
|
||||
}
|
||||
|
||||
CascadingFilter CascadingFilter::LowPass(double cutoff, size_t cascade_size) {
|
||||
std::vector<Filter> cascade(cascade_size);
|
||||
for (size_t i = 0; i < cascade_size; i++) {
|
||||
cascade[i] = Filter::LowPass(cutoff, CascadingBiquadQ(cascade_size, i));
|
||||
}
|
||||
return CascadingFilter{std::move(cascade)};
|
||||
}
|
||||
|
||||
CascadingFilter::CascadingFilter() = default;
|
||||
CascadingFilter::CascadingFilter(std::vector<Filter> filters) : filters(std::move(filters)) {}
|
||||
|
||||
void CascadingFilter::Process(std::vector<s16>& signal) {
|
||||
for (auto& filter : filters) {
|
||||
filter.Process(signal);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace AudioCore
|
||||
62
src/audio_core/algorithm/filter.h
Normal file
62
src/audio_core/algorithm/filter.h
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
/// Digital biquad filter:
|
||||
///
|
||||
/// b0 + b1 z^-1 + b2 z^-2
|
||||
/// H(z) = ------------------------
|
||||
/// a0 + a1 z^-1 + b2 z^-2
|
||||
class Filter {
|
||||
public:
|
||||
/// Creates a low-pass filter.
|
||||
/// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0.
|
||||
/// @param Q Determines the quality factor of this filter.
|
||||
static Filter LowPass(double cutoff, double Q = 0.7071);
|
||||
|
||||
/// Passthrough filter.
|
||||
Filter();
|
||||
|
||||
Filter(double a0, double a1, double a2, double b0, double b1, double b2);
|
||||
|
||||
void Process(std::vector<s16>& signal);
|
||||
|
||||
private:
|
||||
static constexpr size_t channel_count = 2;
|
||||
|
||||
/// Coefficients are in normalized form (a0 = 1.0).
|
||||
double a1, a2, b0, b1, b2;
|
||||
/// Input History
|
||||
std::array<std::array<double, channel_count>, 3> in;
|
||||
/// Output History
|
||||
std::array<std::array<double, channel_count>, 3> out;
|
||||
};
|
||||
|
||||
/// Cascade filters to build up higher-order filters from lower-order ones.
|
||||
class CascadingFilter {
|
||||
public:
|
||||
/// Creates a cascading low-pass filter.
|
||||
/// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0.
|
||||
/// @param cascade_size Number of biquads in cascade.
|
||||
static CascadingFilter LowPass(double cutoff, size_t cascade_size);
|
||||
|
||||
/// Passthrough.
|
||||
CascadingFilter();
|
||||
|
||||
explicit CascadingFilter(std::vector<Filter> filters);
|
||||
|
||||
void Process(std::vector<s16>& signal);
|
||||
|
||||
private:
|
||||
std::vector<Filter> filters;
|
||||
};
|
||||
|
||||
} // namespace AudioCore
|
||||
71
src/audio_core/algorithm/interpolate.cpp
Normal file
71
src/audio_core/algorithm/interpolate.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#define _USE_MATH_DEFINES
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include "audio_core/algorithm/interpolate.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
/// The Lanczos kernel
|
||||
static double Lanczos(size_t a, double x) {
|
||||
if (x == 0.0)
|
||||
return 1.0;
|
||||
const double px = M_PI * x;
|
||||
return a * std::sin(px) * std::sin(px / a) / (px * px);
|
||||
}
|
||||
|
||||
std::vector<s16> Interpolate(InterpolationState& state, std::vector<s16> input, double ratio) {
|
||||
if (input.size() < 2)
|
||||
return {};
|
||||
|
||||
if (ratio <= 0) {
|
||||
LOG_CRITICAL(Audio, "Nonsensical interpolation ratio {}", ratio);
|
||||
ratio = 1.0;
|
||||
}
|
||||
|
||||
if (ratio != state.current_ratio) {
|
||||
const double cutoff_frequency = std::min(0.5 / ratio, 0.5 * ratio);
|
||||
state.nyquist = CascadingFilter::LowPass(std::clamp(cutoff_frequency, 0.0, 0.4), 3);
|
||||
state.current_ratio = ratio;
|
||||
}
|
||||
state.nyquist.Process(input);
|
||||
|
||||
constexpr size_t taps = InterpolationState::lanczos_taps;
|
||||
const size_t num_frames = input.size() / 2;
|
||||
|
||||
std::vector<s16> output;
|
||||
output.reserve(static_cast<size_t>(input.size() / ratio + 4));
|
||||
|
||||
double& pos = state.position;
|
||||
auto& h = state.history;
|
||||
for (size_t i = 0; i < num_frames; ++i) {
|
||||
std::rotate(h.begin(), h.end() - 1, h.end());
|
||||
h[0][0] = input[i * 2 + 0];
|
||||
h[0][1] = input[i * 2 + 1];
|
||||
|
||||
while (pos <= 1.0) {
|
||||
double l = 0.0;
|
||||
double r = 0.0;
|
||||
for (size_t j = 0; j < h.size(); j++) {
|
||||
l += Lanczos(taps, pos + j - taps + 1) * h[j][0];
|
||||
r += Lanczos(taps, pos + j - taps + 1) * h[j][1];
|
||||
}
|
||||
output.emplace_back(static_cast<s16>(std::clamp(l, -32768.0, 32767.0)));
|
||||
output.emplace_back(static_cast<s16>(std::clamp(r, -32768.0, 32767.0)));
|
||||
|
||||
pos += ratio;
|
||||
}
|
||||
pos -= 1.0;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace AudioCore
|
||||
43
src/audio_core/algorithm/interpolate.h
Normal file
43
src/audio_core/algorithm/interpolate.h
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include "audio_core/algorithm/filter.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
struct InterpolationState {
|
||||
static constexpr size_t lanczos_taps = 4;
|
||||
static constexpr size_t history_size = lanczos_taps * 2 - 1;
|
||||
|
||||
double current_ratio = 0.0;
|
||||
CascadingFilter nyquist;
|
||||
std::array<std::array<s16, 2>, history_size> history = {};
|
||||
double position = 0;
|
||||
};
|
||||
|
||||
/// Interpolates input signal to produce output signal.
|
||||
/// @param input The signal to interpolate.
|
||||
/// @param ratio Interpolation ratio.
|
||||
/// ratio > 1.0 results in fewer output samples.
|
||||
/// ratio < 1.0 results in more output samples.
|
||||
/// @returns Output signal.
|
||||
std::vector<s16> Interpolate(InterpolationState& state, std::vector<s16> input, double ratio);
|
||||
|
||||
/// Interpolates input signal to produce output signal.
|
||||
/// @param input The signal to interpolate.
|
||||
/// @param input_rate The sample rate of input.
|
||||
/// @param output_rate The desired sample rate of the output.
|
||||
/// @returns Output signal.
|
||||
inline std::vector<s16> Interpolate(InterpolationState& state, std::vector<s16> input,
|
||||
u32 input_rate, u32 output_rate) {
|
||||
const double ratio = static_cast<double>(input_rate) / static_cast<double>(output_rate);
|
||||
return Interpolate(state, std::move(input), ratio);
|
||||
}
|
||||
|
||||
} // namespace AudioCore
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "audio_core/algorithm/interpolate.h"
|
||||
#include "audio_core/audio_renderer.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -199,6 +200,8 @@ void AudioRenderer::VoiceState::RefreshBuffer() {
|
||||
break;
|
||||
}
|
||||
|
||||
samples = Interpolate(interp_state, std::move(samples), Info().sample_rate, STREAM_SAMPLE_RATE);
|
||||
|
||||
is_refresh_pending = false;
|
||||
}
|
||||
|
||||
@@ -224,7 +227,7 @@ void AudioRenderer::QueueMixedBuffer(Buffer::Tag tag) {
|
||||
break;
|
||||
}
|
||||
|
||||
samples_remaining -= samples.size();
|
||||
samples_remaining -= samples.size() / stream->GetNumChannels();
|
||||
|
||||
for (const auto& sample : samples) {
|
||||
const s32 buffer_sample{buffer[offset]};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "audio_core/algorithm/interpolate.h"
|
||||
#include "audio_core/audio_out.h"
|
||||
#include "audio_core/codec.h"
|
||||
#include "audio_core/stream.h"
|
||||
@@ -194,6 +195,7 @@ private:
|
||||
size_t wave_index{};
|
||||
size_t offset{};
|
||||
Codec::ADPCMState adpcm_state{};
|
||||
InterpolationState interp_state{};
|
||||
std::vector<s16> samples;
|
||||
VoiceOutStatus out_status{};
|
||||
VoiceInfo info{};
|
||||
|
||||
@@ -3,27 +3,23 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include "audio_core/cubeb_sink.h"
|
||||
#include "audio_core/stream.h"
|
||||
#include "audio_core/time_stretch.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/ring_buffer.h"
|
||||
#include "core/settings.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
class SinkStreamImpl final : public SinkStream {
|
||||
class CubebSinkStream final : public SinkStream {
|
||||
public:
|
||||
SinkStreamImpl(cubeb* ctx, u32 sample_rate, u32 num_channels_, cubeb_devid output_device,
|
||||
const std::string& name)
|
||||
: ctx{ctx}, num_channels{num_channels_} {
|
||||
|
||||
if (num_channels == 6) {
|
||||
// 6-channel audio does not seem to work with cubeb + SDL, so we downsample this to 2
|
||||
// channel for now
|
||||
is_6_channel = true;
|
||||
num_channels = 2;
|
||||
}
|
||||
CubebSinkStream(cubeb* ctx, u32 sample_rate, u32 num_channels_, cubeb_devid output_device,
|
||||
const std::string& name)
|
||||
: ctx{ctx}, num_channels{std::min(num_channels_, 2u)}, time_stretch{sample_rate,
|
||||
num_channels} {
|
||||
|
||||
cubeb_stream_params params{};
|
||||
params.rate = sample_rate;
|
||||
@@ -38,7 +34,7 @@ public:
|
||||
|
||||
if (cubeb_stream_init(ctx, &stream_backend, name.c_str(), nullptr, nullptr, output_device,
|
||||
¶ms, std::max(512u, minimum_latency),
|
||||
&SinkStreamImpl::DataCallback, &SinkStreamImpl::StateCallback,
|
||||
&CubebSinkStream::DataCallback, &CubebSinkStream::StateCallback,
|
||||
this) != CUBEB_OK) {
|
||||
LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream");
|
||||
return;
|
||||
@@ -50,7 +46,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
~SinkStreamImpl() {
|
||||
~CubebSinkStream() {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
@@ -62,27 +58,32 @@ public:
|
||||
cubeb_stream_destroy(stream_backend);
|
||||
}
|
||||
|
||||
void EnqueueSamples(u32 num_channels, const std::vector<s16>& samples) override {
|
||||
if (!ctx) {
|
||||
void EnqueueSamples(u32 source_num_channels, const std::vector<s16>& samples) override {
|
||||
if (source_num_channels > num_channels) {
|
||||
// Downsample 6 channels to 2
|
||||
std::vector<s16> buf;
|
||||
buf.reserve(samples.size() * num_channels / source_num_channels);
|
||||
for (size_t i = 0; i < samples.size(); i += source_num_channels) {
|
||||
for (size_t ch = 0; ch < num_channels; ch++) {
|
||||
buf.push_back(samples[i + ch]);
|
||||
}
|
||||
}
|
||||
queue.Push(buf);
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock{queue_mutex};
|
||||
queue.Push(samples);
|
||||
}
|
||||
|
||||
queue.reserve(queue.size() + samples.size() * GetNumChannels());
|
||||
size_t SamplesInQueue(u32 num_channels) const override {
|
||||
if (!ctx)
|
||||
return 0;
|
||||
|
||||
if (is_6_channel) {
|
||||
// Downsample 6 channels to 2
|
||||
const size_t sample_count_copy_size = samples.size() * 2;
|
||||
queue.reserve(sample_count_copy_size);
|
||||
for (size_t i = 0; i < samples.size(); i += num_channels) {
|
||||
queue.push_back(samples[i]);
|
||||
queue.push_back(samples[i + 1]);
|
||||
}
|
||||
} else {
|
||||
// Copy as-is
|
||||
std::copy(samples.begin(), samples.end(), std::back_inserter(queue));
|
||||
}
|
||||
return queue.Size() / num_channels;
|
||||
}
|
||||
|
||||
void Flush() override {
|
||||
should_flush = true;
|
||||
}
|
||||
|
||||
u32 GetNumChannels() const {
|
||||
@@ -95,10 +96,11 @@ private:
|
||||
cubeb* ctx{};
|
||||
cubeb_stream* stream_backend{};
|
||||
u32 num_channels{};
|
||||
bool is_6_channel{};
|
||||
|
||||
std::mutex queue_mutex;
|
||||
std::vector<s16> queue;
|
||||
Common::RingBuffer<s16, 0x10000> queue;
|
||||
std::array<s16, 2> last_frame;
|
||||
std::atomic<bool> should_flush{};
|
||||
TimeStretcher time_stretch;
|
||||
|
||||
static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
|
||||
void* output_buffer, long num_frames);
|
||||
@@ -144,38 +146,52 @@ CubebSink::~CubebSink() {
|
||||
SinkStream& CubebSink::AcquireSinkStream(u32 sample_rate, u32 num_channels,
|
||||
const std::string& name) {
|
||||
sink_streams.push_back(
|
||||
std::make_unique<SinkStreamImpl>(ctx, sample_rate, num_channels, output_device, name));
|
||||
std::make_unique<CubebSinkStream>(ctx, sample_rate, num_channels, output_device, name));
|
||||
return *sink_streams.back();
|
||||
}
|
||||
|
||||
long SinkStreamImpl::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
|
||||
void* output_buffer, long num_frames) {
|
||||
SinkStreamImpl* impl = static_cast<SinkStreamImpl*>(user_data);
|
||||
long CubebSinkStream::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
|
||||
void* output_buffer, long num_frames) {
|
||||
CubebSinkStream* impl = static_cast<CubebSinkStream*>(user_data);
|
||||
u8* buffer = reinterpret_cast<u8*>(output_buffer);
|
||||
|
||||
if (!impl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::lock_guard lock{impl->queue_mutex};
|
||||
const size_t num_channels = impl->GetNumChannels();
|
||||
const size_t samples_to_write = num_channels * num_frames;
|
||||
size_t samples_written;
|
||||
|
||||
const size_t frames_to_write{
|
||||
std::min(impl->queue.size() / impl->GetNumChannels(), static_cast<size_t>(num_frames))};
|
||||
if (Settings::values.enable_audio_stretching) {
|
||||
const std::vector<s16> in{impl->queue.Pop()};
|
||||
const size_t num_in{in.size() / num_channels};
|
||||
s16* const out{reinterpret_cast<s16*>(buffer)};
|
||||
const size_t out_frames = impl->time_stretch.Process(in.data(), num_in, out, num_frames);
|
||||
samples_written = out_frames * num_channels;
|
||||
|
||||
memcpy(buffer, impl->queue.data(), frames_to_write * sizeof(s16) * impl->GetNumChannels());
|
||||
impl->queue.erase(impl->queue.begin(),
|
||||
impl->queue.begin() + frames_to_write * impl->GetNumChannels());
|
||||
if (impl->should_flush) {
|
||||
impl->time_stretch.Flush();
|
||||
impl->should_flush = false;
|
||||
}
|
||||
} else {
|
||||
samples_written = impl->queue.Pop(buffer, samples_to_write);
|
||||
}
|
||||
|
||||
if (frames_to_write < num_frames) {
|
||||
// Fill the rest of the frames with silence
|
||||
memset(buffer + frames_to_write * sizeof(s16) * impl->GetNumChannels(), 0,
|
||||
(num_frames - frames_to_write) * sizeof(s16) * impl->GetNumChannels());
|
||||
if (samples_written >= num_channels) {
|
||||
std::memcpy(&impl->last_frame[0], buffer + (samples_written - num_channels) * sizeof(s16),
|
||||
num_channels * sizeof(s16));
|
||||
}
|
||||
|
||||
// Fill the rest of the frames with last_frame
|
||||
for (size_t i = samples_written; i < samples_to_write; i += num_channels) {
|
||||
std::memcpy(buffer + i * sizeof(s16), &impl->last_frame[0], num_channels * sizeof(s16));
|
||||
}
|
||||
|
||||
return num_frames;
|
||||
}
|
||||
|
||||
void SinkStreamImpl::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {}
|
||||
void CubebSinkStream::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {}
|
||||
|
||||
std::vector<std::string> ListCubebSinkDevices() {
|
||||
std::vector<std::string> device_list;
|
||||
|
||||
@@ -21,6 +21,12 @@ public:
|
||||
private:
|
||||
struct NullSinkStreamImpl final : SinkStream {
|
||||
void EnqueueSamples(u32 /*num_channels*/, const std::vector<s16>& /*samples*/) override {}
|
||||
|
||||
size_t SamplesInQueue(u32 /*num_channels*/) const override {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Flush() override {}
|
||||
} null_sink_stream;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ public:
|
||||
* @param samples Samples in interleaved stereo PCM16 format.
|
||||
*/
|
||||
virtual void EnqueueSamples(u32 num_channels, const std::vector<s16>& samples) = 0;
|
||||
|
||||
virtual std::size_t SamplesInQueue(u32 num_channels) const = 0;
|
||||
|
||||
virtual void Flush() = 0;
|
||||
};
|
||||
|
||||
using SinkStreamPtr = std::unique_ptr<SinkStream>;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "audio_core/stream.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/microprofile.h"
|
||||
#include "core/core_timing.h"
|
||||
#include "core/core_timing_util.h"
|
||||
#include "core/settings.h"
|
||||
@@ -72,6 +73,7 @@ static void VolumeAdjustSamples(std::vector<s16>& samples) {
|
||||
void Stream::PlayNextBuffer() {
|
||||
if (!IsPlaying()) {
|
||||
// Ensure we are in playing state before playing the next buffer
|
||||
sink_stream.Flush();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,6 +84,7 @@ void Stream::PlayNextBuffer() {
|
||||
|
||||
if (queued_buffers.empty()) {
|
||||
// No queued buffers - we are effectively paused
|
||||
sink_stream.Flush();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,12 +92,16 @@ void Stream::PlayNextBuffer() {
|
||||
queued_buffers.pop();
|
||||
|
||||
VolumeAdjustSamples(active_buffer->Samples());
|
||||
|
||||
sink_stream.EnqueueSamples(GetNumChannels(), active_buffer->GetSamples());
|
||||
|
||||
CoreTiming::ScheduleEventThreadsafe(GetBufferReleaseCycles(*active_buffer), release_event, {});
|
||||
}
|
||||
|
||||
MICROPROFILE_DEFINE(AudioOutput, "Audio", "ReleaseActiveBuffer", MP_RGB(100, 100, 255));
|
||||
|
||||
void Stream::ReleaseActiveBuffer() {
|
||||
MICROPROFILE_SCOPE(AudioOutput);
|
||||
ASSERT(active_buffer);
|
||||
released_buffers.push(std::move(active_buffer));
|
||||
release_callback();
|
||||
|
||||
68
src/audio_core/time_stretch.cpp
Normal file
68
src/audio_core/time_stretch.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include "audio_core/time_stretch.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
TimeStretcher::TimeStretcher(u32 sample_rate, u32 channel_count)
|
||||
: m_sample_rate(sample_rate), m_channel_count(channel_count) {
|
||||
m_sound_touch.setChannels(channel_count);
|
||||
m_sound_touch.setSampleRate(sample_rate);
|
||||
m_sound_touch.setPitch(1.0);
|
||||
m_sound_touch.setTempo(1.0);
|
||||
}
|
||||
|
||||
void TimeStretcher::Clear() {
|
||||
m_sound_touch.clear();
|
||||
}
|
||||
|
||||
void TimeStretcher::Flush() {
|
||||
m_sound_touch.flush();
|
||||
}
|
||||
|
||||
size_t TimeStretcher::Process(const s16* in, size_t num_in, s16* out, size_t num_out) {
|
||||
const double time_delta = static_cast<double>(num_out) / m_sample_rate; // seconds
|
||||
|
||||
// We were given actual_samples number of samples, and num_samples were requested from us.
|
||||
double current_ratio = static_cast<double>(num_in) / static_cast<double>(num_out);
|
||||
|
||||
const double max_latency = 1.0; // seconds
|
||||
const double max_backlog = m_sample_rate * max_latency;
|
||||
const double backlog_fullness = m_sound_touch.numSamples() / max_backlog;
|
||||
if (backlog_fullness > 5.0) {
|
||||
// Too many samples in backlog: Don't push anymore on
|
||||
num_in = 0;
|
||||
}
|
||||
|
||||
// We ideally want the backlog to be about 50% full.
|
||||
// This gives some headroom both ways to prevent underflow and overflow.
|
||||
// We tweak current_ratio to encourage this.
|
||||
constexpr double tweak_time_scale = 0.05; // seconds
|
||||
const double tweak_correction = (backlog_fullness - 0.5) * (time_delta / tweak_time_scale);
|
||||
current_ratio *= std::pow(1.0 + 2.0 * tweak_correction, tweak_correction < 0 ? 3.0 : 1.0);
|
||||
|
||||
// This low-pass filter smoothes out variance in the calculated stretch ratio.
|
||||
// The time-scale determines how responsive this filter is.
|
||||
constexpr double lpf_time_scale = 2.0; // seconds
|
||||
const double lpf_gain = 1.0 - std::exp(-time_delta / lpf_time_scale);
|
||||
m_stretch_ratio += lpf_gain * (current_ratio - m_stretch_ratio);
|
||||
|
||||
// Place a lower limit of 5% speed. When a game boots up, there will be
|
||||
// many silence samples. These do not need to be timestretched.
|
||||
m_stretch_ratio = std::max(m_stretch_ratio, 0.05);
|
||||
m_sound_touch.setTempo(m_stretch_ratio);
|
||||
|
||||
LOG_DEBUG(Audio, "{:5}/{:5} ratio:{:0.6f} backlog:{:0.6f}", num_in, num_out, m_stretch_ratio,
|
||||
backlog_fullness);
|
||||
|
||||
m_sound_touch.putSamples(in, num_in);
|
||||
return m_sound_touch.receiveSamples(out, num_out);
|
||||
}
|
||||
|
||||
} // namespace AudioCore
|
||||
36
src/audio_core/time_stretch.h
Normal file
36
src/audio_core/time_stretch.h
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2018 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <SoundTouch.h>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore {
|
||||
|
||||
class TimeStretcher {
|
||||
public:
|
||||
TimeStretcher(u32 sample_rate, u32 channel_count);
|
||||
|
||||
/// @param in Input sample buffer
|
||||
/// @param num_in Number of input frames in `in`
|
||||
/// @param out Output sample buffer
|
||||
/// @param num_out Desired number of output frames in `out`
|
||||
/// @returns Actual number of frames written to `out`
|
||||
size_t Process(const s16* in, size_t num_in, s16* out, size_t num_out);
|
||||
|
||||
void Clear();
|
||||
|
||||
void Flush();
|
||||
|
||||
private:
|
||||
u32 m_sample_rate;
|
||||
u32 m_channel_count;
|
||||
soundtouch::SoundTouch m_sound_touch;
|
||||
double m_stretch_ratio = 1.0;
|
||||
};
|
||||
|
||||
} // namespace AudioCore
|
||||
@@ -1,13 +1,16 @@
|
||||
# Generate cpp with Git revision from template
|
||||
# Also if this is a CI build, add the build name (ie: Nightly, Bleeding Edge) to the scm_rev file as well
|
||||
# Also if this is a CI build, add the build name (ie: Nightly, Canary) to the scm_rev file as well
|
||||
set(REPO_NAME "")
|
||||
set(BUILD_VERSION "0")
|
||||
if ($ENV{CI})
|
||||
if ($ENV{TRAVIS})
|
||||
set(BUILD_REPOSITORY $ENV{TRAVIS_REPO_SLUG})
|
||||
set(BUILD_TAG $ENV{TRAVIS_TAG})
|
||||
elseif($ENV{APPVEYOR})
|
||||
set(BUILD_REPOSITORY $ENV{APPVEYOR_REPO_NAME})
|
||||
set(BUILD_TAG $ENV{APPVEYOR_REPO_TAG_NAME})
|
||||
endif()
|
||||
# regex capture the string nightly or bleeding-edge into CMAKE_MATCH_1
|
||||
# regex capture the string nightly or canary into CMAKE_MATCH_1
|
||||
string(REGEX MATCH "yuzu-emu/yuzu-?(.*)" OUTVAR ${BUILD_REPOSITORY})
|
||||
if (${CMAKE_MATCH_COUNT} GREATER 0)
|
||||
# capitalize the first letter of each word in the repo name.
|
||||
@@ -16,10 +19,21 @@ if ($ENV{CI})
|
||||
string(SUBSTRING ${WORD} 0 1 FIRST_LETTER)
|
||||
string(SUBSTRING ${WORD} 1 -1 REMAINDER)
|
||||
string(TOUPPER ${FIRST_LETTER} FIRST_LETTER)
|
||||
# this leaves a trailing space on the last word, but we actually want that
|
||||
# because of how it's styled in the title bar.
|
||||
set(REPO_NAME "${REPO_NAME}${FIRST_LETTER}${REMAINDER} ")
|
||||
set(REPO_NAME "${REPO_NAME}${FIRST_LETTER}${REMAINDER}")
|
||||
endforeach()
|
||||
if (BUILD_TAG)
|
||||
string(REGEX MATCH "${CMAKE_MATCH_1}-([0-9]+)" OUTVAR ${BUILD_TAG})
|
||||
if (${CMAKE_MATCH_COUNT} GREATER 0)
|
||||
set(BUILD_VERSION ${CMAKE_MATCH_1})
|
||||
endif()
|
||||
if (BUILD_VERSION)
|
||||
# This leaves a trailing space on the last word, but we actually want that
|
||||
# because of how it's styled in the title bar.
|
||||
set(BUILD_FULLNAME "${REPO_NAME} #${BUILD_VERSION} ")
|
||||
else()
|
||||
set(BUILD_FULLNAME "")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp.in" "${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp" @ONLY)
|
||||
@@ -29,8 +43,6 @@ add_library(common STATIC
|
||||
assert.h
|
||||
bit_field.h
|
||||
bit_set.h
|
||||
break_points.cpp
|
||||
break_points.h
|
||||
cityhash.cpp
|
||||
cityhash.h
|
||||
color.h
|
||||
@@ -40,6 +52,8 @@ add_library(common STATIC
|
||||
file_util.cpp
|
||||
file_util.h
|
||||
hash.h
|
||||
hex_util.cpp
|
||||
hex_util.h
|
||||
logging/backend.cpp
|
||||
logging/backend.h
|
||||
logging/filter.cpp
|
||||
@@ -57,6 +71,7 @@ add_library(common STATIC
|
||||
param_package.cpp
|
||||
param_package.h
|
||||
quaternion.h
|
||||
ring_buffer.h
|
||||
scm_rev.cpp
|
||||
scm_rev.h
|
||||
scope_exit.h
|
||||
|
||||
@@ -178,8 +178,7 @@ public:
|
||||
return ExtractValue(storage);
|
||||
}
|
||||
|
||||
// TODO: we may want to change this to explicit operator bool() if it's bug-free in VS2015
|
||||
constexpr FORCE_INLINE bool ToBool() const {
|
||||
constexpr explicit operator bool() const {
|
||||
return Value() != 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include "common/break_points.h"
|
||||
|
||||
bool BreakPoints::IsAddressBreakPoint(u32 iAddress) const {
|
||||
auto cond = [&iAddress](const TBreakPoint& bp) { return bp.iAddress == iAddress; };
|
||||
auto it = std::find_if(m_BreakPoints.begin(), m_BreakPoints.end(), cond);
|
||||
return it != m_BreakPoints.end();
|
||||
}
|
||||
|
||||
bool BreakPoints::IsTempBreakPoint(u32 iAddress) const {
|
||||
auto cond = [&iAddress](const TBreakPoint& bp) {
|
||||
return bp.iAddress == iAddress && bp.bTemporary;
|
||||
};
|
||||
auto it = std::find_if(m_BreakPoints.begin(), m_BreakPoints.end(), cond);
|
||||
return it != m_BreakPoints.end();
|
||||
}
|
||||
|
||||
BreakPoints::TBreakPointsStr BreakPoints::GetStrings() const {
|
||||
TBreakPointsStr bps;
|
||||
for (auto breakpoint : m_BreakPoints) {
|
||||
if (!breakpoint.bTemporary) {
|
||||
std::stringstream bp;
|
||||
bp << std::hex << breakpoint.iAddress << " " << (breakpoint.bOn ? "n" : "");
|
||||
bps.push_back(bp.str());
|
||||
}
|
||||
}
|
||||
|
||||
return bps;
|
||||
}
|
||||
|
||||
void BreakPoints::AddFromStrings(const TBreakPointsStr& bps) {
|
||||
for (auto bps_item : bps) {
|
||||
TBreakPoint bp;
|
||||
std::stringstream bpstr;
|
||||
bpstr << std::hex << bps_item;
|
||||
bpstr >> bp.iAddress;
|
||||
bp.bOn = bps_item.find("n") != bps_item.npos;
|
||||
bp.bTemporary = false;
|
||||
Add(bp);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakPoints::Add(const TBreakPoint& bp) {
|
||||
if (!IsAddressBreakPoint(bp.iAddress)) {
|
||||
m_BreakPoints.push_back(bp);
|
||||
// if (jit)
|
||||
// jit->GetBlockCache()->InvalidateICache(bp.iAddress, 4);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakPoints::Add(u32 em_address, bool temp) {
|
||||
if (!IsAddressBreakPoint(em_address)) // only add new addresses
|
||||
{
|
||||
TBreakPoint pt; // breakpoint settings
|
||||
pt.bOn = true;
|
||||
pt.bTemporary = temp;
|
||||
pt.iAddress = em_address;
|
||||
|
||||
m_BreakPoints.push_back(pt);
|
||||
|
||||
// if (jit)
|
||||
// jit->GetBlockCache()->InvalidateICache(em_address, 4);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakPoints::Remove(u32 em_address) {
|
||||
auto cond = [&em_address](const TBreakPoint& bp) { return bp.iAddress == em_address; };
|
||||
auto it = std::find_if(m_BreakPoints.begin(), m_BreakPoints.end(), cond);
|
||||
if (it != m_BreakPoints.end())
|
||||
m_BreakPoints.erase(it);
|
||||
}
|
||||
|
||||
void BreakPoints::Clear() {
|
||||
// if (jit)
|
||||
//{
|
||||
// std::for_each(m_BreakPoints.begin(), m_BreakPoints.end(),
|
||||
// [](const TBreakPoint& bp)
|
||||
// {
|
||||
// jit->GetBlockCache()->InvalidateICache(bp.iAddress, 4);
|
||||
// }
|
||||
// );
|
||||
//}
|
||||
|
||||
m_BreakPoints.clear();
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2013 Dolphin Emulator Project / 2014 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"
|
||||
|
||||
class DebugInterface;
|
||||
|
||||
struct TBreakPoint {
|
||||
u32 iAddress;
|
||||
bool bOn;
|
||||
bool bTemporary;
|
||||
};
|
||||
|
||||
// Code breakpoints.
|
||||
class BreakPoints {
|
||||
public:
|
||||
typedef std::vector<TBreakPoint> TBreakPoints;
|
||||
typedef std::vector<std::string> TBreakPointsStr;
|
||||
|
||||
const TBreakPoints& GetBreakPoints() {
|
||||
return m_BreakPoints;
|
||||
}
|
||||
|
||||
TBreakPointsStr GetStrings() const;
|
||||
void AddFromStrings(const TBreakPointsStr& bps);
|
||||
|
||||
// is address breakpoint
|
||||
bool IsAddressBreakPoint(u32 iAddress) const;
|
||||
bool IsTempBreakPoint(u32 iAddress) const;
|
||||
|
||||
// Add BreakPoint
|
||||
void Add(u32 em_address, bool temp = false);
|
||||
void Add(const TBreakPoint& bp);
|
||||
|
||||
// Remove Breakpoint
|
||||
void Remove(u32 iAddress);
|
||||
void Clear();
|
||||
|
||||
void DeleteByAddress(u32 Address);
|
||||
|
||||
private:
|
||||
TBreakPoints m_BreakPoints;
|
||||
u32 m_iBreakOnCount;
|
||||
};
|
||||
@@ -750,6 +750,12 @@ std::string GetHactoolConfigurationPath() {
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string GetNANDRegistrationDir(bool system) {
|
||||
if (system)
|
||||
return GetUserPath(UserPath::NANDDir) + "system/Contents/registered/";
|
||||
return GetUserPath(UserPath::NANDDir) + "user/Contents/registered/";
|
||||
}
|
||||
|
||||
size_t WriteStringToFile(bool text_file, const std::string& str, const char* filename) {
|
||||
return FileUtil::IOFile(filename, text_file ? "w" : "wb").WriteBytes(str.data(), str.size());
|
||||
}
|
||||
@@ -758,7 +764,7 @@ size_t ReadFileToString(bool text_file, const char* filename, std::string& str)
|
||||
IOFile file(filename, text_file ? "r" : "rb");
|
||||
|
||||
if (!file.IsOpen())
|
||||
return false;
|
||||
return 0;
|
||||
|
||||
str.resize(static_cast<u32>(file.GetSize()));
|
||||
return file.ReadArray(&str[0], str.size());
|
||||
|
||||
@@ -129,6 +129,8 @@ const std::string& GetUserPath(UserPath path, const std::string& new_path = "");
|
||||
|
||||
std::string GetHactoolConfigurationPath();
|
||||
|
||||
std::string GetNANDRegistrationDir(bool system = false);
|
||||
|
||||
// Returns the path to where the sys file are
|
||||
std::string GetSysDirectory();
|
||||
|
||||
|
||||
43
src/common/hex_util.cpp
Normal file
43
src/common/hex_util.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
u8 ToHexNibble(char c1) {
|
||||
if (c1 >= 65 && c1 <= 70)
|
||||
return c1 - 55;
|
||||
if (c1 >= 97 && c1 <= 102)
|
||||
return c1 - 87;
|
||||
if (c1 >= 48 && c1 <= 57)
|
||||
return c1 - 48;
|
||||
LOG_ERROR(Common, "Invalid hex digit: 0x{:02X}", c1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::array<u8, 16> operator""_array16(const char* str, size_t len) {
|
||||
if (len != 32) {
|
||||
LOG_ERROR(Common,
|
||||
"Attempting to parse string to array that is not of correct size (expected=32, "
|
||||
"actual={}).",
|
||||
len);
|
||||
return {};
|
||||
}
|
||||
return HexStringToArray<16>(str);
|
||||
}
|
||||
|
||||
std::array<u8, 32> operator""_array32(const char* str, size_t len) {
|
||||
if (len != 64) {
|
||||
LOG_ERROR(Common,
|
||||
"Attempting to parse string to array that is not of correct size (expected=64, "
|
||||
"actual={}).",
|
||||
len);
|
||||
return {};
|
||||
}
|
||||
return HexStringToArray<32>(str);
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
41
src/common/hex_util.h
Normal file
41
src/common/hex_util.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <fmt/format.h>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
u8 ToHexNibble(char c1);
|
||||
|
||||
template <size_t Size, bool le = false>
|
||||
std::array<u8, Size> HexStringToArray(std::string_view str) {
|
||||
std::array<u8, Size> out{};
|
||||
if constexpr (le) {
|
||||
for (size_t i = 2 * Size - 2; i <= 2 * Size; i -= 2)
|
||||
out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]);
|
||||
} else {
|
||||
for (size_t i = 0; i < 2 * Size; i += 2)
|
||||
out[i / 2] = (ToHexNibble(str[i]) << 4) | ToHexNibble(str[i + 1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
std::string HexArrayToString(std::array<u8, Size> array, bool upper = true) {
|
||||
std::string out;
|
||||
for (u8 c : array)
|
||||
out += fmt::format(upper ? "{:02X}" : "{:02x}", c);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<u8, 0x10> operator"" _array16(const char* str, size_t len);
|
||||
std::array<u8, 0x20> operator"" _array32(const char* str, size_t len);
|
||||
|
||||
} // namespace Common
|
||||
@@ -302,13 +302,14 @@ Backend* GetBackend(std::string_view backend_name) {
|
||||
void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename,
|
||||
unsigned int line_num, const char* function, const char* format,
|
||||
const fmt::format_args& args) {
|
||||
auto filter = Impl::Instance().GetGlobalFilter();
|
||||
auto& instance = Impl::Instance();
|
||||
const auto& filter = instance.GetGlobalFilter();
|
||||
if (!filter.CheckMessage(log_class, log_level))
|
||||
return;
|
||||
|
||||
Entry entry =
|
||||
CreateEntry(log_class, log_level, filename, line_num, function, fmt::vformat(format, args));
|
||||
|
||||
Impl::Instance().PushEntry(std::move(entry));
|
||||
instance.PushEntry(std::move(entry));
|
||||
}
|
||||
} // namespace Log
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Log {
|
||||
class Filter {
|
||||
public:
|
||||
/// Initializes the filter with all classes having `default_level` as the minimum level.
|
||||
Filter(Level default_level = Level::Info);
|
||||
explicit Filter(Level default_level = Level::Info);
|
||||
|
||||
/// Resets the filter so that all classes have `level` as the minimum displayed level.
|
||||
void ResetAll(Level level);
|
||||
@@ -49,6 +49,6 @@ public:
|
||||
bool IsDebug() const;
|
||||
|
||||
private:
|
||||
std::array<Level, (size_t)Class::Count> class_levels;
|
||||
std::array<Level, static_cast<size_t>(Class::Count)> class_levels;
|
||||
};
|
||||
} // namespace Log
|
||||
|
||||
@@ -12,14 +12,14 @@ namespace Log {
|
||||
/// Specifies the severity or level of detail of the log message.
|
||||
enum class Level : u8 {
|
||||
Trace, ///< Extremely detailed and repetitive debugging information that is likely to
|
||||
/// pollute logs.
|
||||
///< pollute logs.
|
||||
Debug, ///< Less detailed debugging information.
|
||||
Info, ///< Status information from important points during execution.
|
||||
Warning, ///< Minor or potential problems found during execution of a task.
|
||||
Error, ///< Major problems found during execution of a task that prevent it from being
|
||||
/// completed.
|
||||
Critical, ///< Major problems during execution that threathen the stability of the entire
|
||||
/// application.
|
||||
///< completed.
|
||||
Critical, ///< Major problems during execution that threaten the stability of the entire
|
||||
///< application.
|
||||
|
||||
Count ///< Total number of logging levels
|
||||
};
|
||||
@@ -49,7 +49,7 @@ enum class Class : ClassType {
|
||||
Kernel, ///< The HLE implementation of the CTR kernel
|
||||
Kernel_SVC, ///< Kernel system calls
|
||||
Service, ///< HLE implementation of system services. Each major service
|
||||
/// should have its own subclass.
|
||||
///< should have its own subclass.
|
||||
Service_ACC, ///< The ACC (Accounts) service
|
||||
Service_AM, ///< The AM (Applet manager) service
|
||||
Service_AOC, ///< The AOC (AddOn Content) service
|
||||
|
||||
@@ -42,7 +42,7 @@ void PrintColoredMessage(const Entry& entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
CONSOLE_SCREEN_BUFFER_INFO original_info = {0};
|
||||
CONSOLE_SCREEN_BUFFER_INFO original_info = {};
|
||||
GetConsoleScreenBufferInfo(console_handle, &original_info);
|
||||
|
||||
WORD color = 0;
|
||||
|
||||
@@ -15,6 +15,6 @@ struct Entry;
|
||||
std::string FormatLogMessage(const Entry& entry);
|
||||
/// Formats and prints a log entry to stderr.
|
||||
void PrintMessage(const Entry& entry);
|
||||
/// Prints the same message as `PrintMessage`, but colored acoording to the severity level.
|
||||
/// Prints the same message as `PrintMessage`, but colored according to the severity level.
|
||||
void PrintColoredMessage(const Entry& entry);
|
||||
} // namespace Log
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
111
src/common/ring_buffer.h
Normal file
111
src/common/ring_buffer.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/// SPSC ring buffer
|
||||
/// @tparam T Element type
|
||||
/// @tparam capacity Number of slots in ring buffer
|
||||
/// @tparam granularity Slot size in terms of number of elements
|
||||
template <typename T, size_t capacity, size_t granularity = 1>
|
||||
class RingBuffer {
|
||||
/// A "slot" is made of `granularity` elements of `T`.
|
||||
static constexpr size_t slot_size = granularity * sizeof(T);
|
||||
// T must be safely memcpy-able and have a trivial default constructor.
|
||||
static_assert(std::is_trivial_v<T>);
|
||||
// Ensure capacity is sensible.
|
||||
static_assert(capacity < std::numeric_limits<size_t>::max() / 2 / granularity);
|
||||
static_assert((capacity & (capacity - 1)) == 0, "capacity must be a power of two");
|
||||
// Ensure lock-free.
|
||||
static_assert(std::atomic<size_t>::is_always_lock_free);
|
||||
|
||||
public:
|
||||
/// Pushes slots into the ring buffer
|
||||
/// @param new_slots Pointer to the slots to push
|
||||
/// @param slot_count Number of slots to push
|
||||
/// @returns The number of slots actually pushed
|
||||
size_t Push(const void* new_slots, size_t slot_count) {
|
||||
const size_t write_index = m_write_index.load();
|
||||
const size_t slots_free = capacity + m_read_index.load() - write_index;
|
||||
const size_t push_count = std::min(slot_count, slots_free);
|
||||
|
||||
const size_t pos = write_index % capacity;
|
||||
const size_t first_copy = std::min(capacity - pos, push_count);
|
||||
const size_t second_copy = push_count - first_copy;
|
||||
|
||||
const char* in = static_cast<const char*>(new_slots);
|
||||
std::memcpy(m_data.data() + pos * granularity, in, first_copy * slot_size);
|
||||
in += first_copy * slot_size;
|
||||
std::memcpy(m_data.data(), in, second_copy * slot_size);
|
||||
|
||||
m_write_index.store(write_index + push_count);
|
||||
|
||||
return push_count;
|
||||
}
|
||||
|
||||
size_t Push(const std::vector<T>& input) {
|
||||
return Push(input.data(), input.size());
|
||||
}
|
||||
|
||||
/// Pops slots from the ring buffer
|
||||
/// @param output Where to store the popped slots
|
||||
/// @param max_slots Maximum number of slots to pop
|
||||
/// @returns The number of slots actually popped
|
||||
size_t Pop(void* output, size_t max_slots = ~size_t(0)) {
|
||||
const size_t read_index = m_read_index.load();
|
||||
const size_t slots_filled = m_write_index.load() - read_index;
|
||||
const size_t pop_count = std::min(slots_filled, max_slots);
|
||||
|
||||
const size_t pos = read_index % capacity;
|
||||
const size_t first_copy = std::min(capacity - pos, pop_count);
|
||||
const size_t second_copy = pop_count - first_copy;
|
||||
|
||||
char* out = static_cast<char*>(output);
|
||||
std::memcpy(out, m_data.data() + pos * granularity, first_copy * slot_size);
|
||||
out += first_copy * slot_size;
|
||||
std::memcpy(out, m_data.data(), second_copy * slot_size);
|
||||
|
||||
m_read_index.store(read_index + pop_count);
|
||||
|
||||
return pop_count;
|
||||
}
|
||||
|
||||
std::vector<T> Pop(size_t max_slots = ~size_t(0)) {
|
||||
std::vector<T> out(std::min(max_slots, capacity) * granularity);
|
||||
const size_t count = Pop(out.data(), out.size() / granularity);
|
||||
out.resize(count * granularity);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @returns Number of slots used
|
||||
size_t Size() const {
|
||||
return m_write_index.load() - m_read_index.load();
|
||||
}
|
||||
|
||||
/// @returns Maximum size of ring buffer
|
||||
constexpr size_t Capacity() const {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
private:
|
||||
// It is important to align the below variables for performance reasons:
|
||||
// Having them on the same cache-line would result in false-sharing between them.
|
||||
alignas(128) std::atomic<size_t> m_read_index{0};
|
||||
alignas(128) std::atomic<size_t> m_write_index{0};
|
||||
|
||||
std::array<T, granularity * capacity> m_data;
|
||||
};
|
||||
|
||||
} // namespace Common
|
||||
@@ -9,6 +9,8 @@
|
||||
#define GIT_DESC "@GIT_DESC@"
|
||||
#define BUILD_NAME "@REPO_NAME@"
|
||||
#define BUILD_DATE "@BUILD_DATE@"
|
||||
#define BUILD_FULLNAME "@BUILD_FULLNAME@"
|
||||
#define BUILD_VERSION "@BUILD_VERSION@"
|
||||
|
||||
namespace Common {
|
||||
|
||||
@@ -17,6 +19,8 @@ const char g_scm_branch[] = GIT_BRANCH;
|
||||
const char g_scm_desc[] = GIT_DESC;
|
||||
const char g_build_name[] = BUILD_NAME;
|
||||
const char g_build_date[] = BUILD_DATE;
|
||||
const char g_build_fullname[] = BUILD_FULLNAME;
|
||||
const char g_build_version[] = BUILD_VERSION;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -11,5 +11,7 @@ extern const char g_scm_branch[];
|
||||
extern const char g_scm_desc[];
|
||||
extern const char g_build_name[];
|
||||
extern const char g_build_date[];
|
||||
extern const char g_build_fullname[];
|
||||
extern const char g_build_version[];
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "common/assert.h"
|
||||
#include "common/scm_rev.h"
|
||||
#include "common/telemetry.h"
|
||||
|
||||
#ifdef ARCHITECTURE_x86_64
|
||||
#include "common/x64/cpu_detect.h"
|
||||
#endif
|
||||
|
||||
namespace Telemetry {
|
||||
|
||||
void FieldCollection::Accept(VisitorInterface& visitor) const {
|
||||
@@ -37,4 +44,62 @@ template class Field<std::string>;
|
||||
template class Field<const char*>;
|
||||
template class Field<std::chrono::microseconds>;
|
||||
|
||||
#ifdef ARCHITECTURE_x86_64
|
||||
static const char* CpuVendorToStr(Common::CPUVendor vendor) {
|
||||
switch (vendor) {
|
||||
case Common::CPUVendor::INTEL:
|
||||
return "Intel";
|
||||
case Common::CPUVendor::AMD:
|
||||
return "Amd";
|
||||
case Common::CPUVendor::OTHER:
|
||||
return "Other";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
#endif
|
||||
|
||||
void AppendBuildInfo(FieldCollection& fc) {
|
||||
const bool is_git_dirty{std::strstr(Common::g_scm_desc, "dirty") != nullptr};
|
||||
fc.AddField(FieldType::App, "Git_IsDirty", is_git_dirty);
|
||||
fc.AddField(FieldType::App, "Git_Branch", Common::g_scm_branch);
|
||||
fc.AddField(FieldType::App, "Git_Revision", Common::g_scm_rev);
|
||||
fc.AddField(FieldType::App, "BuildDate", Common::g_build_date);
|
||||
fc.AddField(FieldType::App, "BuildName", Common::g_build_name);
|
||||
}
|
||||
|
||||
void AppendCPUInfo(FieldCollection& fc) {
|
||||
#ifdef ARCHITECTURE_x86_64
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Model", Common::GetCPUCaps().cpu_string);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_BrandString", Common::GetCPUCaps().brand_string);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Vendor", CpuVendorToStr(Common::GetCPUCaps().vendor));
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_AES", Common::GetCPUCaps().aes);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_AVX", Common::GetCPUCaps().avx);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_AVX2", Common::GetCPUCaps().avx2);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_BMI1", Common::GetCPUCaps().bmi1);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_BMI2", Common::GetCPUCaps().bmi2);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_FMA", Common::GetCPUCaps().fma);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_FMA4", Common::GetCPUCaps().fma4);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_SSE", Common::GetCPUCaps().sse);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_SSE2", Common::GetCPUCaps().sse2);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_SSE3", Common::GetCPUCaps().sse3);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_SSSE3", Common::GetCPUCaps().ssse3);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_SSE41", Common::GetCPUCaps().sse4_1);
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Extension_x64_SSE42", Common::GetCPUCaps().sse4_2);
|
||||
#else
|
||||
fc.AddField(FieldType::UserSystem, "CPU_Model", "Other");
|
||||
#endif
|
||||
}
|
||||
|
||||
void AppendOSInfo(FieldCollection& fc) {
|
||||
#ifdef __APPLE__
|
||||
fc.AddField(FieldType::UserSystem, "OsPlatform", "Apple");
|
||||
#elif defined(_WIN32)
|
||||
fc.AddField(FieldType::UserSystem, "OsPlatform", "Windows");
|
||||
#elif defined(__linux__) || defined(linux) || defined(__linux)
|
||||
fc.AddField(FieldType::UserSystem, "OsPlatform", "Linux");
|
||||
#else
|
||||
fc.AddField(FieldType::UserSystem, "OsPlatform", "Unknown");
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Telemetry
|
||||
|
||||
@@ -180,4 +180,16 @@ struct NullVisitor : public VisitorInterface {
|
||||
void Complete() override {}
|
||||
};
|
||||
|
||||
/// Appends build-specific information to the given FieldCollection,
|
||||
/// such as branch name, revision hash, etc.
|
||||
void AppendBuildInfo(FieldCollection& fc);
|
||||
|
||||
/// Appends CPU-specific information to the given FieldCollection,
|
||||
/// such as instruction set extensions, etc.
|
||||
void AppendCPUInfo(FieldCollection& fc);
|
||||
|
||||
/// Appends OS-specific information to the given FieldCollection,
|
||||
/// such as platform name, etc.
|
||||
void AppendOSInfo(FieldCollection& fc);
|
||||
|
||||
} // namespace Telemetry
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/bit_set.h"
|
||||
|
||||
namespace Common {
|
||||
namespace X64 {
|
||||
namespace Common::X64 {
|
||||
|
||||
int RegToIndex(const Xbyak::Reg& reg) {
|
||||
inline int RegToIndex(const Xbyak::Reg& reg) {
|
||||
using Kind = Xbyak::Reg::Kind;
|
||||
ASSERT_MSG((reg.getKind() & (Kind::REG | Kind::XMM)) != 0,
|
||||
"RegSet only support GPRs and XMM registers.");
|
||||
@@ -152,8 +151,8 @@ constexpr size_t ABI_SHADOW_SPACE = 0;
|
||||
|
||||
#endif
|
||||
|
||||
void ABI_CalculateFrameSize(BitSet32 regs, size_t rsp_alignment, size_t needed_frame_size,
|
||||
s32* out_subtraction, s32* out_xmm_offset) {
|
||||
inline void ABI_CalculateFrameSize(BitSet32 regs, size_t rsp_alignment, size_t needed_frame_size,
|
||||
s32* out_subtraction, s32* out_xmm_offset) {
|
||||
int count = (regs & ABI_ALL_GPRS).Count();
|
||||
rsp_alignment -= count * 8;
|
||||
size_t subtraction = 0;
|
||||
@@ -174,8 +173,8 @@ void ABI_CalculateFrameSize(BitSet32 regs, size_t rsp_alignment, size_t needed_f
|
||||
*out_xmm_offset = (s32)(subtraction - xmm_base_subtraction);
|
||||
}
|
||||
|
||||
size_t ABI_PushRegistersAndAdjustStack(Xbyak::CodeGenerator& code, BitSet32 regs,
|
||||
size_t rsp_alignment, size_t needed_frame_size = 0) {
|
||||
inline size_t ABI_PushRegistersAndAdjustStack(Xbyak::CodeGenerator& code, BitSet32 regs,
|
||||
size_t rsp_alignment, size_t needed_frame_size = 0) {
|
||||
s32 subtraction, xmm_offset;
|
||||
ABI_CalculateFrameSize(regs, rsp_alignment, needed_frame_size, &subtraction, &xmm_offset);
|
||||
|
||||
@@ -195,8 +194,8 @@ size_t ABI_PushRegistersAndAdjustStack(Xbyak::CodeGenerator& code, BitSet32 regs
|
||||
return ABI_SHADOW_SPACE;
|
||||
}
|
||||
|
||||
void ABI_PopRegistersAndAdjustStack(Xbyak::CodeGenerator& code, BitSet32 regs, size_t rsp_alignment,
|
||||
size_t needed_frame_size = 0) {
|
||||
inline void ABI_PopRegistersAndAdjustStack(Xbyak::CodeGenerator& code, BitSet32 regs,
|
||||
size_t rsp_alignment, size_t needed_frame_size = 0) {
|
||||
s32 subtraction, xmm_offset;
|
||||
ABI_CalculateFrameSize(regs, rsp_alignment, needed_frame_size, &subtraction, &xmm_offset);
|
||||
|
||||
@@ -217,5 +216,4 @@ void ABI_PopRegistersAndAdjustStack(Xbyak::CodeGenerator& code, BitSet32 regs, s
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace X64
|
||||
} // namespace Common
|
||||
} // namespace Common::X64
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#include <xbyak.h>
|
||||
#include "common/x64/xbyak_abi.h"
|
||||
|
||||
namespace Common {
|
||||
namespace X64 {
|
||||
namespace Common::X64 {
|
||||
|
||||
// Constants for use with cmpps/cmpss
|
||||
enum {
|
||||
@@ -45,5 +44,4 @@ inline void CallFarFunction(Xbyak::CodeGenerator& code, const T f) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace X64
|
||||
} // namespace Common
|
||||
} // namespace Common::X64
|
||||
|
||||
@@ -20,6 +20,10 @@ add_library(core STATIC
|
||||
crypto/key_manager.h
|
||||
crypto/ctr_encryption_layer.cpp
|
||||
crypto/ctr_encryption_layer.h
|
||||
crypto/xts_encryption_layer.cpp
|
||||
crypto/xts_encryption_layer.h
|
||||
file_sys/bis_factory.cpp
|
||||
file_sys/bis_factory.h
|
||||
file_sys/card_image.cpp
|
||||
file_sys/card_image.h
|
||||
file_sys/content_archive.cpp
|
||||
@@ -29,10 +33,18 @@ add_library(core STATIC
|
||||
file_sys/directory.h
|
||||
file_sys/errors.h
|
||||
file_sys/mode.h
|
||||
file_sys/nca_metadata.cpp
|
||||
file_sys/nca_metadata.h
|
||||
file_sys/nca_patch.cpp
|
||||
file_sys/nca_patch.h
|
||||
file_sys/partition_filesystem.cpp
|
||||
file_sys/partition_filesystem.h
|
||||
file_sys/patch_manager.cpp
|
||||
file_sys/patch_manager.h
|
||||
file_sys/program_metadata.cpp
|
||||
file_sys/program_metadata.h
|
||||
file_sys/registered_cache.cpp
|
||||
file_sys/registered_cache.h
|
||||
file_sys/romfs.cpp
|
||||
file_sys/romfs.h
|
||||
file_sys/romfs_factory.cpp
|
||||
@@ -41,14 +53,20 @@ add_library(core STATIC
|
||||
file_sys/savedata_factory.h
|
||||
file_sys/sdmc_factory.cpp
|
||||
file_sys/sdmc_factory.h
|
||||
file_sys/submission_package.cpp
|
||||
file_sys/submission_package.h
|
||||
file_sys/vfs.cpp
|
||||
file_sys/vfs.h
|
||||
file_sys/vfs_concat.cpp
|
||||
file_sys/vfs_concat.h
|
||||
file_sys/vfs_offset.cpp
|
||||
file_sys/vfs_offset.h
|
||||
file_sys/vfs_real.cpp
|
||||
file_sys/vfs_real.h
|
||||
file_sys/vfs_vector.cpp
|
||||
file_sys/vfs_vector.h
|
||||
file_sys/xts_archive.cpp
|
||||
file_sys/xts_archive.h
|
||||
frontend/emu_window.cpp
|
||||
frontend/emu_window.h
|
||||
frontend/framebuffer_layout.cpp
|
||||
@@ -114,6 +132,8 @@ add_library(core STATIC
|
||||
hle/service/acc/acc_u0.h
|
||||
hle/service/acc/acc_u1.cpp
|
||||
hle/service/acc/acc_u1.h
|
||||
hle/service/acc/profile_manager.cpp
|
||||
hle/service/acc/profile_manager.h
|
||||
hle/service/am/am.cpp
|
||||
hle/service/am/am.h
|
||||
hle/service/am/applet_ae.cpp
|
||||
@@ -249,6 +269,10 @@ add_library(core STATIC
|
||||
hle/service/nvdrv/devices/nvhost_gpu.h
|
||||
hle/service/nvdrv/devices/nvhost_nvdec.cpp
|
||||
hle/service/nvdrv/devices/nvhost_nvdec.h
|
||||
hle/service/nvdrv/devices/nvhost_nvjpg.cpp
|
||||
hle/service/nvdrv/devices/nvhost_nvjpg.h
|
||||
hle/service/nvdrv/devices/nvhost_vic.cpp
|
||||
hle/service/nvdrv/devices/nvhost_vic.h
|
||||
hle/service/nvdrv/devices/nvmap.cpp
|
||||
hle/service/nvdrv/devices/nvmap.h
|
||||
hle/service/nvdrv/interface.cpp
|
||||
@@ -333,12 +357,16 @@ add_library(core STATIC
|
||||
loader/linker.h
|
||||
loader/loader.cpp
|
||||
loader/loader.h
|
||||
loader/nax.cpp
|
||||
loader/nax.h
|
||||
loader/nca.cpp
|
||||
loader/nca.h
|
||||
loader/nro.cpp
|
||||
loader/nro.h
|
||||
loader/nso.cpp
|
||||
loader/nso.h
|
||||
loader/nsp.cpp
|
||||
loader/nsp.h
|
||||
loader/xci.cpp
|
||||
loader/xci.h
|
||||
memory.cpp
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "common/common_types.h"
|
||||
#include "core/hle/kernel/vm_manager.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
/// Generic ARM11 CPU interface
|
||||
class ARM_Interface : NonCopyable {
|
||||
public:
|
||||
@@ -122,3 +124,5 @@ public:
|
||||
/// Prepare core for thread reschedule (if needed to correctly handle state)
|
||||
virtual void PrepareReschedule() = 0;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -7,13 +7,17 @@
|
||||
#include <dynarmic/A64/a64.h>
|
||||
#include <dynarmic/A64/config.h>
|
||||
#include "common/logging/log.h"
|
||||
#include "common/microprofile.h"
|
||||
#include "core/arm/dynarmic/arm_dynarmic.h"
|
||||
#include "core/core.h"
|
||||
#include "core/core_cpu.h"
|
||||
#include "core/core_timing.h"
|
||||
#include "core/hle/kernel/process.h"
|
||||
#include "core/hle/kernel/svc.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
using Vector = Dynarmic::A64::Vector;
|
||||
|
||||
class ARM_Dynarmic_Callbacks : public Dynarmic::A64::UserCallbacks {
|
||||
@@ -134,10 +138,16 @@ std::unique_ptr<Dynarmic::A64::Jit> ARM_Dynarmic::MakeJit() const {
|
||||
config.dczid_el0 = 4;
|
||||
config.ctr_el0 = 0x8444c004;
|
||||
|
||||
// Unpredictable instructions
|
||||
config.define_unpredictable_behaviour = true;
|
||||
|
||||
return std::make_unique<Dynarmic::A64::Jit>(config);
|
||||
}
|
||||
|
||||
MICROPROFILE_DEFINE(ARM_Jit_Dynarmic, "ARM JIT", "Dynarmic", MP_RGB(255, 64, 64));
|
||||
|
||||
void ARM_Dynarmic::Run() {
|
||||
MICROPROFILE_SCOPE(ARM_Jit_Dynarmic);
|
||||
ASSERT(Memory::GetCurrentPageTable() == current_page_table);
|
||||
|
||||
jit->Run();
|
||||
@@ -297,3 +307,5 @@ bool DynarmicExclusiveMonitor::ExclusiveWrite128(size_t core_index, VAddr vaddr,
|
||||
Memory::Write64(vaddr, value[1]);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "core/arm/exclusive_monitor.h"
|
||||
#include "core/arm/unicorn/arm_unicorn.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
class ARM_Dynarmic_Callbacks;
|
||||
class DynarmicExclusiveMonitor;
|
||||
|
||||
@@ -81,3 +83,5 @@ private:
|
||||
friend class ARM_Dynarmic;
|
||||
Dynarmic::A64::ExclusiveMonitor monitor;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -4,4 +4,8 @@
|
||||
|
||||
#include "core/arm/exclusive_monitor.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
ExclusiveMonitor::~ExclusiveMonitor() = default;
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
class ExclusiveMonitor {
|
||||
public:
|
||||
virtual ~ExclusiveMonitor();
|
||||
@@ -19,3 +21,5 @@ public:
|
||||
virtual bool ExclusiveWrite64(size_t core_index, VAddr vaddr, u64 value) = 0;
|
||||
virtual bool ExclusiveWrite128(size_t core_index, VAddr vaddr, u128 value) = 0;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "core/core_timing.h"
|
||||
#include "core/hle/kernel/svc.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
// Load Unicorn DLL once on Windows using RAII
|
||||
#ifdef _MSC_VER
|
||||
#include <unicorn_dynload.h>
|
||||
@@ -191,10 +193,10 @@ void ARM_Unicorn::Step() {
|
||||
ExecuteInstructions(1);
|
||||
}
|
||||
|
||||
MICROPROFILE_DEFINE(ARM_Jit, "ARM JIT", "ARM JIT", MP_RGB(255, 64, 64));
|
||||
MICROPROFILE_DEFINE(ARM_Jit_Unicorn, "ARM JIT", "Unicorn", MP_RGB(255, 64, 64));
|
||||
|
||||
void ARM_Unicorn::ExecuteInstructions(int num_instructions) {
|
||||
MICROPROFILE_SCOPE(ARM_Jit);
|
||||
MICROPROFILE_SCOPE(ARM_Jit_Unicorn);
|
||||
CHECKED(uc_emu_start(uc, GetPC(), 1ULL << 63, 0, num_instructions));
|
||||
CoreTiming::AddTicks(num_instructions);
|
||||
if (GDBStub::IsServerEnabled()) {
|
||||
@@ -211,7 +213,7 @@ void ARM_Unicorn::ExecuteInstructions(int num_instructions) {
|
||||
}
|
||||
}
|
||||
|
||||
void ARM_Unicorn::SaveContext(ARM_Interface::ThreadContext& ctx) {
|
||||
void ARM_Unicorn::SaveContext(ThreadContext& ctx) {
|
||||
int uregs[32];
|
||||
void* tregs[32];
|
||||
|
||||
@@ -238,7 +240,7 @@ void ARM_Unicorn::SaveContext(ARM_Interface::ThreadContext& ctx) {
|
||||
CHECKED(uc_reg_read_batch(uc, uregs, tregs, 32));
|
||||
}
|
||||
|
||||
void ARM_Unicorn::LoadContext(const ARM_Interface::ThreadContext& ctx) {
|
||||
void ARM_Unicorn::LoadContext(const ThreadContext& ctx) {
|
||||
int uregs[32];
|
||||
void* tregs[32];
|
||||
|
||||
@@ -277,3 +279,5 @@ void ARM_Unicorn::RecordBreak(GDBStub::BreakpointAddress bkpt) {
|
||||
last_bkpt = bkpt;
|
||||
last_bkpt_hit = true;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "core/arm/arm_interface.h"
|
||||
#include "core/gdbstub/gdbstub.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
class ARM_Unicorn final : public ARM_Interface {
|
||||
public:
|
||||
ARM_Unicorn();
|
||||
@@ -46,3 +48,5 @@ private:
|
||||
GDBStub::BreakpointAddress last_bkpt{};
|
||||
bool last_bkpt_hit;
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
|
||||
@@ -2,22 +2,35 @@
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/arm/exclusive_monitor.h"
|
||||
#include "core/core.h"
|
||||
#include "core/core_cpu.h"
|
||||
#include "core/core_timing.h"
|
||||
#include "core/file_sys/mode.h"
|
||||
#include "core/file_sys/vfs_concat.h"
|
||||
#include "core/file_sys/vfs_real.h"
|
||||
#include "core/gdbstub/gdbstub.h"
|
||||
#include "core/hle/kernel/client_port.h"
|
||||
#include "core/hle/kernel/kernel.h"
|
||||
#include "core/hle/kernel/process.h"
|
||||
#include "core/hle/kernel/scheduler.h"
|
||||
#include "core/hle/kernel/thread.h"
|
||||
#include "core/hle/service/service.h"
|
||||
#include "core/hle/service/sm/controller.h"
|
||||
#include "core/hle/service/sm/sm.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/perf_stats.h"
|
||||
#include "core/settings.h"
|
||||
#include "file_sys/vfs_real.h"
|
||||
#include "core/telemetry_session.h"
|
||||
#include "video_core/debug_utils/debug_utils.h"
|
||||
#include "video_core/gpu.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/video_core.h"
|
||||
|
||||
@@ -25,231 +38,437 @@ namespace Core {
|
||||
|
||||
/*static*/ System System::s_instance;
|
||||
|
||||
System::System() = default;
|
||||
namespace {
|
||||
FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
|
||||
const std::string& path) {
|
||||
// To account for split 00+01+etc files.
|
||||
std::string dir_name;
|
||||
std::string filename;
|
||||
Common::SplitPath(path, &dir_name, &filename, nullptr);
|
||||
if (filename == "00") {
|
||||
const auto dir = vfs->OpenDirectory(dir_name, FileSys::Mode::Read);
|
||||
std::vector<FileSys::VirtualFile> concat;
|
||||
for (u8 i = 0; i < 0x10; ++i) {
|
||||
auto next = dir->GetFile(fmt::format("{:02X}", i));
|
||||
if (next != nullptr)
|
||||
concat.push_back(std::move(next));
|
||||
else {
|
||||
next = dir->GetFile(fmt::format("{:02x}", i));
|
||||
if (next != nullptr)
|
||||
concat.push_back(std::move(next));
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
System::~System() = default;
|
||||
if (concat.empty())
|
||||
return nullptr;
|
||||
|
||||
return FileSys::ConcatenateFiles(concat, dir->GetName());
|
||||
}
|
||||
|
||||
return vfs->OpenFile(path, FileSys::Mode::Read);
|
||||
}
|
||||
|
||||
/// Runs a CPU core while the system is powered on
|
||||
static void RunCpuCore(std::shared_ptr<Cpu> cpu_state) {
|
||||
void RunCpuCore(std::shared_ptr<Cpu> cpu_state) {
|
||||
while (Core::System::GetInstance().IsPoweredOn()) {
|
||||
cpu_state->RunLoop(true);
|
||||
}
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
Cpu& System::CurrentCpuCore() {
|
||||
// If multicore is enabled, use host thread to figure out the current CPU core
|
||||
if (Settings::values.use_multi_core) {
|
||||
const auto& search = thread_to_cpu.find(std::this_thread::get_id());
|
||||
ASSERT(search != thread_to_cpu.end());
|
||||
ASSERT(search->second);
|
||||
return *search->second;
|
||||
struct System::Impl {
|
||||
Cpu& CurrentCpuCore() {
|
||||
if (Settings::values.use_multi_core) {
|
||||
const auto& search = thread_to_cpu.find(std::this_thread::get_id());
|
||||
ASSERT(search != thread_to_cpu.end());
|
||||
ASSERT(search->second);
|
||||
return *search->second;
|
||||
}
|
||||
|
||||
// Otherwise, use single-threaded mode active_core variable
|
||||
return *cpu_cores[active_core];
|
||||
}
|
||||
|
||||
// Otherwise, use single-threaded mode active_core variable
|
||||
return *cpu_cores[active_core];
|
||||
ResultStatus RunLoop(bool tight_loop) {
|
||||
status = ResultStatus::Success;
|
||||
|
||||
// Update thread_to_cpu in case Core 0 is run from a different host thread
|
||||
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
||||
|
||||
if (GDBStub::IsServerEnabled()) {
|
||||
GDBStub::HandlePacket();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (active_core = 0; active_core < NUM_CPU_CORES; ++active_core) {
|
||||
cpu_cores[active_core]->RunLoop(tight_loop);
|
||||
if (Settings::values.use_multi_core) {
|
||||
// Cores 1-3 are run on other threads in this mode
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (GDBStub::IsServerEnabled()) {
|
||||
GDBStub::SetCpuStepFlag(false);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
ResultStatus Init(Frontend::EmuWindow& emu_window) {
|
||||
LOG_DEBUG(HW_Memory, "initialized OK");
|
||||
|
||||
CoreTiming::Init();
|
||||
kernel.Initialize();
|
||||
|
||||
// Create a default fs if one doesn't already exist.
|
||||
if (virtual_filesystem == nullptr)
|
||||
virtual_filesystem = std::make_shared<FileSys::RealVfsFilesystem>();
|
||||
|
||||
kernel.MakeCurrentProcess(Kernel::Process::Create(kernel, "main"));
|
||||
|
||||
cpu_barrier = std::make_shared<CpuBarrier>();
|
||||
cpu_exclusive_monitor = Cpu::MakeExclusiveMonitor(cpu_cores.size());
|
||||
for (size_t index = 0; index < cpu_cores.size(); ++index) {
|
||||
cpu_cores[index] = std::make_shared<Cpu>(cpu_exclusive_monitor, cpu_barrier, index);
|
||||
}
|
||||
|
||||
telemetry_session = std::make_unique<Core::TelemetrySession>();
|
||||
service_manager = std::make_shared<Service::SM::ServiceManager>();
|
||||
|
||||
Service::Init(service_manager, virtual_filesystem);
|
||||
GDBStub::Init();
|
||||
|
||||
renderer = VideoCore::CreateRenderer(emu_window);
|
||||
if (!renderer->Init()) {
|
||||
return ResultStatus::ErrorVideoCore;
|
||||
}
|
||||
|
||||
gpu_core = std::make_unique<Tegra::GPU>(renderer->Rasterizer());
|
||||
|
||||
// Create threads for CPU cores 1-3, and build thread_to_cpu map
|
||||
// CPU core 0 is run on the main thread
|
||||
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
||||
if (Settings::values.use_multi_core) {
|
||||
for (size_t index = 0; index < cpu_core_threads.size(); ++index) {
|
||||
cpu_core_threads[index] =
|
||||
std::make_unique<std::thread>(RunCpuCore, cpu_cores[index + 1]);
|
||||
thread_to_cpu[cpu_core_threads[index]->get_id()] = cpu_cores[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(Core, "Initialized OK");
|
||||
|
||||
// Reset counters and set time origin to current frame
|
||||
GetAndResetPerfStats();
|
||||
perf_stats.BeginSystemFrame();
|
||||
|
||||
return ResultStatus::Success;
|
||||
}
|
||||
|
||||
ResultStatus Load(Frontend::EmuWindow& emu_window, const std::string& filepath) {
|
||||
app_loader = Loader::GetLoader(GetGameFileFromPath(virtual_filesystem, filepath));
|
||||
|
||||
if (!app_loader) {
|
||||
LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);
|
||||
return ResultStatus::ErrorGetLoader;
|
||||
}
|
||||
std::pair<boost::optional<u32>, Loader::ResultStatus> system_mode =
|
||||
app_loader->LoadKernelSystemMode();
|
||||
|
||||
if (system_mode.second != Loader::ResultStatus::Success) {
|
||||
LOG_CRITICAL(Core, "Failed to determine system mode (Error {})!",
|
||||
static_cast<int>(system_mode.second));
|
||||
|
||||
return ResultStatus::ErrorSystemMode;
|
||||
}
|
||||
|
||||
ResultStatus init_result{Init(emu_window)};
|
||||
if (init_result != ResultStatus::Success) {
|
||||
LOG_CRITICAL(Core, "Failed to initialize system (Error {})!",
|
||||
static_cast<int>(init_result));
|
||||
Shutdown();
|
||||
return init_result;
|
||||
}
|
||||
|
||||
const Loader::ResultStatus load_result{app_loader->Load(kernel.CurrentProcess())};
|
||||
if (load_result != Loader::ResultStatus::Success) {
|
||||
LOG_CRITICAL(Core, "Failed to load ROM (Error {})!", static_cast<int>(load_result));
|
||||
Shutdown();
|
||||
|
||||
return static_cast<ResultStatus>(static_cast<u32>(ResultStatus::ErrorLoader) +
|
||||
static_cast<u32>(load_result));
|
||||
}
|
||||
status = ResultStatus::Success;
|
||||
return status;
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
// Log last frame performance stats
|
||||
auto perf_results = GetAndResetPerfStats();
|
||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_EmulationSpeed",
|
||||
perf_results.emulation_speed * 100.0);
|
||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Framerate",
|
||||
perf_results.game_fps);
|
||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Frametime",
|
||||
perf_results.frametime * 1000.0);
|
||||
|
||||
// Shutdown emulation session
|
||||
renderer.reset();
|
||||
GDBStub::Shutdown();
|
||||
Service::Shutdown();
|
||||
service_manager.reset();
|
||||
telemetry_session.reset();
|
||||
gpu_core.reset();
|
||||
|
||||
// Close all CPU/threading state
|
||||
cpu_barrier->NotifyEnd();
|
||||
if (Settings::values.use_multi_core) {
|
||||
for (auto& thread : cpu_core_threads) {
|
||||
thread->join();
|
||||
thread.reset();
|
||||
}
|
||||
}
|
||||
thread_to_cpu.clear();
|
||||
for (auto& cpu_core : cpu_cores) {
|
||||
cpu_core.reset();
|
||||
}
|
||||
cpu_barrier.reset();
|
||||
|
||||
// Shutdown kernel and core timing
|
||||
kernel.Shutdown();
|
||||
CoreTiming::Shutdown();
|
||||
|
||||
// Close app loader
|
||||
app_loader.reset();
|
||||
|
||||
LOG_DEBUG(Core, "Shutdown OK");
|
||||
}
|
||||
|
||||
Loader::ResultStatus GetGameName(std::string& out) const {
|
||||
if (app_loader == nullptr)
|
||||
return Loader::ResultStatus::ErrorNotInitialized;
|
||||
return app_loader->ReadTitle(out);
|
||||
}
|
||||
|
||||
void SetStatus(ResultStatus new_status, const char* details = nullptr) {
|
||||
status = new_status;
|
||||
if (details) {
|
||||
status_details = details;
|
||||
}
|
||||
}
|
||||
|
||||
PerfStatsResults GetAndResetPerfStats() {
|
||||
return perf_stats.GetAndResetStats(CoreTiming::GetGlobalTimeUs());
|
||||
}
|
||||
|
||||
Kernel::KernelCore kernel;
|
||||
/// RealVfsFilesystem instance
|
||||
FileSys::VirtualFilesystem virtual_filesystem;
|
||||
/// AppLoader used to load the current executing application
|
||||
std::unique_ptr<Loader::AppLoader> app_loader;
|
||||
std::unique_ptr<VideoCore::RendererBase> renderer;
|
||||
std::unique_ptr<Tegra::GPU> gpu_core;
|
||||
std::shared_ptr<Tegra::DebugContext> debug_context;
|
||||
std::shared_ptr<ExclusiveMonitor> cpu_exclusive_monitor;
|
||||
std::shared_ptr<CpuBarrier> cpu_barrier;
|
||||
std::array<std::shared_ptr<Cpu>, NUM_CPU_CORES> cpu_cores;
|
||||
std::array<std::unique_ptr<std::thread>, NUM_CPU_CORES - 1> cpu_core_threads;
|
||||
size_t active_core{}; ///< Active core, only used in single thread mode
|
||||
|
||||
/// Service manager
|
||||
std::shared_ptr<Service::SM::ServiceManager> service_manager;
|
||||
|
||||
/// Telemetry session for this emulation session
|
||||
std::unique_ptr<Core::TelemetrySession> telemetry_session;
|
||||
|
||||
ResultStatus status = ResultStatus::Success;
|
||||
std::string status_details = "";
|
||||
|
||||
/// Map of guest threads to CPU cores
|
||||
std::map<std::thread::id, std::shared_ptr<Cpu>> thread_to_cpu;
|
||||
|
||||
Core::PerfStats perf_stats;
|
||||
Core::FrameLimiter frame_limiter;
|
||||
};
|
||||
|
||||
System::System() : impl{std::make_unique<Impl>()} {}
|
||||
System::~System() = default;
|
||||
|
||||
Cpu& System::CurrentCpuCore() {
|
||||
return impl->CurrentCpuCore();
|
||||
}
|
||||
|
||||
System::ResultStatus System::RunLoop(bool tight_loop) {
|
||||
status = ResultStatus::Success;
|
||||
|
||||
// Update thread_to_cpu in case Core 0 is run from a different host thread
|
||||
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
||||
|
||||
if (GDBStub::IsServerEnabled()) {
|
||||
GDBStub::HandlePacket();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (active_core = 0; active_core < NUM_CPU_CORES; ++active_core) {
|
||||
cpu_cores[active_core]->RunLoop(tight_loop);
|
||||
if (Settings::values.use_multi_core) {
|
||||
// Cores 1-3 are run on other threads in this mode
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (GDBStub::IsServerEnabled()) {
|
||||
GDBStub::SetCpuStepFlag(false);
|
||||
}
|
||||
|
||||
return status;
|
||||
return impl->RunLoop(tight_loop);
|
||||
}
|
||||
|
||||
System::ResultStatus System::SingleStep() {
|
||||
return RunLoop(false);
|
||||
}
|
||||
|
||||
void System::InvalidateCpuInstructionCaches() {
|
||||
for (auto& cpu : impl->cpu_cores) {
|
||||
cpu->ArmInterface().ClearInstructionCache();
|
||||
}
|
||||
}
|
||||
|
||||
System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::string& filepath) {
|
||||
app_loader = Loader::GetLoader(virtual_filesystem->OpenFile(filepath, FileSys::Mode::Read));
|
||||
return impl->Load(emu_window, filepath);
|
||||
}
|
||||
|
||||
if (!app_loader) {
|
||||
LOG_CRITICAL(Core, "Failed to obtain loader for {}!", filepath);
|
||||
return ResultStatus::ErrorGetLoader;
|
||||
}
|
||||
std::pair<boost::optional<u32>, Loader::ResultStatus> system_mode =
|
||||
app_loader->LoadKernelSystemMode();
|
||||
|
||||
if (system_mode.second != Loader::ResultStatus::Success) {
|
||||
LOG_CRITICAL(Core, "Failed to determine system mode (Error {})!",
|
||||
static_cast<int>(system_mode.second));
|
||||
|
||||
if (system_mode.second != Loader::ResultStatus::Success)
|
||||
return ResultStatus::ErrorSystemMode;
|
||||
}
|
||||
|
||||
ResultStatus init_result{Init(emu_window)};
|
||||
if (init_result != ResultStatus::Success) {
|
||||
LOG_CRITICAL(Core, "Failed to initialize system (Error {})!",
|
||||
static_cast<int>(init_result));
|
||||
System::Shutdown();
|
||||
return init_result;
|
||||
}
|
||||
|
||||
const Loader::ResultStatus load_result{app_loader->Load(current_process)};
|
||||
if (Loader::ResultStatus::Success != load_result) {
|
||||
LOG_CRITICAL(Core, "Failed to load ROM (Error {})!", static_cast<int>(load_result));
|
||||
System::Shutdown();
|
||||
|
||||
if (load_result != Loader::ResultStatus::Success) {
|
||||
return static_cast<ResultStatus>(static_cast<u32>(ResultStatus::ErrorLoader) +
|
||||
static_cast<u32>(load_result));
|
||||
}
|
||||
}
|
||||
status = ResultStatus::Success;
|
||||
return status;
|
||||
bool System::IsPoweredOn() const {
|
||||
return impl->cpu_barrier && impl->cpu_barrier->IsAlive();
|
||||
}
|
||||
|
||||
void System::PrepareReschedule() {
|
||||
CurrentCpuCore().PrepareReschedule();
|
||||
}
|
||||
|
||||
PerfStats::Results System::GetAndResetPerfStats() {
|
||||
return perf_stats.GetAndResetStats(CoreTiming::GetGlobalTimeUs());
|
||||
PerfStatsResults System::GetAndResetPerfStats() {
|
||||
return impl->GetAndResetPerfStats();
|
||||
}
|
||||
|
||||
Core::TelemetrySession& System::TelemetrySession() const {
|
||||
return *impl->telemetry_session;
|
||||
}
|
||||
|
||||
ARM_Interface& System::CurrentArmInterface() {
|
||||
return CurrentCpuCore().ArmInterface();
|
||||
}
|
||||
|
||||
size_t System::CurrentCoreIndex() {
|
||||
return CurrentCpuCore().CoreIndex();
|
||||
}
|
||||
|
||||
Kernel::Scheduler& System::CurrentScheduler() {
|
||||
return *CurrentCpuCore().Scheduler();
|
||||
}
|
||||
|
||||
const std::shared_ptr<Kernel::Scheduler>& System::Scheduler(size_t core_index) {
|
||||
ASSERT(core_index < NUM_CPU_CORES);
|
||||
return cpu_cores[core_index]->Scheduler();
|
||||
return impl->cpu_cores[core_index]->Scheduler();
|
||||
}
|
||||
|
||||
Kernel::SharedPtr<Kernel::Process>& System::CurrentProcess() {
|
||||
return impl->kernel.CurrentProcess();
|
||||
}
|
||||
|
||||
const Kernel::SharedPtr<Kernel::Process>& System::CurrentProcess() const {
|
||||
return impl->kernel.CurrentProcess();
|
||||
}
|
||||
|
||||
ARM_Interface& System::ArmInterface(size_t core_index) {
|
||||
ASSERT(core_index < NUM_CPU_CORES);
|
||||
return cpu_cores[core_index]->ArmInterface();
|
||||
return impl->cpu_cores[core_index]->ArmInterface();
|
||||
}
|
||||
|
||||
Cpu& System::CpuCore(size_t core_index) {
|
||||
ASSERT(core_index < NUM_CPU_CORES);
|
||||
return *cpu_cores[core_index];
|
||||
return *impl->cpu_cores[core_index];
|
||||
}
|
||||
|
||||
ExclusiveMonitor& System::Monitor() {
|
||||
return *impl->cpu_exclusive_monitor;
|
||||
}
|
||||
|
||||
Tegra::GPU& System::GPU() {
|
||||
return *impl->gpu_core;
|
||||
}
|
||||
|
||||
const Tegra::GPU& System::GPU() const {
|
||||
return *impl->gpu_core;
|
||||
}
|
||||
|
||||
VideoCore::RendererBase& System::Renderer() {
|
||||
return *impl->renderer;
|
||||
}
|
||||
|
||||
const VideoCore::RendererBase& System::Renderer() const {
|
||||
return *impl->renderer;
|
||||
}
|
||||
|
||||
Kernel::KernelCore& System::Kernel() {
|
||||
return impl->kernel;
|
||||
}
|
||||
|
||||
const Kernel::KernelCore& System::Kernel() const {
|
||||
return impl->kernel;
|
||||
}
|
||||
|
||||
Core::PerfStats& System::GetPerfStats() {
|
||||
return impl->perf_stats;
|
||||
}
|
||||
|
||||
const Core::PerfStats& System::GetPerfStats() const {
|
||||
return impl->perf_stats;
|
||||
}
|
||||
|
||||
Core::FrameLimiter& System::FrameLimiter() {
|
||||
return impl->frame_limiter;
|
||||
}
|
||||
|
||||
const Core::FrameLimiter& System::FrameLimiter() const {
|
||||
return impl->frame_limiter;
|
||||
}
|
||||
|
||||
Loader::ResultStatus System::GetGameName(std::string& out) const {
|
||||
return impl->GetGameName(out);
|
||||
}
|
||||
|
||||
void System::SetStatus(ResultStatus new_status, const char* details) {
|
||||
impl->SetStatus(new_status, details);
|
||||
}
|
||||
|
||||
const std::string& System::GetStatusDetails() const {
|
||||
return impl->status_details;
|
||||
}
|
||||
|
||||
Loader::AppLoader& System::GetAppLoader() const {
|
||||
return *impl->app_loader;
|
||||
}
|
||||
|
||||
void System::SetGPUDebugContext(std::shared_ptr<Tegra::DebugContext> context) {
|
||||
impl->debug_context = std::move(context);
|
||||
}
|
||||
|
||||
Tegra::DebugContext* System::GetGPUDebugContext() const {
|
||||
return impl->debug_context.get();
|
||||
}
|
||||
|
||||
void System::SetFilesystem(std::shared_ptr<FileSys::VfsFilesystem> vfs) {
|
||||
impl->virtual_filesystem = std::move(vfs);
|
||||
}
|
||||
|
||||
std::shared_ptr<FileSys::VfsFilesystem> System::GetFilesystem() const {
|
||||
return impl->virtual_filesystem;
|
||||
}
|
||||
|
||||
System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) {
|
||||
LOG_DEBUG(HW_Memory, "initialized OK");
|
||||
|
||||
CoreTiming::Init();
|
||||
|
||||
// Create a default fs if one doesn't already exist.
|
||||
if (virtual_filesystem == nullptr)
|
||||
virtual_filesystem = std::make_shared<FileSys::RealVfsFilesystem>();
|
||||
|
||||
current_process = Kernel::Process::Create("main");
|
||||
|
||||
cpu_barrier = std::make_shared<CpuBarrier>();
|
||||
cpu_exclusive_monitor = Cpu::MakeExclusiveMonitor(cpu_cores.size());
|
||||
for (size_t index = 0; index < cpu_cores.size(); ++index) {
|
||||
cpu_cores[index] = std::make_shared<Cpu>(cpu_exclusive_monitor, cpu_barrier, index);
|
||||
}
|
||||
|
||||
telemetry_session = std::make_unique<Core::TelemetrySession>();
|
||||
service_manager = std::make_shared<Service::SM::ServiceManager>();
|
||||
|
||||
Kernel::Init();
|
||||
Service::Init(service_manager, virtual_filesystem);
|
||||
GDBStub::Init();
|
||||
|
||||
renderer = VideoCore::CreateRenderer(emu_window);
|
||||
if (!renderer->Init()) {
|
||||
return ResultStatus::ErrorVideoCore;
|
||||
}
|
||||
|
||||
gpu_core = std::make_unique<Tegra::GPU>(renderer->Rasterizer());
|
||||
|
||||
// Create threads for CPU cores 1-3, and build thread_to_cpu map
|
||||
// CPU core 0 is run on the main thread
|
||||
thread_to_cpu[std::this_thread::get_id()] = cpu_cores[0];
|
||||
if (Settings::values.use_multi_core) {
|
||||
for (size_t index = 0; index < cpu_core_threads.size(); ++index) {
|
||||
cpu_core_threads[index] =
|
||||
std::make_unique<std::thread>(RunCpuCore, cpu_cores[index + 1]);
|
||||
thread_to_cpu[cpu_core_threads[index]->get_id()] = cpu_cores[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(Core, "Initialized OK");
|
||||
|
||||
// Reset counters and set time origin to current frame
|
||||
GetAndResetPerfStats();
|
||||
perf_stats.BeginSystemFrame();
|
||||
|
||||
return ResultStatus::Success;
|
||||
return impl->Init(emu_window);
|
||||
}
|
||||
|
||||
void System::Shutdown() {
|
||||
// Log last frame performance stats
|
||||
auto perf_results = GetAndResetPerfStats();
|
||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_EmulationSpeed",
|
||||
perf_results.emulation_speed * 100.0);
|
||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Framerate",
|
||||
perf_results.game_fps);
|
||||
Telemetry().AddField(Telemetry::FieldType::Performance, "Shutdown_Frametime",
|
||||
perf_results.frametime * 1000.0);
|
||||
|
||||
// Shutdown emulation session
|
||||
renderer.reset();
|
||||
GDBStub::Shutdown();
|
||||
Service::Shutdown();
|
||||
Kernel::Shutdown();
|
||||
service_manager.reset();
|
||||
telemetry_session.reset();
|
||||
gpu_core.reset();
|
||||
|
||||
// Close all CPU/threading state
|
||||
cpu_barrier->NotifyEnd();
|
||||
if (Settings::values.use_multi_core) {
|
||||
for (auto& thread : cpu_core_threads) {
|
||||
thread->join();
|
||||
thread.reset();
|
||||
}
|
||||
}
|
||||
thread_to_cpu.clear();
|
||||
for (auto& cpu_core : cpu_cores) {
|
||||
cpu_core.reset();
|
||||
}
|
||||
cpu_barrier.reset();
|
||||
|
||||
// Close core timing
|
||||
CoreTiming::Shutdown();
|
||||
|
||||
// Close app loader
|
||||
app_loader.reset();
|
||||
|
||||
LOG_DEBUG(Core, "Shutdown OK");
|
||||
impl->Shutdown();
|
||||
}
|
||||
|
||||
Service::SM::ServiceManager& System::ServiceManager() {
|
||||
return *service_manager;
|
||||
return *impl->service_manager;
|
||||
}
|
||||
|
||||
const Service::SM::ServiceManager& System::ServiceManager() const {
|
||||
return *service_manager;
|
||||
return *impl->service_manager;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
||||
205
src/core/core.h
205
src/core/core.h
@@ -4,42 +4,64 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include "common/common_types.h"
|
||||
#include "core/arm/exclusive_monitor.h"
|
||||
#include "core/core_cpu.h"
|
||||
#include "core/hle/kernel/object.h"
|
||||
#include "core/hle/kernel/scheduler.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/memory.h"
|
||||
#include "core/perf_stats.h"
|
||||
#include "core/telemetry_session.h"
|
||||
#include "file_sys/vfs_real.h"
|
||||
#include "hle/service/filesystem/filesystem.h"
|
||||
#include "video_core/debug_utils/debug_utils.h"
|
||||
#include "video_core/gpu.h"
|
||||
|
||||
class ARM_Interface;
|
||||
#include "common/common_types.h"
|
||||
#include "core/hle/kernel/object.h"
|
||||
|
||||
namespace Core::Frontend {
|
||||
class EmuWindow;
|
||||
}
|
||||
} // namespace Core::Frontend
|
||||
|
||||
namespace FileSys {
|
||||
class VfsFilesystem;
|
||||
} // namespace FileSys
|
||||
|
||||
namespace Kernel {
|
||||
class KernelCore;
|
||||
class Process;
|
||||
class Scheduler;
|
||||
} // namespace Kernel
|
||||
|
||||
namespace Loader {
|
||||
class AppLoader;
|
||||
enum class ResultStatus : u16;
|
||||
} // namespace Loader
|
||||
|
||||
namespace Service::SM {
|
||||
class ServiceManager;
|
||||
}
|
||||
} // namespace Service::SM
|
||||
|
||||
namespace Tegra {
|
||||
class DebugContext;
|
||||
class GPU;
|
||||
} // namespace Tegra
|
||||
|
||||
namespace VideoCore {
|
||||
class RendererBase;
|
||||
}
|
||||
} // namespace VideoCore
|
||||
|
||||
namespace Core {
|
||||
|
||||
class ARM_Interface;
|
||||
class Cpu;
|
||||
class ExclusiveMonitor;
|
||||
class FrameLimiter;
|
||||
class PerfStats;
|
||||
class TelemetrySession;
|
||||
|
||||
struct PerfStatsResults;
|
||||
|
||||
class System {
|
||||
public:
|
||||
System(const System&) = delete;
|
||||
System& operator=(const System&) = delete;
|
||||
|
||||
System(System&&) = delete;
|
||||
System& operator=(System&&) = delete;
|
||||
|
||||
~System();
|
||||
|
||||
/**
|
||||
@@ -86,11 +108,7 @@ public:
|
||||
* This function should only be used by GDB Stub to support breakpoints, memory updates and
|
||||
* step/continue commands.
|
||||
*/
|
||||
void InvalidateCpuInstructionCaches() {
|
||||
for (auto& cpu : cpu_cores) {
|
||||
cpu->ArmInterface().ClearInstructionCache();
|
||||
}
|
||||
}
|
||||
void InvalidateCpuInstructionCaches();
|
||||
|
||||
/// Shutdown the emulated system.
|
||||
void Shutdown();
|
||||
@@ -109,33 +127,28 @@ public:
|
||||
* application).
|
||||
* @returns True if the emulated system is powered on, otherwise false.
|
||||
*/
|
||||
bool IsPoweredOn() const {
|
||||
return cpu_barrier && cpu_barrier->IsAlive();
|
||||
}
|
||||
bool IsPoweredOn() const;
|
||||
|
||||
/**
|
||||
* Returns a reference to the telemetry session for this emulation session.
|
||||
* @returns Reference to the telemetry session.
|
||||
*/
|
||||
Core::TelemetrySession& TelemetrySession() const {
|
||||
return *telemetry_session;
|
||||
}
|
||||
Core::TelemetrySession& TelemetrySession() const;
|
||||
|
||||
/// Prepare the core emulation for a reschedule
|
||||
void PrepareReschedule();
|
||||
|
||||
/// Gets and resets core performance statistics
|
||||
PerfStats::Results GetAndResetPerfStats();
|
||||
PerfStatsResults GetAndResetPerfStats();
|
||||
|
||||
/// Gets an ARM interface to the CPU core that is currently running
|
||||
ARM_Interface& CurrentArmInterface() {
|
||||
return CurrentCpuCore().ArmInterface();
|
||||
}
|
||||
ARM_Interface& CurrentArmInterface();
|
||||
|
||||
/// Gets the index of the currently running CPU core
|
||||
size_t CurrentCoreIndex() {
|
||||
return CurrentCpuCore().CoreIndex();
|
||||
}
|
||||
size_t CurrentCoreIndex();
|
||||
|
||||
/// Gets the scheduler for the CPU core that is currently running
|
||||
Kernel::Scheduler& CurrentScheduler();
|
||||
|
||||
/// Gets an ARM interface to the CPU core with the specified index
|
||||
ARM_Interface& ArmInterface(size_t core_index);
|
||||
@@ -143,80 +156,67 @@ public:
|
||||
/// Gets a CPU interface to the CPU core with the specified index
|
||||
Cpu& CpuCore(size_t core_index);
|
||||
|
||||
/// Gets the exclusive monitor
|
||||
ExclusiveMonitor& Monitor();
|
||||
|
||||
/// Gets a mutable reference to the GPU interface
|
||||
Tegra::GPU& GPU() {
|
||||
return *gpu_core;
|
||||
}
|
||||
Tegra::GPU& GPU();
|
||||
|
||||
/// Gets an immutable reference to the GPU interface.
|
||||
const Tegra::GPU& GPU() const {
|
||||
return *gpu_core;
|
||||
}
|
||||
const Tegra::GPU& GPU() const;
|
||||
|
||||
/// Gets a mutable reference to the renderer.
|
||||
VideoCore::RendererBase& Renderer() {
|
||||
return *renderer;
|
||||
}
|
||||
VideoCore::RendererBase& Renderer();
|
||||
|
||||
/// Gets an immutable reference to the renderer.
|
||||
const VideoCore::RendererBase& Renderer() const {
|
||||
return *renderer;
|
||||
}
|
||||
|
||||
/// Gets the scheduler for the CPU core that is currently running
|
||||
Kernel::Scheduler& CurrentScheduler() {
|
||||
return *CurrentCpuCore().Scheduler();
|
||||
}
|
||||
|
||||
/// Gets the exclusive monitor
|
||||
ExclusiveMonitor& Monitor() {
|
||||
return *cpu_exclusive_monitor;
|
||||
}
|
||||
const VideoCore::RendererBase& Renderer() const;
|
||||
|
||||
/// Gets the scheduler for the CPU core with the specified index
|
||||
const std::shared_ptr<Kernel::Scheduler>& Scheduler(size_t core_index);
|
||||
|
||||
/// Gets the current process
|
||||
Kernel::SharedPtr<Kernel::Process>& CurrentProcess() {
|
||||
return current_process;
|
||||
}
|
||||
/// Provides a reference to the current process
|
||||
Kernel::SharedPtr<Kernel::Process>& CurrentProcess();
|
||||
|
||||
PerfStats perf_stats;
|
||||
FrameLimiter frame_limiter;
|
||||
/// Provides a constant reference to the current process.
|
||||
const Kernel::SharedPtr<Kernel::Process>& CurrentProcess() const;
|
||||
|
||||
void SetStatus(ResultStatus new_status, const char* details = nullptr) {
|
||||
status = new_status;
|
||||
if (details) {
|
||||
status_details = details;
|
||||
}
|
||||
}
|
||||
/// Provides a reference to the kernel instance.
|
||||
Kernel::KernelCore& Kernel();
|
||||
|
||||
const std::string& GetStatusDetails() const {
|
||||
return status_details;
|
||||
}
|
||||
/// Provides a constant reference to the kernel instance.
|
||||
const Kernel::KernelCore& Kernel() const;
|
||||
|
||||
Loader::AppLoader& GetAppLoader() const {
|
||||
return *app_loader;
|
||||
}
|
||||
/// Provides a reference to the internal PerfStats instance.
|
||||
Core::PerfStats& GetPerfStats();
|
||||
|
||||
/// Provides a constant reference to the internal PerfStats instance.
|
||||
const Core::PerfStats& GetPerfStats() const;
|
||||
|
||||
/// Provides a reference to the frame limiter;
|
||||
Core::FrameLimiter& FrameLimiter();
|
||||
|
||||
/// Provides a constant referent to the frame limiter
|
||||
const Core::FrameLimiter& FrameLimiter() const;
|
||||
|
||||
/// Gets the name of the current game
|
||||
Loader::ResultStatus GetGameName(std::string& out) const;
|
||||
|
||||
void SetStatus(ResultStatus new_status, const char* details);
|
||||
|
||||
const std::string& GetStatusDetails() const;
|
||||
|
||||
Loader::AppLoader& GetAppLoader() const;
|
||||
|
||||
Service::SM::ServiceManager& ServiceManager();
|
||||
const Service::SM::ServiceManager& ServiceManager() const;
|
||||
|
||||
void SetGPUDebugContext(std::shared_ptr<Tegra::DebugContext> context) {
|
||||
debug_context = std::move(context);
|
||||
}
|
||||
void SetGPUDebugContext(std::shared_ptr<Tegra::DebugContext> context);
|
||||
|
||||
std::shared_ptr<Tegra::DebugContext> GetGPUDebugContext() const {
|
||||
return debug_context;
|
||||
}
|
||||
Tegra::DebugContext* GetGPUDebugContext() const;
|
||||
|
||||
void SetFilesystem(FileSys::VirtualFilesystem vfs) {
|
||||
virtual_filesystem = std::move(vfs);
|
||||
}
|
||||
void SetFilesystem(std::shared_ptr<FileSys::VfsFilesystem> vfs);
|
||||
|
||||
FileSys::VirtualFilesystem GetFilesystem() const {
|
||||
return virtual_filesystem;
|
||||
}
|
||||
std::shared_ptr<FileSys::VfsFilesystem> GetFilesystem() const;
|
||||
|
||||
private:
|
||||
System();
|
||||
@@ -232,33 +232,10 @@ private:
|
||||
*/
|
||||
ResultStatus Init(Frontend::EmuWindow& emu_window);
|
||||
|
||||
/// RealVfsFilesystem instance
|
||||
FileSys::VirtualFilesystem virtual_filesystem;
|
||||
/// AppLoader used to load the current executing application
|
||||
std::unique_ptr<Loader::AppLoader> app_loader;
|
||||
std::unique_ptr<VideoCore::RendererBase> renderer;
|
||||
std::unique_ptr<Tegra::GPU> gpu_core;
|
||||
std::shared_ptr<Tegra::DebugContext> debug_context;
|
||||
Kernel::SharedPtr<Kernel::Process> current_process;
|
||||
std::shared_ptr<ExclusiveMonitor> cpu_exclusive_monitor;
|
||||
std::shared_ptr<CpuBarrier> cpu_barrier;
|
||||
std::array<std::shared_ptr<Cpu>, NUM_CPU_CORES> cpu_cores;
|
||||
std::array<std::unique_ptr<std::thread>, NUM_CPU_CORES - 1> cpu_core_threads;
|
||||
size_t active_core{}; ///< Active core, only used in single thread mode
|
||||
|
||||
/// Service manager
|
||||
std::shared_ptr<Service::SM::ServiceManager> service_manager;
|
||||
|
||||
/// Telemetry session for this emulation session
|
||||
std::unique_ptr<Core::TelemetrySession> telemetry_session;
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
|
||||
static System s_instance;
|
||||
|
||||
ResultStatus status = ResultStatus::Success;
|
||||
std::string status_details = "";
|
||||
|
||||
/// Map of guest threads to CPU cores
|
||||
std::map<std::thread::id, std::shared_ptr<Cpu>> thread_to_cpu;
|
||||
};
|
||||
|
||||
inline ARM_Interface& CurrentArmInterface() {
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
#include "common/common_types.h"
|
||||
#include "core/arm/exclusive_monitor.h"
|
||||
|
||||
class ARM_Interface;
|
||||
|
||||
namespace Kernel {
|
||||
class Scheduler;
|
||||
}
|
||||
|
||||
namespace Core {
|
||||
|
||||
class ARM_Interface;
|
||||
|
||||
constexpr unsigned NUM_CPU_CORES{4};
|
||||
|
||||
class CpuBarrier {
|
||||
|
||||
@@ -56,6 +56,9 @@ static u64 event_fifo_id;
|
||||
// to the event_queue by the emu thread
|
||||
static Common::MPSCQueue<Event, false> ts_queue;
|
||||
|
||||
// the queue for unscheduling the events from other threads threadsafe
|
||||
static Common::MPSCQueue<std::pair<const EventType*, u64>, false> unschedule_queue;
|
||||
|
||||
constexpr int MAX_SLICE_LENGTH = 20000;
|
||||
|
||||
static s64 idled_cycles;
|
||||
@@ -158,6 +161,10 @@ void UnscheduleEvent(const EventType* event_type, u64 userdata) {
|
||||
}
|
||||
}
|
||||
|
||||
void UnscheduleEventThreadsafe(const EventType* event_type, u64 userdata) {
|
||||
unschedule_queue.Push(std::make_pair(event_type, userdata));
|
||||
}
|
||||
|
||||
void RemoveEvent(const EventType* event_type) {
|
||||
auto itr = std::remove_if(event_queue.begin(), event_queue.end(),
|
||||
[&](const Event& e) { return e.type == event_type; });
|
||||
@@ -194,6 +201,9 @@ void MoveEvents() {
|
||||
|
||||
void Advance() {
|
||||
MoveEvents();
|
||||
for (std::pair<const EventType*, u64> ev; unschedule_queue.Pop(ev);) {
|
||||
UnscheduleEvent(ev.first, ev.second);
|
||||
}
|
||||
|
||||
int cycles_executed = slice_length - downcount;
|
||||
global_timer += cycles_executed;
|
||||
|
||||
@@ -65,6 +65,7 @@ void ScheduleEvent(s64 cycles_into_future, const EventType* event_type, u64 user
|
||||
void ScheduleEventThreadsafe(s64 cycles_into_future, const EventType* event_type, u64 userdata);
|
||||
|
||||
void UnscheduleEvent(const EventType* event_type, u64 userdata);
|
||||
void UnscheduleEventThreadsafe(const EventType* event_type, u64 userdata);
|
||||
|
||||
/// We only permit one event of each type in the queue at a time.
|
||||
void RemoveEvent(const EventType* event_type);
|
||||
|
||||
@@ -82,11 +82,25 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, size_t size, u8* dest, Op
|
||||
}
|
||||
} else {
|
||||
const auto block_size = mbedtls_cipher_get_block_size(context);
|
||||
if (size < block_size) {
|
||||
std::vector<u8> block(block_size);
|
||||
std::memcpy(block.data(), src, size);
|
||||
Transcode(block.data(), block.size(), block.data(), op);
|
||||
std::memcpy(dest, block.data(), size);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t offset = 0; offset < size; offset += block_size) {
|
||||
auto length = std::min<size_t>(block_size, size - offset);
|
||||
mbedtls_cipher_update(context, src + offset, length, dest + offset, &written);
|
||||
if (written != length) {
|
||||
if (length < block_size) {
|
||||
std::vector<u8> block(block_size);
|
||||
std::memcpy(block.data(), src + offset, length);
|
||||
Transcode(block.data(), block.size(), block.data(), op);
|
||||
std::memcpy(dest + offset, block.data(), length);
|
||||
return;
|
||||
}
|
||||
LOG_WARNING(Crypto, "Not all data was decrypted requested={:016X}, actual={:016X}.",
|
||||
length, written);
|
||||
}
|
||||
@@ -99,10 +113,7 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, size_t size, u8* dest, Op
|
||||
template <typename Key, size_t KeySize>
|
||||
void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, size_t size, u8* dest, size_t sector_id,
|
||||
size_t sector_size, Op op) {
|
||||
if (size % sector_size > 0) {
|
||||
LOG_CRITICAL(Crypto, "Data size must be a multiple of sector size.");
|
||||
return;
|
||||
}
|
||||
ASSERT_MSG(size % sector_size == 0, "XTS decryption size must be a multiple of sector size.");
|
||||
|
||||
for (size_t i = 0; i < size; i += sector_size) {
|
||||
SetIV(CalculateNintendoTweak(sector_id++));
|
||||
@@ -112,4 +123,4 @@ void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, size_t size, u8* dest,
|
||||
|
||||
template class AESCipher<Key128>;
|
||||
template class AESCipher<Key256>;
|
||||
} // namespace Core::Crypto
|
||||
} // namespace Core::Crypto
|
||||
|
||||
@@ -20,9 +20,7 @@ size_t CTREncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
|
||||
if (sector_offset == 0) {
|
||||
UpdateIV(base_offset + offset);
|
||||
std::vector<u8> raw = base->ReadBytes(length, offset);
|
||||
if (raw.size() != length)
|
||||
return Read(data, raw.size(), offset);
|
||||
cipher.Transcode(raw.data(), length, data, Op::Decrypt);
|
||||
cipher.Transcode(raw.data(), raw.size(), data, Op::Decrypt);
|
||||
return length;
|
||||
}
|
||||
|
||||
@@ -34,7 +32,7 @@ size_t CTREncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
|
||||
|
||||
if (length + sector_offset < 0x10) {
|
||||
std::memcpy(data, block.data() + sector_offset, std::min<u64>(length, read));
|
||||
return read;
|
||||
return std::min<u64>(length, read);
|
||||
}
|
||||
std::memcpy(data, block.data() + sector_offset, read);
|
||||
return read + Read(data + read, length - read, offset + read);
|
||||
|
||||
@@ -8,44 +8,117 @@
|
||||
#include <locale>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
#include "common/common_paths.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/settings.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
static u8 ToHexNibble(char c1) {
|
||||
if (c1 >= 65 && c1 <= 70)
|
||||
return c1 - 55;
|
||||
if (c1 >= 97 && c1 <= 102)
|
||||
return c1 - 87;
|
||||
if (c1 >= 48 && c1 <= 57)
|
||||
return c1 - 48;
|
||||
throw std::logic_error("Invalid hex digit");
|
||||
}
|
||||
Key128 GenerateKeyEncryptionKey(Key128 source, Key128 master, Key128 kek_seed, Key128 key_seed) {
|
||||
Key128 out{};
|
||||
|
||||
template <size_t Size>
|
||||
static std::array<u8, Size> HexStringToArray(std::string_view str) {
|
||||
std::array<u8, Size> out{};
|
||||
for (size_t i = 0; i < 2 * Size; i += 2) {
|
||||
auto d1 = str[i];
|
||||
auto d2 = str[i + 1];
|
||||
out[i / 2] = (ToHexNibble(d1) << 4) | ToHexNibble(d2);
|
||||
AESCipher<Key128> cipher1(master, Mode::ECB);
|
||||
cipher1.Transcode(kek_seed.data(), kek_seed.size(), out.data(), Op::Decrypt);
|
||||
AESCipher<Key128> cipher2(out, Mode::ECB);
|
||||
cipher2.Transcode(source.data(), source.size(), out.data(), Op::Decrypt);
|
||||
|
||||
if (key_seed != Key128{}) {
|
||||
AESCipher<Key128> cipher3(out, Mode::ECB);
|
||||
cipher3.Transcode(key_seed.data(), key_seed.size(), out.data(), Op::Decrypt);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<u8, 16> operator""_array16(const char* str, size_t len) {
|
||||
if (len != 32)
|
||||
throw std::logic_error("Not of correct size.");
|
||||
return HexStringToArray<16>(str);
|
||||
boost::optional<Key128> DeriveSDSeed() {
|
||||
const FileUtil::IOFile save_43(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
|
||||
"/system/save/8000000000000043",
|
||||
"rb+");
|
||||
if (!save_43.IsOpen())
|
||||
return boost::none;
|
||||
const FileUtil::IOFile sd_private(
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "/Nintendo/Contents/private", "rb+");
|
||||
if (!sd_private.IsOpen())
|
||||
return boost::none;
|
||||
|
||||
sd_private.Seek(0, SEEK_SET);
|
||||
std::array<u8, 0x10> private_seed{};
|
||||
if (sd_private.ReadBytes(private_seed.data(), private_seed.size()) != 0x10)
|
||||
return boost::none;
|
||||
|
||||
std::array<u8, 0x10> buffer{};
|
||||
size_t offset = 0;
|
||||
for (; offset + 0x10 < save_43.GetSize(); ++offset) {
|
||||
save_43.Seek(offset, SEEK_SET);
|
||||
save_43.ReadBytes(buffer.data(), buffer.size());
|
||||
if (buffer == private_seed)
|
||||
break;
|
||||
}
|
||||
|
||||
if (offset + 0x10 >= save_43.GetSize())
|
||||
return boost::none;
|
||||
|
||||
Key128 seed{};
|
||||
save_43.Seek(offset + 0x10, SEEK_SET);
|
||||
save_43.ReadBytes(seed.data(), seed.size());
|
||||
return seed;
|
||||
}
|
||||
|
||||
std::array<u8, 32> operator""_array32(const char* str, size_t len) {
|
||||
if (len != 64)
|
||||
throw std::logic_error("Not of correct size.");
|
||||
return HexStringToArray<32>(str);
|
||||
Loader::ResultStatus DeriveSDKeys(std::array<Key256, 2>& sd_keys, const KeyManager& keys) {
|
||||
if (!keys.HasKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::SDKEK)))
|
||||
return Loader::ResultStatus::ErrorMissingSDKEKSource;
|
||||
if (!keys.HasKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKEKGeneration)))
|
||||
return Loader::ResultStatus::ErrorMissingAESKEKGenerationSource;
|
||||
if (!keys.HasKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKeyGeneration)))
|
||||
return Loader::ResultStatus::ErrorMissingAESKeyGenerationSource;
|
||||
|
||||
const auto sd_kek_source =
|
||||
keys.GetKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::SDKEK));
|
||||
const auto aes_kek_gen =
|
||||
keys.GetKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKEKGeneration));
|
||||
const auto aes_key_gen =
|
||||
keys.GetKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKeyGeneration));
|
||||
const auto master_00 = keys.GetKey(S128KeyType::Master);
|
||||
const auto sd_kek =
|
||||
GenerateKeyEncryptionKey(sd_kek_source, master_00, aes_kek_gen, aes_key_gen);
|
||||
|
||||
if (!keys.HasKey(S128KeyType::SDSeed))
|
||||
return Loader::ResultStatus::ErrorMissingSDSeed;
|
||||
const auto sd_seed = keys.GetKey(S128KeyType::SDSeed);
|
||||
|
||||
if (!keys.HasKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::Save)))
|
||||
return Loader::ResultStatus::ErrorMissingSDSaveKeySource;
|
||||
if (!keys.HasKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::NCA)))
|
||||
return Loader::ResultStatus::ErrorMissingSDNCAKeySource;
|
||||
|
||||
std::array<Key256, 2> sd_key_sources{
|
||||
keys.GetKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::Save)),
|
||||
keys.GetKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::NCA)),
|
||||
};
|
||||
|
||||
// Combine sources and seed
|
||||
for (auto& source : sd_key_sources) {
|
||||
for (size_t i = 0; i < source.size(); ++i)
|
||||
source[i] ^= sd_seed[i & 0xF];
|
||||
}
|
||||
|
||||
AESCipher<Key128> cipher(sd_kek, Mode::ECB);
|
||||
// The transform manipulates sd_keys as part of the Transcode, so the return/output is
|
||||
// unnecessary. This does not alter sd_keys_sources.
|
||||
std::transform(sd_key_sources.begin(), sd_key_sources.end(), sd_keys.begin(),
|
||||
sd_key_sources.begin(), [&cipher](const Key256& source, Key256& out) {
|
||||
cipher.Transcode(source.data(), source.size(), out.data(), Op::Decrypt);
|
||||
return source; ///< Return unaltered source to satisfy output requirement.
|
||||
});
|
||||
|
||||
return Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
KeyManager::KeyManager() {
|
||||
@@ -55,12 +128,15 @@ KeyManager::KeyManager() {
|
||||
if (Settings::values.use_dev_keys) {
|
||||
dev_mode = true;
|
||||
AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "dev.keys", false);
|
||||
AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "dev.keys_autogenerated", false);
|
||||
} else {
|
||||
dev_mode = false;
|
||||
AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "prod.keys", false);
|
||||
AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "prod.keys_autogenerated", false);
|
||||
}
|
||||
|
||||
AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "title.keys", true);
|
||||
AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "title.keys_autogenerated", true);
|
||||
}
|
||||
|
||||
void KeyManager::LoadFromFile(const std::string& filename, bool is_title_keys) {
|
||||
@@ -83,21 +159,21 @@ void KeyManager::LoadFromFile(const std::string& filename, bool is_title_keys) {
|
||||
out[1].erase(std::remove(out[1].begin(), out[1].end(), ' '), out[1].end());
|
||||
|
||||
if (is_title_keys) {
|
||||
auto rights_id_raw = HexStringToArray<16>(out[0]);
|
||||
auto rights_id_raw = Common::HexStringToArray<16>(out[0]);
|
||||
u128 rights_id{};
|
||||
std::memcpy(rights_id.data(), rights_id_raw.data(), rights_id_raw.size());
|
||||
Key128 key = HexStringToArray<16>(out[1]);
|
||||
SetKey(S128KeyType::Titlekey, key, rights_id[1], rights_id[0]);
|
||||
Key128 key = Common::HexStringToArray<16>(out[1]);
|
||||
s128_keys[{S128KeyType::Titlekey, rights_id[1], rights_id[0]}] = key;
|
||||
} else {
|
||||
std::transform(out[0].begin(), out[0].end(), out[0].begin(), ::tolower);
|
||||
if (s128_file_id.find(out[0]) != s128_file_id.end()) {
|
||||
const auto index = s128_file_id.at(out[0]);
|
||||
Key128 key = HexStringToArray<16>(out[1]);
|
||||
SetKey(index.type, key, index.field1, index.field2);
|
||||
Key128 key = Common::HexStringToArray<16>(out[1]);
|
||||
s128_keys[{index.type, index.field1, index.field2}] = key;
|
||||
} else if (s256_file_id.find(out[0]) != s256_file_id.end()) {
|
||||
const auto index = s256_file_id.at(out[0]);
|
||||
Key256 key = HexStringToArray<32>(out[1]);
|
||||
SetKey(index.type, key, index.field1, index.field2);
|
||||
Key256 key = Common::HexStringToArray<32>(out[1]);
|
||||
s256_keys[{index.type, index.field1, index.field2}] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,11 +207,60 @@ Key256 KeyManager::GetKey(S256KeyType id, u64 field1, u64 field2) const {
|
||||
return s256_keys.at({id, field1, field2});
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
void KeyManager::WriteKeyToFile(bool title_key, std::string_view keyname,
|
||||
const std::array<u8, Size>& key) {
|
||||
const std::string yuzu_keys_dir = FileUtil::GetUserPath(FileUtil::UserPath::KeysDir);
|
||||
std::string filename = "title.keys_autogenerated";
|
||||
if (!title_key)
|
||||
filename = dev_mode ? "dev.keys_autogenerated" : "prod.keys_autogenerated";
|
||||
const auto add_info_text = !FileUtil::Exists(yuzu_keys_dir + DIR_SEP + filename);
|
||||
FileUtil::CreateFullPath(yuzu_keys_dir + DIR_SEP + filename);
|
||||
std::ofstream file(yuzu_keys_dir + DIR_SEP + filename, std::ios::app);
|
||||
if (!file.is_open())
|
||||
return;
|
||||
if (add_info_text) {
|
||||
file
|
||||
<< "# This file is autogenerated by Yuzu\n"
|
||||
<< "# It serves to store keys that were automatically generated from the normal keys\n"
|
||||
<< "# If you are experiencing issues involving keys, it may help to delete this file\n";
|
||||
}
|
||||
|
||||
file << fmt::format("\n{} = {}", keyname, Common::HexArrayToString(key));
|
||||
AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, filename, title_key);
|
||||
}
|
||||
|
||||
void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
|
||||
if (s128_keys.find({id, field1, field2}) != s128_keys.end())
|
||||
return;
|
||||
if (id == S128KeyType::Titlekey) {
|
||||
Key128 rights_id;
|
||||
std::memcpy(rights_id.data(), &field2, sizeof(u64));
|
||||
std::memcpy(rights_id.data() + sizeof(u64), &field1, sizeof(u64));
|
||||
WriteKeyToFile(true, Common::HexArrayToString(rights_id), key);
|
||||
}
|
||||
const auto iter2 = std::find_if(
|
||||
s128_file_id.begin(), s128_file_id.end(),
|
||||
[&id, &field1, &field2](const std::pair<std::string, KeyIndex<S128KeyType>> elem) {
|
||||
return std::tie(elem.second.type, elem.second.field1, elem.second.field2) ==
|
||||
std::tie(id, field1, field2);
|
||||
});
|
||||
if (iter2 != s128_file_id.end())
|
||||
WriteKeyToFile(false, iter2->first, key);
|
||||
s128_keys[{id, field1, field2}] = key;
|
||||
}
|
||||
|
||||
void KeyManager::SetKey(S256KeyType id, Key256 key, u64 field1, u64 field2) {
|
||||
if (s256_keys.find({id, field1, field2}) != s256_keys.end())
|
||||
return;
|
||||
const auto iter = std::find_if(
|
||||
s256_file_id.begin(), s256_file_id.end(),
|
||||
[&id, &field1, &field2](const std::pair<std::string, KeyIndex<S256KeyType>> elem) {
|
||||
return std::tie(elem.second.type, elem.second.field1, elem.second.field2) ==
|
||||
std::tie(id, field1, field2);
|
||||
});
|
||||
if (iter != s256_file_id.end())
|
||||
WriteKeyToFile(false, iter->first, key);
|
||||
s256_keys[{id, field1, field2}] = key;
|
||||
}
|
||||
|
||||
@@ -156,7 +281,16 @@ bool KeyManager::KeyFileExists(bool title) {
|
||||
FileUtil::Exists(yuzu_keys_dir + DIR_SEP + "prod.keys");
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_file_id = {
|
||||
void KeyManager::DeriveSDSeedLazy() {
|
||||
if (HasKey(S128KeyType::SDSeed))
|
||||
return;
|
||||
|
||||
const auto res = DeriveSDSeed();
|
||||
if (res != boost::none)
|
||||
SetKey(S128KeyType::SDSeed, res.get());
|
||||
}
|
||||
|
||||
const boost::container::flat_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_file_id = {
|
||||
{"master_key_00", {S128KeyType::Master, 0, 0}},
|
||||
{"master_key_01", {S128KeyType::Master, 1, 0}},
|
||||
{"master_key_02", {S128KeyType::Master, 2, 0}},
|
||||
@@ -198,11 +332,17 @@ const std::unordered_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_fi
|
||||
{"key_area_key_system_02", {S128KeyType::KeyArea, 2, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
{"key_area_key_system_03", {S128KeyType::KeyArea, 3, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
{"key_area_key_system_04", {S128KeyType::KeyArea, 4, static_cast<u64>(KeyAreaKeyType::System)}},
|
||||
{"sd_card_kek_source", {S128KeyType::Source, static_cast<u64>(SourceKeyType::SDKEK), 0}},
|
||||
{"aes_kek_generation_source",
|
||||
{S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKEKGeneration), 0}},
|
||||
{"aes_key_generation_source",
|
||||
{S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKeyGeneration), 0}},
|
||||
{"sd_seed", {S128KeyType::SDSeed, 0, 0}},
|
||||
};
|
||||
|
||||
const std::unordered_map<std::string, KeyIndex<S256KeyType>> KeyManager::s256_file_id = {
|
||||
const boost::container::flat_map<std::string, KeyIndex<S256KeyType>> KeyManager::s256_file_id = {
|
||||
{"header_key", {S256KeyType::Header, 0, 0}},
|
||||
{"sd_card_save_key", {S256KeyType::SDSave, 0, 0}},
|
||||
{"sd_card_nca_key", {S256KeyType::SDNCA, 0, 0}},
|
||||
{"sd_card_save_key_source", {S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::Save), 0}},
|
||||
{"sd_card_nca_key_source", {S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::NCA), 0}},
|
||||
};
|
||||
} // namespace Core::Crypto
|
||||
|
||||
@@ -6,14 +6,19 @@
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Loader {
|
||||
enum class ResultStatus : u16;
|
||||
}
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
constexpr u64 TICKET_FILE_TITLEKEY_OFFSET = 0x180;
|
||||
|
||||
using Key128 = std::array<u8, 0x10>;
|
||||
using Key256 = std::array<u8, 0x20>;
|
||||
using SHA256Hash = std::array<u8, 0x20>;
|
||||
@@ -22,9 +27,8 @@ static_assert(sizeof(Key128) == 16, "Key128 must be 128 bytes big.");
|
||||
static_assert(sizeof(Key256) == 32, "Key128 must be 128 bytes big.");
|
||||
|
||||
enum class S256KeyType : u64 {
|
||||
Header, //
|
||||
SDSave, //
|
||||
SDNCA, //
|
||||
Header, //
|
||||
SDKeySource, // f1=SDKeyType
|
||||
};
|
||||
|
||||
enum class S128KeyType : u64 {
|
||||
@@ -36,6 +40,7 @@ enum class S128KeyType : u64 {
|
||||
KeyArea, // f1=crypto revision f2=type {app, ocean, system}
|
||||
SDSeed, //
|
||||
Titlekey, // f1=rights id LSB f2=rights id MSB
|
||||
Source, // f1=source type, f2= sub id
|
||||
};
|
||||
|
||||
enum class KeyAreaKeyType : u8 {
|
||||
@@ -44,6 +49,17 @@ enum class KeyAreaKeyType : u8 {
|
||||
System,
|
||||
};
|
||||
|
||||
enum class SourceKeyType : u8 {
|
||||
SDKEK,
|
||||
AESKEKGeneration,
|
||||
AESKeyGeneration,
|
||||
};
|
||||
|
||||
enum class SDKeyType : u8 {
|
||||
Save,
|
||||
NCA,
|
||||
};
|
||||
|
||||
template <typename KeyType>
|
||||
struct KeyIndex {
|
||||
KeyType type;
|
||||
@@ -59,37 +75,12 @@ struct KeyIndex {
|
||||
}
|
||||
};
|
||||
|
||||
// The following two (== and hash) are so KeyIndex can be a key in unordered_map
|
||||
|
||||
// boost flat_map requires operator< for O(log(n)) lookups.
|
||||
template <typename KeyType>
|
||||
bool operator==(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) {
|
||||
return std::tie(lhs.type, lhs.field1, lhs.field2) == std::tie(rhs.type, rhs.field1, rhs.field2);
|
||||
bool operator<(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) {
|
||||
return std::tie(lhs.type, lhs.field1, lhs.field2) < std::tie(rhs.type, rhs.field1, rhs.field2);
|
||||
}
|
||||
|
||||
template <typename KeyType>
|
||||
bool operator!=(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) {
|
||||
return !operator==(lhs, rhs);
|
||||
}
|
||||
|
||||
} // namespace Core::Crypto
|
||||
|
||||
namespace std {
|
||||
template <typename KeyType>
|
||||
struct hash<Core::Crypto::KeyIndex<KeyType>> {
|
||||
size_t operator()(const Core::Crypto::KeyIndex<KeyType>& k) const {
|
||||
using std::hash;
|
||||
|
||||
return ((hash<u64>()(static_cast<u64>(k.type)) ^ (hash<u64>()(k.field1) << 1)) >> 1) ^
|
||||
(hash<u64>()(k.field2) << 1);
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
std::array<u8, 0x10> operator"" _array16(const char* str, size_t len);
|
||||
std::array<u8, 0x20> operator"" _array32(const char* str, size_t len);
|
||||
|
||||
class KeyManager {
|
||||
public:
|
||||
KeyManager();
|
||||
@@ -105,16 +96,27 @@ public:
|
||||
|
||||
static bool KeyFileExists(bool title);
|
||||
|
||||
// Call before using the sd seed to attempt to derive it if it dosen't exist. Needs system save
|
||||
// 8*43 and the private file to exist.
|
||||
void DeriveSDSeedLazy();
|
||||
|
||||
private:
|
||||
std::unordered_map<KeyIndex<S128KeyType>, Key128> s128_keys;
|
||||
std::unordered_map<KeyIndex<S256KeyType>, Key256> s256_keys;
|
||||
boost::container::flat_map<KeyIndex<S128KeyType>, Key128> s128_keys;
|
||||
boost::container::flat_map<KeyIndex<S256KeyType>, Key256> s256_keys;
|
||||
|
||||
bool dev_mode;
|
||||
void LoadFromFile(const std::string& filename, bool is_title_keys);
|
||||
void AttemptLoadKeyFile(const std::string& dir1, const std::string& dir2,
|
||||
const std::string& filename, bool title);
|
||||
template <size_t Size>
|
||||
void WriteKeyToFile(bool title_key, std::string_view keyname, const std::array<u8, Size>& key);
|
||||
|
||||
static const std::unordered_map<std::string, KeyIndex<S128KeyType>> s128_file_id;
|
||||
static const std::unordered_map<std::string, KeyIndex<S256KeyType>> s256_file_id;
|
||||
static const boost::container::flat_map<std::string, KeyIndex<S128KeyType>> s128_file_id;
|
||||
static const boost::container::flat_map<std::string, KeyIndex<S256KeyType>> s256_file_id;
|
||||
};
|
||||
|
||||
Key128 GenerateKeyEncryptionKey(Key128 source, Key128 master, Key128 kek_seed, Key128 key_seed);
|
||||
boost::optional<Key128> DeriveSDSeed();
|
||||
Loader::ResultStatus DeriveSDKeys(std::array<Key256, 2>& sd_keys, const KeyManager& keys);
|
||||
|
||||
} // namespace Core::Crypto
|
||||
|
||||
58
src/core/crypto/xts_encryption_layer.cpp
Normal file
58
src/core/crypto/xts_encryption_layer.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "common/assert.h"
|
||||
#include "core/crypto/xts_encryption_layer.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
constexpr u64 XTS_SECTOR_SIZE = 0x4000;
|
||||
|
||||
XTSEncryptionLayer::XTSEncryptionLayer(FileSys::VirtualFile base_, Key256 key_)
|
||||
: EncryptionLayer(std::move(base_)), cipher(key_, Mode::XTS) {}
|
||||
|
||||
size_t XTSEncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
|
||||
if (length == 0)
|
||||
return 0;
|
||||
|
||||
const auto sector_offset = offset & 0x3FFF;
|
||||
if (sector_offset == 0) {
|
||||
if (length % XTS_SECTOR_SIZE == 0) {
|
||||
std::vector<u8> raw = base->ReadBytes(length, offset);
|
||||
cipher.XTSTranscode(raw.data(), raw.size(), data, offset / XTS_SECTOR_SIZE,
|
||||
XTS_SECTOR_SIZE, Op::Decrypt);
|
||||
return raw.size();
|
||||
}
|
||||
if (length > XTS_SECTOR_SIZE) {
|
||||
const auto rem = length % XTS_SECTOR_SIZE;
|
||||
const auto read = length - rem;
|
||||
return Read(data, read, offset) + Read(data + read, rem, offset + read);
|
||||
}
|
||||
std::vector<u8> buffer = base->ReadBytes(XTS_SECTOR_SIZE, offset);
|
||||
if (buffer.size() < XTS_SECTOR_SIZE)
|
||||
buffer.resize(XTS_SECTOR_SIZE);
|
||||
cipher.XTSTranscode(buffer.data(), buffer.size(), buffer.data(), offset / XTS_SECTOR_SIZE,
|
||||
XTS_SECTOR_SIZE, Op::Decrypt);
|
||||
std::memcpy(data, buffer.data(), std::min(buffer.size(), length));
|
||||
return std::min(buffer.size(), length);
|
||||
}
|
||||
|
||||
// offset does not fall on block boundary (0x4000)
|
||||
std::vector<u8> block = base->ReadBytes(0x4000, offset - sector_offset);
|
||||
if (block.size() < XTS_SECTOR_SIZE)
|
||||
block.resize(XTS_SECTOR_SIZE);
|
||||
cipher.XTSTranscode(block.data(), block.size(), block.data(),
|
||||
(offset - sector_offset) / XTS_SECTOR_SIZE, XTS_SECTOR_SIZE, Op::Decrypt);
|
||||
const size_t read = XTS_SECTOR_SIZE - sector_offset;
|
||||
|
||||
if (length + sector_offset < XTS_SECTOR_SIZE) {
|
||||
std::memcpy(data, block.data() + sector_offset, std::min<u64>(length, read));
|
||||
return std::min<u64>(length, read);
|
||||
}
|
||||
std::memcpy(data, block.data() + sector_offset, read);
|
||||
return read + Read(data + read, length - read, offset + read);
|
||||
}
|
||||
} // namespace Core::Crypto
|
||||
25
src/core/crypto/xts_encryption_layer.h
Normal file
25
src/core/crypto/xts_encryption_layer.h
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/encryption_layer.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
|
||||
namespace Core::Crypto {
|
||||
|
||||
// Sits on top of a VirtualFile and provides XTS-mode AES decription.
|
||||
class XTSEncryptionLayer : public EncryptionLayer {
|
||||
public:
|
||||
XTSEncryptionLayer(FileSys::VirtualFile base, Key256 key);
|
||||
|
||||
size_t Read(u8* data, size_t length, size_t offset) const override;
|
||||
|
||||
private:
|
||||
// Must be mutable as operations modify cipher contexts.
|
||||
mutable AESCipher<Key256> cipher;
|
||||
};
|
||||
|
||||
} // namespace Core::Crypto
|
||||
27
src/core/file_sys/bis_factory.cpp
Normal file
27
src/core/file_sys/bis_factory.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "core/file_sys/bis_factory.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
BISFactory::BISFactory(VirtualDir nand_root_)
|
||||
: nand_root(std::move(nand_root_)),
|
||||
sysnand_cache(std::make_shared<RegisteredCache>(
|
||||
GetOrCreateDirectoryRelative(nand_root, "/system/Contents/registered"))),
|
||||
usrnand_cache(std::make_shared<RegisteredCache>(
|
||||
GetOrCreateDirectoryRelative(nand_root, "/user/Contents/registered"))) {}
|
||||
|
||||
BISFactory::~BISFactory() = default;
|
||||
|
||||
std::shared_ptr<RegisteredCache> BISFactory::GetSystemNANDContents() const {
|
||||
return sysnand_cache;
|
||||
}
|
||||
|
||||
std::shared_ptr<RegisteredCache> BISFactory::GetUserNANDContents() const {
|
||||
return usrnand_cache;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
33
src/core/file_sys/bis_factory.h
Normal file
33
src/core/file_sys/bis_factory.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class RegisteredCache;
|
||||
|
||||
/// File system interface to the Built-In Storage
|
||||
/// This is currently missing accessors to BIS partitions, but seemed like a good place for the NAND
|
||||
/// registered caches.
|
||||
class BISFactory {
|
||||
public:
|
||||
explicit BISFactory(VirtualDir nand_root);
|
||||
~BISFactory();
|
||||
|
||||
std::shared_ptr<RegisteredCache> GetSystemNANDContents() const;
|
||||
std::shared_ptr<RegisteredCache> GetUserNANDContents() const;
|
||||
|
||||
private:
|
||||
VirtualDir nand_root;
|
||||
|
||||
std::shared_ptr<RegisteredCache> sysnand_cache;
|
||||
std::shared_ptr<RegisteredCache> usrnand_cache;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
@@ -4,11 +4,17 @@
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <core/loader/loader.h>
|
||||
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/partition_filesystem.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
@@ -40,13 +46,19 @@ XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
|
||||
partitions[static_cast<size_t>(partition)] = std::make_shared<PartitionFilesystem>(raw);
|
||||
}
|
||||
|
||||
auto result = AddNCAFromPartition(XCIPartition::Secure);
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
status = result;
|
||||
return;
|
||||
}
|
||||
secure_partition = std::make_shared<NSP>(
|
||||
main_hfs.GetFile(partition_names[static_cast<size_t>(XCIPartition::Secure)]));
|
||||
|
||||
result = AddNCAFromPartition(XCIPartition::Update);
|
||||
const auto secure_ncas = secure_partition->GetNCAsCollapsed();
|
||||
std::copy(secure_ncas.begin(), secure_ncas.end(), std::back_inserter(ncas));
|
||||
|
||||
program =
|
||||
secure_partition->GetNCA(secure_partition->GetProgramTitleID(), ContentRecordType::Program);
|
||||
program_nca_status = secure_partition->GetProgramStatus(secure_partition->GetProgramTitleID());
|
||||
if (program_nca_status == Loader::ResultStatus::ErrorNSPMissingProgramNCA)
|
||||
program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA;
|
||||
|
||||
auto result = AddNCAFromPartition(XCIPartition::Update);
|
||||
if (result != Loader::ResultStatus::Success) {
|
||||
status = result;
|
||||
return;
|
||||
@@ -69,14 +81,24 @@ XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
|
||||
status = Loader::ResultStatus::Success;
|
||||
}
|
||||
|
||||
XCI::~XCI() = default;
|
||||
|
||||
Loader::ResultStatus XCI::GetStatus() const {
|
||||
return status;
|
||||
}
|
||||
|
||||
Loader::ResultStatus XCI::GetProgramNCAStatus() const {
|
||||
return program_nca_status;
|
||||
}
|
||||
|
||||
VirtualDir XCI::GetPartition(XCIPartition partition) const {
|
||||
return partitions[static_cast<size_t>(partition)];
|
||||
}
|
||||
|
||||
std::shared_ptr<NSP> XCI::GetSecurePartitionNSP() const {
|
||||
return secure_partition;
|
||||
}
|
||||
|
||||
VirtualDir XCI::GetSecurePartition() const {
|
||||
return GetPartition(XCIPartition::Secure);
|
||||
}
|
||||
@@ -93,6 +115,24 @@ VirtualDir XCI::GetLogoPartition() const {
|
||||
return GetPartition(XCIPartition::Logo);
|
||||
}
|
||||
|
||||
u64 XCI::GetProgramTitleID() const {
|
||||
return secure_partition->GetProgramTitleID();
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> XCI::GetProgramNCA() const {
|
||||
return program;
|
||||
}
|
||||
|
||||
VirtualFile XCI::GetProgramNCAFile() const {
|
||||
if (GetProgramNCA() == nullptr)
|
||||
return nullptr;
|
||||
return GetProgramNCA()->GetBaseFile();
|
||||
}
|
||||
|
||||
const std::vector<std::shared_ptr<NCA>>& XCI::GetNCAs() const {
|
||||
return ncas;
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const {
|
||||
const auto iter =
|
||||
std::find_if(ncas.begin(), ncas.end(),
|
||||
@@ -136,13 +176,19 @@ Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
|
||||
if (file->GetExtension() != "nca")
|
||||
continue;
|
||||
auto nca = std::make_shared<NCA>(file);
|
||||
// TODO(DarkLordZach): Add proper Rev1+ Support
|
||||
if (nca->IsUpdate())
|
||||
continue;
|
||||
if (nca->GetType() == NCAContentType::Program) {
|
||||
program_nca_status = nca->GetStatus();
|
||||
}
|
||||
if (nca->GetStatus() == Loader::ResultStatus::Success) {
|
||||
ncas.push_back(std::move(nca));
|
||||
} else {
|
||||
const u16 error_id = static_cast<u16>(nca->GetStatus());
|
||||
LOG_CRITICAL(Loader, "Could not load NCA {}/{}, failed with error code {:04X} ({})",
|
||||
partition_names[static_cast<size_t>(part)], nca->GetName(), error_id,
|
||||
Loader::GetMessageForResultStatus(nca->GetStatus()));
|
||||
nca->GetStatus());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace Loader {
|
||||
enum class ResultStatus : u16;
|
||||
}
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class NCA;
|
||||
enum class NCAContentType : u8;
|
||||
class NSP;
|
||||
|
||||
enum class GamecardSize : u8 {
|
||||
S_1GB = 0xFA,
|
||||
S_2GB = 0xF8,
|
||||
@@ -57,17 +64,25 @@ enum class XCIPartition : u8 { Update, Normal, Secure, Logo };
|
||||
class XCI : public ReadOnlyVfsDirectory {
|
||||
public:
|
||||
explicit XCI(VirtualFile file);
|
||||
~XCI() override;
|
||||
|
||||
Loader::ResultStatus GetStatus() const;
|
||||
Loader::ResultStatus GetProgramNCAStatus() const;
|
||||
|
||||
u8 GetFormatVersion() const;
|
||||
|
||||
VirtualDir GetPartition(XCIPartition partition) const;
|
||||
std::shared_ptr<NSP> GetSecurePartitionNSP() const;
|
||||
VirtualDir GetSecurePartition() const;
|
||||
VirtualDir GetNormalPartition() const;
|
||||
VirtualDir GetUpdatePartition() const;
|
||||
VirtualDir GetLogoPartition() const;
|
||||
|
||||
u64 GetProgramTitleID() const;
|
||||
|
||||
std::shared_ptr<NCA> GetProgramNCA() const;
|
||||
VirtualFile GetProgramNCAFile() const;
|
||||
const std::vector<std::shared_ptr<NCA>>& GetNCAs() const;
|
||||
std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const;
|
||||
VirtualFile GetNCAFileByType(NCAContentType type) const;
|
||||
|
||||
@@ -89,8 +104,11 @@ private:
|
||||
GamecardHeader header{};
|
||||
|
||||
Loader::ResultStatus status;
|
||||
Loader::ResultStatus program_nca_status;
|
||||
|
||||
std::vector<VirtualDir> partitions;
|
||||
std::shared_ptr<NSP> secure_partition;
|
||||
std::shared_ptr<NCA> program;
|
||||
std::vector<std::shared_ptr<NCA>> ncas;
|
||||
};
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/crypto/ctr_encryption_layer.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_patch.h"
|
||||
#include "core/file_sys/partition_filesystem.h"
|
||||
#include "core/file_sys/romfs.h"
|
||||
#include "core/file_sys/vfs_offset.h"
|
||||
#include "core/loader/loader.h"
|
||||
@@ -64,10 +69,31 @@ struct RomFSSuperblock {
|
||||
};
|
||||
static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size.");
|
||||
|
||||
struct BKTRHeader {
|
||||
u64_le offset;
|
||||
u64_le size;
|
||||
u32_le magic;
|
||||
INSERT_PADDING_BYTES(0x4);
|
||||
u32_le number_entries;
|
||||
INSERT_PADDING_BYTES(0x4);
|
||||
};
|
||||
static_assert(sizeof(BKTRHeader) == 0x20, "BKTRHeader has incorrect size.");
|
||||
|
||||
struct BKTRSuperblock {
|
||||
NCASectionHeaderBlock header_block;
|
||||
IVFCHeader ivfc;
|
||||
INSERT_PADDING_BYTES(0x18);
|
||||
BKTRHeader relocation;
|
||||
BKTRHeader subsection;
|
||||
INSERT_PADDING_BYTES(0xC0);
|
||||
};
|
||||
static_assert(sizeof(BKTRSuperblock) == 0x200, "BKTRSuperblock has incorrect size.");
|
||||
|
||||
union NCASectionHeader {
|
||||
NCASectionRaw raw;
|
||||
PFS0Superblock pfs0;
|
||||
RomFSSuperblock romfs;
|
||||
BKTRSuperblock bktr;
|
||||
};
|
||||
static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size.");
|
||||
|
||||
@@ -100,7 +126,7 @@ boost::optional<Core::Crypto::Key128> NCA::GetKeyAreaKey(NCASectionCryptoType ty
|
||||
Core::Crypto::Key128 out;
|
||||
if (type == NCASectionCryptoType::XTS)
|
||||
std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin());
|
||||
else if (type == NCASectionCryptoType::CTR)
|
||||
else if (type == NCASectionCryptoType::CTR || type == NCASectionCryptoType::BKTR)
|
||||
std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin());
|
||||
else
|
||||
LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}",
|
||||
@@ -150,6 +176,9 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting
|
||||
LOG_DEBUG(Crypto, "called with mode=NONE");
|
||||
return in;
|
||||
case NCASectionCryptoType::CTR:
|
||||
// During normal BKTR decryption, this entire function is skipped. This is for the metadata,
|
||||
// which uses the same CTR as usual.
|
||||
case NCASectionCryptoType::BKTR:
|
||||
LOG_DEBUG(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset);
|
||||
{
|
||||
boost::optional<Core::Crypto::Key128> key = boost::none;
|
||||
@@ -178,7 +207,7 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting
|
||||
return std::static_pointer_cast<VfsFile>(out);
|
||||
}
|
||||
case NCASectionCryptoType::XTS:
|
||||
// TODO(DarkLordZach): Implement XTSEncryptionLayer.
|
||||
// TODO(DarkLordZach): Find a test case for XTS-encrypted NCAs
|
||||
default:
|
||||
LOG_ERROR(Crypto, "called with unhandled crypto type={:02X}",
|
||||
static_cast<u8>(s_header.raw.header.crypto_type));
|
||||
@@ -186,7 +215,9 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting
|
||||
}
|
||||
}
|
||||
|
||||
NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
|
||||
NCA::NCA(VirtualFile file_, VirtualFile bktr_base_romfs_, u64 bktr_base_ivfc_offset)
|
||||
: file(std::move(file_)),
|
||||
bktr_base_romfs(bktr_base_romfs_ ? std::move(bktr_base_romfs_) : nullptr) {
|
||||
status = Loader::ResultStatus::Success;
|
||||
|
||||
if (file == nullptr) {
|
||||
@@ -258,21 +289,24 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
|
||||
file->ReadBytes(sections.data(), length_sections, SECTION_HEADER_OFFSET);
|
||||
}
|
||||
|
||||
is_update = std::find_if(sections.begin(), sections.end(), [](const NCASectionHeader& header) {
|
||||
return header.raw.header.crypto_type == NCASectionCryptoType::BKTR;
|
||||
}) != sections.end();
|
||||
ivfc_offset = 0;
|
||||
|
||||
for (std::ptrdiff_t i = 0; i < number_sections; ++i) {
|
||||
auto section = sections[i];
|
||||
|
||||
if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) {
|
||||
const size_t romfs_offset =
|
||||
header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER +
|
||||
section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
|
||||
const size_t base_offset =
|
||||
header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER;
|
||||
ivfc_offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
|
||||
const size_t romfs_offset = base_offset + ivfc_offset;
|
||||
const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
|
||||
auto dec =
|
||||
Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset),
|
||||
romfs_offset);
|
||||
if (dec != nullptr) {
|
||||
files.push_back(std::move(dec));
|
||||
romfs = files.back();
|
||||
} else {
|
||||
auto raw = std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset);
|
||||
auto dec = Decrypt(section, raw, romfs_offset);
|
||||
|
||||
if (dec == nullptr) {
|
||||
if (status != Loader::ResultStatus::Success)
|
||||
return;
|
||||
if (has_rights_id)
|
||||
@@ -281,6 +315,117 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
|
||||
status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.raw.header.crypto_type == NCASectionCryptoType::BKTR) {
|
||||
if (section.bktr.relocation.magic != Common::MakeMagic('B', 'K', 'T', 'R') ||
|
||||
section.bktr.subsection.magic != Common::MakeMagic('B', 'K', 'T', 'R')) {
|
||||
status = Loader::ResultStatus::ErrorBadBKTRHeader;
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.bktr.relocation.offset + section.bktr.relocation.size !=
|
||||
section.bktr.subsection.offset) {
|
||||
status = Loader::ResultStatus::ErrorBKTRSubsectionNotAfterRelocation;
|
||||
return;
|
||||
}
|
||||
|
||||
const u64 size =
|
||||
MEDIA_OFFSET_MULTIPLIER * (header.section_tables[i].media_end_offset -
|
||||
header.section_tables[i].media_offset);
|
||||
if (section.bktr.subsection.offset + section.bktr.subsection.size != size) {
|
||||
status = Loader::ResultStatus::ErrorBKTRSubsectionNotAtEnd;
|
||||
return;
|
||||
}
|
||||
|
||||
const u64 offset = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
|
||||
RelocationBlock relocation_block{};
|
||||
if (dec->ReadObject(&relocation_block, section.bktr.relocation.offset - offset) !=
|
||||
sizeof(RelocationBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadRelocationBlock;
|
||||
return;
|
||||
}
|
||||
SubsectionBlock subsection_block{};
|
||||
if (dec->ReadObject(&subsection_block, section.bktr.subsection.offset - offset) !=
|
||||
sizeof(RelocationBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadSubsectionBlock;
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<RelocationBucketRaw> relocation_buckets_raw(
|
||||
(section.bktr.relocation.size - sizeof(RelocationBlock)) /
|
||||
sizeof(RelocationBucketRaw));
|
||||
if (dec->ReadBytes(relocation_buckets_raw.data(),
|
||||
section.bktr.relocation.size - sizeof(RelocationBlock),
|
||||
section.bktr.relocation.offset + sizeof(RelocationBlock) -
|
||||
offset) !=
|
||||
section.bktr.relocation.size - sizeof(RelocationBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadRelocationBuckets;
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<SubsectionBucketRaw> subsection_buckets_raw(
|
||||
(section.bktr.subsection.size - sizeof(SubsectionBlock)) /
|
||||
sizeof(SubsectionBucketRaw));
|
||||
if (dec->ReadBytes(subsection_buckets_raw.data(),
|
||||
section.bktr.subsection.size - sizeof(SubsectionBlock),
|
||||
section.bktr.subsection.offset + sizeof(SubsectionBlock) -
|
||||
offset) !=
|
||||
section.bktr.subsection.size - sizeof(SubsectionBlock)) {
|
||||
status = Loader::ResultStatus::ErrorBadSubsectionBuckets;
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<RelocationBucket> relocation_buckets(relocation_buckets_raw.size());
|
||||
std::transform(relocation_buckets_raw.begin(), relocation_buckets_raw.end(),
|
||||
relocation_buckets.begin(), &ConvertRelocationBucketRaw);
|
||||
std::vector<SubsectionBucket> subsection_buckets(subsection_buckets_raw.size());
|
||||
std::transform(subsection_buckets_raw.begin(), subsection_buckets_raw.end(),
|
||||
subsection_buckets.begin(), &ConvertSubsectionBucketRaw);
|
||||
|
||||
u32 ctr_low;
|
||||
std::memcpy(&ctr_low, section.raw.section_ctr.data(), sizeof(ctr_low));
|
||||
subsection_buckets.back().entries.push_back(
|
||||
{section.bktr.relocation.offset, {0}, ctr_low});
|
||||
subsection_buckets.back().entries.push_back({size, {0}, 0});
|
||||
|
||||
boost::optional<Core::Crypto::Key128> key = boost::none;
|
||||
if (encrypted) {
|
||||
if (has_rights_id) {
|
||||
status = Loader::ResultStatus::Success;
|
||||
key = GetTitlekey();
|
||||
if (key == boost::none) {
|
||||
status = Loader::ResultStatus::ErrorMissingTitlekey;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
key = GetKeyAreaKey(NCASectionCryptoType::BKTR);
|
||||
if (key == boost::none) {
|
||||
status = Loader::ResultStatus::ErrorMissingKeyAreaKey;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bktr_base_romfs == nullptr) {
|
||||
status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS;
|
||||
return;
|
||||
}
|
||||
|
||||
auto bktr = std::make_shared<BKTR>(
|
||||
bktr_base_romfs, std::make_shared<OffsetVfsFile>(file, romfs_size, base_offset),
|
||||
relocation_block, relocation_buckets, subsection_block, subsection_buckets,
|
||||
encrypted, encrypted ? key.get() : Core::Crypto::Key128{}, base_offset,
|
||||
bktr_base_ivfc_offset, section.raw.section_ctr);
|
||||
|
||||
// BKTR applies to entire IVFC, so make an offset version to level 6
|
||||
|
||||
files.push_back(std::make_shared<OffsetVfsFile>(
|
||||
bktr, romfs_size, section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset));
|
||||
romfs = files.back();
|
||||
} else {
|
||||
files.push_back(std::move(dec));
|
||||
romfs = files.back();
|
||||
}
|
||||
} else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) {
|
||||
u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) *
|
||||
MEDIA_OFFSET_MULTIPLIER) +
|
||||
@@ -296,6 +441,12 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
|
||||
dirs.push_back(std::move(npfs));
|
||||
if (IsDirectoryExeFS(dirs.back()))
|
||||
exefs = dirs.back();
|
||||
} else {
|
||||
if (has_rights_id)
|
||||
status = Loader::ResultStatus::ErrorIncorrectTitlekeyOrTitlekek;
|
||||
else
|
||||
status = Loader::ResultStatus::ErrorIncorrectKeyAreaKey;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (status != Loader::ResultStatus::Success)
|
||||
@@ -341,11 +492,15 @@ NCAContentType NCA::GetType() const {
|
||||
}
|
||||
|
||||
u64 NCA::GetTitleId() const {
|
||||
if (status != Loader::ResultStatus::Success)
|
||||
return {};
|
||||
if (is_update || status == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS)
|
||||
return header.title_id | 0x800;
|
||||
return header.title_id;
|
||||
}
|
||||
|
||||
bool NCA::IsUpdate() const {
|
||||
return is_update;
|
||||
}
|
||||
|
||||
VirtualFile NCA::GetRomFS() const {
|
||||
return romfs;
|
||||
}
|
||||
@@ -358,6 +513,10 @@ VirtualFile NCA::GetBaseFile() const {
|
||||
return file;
|
||||
}
|
||||
|
||||
u64 NCA::GetBaseIVFCOffset() const {
|
||||
return ivfc_offset;
|
||||
}
|
||||
|
||||
bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "control_metadata.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/partition_filesystem.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace Loader {
|
||||
enum class ResultStatus : u16;
|
||||
}
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
@@ -27,6 +29,7 @@ enum class NCAContentType : u8 {
|
||||
Control = 2,
|
||||
Manual = 3,
|
||||
Data = 4,
|
||||
Data_Unknown5 = 5, ///< Seems to be used on some system archives
|
||||
};
|
||||
|
||||
enum class NCASectionCryptoType : u8 {
|
||||
@@ -76,7 +79,8 @@ bool IsValidNCA(const NCAHeader& header);
|
||||
// After construction, use GetStatus to determine if the file is valid and ready to be used.
|
||||
class NCA : public ReadOnlyVfsDirectory {
|
||||
public:
|
||||
explicit NCA(VirtualFile file);
|
||||
explicit NCA(VirtualFile file, VirtualFile bktr_base_romfs = nullptr,
|
||||
u64 bktr_base_ivfc_offset = 0);
|
||||
Loader::ResultStatus GetStatus() const;
|
||||
|
||||
std::vector<std::shared_ptr<VfsFile>> GetFiles() const override;
|
||||
@@ -86,12 +90,16 @@ public:
|
||||
|
||||
NCAContentType GetType() const;
|
||||
u64 GetTitleId() const;
|
||||
bool IsUpdate() const;
|
||||
|
||||
VirtualFile GetRomFS() const;
|
||||
VirtualDir GetExeFS() const;
|
||||
|
||||
VirtualFile GetBaseFile() const;
|
||||
|
||||
// Returns the base ivfc offset used in BKTR patching.
|
||||
u64 GetBaseIVFCOffset() const;
|
||||
|
||||
protected:
|
||||
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
|
||||
|
||||
@@ -107,6 +115,8 @@ private:
|
||||
VirtualFile romfs = nullptr;
|
||||
VirtualDir exefs = nullptr;
|
||||
VirtualFile file;
|
||||
VirtualFile bktr_base_romfs;
|
||||
u64 ivfc_offset;
|
||||
|
||||
NCAHeader header{};
|
||||
bool has_rights_id{};
|
||||
@@ -114,6 +124,7 @@ private:
|
||||
Loader::ResultStatus status{};
|
||||
|
||||
bool encrypted;
|
||||
bool is_update;
|
||||
|
||||
Core::Crypto::KeyManager keys;
|
||||
};
|
||||
|
||||
@@ -16,12 +16,22 @@ std::string LanguageEntry::GetDeveloperName() const {
|
||||
return Common::StringFromFixedZeroTerminatedBuffer(developer_name.data(), 0x100);
|
||||
}
|
||||
|
||||
NACP::NACP(VirtualFile file_) : file(std::move(file_)), raw(std::make_unique<RawNACP>()) {
|
||||
NACP::NACP(VirtualFile file) : raw(std::make_unique<RawNACP>()) {
|
||||
file->ReadObject(raw.get());
|
||||
}
|
||||
|
||||
const LanguageEntry& NACP::GetLanguageEntry(Language language) const {
|
||||
return raw->language_entries.at(static_cast<u8>(language));
|
||||
if (language != Language::Default) {
|
||||
return raw->language_entries.at(static_cast<u8>(language));
|
||||
} else {
|
||||
for (const auto& language_entry : raw->language_entries) {
|
||||
if (!language_entry.GetApplicationName().empty())
|
||||
return language_entry;
|
||||
}
|
||||
|
||||
// Fallback to English
|
||||
return GetLanguageEntry(Language::AmericanEnglish);
|
||||
}
|
||||
}
|
||||
|
||||
std::string NACP::GetApplicationName(Language language) const {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
@@ -60,6 +62,8 @@ enum class Language : u8 {
|
||||
Korean = 12,
|
||||
Taiwanese = 13,
|
||||
Chinese = 14,
|
||||
|
||||
Default = 255,
|
||||
};
|
||||
|
||||
static constexpr std::array<const char*, 15> LANGUAGE_NAMES = {
|
||||
@@ -74,14 +78,13 @@ static constexpr std::array<const char*, 15> LANGUAGE_NAMES = {
|
||||
class NACP {
|
||||
public:
|
||||
explicit NACP(VirtualFile file);
|
||||
const LanguageEntry& GetLanguageEntry(Language language = Language::AmericanEnglish) const;
|
||||
std::string GetApplicationName(Language language = Language::AmericanEnglish) const;
|
||||
std::string GetDeveloperName(Language language = Language::AmericanEnglish) const;
|
||||
const LanguageEntry& GetLanguageEntry(Language language = Language::Default) const;
|
||||
std::string GetApplicationName(Language language = Language::Default) const;
|
||||
std::string GetDeveloperName(Language language = Language::Default) const;
|
||||
u64 GetTitleId() const;
|
||||
std::string GetVersionString() const;
|
||||
|
||||
private:
|
||||
VirtualFile file;
|
||||
std::unique_ptr<RawNACP> raw;
|
||||
};
|
||||
|
||||
|
||||
130
src/core/file_sys/nca_metadata.cpp
Normal file
130
src/core/file_sys/nca_metadata.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <cstring>
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
bool operator>=(TitleType lhs, TitleType rhs) {
|
||||
return static_cast<size_t>(lhs) >= static_cast<size_t>(rhs);
|
||||
}
|
||||
|
||||
bool operator<=(TitleType lhs, TitleType rhs) {
|
||||
return static_cast<size_t>(lhs) <= static_cast<size_t>(rhs);
|
||||
}
|
||||
|
||||
CNMT::CNMT(VirtualFile file) {
|
||||
if (file->ReadObject(&header) != sizeof(CNMTHeader))
|
||||
return;
|
||||
|
||||
// If type is {Application, Update, AOC} has opt-header.
|
||||
if (header.type >= TitleType::Application && header.type <= TitleType::AOC) {
|
||||
if (file->ReadObject(&opt_header, sizeof(CNMTHeader)) != sizeof(OptionalHeader)) {
|
||||
LOG_WARNING(Loader, "Failed to read optional header.");
|
||||
}
|
||||
}
|
||||
|
||||
for (u16 i = 0; i < header.number_content_entries; ++i) {
|
||||
auto& next = content_records.emplace_back(ContentRecord{});
|
||||
if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(ContentRecord) +
|
||||
header.table_offset) != sizeof(ContentRecord)) {
|
||||
content_records.erase(content_records.end() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (u16 i = 0; i < header.number_meta_entries; ++i) {
|
||||
auto& next = meta_records.emplace_back(MetaRecord{});
|
||||
if (file->ReadObject(&next, sizeof(CNMTHeader) + i * sizeof(MetaRecord) +
|
||||
header.table_offset) != sizeof(MetaRecord)) {
|
||||
meta_records.erase(meta_records.end() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CNMT::CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records,
|
||||
std::vector<MetaRecord> meta_records)
|
||||
: header(std::move(header)), opt_header(std::move(opt_header)),
|
||||
content_records(std::move(content_records)), meta_records(std::move(meta_records)) {}
|
||||
|
||||
u64 CNMT::GetTitleID() const {
|
||||
return header.title_id;
|
||||
}
|
||||
|
||||
u32 CNMT::GetTitleVersion() const {
|
||||
return header.title_version;
|
||||
}
|
||||
|
||||
TitleType CNMT::GetType() const {
|
||||
return header.type;
|
||||
}
|
||||
|
||||
const std::vector<ContentRecord>& CNMT::GetContentRecords() const {
|
||||
return content_records;
|
||||
}
|
||||
|
||||
const std::vector<MetaRecord>& CNMT::GetMetaRecords() const {
|
||||
return meta_records;
|
||||
}
|
||||
|
||||
bool CNMT::UnionRecords(const CNMT& other) {
|
||||
bool change = false;
|
||||
for (const auto& rec : other.content_records) {
|
||||
const auto iter = std::find_if(content_records.begin(), content_records.end(),
|
||||
[&rec](const ContentRecord& r) {
|
||||
return r.nca_id == rec.nca_id && r.type == rec.type;
|
||||
});
|
||||
if (iter == content_records.end()) {
|
||||
content_records.emplace_back(rec);
|
||||
++header.number_content_entries;
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
for (const auto& rec : other.meta_records) {
|
||||
const auto iter =
|
||||
std::find_if(meta_records.begin(), meta_records.end(), [&rec](const MetaRecord& r) {
|
||||
return r.title_id == rec.title_id && r.title_version == rec.title_version &&
|
||||
r.type == rec.type;
|
||||
});
|
||||
if (iter == meta_records.end()) {
|
||||
meta_records.emplace_back(rec);
|
||||
++header.number_meta_entries;
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
return change;
|
||||
}
|
||||
|
||||
std::vector<u8> CNMT::Serialize() const {
|
||||
const bool has_opt_header =
|
||||
header.type >= TitleType::Application && header.type <= TitleType::AOC;
|
||||
const auto dead_zone = header.table_offset + sizeof(CNMTHeader);
|
||||
std::vector<u8> out(
|
||||
std::max(sizeof(CNMTHeader) + (has_opt_header ? sizeof(OptionalHeader) : 0), dead_zone) +
|
||||
content_records.size() * sizeof(ContentRecord) + meta_records.size() * sizeof(MetaRecord));
|
||||
memcpy(out.data(), &header, sizeof(CNMTHeader));
|
||||
|
||||
// Optional Header
|
||||
if (has_opt_header) {
|
||||
memcpy(out.data() + sizeof(CNMTHeader), &opt_header, sizeof(OptionalHeader));
|
||||
}
|
||||
|
||||
auto offset = header.table_offset;
|
||||
|
||||
for (const auto& rec : content_records) {
|
||||
memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(ContentRecord));
|
||||
offset += sizeof(ContentRecord);
|
||||
}
|
||||
|
||||
for (const auto& rec : meta_records) {
|
||||
memcpy(out.data() + offset + sizeof(CNMTHeader), &rec, sizeof(MetaRecord));
|
||||
offset += sizeof(MetaRecord);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
} // namespace FileSys
|
||||
111
src/core/file_sys/nca_metadata.h
Normal file
111
src/core/file_sys/nca_metadata.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
class CNMT;
|
||||
|
||||
struct CNMTHeader;
|
||||
struct OptionalHeader;
|
||||
|
||||
enum class TitleType : u8 {
|
||||
SystemProgram = 0x01,
|
||||
SystemDataArchive = 0x02,
|
||||
SystemUpdate = 0x03,
|
||||
FirmwarePackageA = 0x04,
|
||||
FirmwarePackageB = 0x05,
|
||||
Application = 0x80,
|
||||
Update = 0x81,
|
||||
AOC = 0x82,
|
||||
DeltaTitle = 0x83,
|
||||
};
|
||||
|
||||
bool operator>=(TitleType lhs, TitleType rhs);
|
||||
bool operator<=(TitleType lhs, TitleType rhs);
|
||||
|
||||
enum class ContentRecordType : u8 {
|
||||
Meta = 0,
|
||||
Program = 1,
|
||||
Data = 2,
|
||||
Control = 3,
|
||||
Manual = 4,
|
||||
Legal = 5,
|
||||
Patch = 6,
|
||||
};
|
||||
|
||||
struct ContentRecord {
|
||||
std::array<u8, 0x20> hash;
|
||||
std::array<u8, 0x10> nca_id;
|
||||
std::array<u8, 0x6> size;
|
||||
ContentRecordType type;
|
||||
INSERT_PADDING_BYTES(1);
|
||||
};
|
||||
static_assert(sizeof(ContentRecord) == 0x38, "ContentRecord has incorrect size.");
|
||||
|
||||
constexpr ContentRecord EMPTY_META_CONTENT_RECORD{{}, {}, {}, ContentRecordType::Meta, {}};
|
||||
|
||||
struct MetaRecord {
|
||||
u64_le title_id;
|
||||
u32_le title_version;
|
||||
TitleType type;
|
||||
u8 install_byte;
|
||||
INSERT_PADDING_BYTES(2);
|
||||
};
|
||||
static_assert(sizeof(MetaRecord) == 0x10, "MetaRecord has incorrect size.");
|
||||
|
||||
struct OptionalHeader {
|
||||
u64_le title_id;
|
||||
u64_le minimum_version;
|
||||
};
|
||||
static_assert(sizeof(OptionalHeader) == 0x10, "OptionalHeader has incorrect size.");
|
||||
|
||||
struct CNMTHeader {
|
||||
u64_le title_id;
|
||||
u32_le title_version;
|
||||
TitleType type;
|
||||
INSERT_PADDING_BYTES(1);
|
||||
u16_le table_offset;
|
||||
u16_le number_content_entries;
|
||||
u16_le number_meta_entries;
|
||||
INSERT_PADDING_BYTES(12);
|
||||
};
|
||||
static_assert(sizeof(CNMTHeader) == 0x20, "CNMTHeader has incorrect size.");
|
||||
|
||||
// A class representing the format used by NCA metadata files, typically named {}.cnmt.nca or
|
||||
// meta0.ncd. These describe which NCA's belong with which titles in the registered cache.
|
||||
class CNMT {
|
||||
public:
|
||||
explicit CNMT(VirtualFile file);
|
||||
CNMT(CNMTHeader header, OptionalHeader opt_header, std::vector<ContentRecord> content_records,
|
||||
std::vector<MetaRecord> meta_records);
|
||||
|
||||
u64 GetTitleID() const;
|
||||
u32 GetTitleVersion() const;
|
||||
TitleType GetType() const;
|
||||
|
||||
const std::vector<ContentRecord>& GetContentRecords() const;
|
||||
const std::vector<MetaRecord>& GetMetaRecords() const;
|
||||
|
||||
bool UnionRecords(const CNMT& other);
|
||||
std::vector<u8> Serialize() const;
|
||||
|
||||
private:
|
||||
CNMTHeader header;
|
||||
OptionalHeader opt_header;
|
||||
std::vector<ContentRecord> content_records;
|
||||
std::vector<MetaRecord> meta_records;
|
||||
|
||||
// TODO(DarkLordZach): According to switchbrew, for Patch-type there is additional data
|
||||
// after the table. This is not documented, unfortunately.
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
210
src/core/file_sys/nca_patch.cpp
Normal file
210
src/core/file_sys/nca_patch.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "core/crypto/aes_util.h"
|
||||
#include "core/file_sys/nca_patch.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
BKTR::BKTR(VirtualFile base_romfs_, VirtualFile bktr_romfs_, RelocationBlock relocation_,
|
||||
std::vector<RelocationBucket> relocation_buckets_, SubsectionBlock subsection_,
|
||||
std::vector<SubsectionBucket> subsection_buckets_, bool is_encrypted_,
|
||||
Core::Crypto::Key128 key_, u64 base_offset_, u64 ivfc_offset_,
|
||||
std::array<u8, 8> section_ctr_)
|
||||
: relocation(relocation_), relocation_buckets(std::move(relocation_buckets_)),
|
||||
subsection(subsection_), subsection_buckets(std::move(subsection_buckets_)),
|
||||
base_romfs(std::move(base_romfs_)), bktr_romfs(std::move(bktr_romfs_)),
|
||||
encrypted(is_encrypted_), key(key_), base_offset(base_offset_), ivfc_offset(ivfc_offset_),
|
||||
section_ctr(section_ctr_) {
|
||||
for (size_t i = 0; i < relocation.number_buckets - 1; ++i) {
|
||||
relocation_buckets[i].entries.push_back({relocation.base_offsets[i + 1], 0, 0});
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < subsection.number_buckets - 1; ++i) {
|
||||
subsection_buckets[i].entries.push_back({subsection_buckets[i + 1].entries[0].address_patch,
|
||||
{0},
|
||||
subsection_buckets[i + 1].entries[0].ctr});
|
||||
}
|
||||
|
||||
relocation_buckets.back().entries.push_back({relocation.size, 0, 0});
|
||||
}
|
||||
|
||||
BKTR::~BKTR() = default;
|
||||
|
||||
size_t BKTR::Read(u8* data, size_t length, size_t offset) const {
|
||||
// Read out of bounds.
|
||||
if (offset >= relocation.size)
|
||||
return 0;
|
||||
const auto relocation = GetRelocationEntry(offset);
|
||||
const auto section_offset = offset - relocation.address_patch + relocation.address_source;
|
||||
const auto bktr_read = relocation.from_patch;
|
||||
|
||||
const auto next_relocation = GetNextRelocationEntry(offset);
|
||||
|
||||
if (offset + length > next_relocation.address_patch) {
|
||||
const u64 partition = next_relocation.address_patch - offset;
|
||||
return Read(data, partition, offset) +
|
||||
Read(data + partition, length - partition, offset + partition);
|
||||
}
|
||||
|
||||
if (!bktr_read) {
|
||||
ASSERT_MSG(section_offset >= ivfc_offset, "Offset calculation negative.");
|
||||
return base_romfs->Read(data, length, section_offset - ivfc_offset);
|
||||
}
|
||||
|
||||
if (!encrypted) {
|
||||
return bktr_romfs->Read(data, length, section_offset);
|
||||
}
|
||||
|
||||
const auto subsection = GetSubsectionEntry(section_offset);
|
||||
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(key, Core::Crypto::Mode::CTR);
|
||||
|
||||
// Calculate AES IV
|
||||
std::vector<u8> iv(16);
|
||||
auto subsection_ctr = subsection.ctr;
|
||||
auto offset_iv = section_offset + base_offset;
|
||||
for (size_t i = 0; i < section_ctr.size(); ++i)
|
||||
iv[i] = section_ctr[0x8 - i - 1];
|
||||
offset_iv >>= 4;
|
||||
for (size_t i = 0; i < sizeof(u64); ++i) {
|
||||
iv[0xF - i] = static_cast<u8>(offset_iv & 0xFF);
|
||||
offset_iv >>= 8;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(u32); ++i) {
|
||||
iv[0x7 - i] = static_cast<u8>(subsection_ctr & 0xFF);
|
||||
subsection_ctr >>= 8;
|
||||
}
|
||||
cipher.SetIV(iv);
|
||||
|
||||
const auto next_subsection = GetNextSubsectionEntry(section_offset);
|
||||
|
||||
if (section_offset + length > next_subsection.address_patch) {
|
||||
const u64 partition = next_subsection.address_patch - section_offset;
|
||||
return Read(data, partition, offset) +
|
||||
Read(data + partition, length - partition, offset + partition);
|
||||
}
|
||||
|
||||
const auto block_offset = section_offset & 0xF;
|
||||
if (block_offset != 0) {
|
||||
auto block = bktr_romfs->ReadBytes(0x10, section_offset & ~0xF);
|
||||
cipher.Transcode(block.data(), block.size(), block.data(), Core::Crypto::Op::Decrypt);
|
||||
if (length + block_offset < 0x10) {
|
||||
std::memcpy(data, block.data() + block_offset, std::min(length, block.size()));
|
||||
return std::min(length, block.size());
|
||||
}
|
||||
|
||||
const auto read = 0x10 - block_offset;
|
||||
std::memcpy(data, block.data() + block_offset, read);
|
||||
return read + Read(data + read, length - read, offset + read);
|
||||
}
|
||||
|
||||
const auto raw_read = bktr_romfs->Read(data, length, section_offset);
|
||||
cipher.Transcode(data, raw_read, data, Core::Crypto::Op::Decrypt);
|
||||
return raw_read;
|
||||
}
|
||||
|
||||
template <bool Subsection, typename BlockType, typename BucketType>
|
||||
std::pair<size_t, size_t> BKTR::SearchBucketEntry(u64 offset, BlockType block,
|
||||
BucketType buckets) const {
|
||||
if constexpr (Subsection) {
|
||||
const auto last_bucket = buckets[block.number_buckets - 1];
|
||||
if (offset >= last_bucket.entries[last_bucket.number_entries].address_patch)
|
||||
return {block.number_buckets - 1, last_bucket.number_entries};
|
||||
} else {
|
||||
ASSERT_MSG(offset <= block.size, "Offset is out of bounds in BKTR relocation block.");
|
||||
}
|
||||
|
||||
size_t bucket_id = std::count_if(block.base_offsets.begin() + 1,
|
||||
block.base_offsets.begin() + block.number_buckets,
|
||||
[&offset](u64 base_offset) { return base_offset <= offset; });
|
||||
|
||||
const auto bucket = buckets[bucket_id];
|
||||
|
||||
if (bucket.number_entries == 1)
|
||||
return {bucket_id, 0};
|
||||
|
||||
size_t low = 0;
|
||||
size_t mid = 0;
|
||||
size_t high = bucket.number_entries - 1;
|
||||
while (low <= high) {
|
||||
mid = (low + high) / 2;
|
||||
if (bucket.entries[mid].address_patch > offset) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
if (mid == bucket.number_entries - 1 ||
|
||||
bucket.entries[mid + 1].address_patch > offset) {
|
||||
return {bucket_id, mid};
|
||||
}
|
||||
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
UNREACHABLE_MSG("Offset could not be found in BKTR block.");
|
||||
}
|
||||
|
||||
RelocationEntry BKTR::GetRelocationEntry(u64 offset) const {
|
||||
const auto res = SearchBucketEntry<false>(offset, relocation, relocation_buckets);
|
||||
return relocation_buckets[res.first].entries[res.second];
|
||||
}
|
||||
|
||||
RelocationEntry BKTR::GetNextRelocationEntry(u64 offset) const {
|
||||
const auto res = SearchBucketEntry<false>(offset, relocation, relocation_buckets);
|
||||
const auto bucket = relocation_buckets[res.first];
|
||||
if (res.second + 1 < bucket.entries.size())
|
||||
return bucket.entries[res.second + 1];
|
||||
return relocation_buckets[res.first + 1].entries[0];
|
||||
}
|
||||
|
||||
SubsectionEntry BKTR::GetSubsectionEntry(u64 offset) const {
|
||||
const auto res = SearchBucketEntry<true>(offset, subsection, subsection_buckets);
|
||||
return subsection_buckets[res.first].entries[res.second];
|
||||
}
|
||||
|
||||
SubsectionEntry BKTR::GetNextSubsectionEntry(u64 offset) const {
|
||||
const auto res = SearchBucketEntry<true>(offset, subsection, subsection_buckets);
|
||||
const auto bucket = subsection_buckets[res.first];
|
||||
if (res.second + 1 < bucket.entries.size())
|
||||
return bucket.entries[res.second + 1];
|
||||
return subsection_buckets[res.first + 1].entries[0];
|
||||
}
|
||||
|
||||
std::string BKTR::GetName() const {
|
||||
return base_romfs->GetName();
|
||||
}
|
||||
|
||||
size_t BKTR::GetSize() const {
|
||||
return relocation.size;
|
||||
}
|
||||
|
||||
bool BKTR::Resize(size_t new_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<VfsDirectory> BKTR::GetContainingDirectory() const {
|
||||
return base_romfs->GetContainingDirectory();
|
||||
}
|
||||
|
||||
bool BKTR::IsWritable() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BKTR::IsReadable() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t BKTR::Write(const u8* data, size_t length, size_t offset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool BKTR::Rename(std::string_view name) {
|
||||
return base_romfs->Rename(name);
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
150
src/core/file_sys/nca_patch.h
Normal file
150
src/core/file_sys/nca_patch.h
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct RelocationEntry {
|
||||
u64_le address_patch;
|
||||
u64_le address_source;
|
||||
u32 from_patch;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(RelocationEntry) == 0x14, "RelocationEntry has incorrect size.");
|
||||
|
||||
struct RelocationBucketRaw {
|
||||
INSERT_PADDING_BYTES(4);
|
||||
u32_le number_entries;
|
||||
u64_le end_offset;
|
||||
std::array<RelocationEntry, 0x332> relocation_entries;
|
||||
INSERT_PADDING_BYTES(8);
|
||||
};
|
||||
static_assert(sizeof(RelocationBucketRaw) == 0x4000, "RelocationBucketRaw has incorrect size.");
|
||||
|
||||
// Vector version of RelocationBucketRaw
|
||||
struct RelocationBucket {
|
||||
u32 number_entries;
|
||||
u64 end_offset;
|
||||
std::vector<RelocationEntry> entries;
|
||||
};
|
||||
|
||||
struct RelocationBlock {
|
||||
INSERT_PADDING_BYTES(4);
|
||||
u32_le number_buckets;
|
||||
u64_le size;
|
||||
std::array<u64, 0x7FE> base_offsets;
|
||||
};
|
||||
static_assert(sizeof(RelocationBlock) == 0x4000, "RelocationBlock has incorrect size.");
|
||||
|
||||
struct SubsectionEntry {
|
||||
u64_le address_patch;
|
||||
INSERT_PADDING_BYTES(0x4);
|
||||
u32_le ctr;
|
||||
};
|
||||
static_assert(sizeof(SubsectionEntry) == 0x10, "SubsectionEntry has incorrect size.");
|
||||
|
||||
struct SubsectionBucketRaw {
|
||||
INSERT_PADDING_BYTES(4);
|
||||
u32_le number_entries;
|
||||
u64_le end_offset;
|
||||
std::array<SubsectionEntry, 0x3FF> subsection_entries;
|
||||
};
|
||||
static_assert(sizeof(SubsectionBucketRaw) == 0x4000, "SubsectionBucketRaw has incorrect size.");
|
||||
|
||||
// Vector version of SubsectionBucketRaw
|
||||
struct SubsectionBucket {
|
||||
u32 number_entries;
|
||||
u64 end_offset;
|
||||
std::vector<SubsectionEntry> entries;
|
||||
};
|
||||
|
||||
struct SubsectionBlock {
|
||||
INSERT_PADDING_BYTES(4);
|
||||
u32_le number_buckets;
|
||||
u64_le size;
|
||||
std::array<u64, 0x7FE> base_offsets;
|
||||
};
|
||||
static_assert(sizeof(SubsectionBlock) == 0x4000, "SubsectionBlock has incorrect size.");
|
||||
|
||||
inline RelocationBucket ConvertRelocationBucketRaw(RelocationBucketRaw raw) {
|
||||
return {raw.number_entries,
|
||||
raw.end_offset,
|
||||
{raw.relocation_entries.begin(), raw.relocation_entries.begin() + raw.number_entries}};
|
||||
}
|
||||
|
||||
inline SubsectionBucket ConvertSubsectionBucketRaw(SubsectionBucketRaw raw) {
|
||||
return {raw.number_entries,
|
||||
raw.end_offset,
|
||||
{raw.subsection_entries.begin(), raw.subsection_entries.begin() + raw.number_entries}};
|
||||
}
|
||||
|
||||
class BKTR : public VfsFile {
|
||||
public:
|
||||
BKTR(VirtualFile base_romfs, VirtualFile bktr_romfs, RelocationBlock relocation,
|
||||
std::vector<RelocationBucket> relocation_buckets, SubsectionBlock subsection,
|
||||
std::vector<SubsectionBucket> subsection_buckets, bool is_encrypted,
|
||||
Core::Crypto::Key128 key, u64 base_offset, u64 ivfc_offset, std::array<u8, 8> section_ctr);
|
||||
~BKTR() override;
|
||||
|
||||
size_t Read(u8* data, size_t length, size_t offset) const override;
|
||||
|
||||
std::string GetName() const override;
|
||||
|
||||
size_t GetSize() const override;
|
||||
|
||||
bool Resize(size_t new_size) override;
|
||||
|
||||
std::shared_ptr<VfsDirectory> GetContainingDirectory() const override;
|
||||
|
||||
bool IsWritable() const override;
|
||||
|
||||
bool IsReadable() const override;
|
||||
|
||||
size_t Write(const u8* data, size_t length, size_t offset) override;
|
||||
|
||||
bool Rename(std::string_view name) override;
|
||||
|
||||
private:
|
||||
template <bool Subsection, typename BlockType, typename BucketType>
|
||||
std::pair<size_t, size_t> SearchBucketEntry(u64 offset, BlockType block,
|
||||
BucketType buckets) const;
|
||||
|
||||
RelocationEntry GetRelocationEntry(u64 offset) const;
|
||||
RelocationEntry GetNextRelocationEntry(u64 offset) const;
|
||||
|
||||
SubsectionEntry GetSubsectionEntry(u64 offset) const;
|
||||
SubsectionEntry GetNextSubsectionEntry(u64 offset) const;
|
||||
|
||||
RelocationBlock relocation;
|
||||
std::vector<RelocationBucket> relocation_buckets;
|
||||
SubsectionBlock subsection;
|
||||
std::vector<SubsectionBucket> subsection_buckets;
|
||||
|
||||
// Should be the raw base romfs, decrypted.
|
||||
VirtualFile base_romfs;
|
||||
// Should be the raw BKTR romfs, (located at media_offset with size media_size).
|
||||
VirtualFile bktr_romfs;
|
||||
|
||||
bool encrypted;
|
||||
Core::Crypto::Key128 key;
|
||||
|
||||
// Base offset into NCA, used for IV calculation.
|
||||
u64 base_offset;
|
||||
// Distance between IVFC start and RomFS start, used for base reads
|
||||
u64 ivfc_offset;
|
||||
std::array<u8, 8> section_ctr;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
157
src/core/file_sys/patch_manager.cpp
Normal file
157
src/core/file_sys/patch_manager.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/romfs.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
constexpr u64 SINGLE_BYTE_MODULUS = 0x100;
|
||||
|
||||
std::string FormatTitleVersion(u32 version, TitleVersionFormat format) {
|
||||
std::array<u8, sizeof(u32)> bytes{};
|
||||
bytes[0] = version % SINGLE_BYTE_MODULUS;
|
||||
for (size_t i = 1; i < bytes.size(); ++i) {
|
||||
version /= SINGLE_BYTE_MODULUS;
|
||||
bytes[i] = version % SINGLE_BYTE_MODULUS;
|
||||
}
|
||||
|
||||
if (format == TitleVersionFormat::FourElements)
|
||||
return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
|
||||
return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
|
||||
}
|
||||
|
||||
constexpr std::array<const char*, 1> PATCH_TYPE_NAMES{
|
||||
"Update",
|
||||
};
|
||||
|
||||
std::string FormatPatchTypeName(PatchType type) {
|
||||
return PATCH_TYPE_NAMES.at(static_cast<size_t>(type));
|
||||
}
|
||||
|
||||
PatchManager::PatchManager(u64 title_id) : title_id(title_id) {}
|
||||
|
||||
VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
||||
LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
|
||||
|
||||
if (exefs == nullptr)
|
||||
return exefs;
|
||||
|
||||
const auto installed = Service::FileSystem::GetUnionContents();
|
||||
|
||||
// Game Updates
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
const auto update = installed->GetEntry(update_tid, ContentRecordType::Program);
|
||||
if (update != nullptr) {
|
||||
if (update->GetStatus() == Loader::ResultStatus::ErrorMissingBKTRBaseRomFS &&
|
||||
update->GetExeFS() != nullptr) {
|
||||
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
|
||||
FormatTitleVersion(installed->GetEntryVersion(update_tid).get_value_or(0)));
|
||||
exefs = update->GetExeFS();
|
||||
}
|
||||
}
|
||||
|
||||
return exefs;
|
||||
}
|
||||
|
||||
VirtualFile PatchManager::PatchRomFS(VirtualFile romfs, u64 ivfc_offset,
|
||||
ContentRecordType type) const {
|
||||
LOG_INFO(Loader, "Patching RomFS for title_id={:016X}, type={:02X}", title_id,
|
||||
static_cast<u8>(type));
|
||||
|
||||
if (romfs == nullptr)
|
||||
return romfs;
|
||||
|
||||
const auto installed = Service::FileSystem::GetUnionContents();
|
||||
|
||||
// Game Updates
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
const auto update = installed->GetEntryRaw(update_tid, type);
|
||||
if (update != nullptr) {
|
||||
const auto new_nca = std::make_shared<NCA>(update, romfs, ivfc_offset);
|
||||
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
||||
new_nca->GetRomFS() != nullptr) {
|
||||
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
|
||||
FormatTitleVersion(installed->GetEntryVersion(update_tid).get_value_or(0)));
|
||||
romfs = new_nca->GetRomFS();
|
||||
}
|
||||
}
|
||||
|
||||
return romfs;
|
||||
}
|
||||
|
||||
std::map<PatchType, std::string> PatchManager::GetPatchVersionNames() const {
|
||||
std::map<PatchType, std::string> out;
|
||||
const auto installed = Service::FileSystem::GetUnionContents();
|
||||
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
PatchManager update{update_tid};
|
||||
auto [nacp, discard_icon_file] = update.GetControlMetadata();
|
||||
|
||||
if (nacp != nullptr) {
|
||||
out[PatchType::Update] = nacp->GetVersionString();
|
||||
} else {
|
||||
if (installed->HasEntry(update_tid, ContentRecordType::Program)) {
|
||||
const auto meta_ver = installed->GetEntryVersion(update_tid);
|
||||
if (meta_ver == boost::none || meta_ver.get() == 0) {
|
||||
out[PatchType::Update] = "";
|
||||
} else {
|
||||
out[PatchType::Update] =
|
||||
FormatTitleVersion(meta_ver.get(), TitleVersionFormat::ThreeElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::pair<std::shared_ptr<NACP>, VirtualFile> PatchManager::GetControlMetadata() const {
|
||||
const auto& installed{Service::FileSystem::GetUnionContents()};
|
||||
|
||||
const auto base_control_nca = installed->GetEntry(title_id, ContentRecordType::Control);
|
||||
if (base_control_nca == nullptr)
|
||||
return {};
|
||||
|
||||
return ParseControlNCA(base_control_nca);
|
||||
}
|
||||
|
||||
std::pair<std::shared_ptr<NACP>, VirtualFile> PatchManager::ParseControlNCA(
|
||||
const std::shared_ptr<NCA>& nca) const {
|
||||
const auto base_romfs = nca->GetRomFS();
|
||||
if (base_romfs == nullptr)
|
||||
return {};
|
||||
|
||||
const auto romfs = PatchRomFS(base_romfs, nca->GetBaseIVFCOffset(), ContentRecordType::Control);
|
||||
if (romfs == nullptr)
|
||||
return {};
|
||||
|
||||
const auto extracted = ExtractRomFS(romfs);
|
||||
if (extracted == nullptr)
|
||||
return {};
|
||||
|
||||
auto nacp_file = extracted->GetFile("control.nacp");
|
||||
if (nacp_file == nullptr)
|
||||
nacp_file = extracted->GetFile("Control.nacp");
|
||||
|
||||
const auto nacp = nacp_file == nullptr ? nullptr : std::make_shared<NACP>(nacp_file);
|
||||
|
||||
VirtualFile icon_file;
|
||||
for (const auto& language : FileSys::LANGUAGE_NAMES) {
|
||||
icon_file = extracted->GetFile("icon_" + std::string(language) + ".dat");
|
||||
if (icon_file != nullptr)
|
||||
break;
|
||||
}
|
||||
|
||||
return {nacp, icon_file};
|
||||
}
|
||||
} // namespace FileSys
|
||||
63
src/core/file_sys/patch_manager.h
Normal file
63
src/core/file_sys/patch_manager.h
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class NCA;
|
||||
class NACP;
|
||||
|
||||
enum class TitleVersionFormat : u8 {
|
||||
ThreeElements, ///< vX.Y.Z
|
||||
FourElements, ///< vX.Y.Z.W
|
||||
};
|
||||
|
||||
std::string FormatTitleVersion(u32 version,
|
||||
TitleVersionFormat format = TitleVersionFormat::ThreeElements);
|
||||
|
||||
enum class PatchType {
|
||||
Update,
|
||||
};
|
||||
|
||||
std::string FormatPatchTypeName(PatchType type);
|
||||
|
||||
// A centralized class to manage patches to games.
|
||||
class PatchManager {
|
||||
public:
|
||||
explicit PatchManager(u64 title_id);
|
||||
|
||||
// Currently tracked ExeFS patches:
|
||||
// - Game Updates
|
||||
VirtualDir PatchExeFS(VirtualDir exefs) const;
|
||||
|
||||
// Currently tracked RomFS patches:
|
||||
// - Game Updates
|
||||
VirtualFile PatchRomFS(VirtualFile base, u64 ivfc_offset,
|
||||
ContentRecordType type = ContentRecordType::Program) const;
|
||||
|
||||
// Returns a vector of pairs between patch names and patch versions.
|
||||
// i.e. Update v80 will return {Update, 80}
|
||||
std::map<PatchType, std::string> GetPatchVersionNames() const;
|
||||
|
||||
// Given title_id of the program, attempts to get the control data of the update and parse it,
|
||||
// falling back to the base control data.
|
||||
std::pair<std::shared_ptr<NACP>, VirtualFile> GetControlMetadata() const;
|
||||
|
||||
// Version of GetControlMetadata that takes an arbitrary NCA
|
||||
std::pair<std::shared_ptr<NACP>, VirtualFile> ParseControlNCA(
|
||||
const std::shared_ptr<NCA>& nca) const;
|
||||
|
||||
private:
|
||||
u64 title_id;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
@@ -2,7 +2,10 @@
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "common/file_util.h"
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/file_sys/program_metadata.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/bit_field.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "partition_filesystem.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace Loader {
|
||||
enum class ResultStatus : u16;
|
||||
|
||||
616
src/core/file_sys/registered_cache.cpp
Normal file
616
src/core/file_sys/registered_cache.cpp
Normal file
@@ -0,0 +1,616 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <regex>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include "common/assert.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/file_sys/vfs_concat.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace FileSys {
|
||||
std::string RegisteredCacheEntry::DebugInfo() const {
|
||||
return fmt::format("title_id={:016X}, content_type={:02X}", title_id, static_cast<u8>(type));
|
||||
}
|
||||
|
||||
bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs) {
|
||||
return (lhs.title_id < rhs.title_id) || (lhs.title_id == rhs.title_id && lhs.type < rhs.type);
|
||||
}
|
||||
|
||||
static bool FollowsTwoDigitDirFormat(std::string_view name) {
|
||||
static const std::regex two_digit_regex("000000[0-9A-F]{2}", std::regex_constants::ECMAScript |
|
||||
std::regex_constants::icase);
|
||||
return std::regex_match(name.begin(), name.end(), two_digit_regex);
|
||||
}
|
||||
|
||||
static bool FollowsNcaIdFormat(std::string_view name) {
|
||||
static const std::regex nca_id_regex("[0-9A-F]{32}\\.nca", std::regex_constants::ECMAScript |
|
||||
std::regex_constants::icase);
|
||||
return name.size() == 36 && std::regex_match(name.begin(), name.end(), nca_id_regex);
|
||||
}
|
||||
|
||||
static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper,
|
||||
bool within_two_digit) {
|
||||
if (!within_two_digit)
|
||||
return fmt::format("/{}.nca", Common::HexArrayToString(nca_id, second_hex_upper));
|
||||
|
||||
Core::Crypto::SHA256Hash hash{};
|
||||
mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0);
|
||||
return fmt::format("/000000{:02X}/{}.nca", hash[0],
|
||||
Common::HexArrayToString(nca_id, second_hex_upper));
|
||||
}
|
||||
|
||||
static std::string GetCNMTName(TitleType type, u64 title_id) {
|
||||
constexpr std::array<const char*, 9> TITLE_TYPE_NAMES{
|
||||
"SystemProgram",
|
||||
"SystemData",
|
||||
"SystemUpdate",
|
||||
"BootImagePackage",
|
||||
"BootImagePackageSafe",
|
||||
"Application",
|
||||
"Patch",
|
||||
"AddOnContent",
|
||||
"" ///< Currently unknown 'DeltaTitle'
|
||||
};
|
||||
|
||||
auto index = static_cast<size_t>(type);
|
||||
// If the index is after the jump in TitleType, subtract it out.
|
||||
if (index >= static_cast<size_t>(TitleType::Application)) {
|
||||
index -= static_cast<size_t>(TitleType::Application) -
|
||||
static_cast<size_t>(TitleType::FirmwarePackageB);
|
||||
}
|
||||
return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
|
||||
}
|
||||
|
||||
static ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
|
||||
switch (type) {
|
||||
case NCAContentType::Program:
|
||||
// TODO(DarkLordZach): Differentiate between Program and Patch
|
||||
return ContentRecordType::Program;
|
||||
case NCAContentType::Meta:
|
||||
return ContentRecordType::Meta;
|
||||
case NCAContentType::Control:
|
||||
return ContentRecordType::Control;
|
||||
case NCAContentType::Data:
|
||||
case NCAContentType::Data_Unknown5:
|
||||
return ContentRecordType::Data;
|
||||
case NCAContentType::Manual:
|
||||
// TODO(DarkLordZach): Peek at NCA contents to differentiate Manual and Legal.
|
||||
return ContentRecordType::Manual;
|
||||
default:
|
||||
UNREACHABLE_MSG("Invalid NCAContentType={:02X}", static_cast<u8>(type));
|
||||
}
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCache::OpenFileOrDirectoryConcat(const VirtualDir& dir,
|
||||
std::string_view path) const {
|
||||
if (dir->GetFileRelative(path) != nullptr)
|
||||
return dir->GetFileRelative(path);
|
||||
if (dir->GetDirectoryRelative(path) != nullptr) {
|
||||
const auto nca_dir = dir->GetDirectoryRelative(path);
|
||||
VirtualFile file = nullptr;
|
||||
|
||||
const auto files = nca_dir->GetFiles();
|
||||
if (files.size() == 1 && files[0]->GetName() == "00") {
|
||||
file = files[0];
|
||||
} else {
|
||||
std::vector<VirtualFile> concat;
|
||||
// Since the files are a two-digit hex number, max is FF.
|
||||
for (size_t i = 0; i < 0x100; ++i) {
|
||||
auto next = nca_dir->GetFile(fmt::format("{:02X}", i));
|
||||
if (next != nullptr) {
|
||||
concat.push_back(std::move(next));
|
||||
} else {
|
||||
next = nca_dir->GetFile(fmt::format("{:02x}", i));
|
||||
if (next != nullptr)
|
||||
concat.push_back(std::move(next));
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (concat.empty())
|
||||
return nullptr;
|
||||
|
||||
file = FileSys::ConcatenateFiles(concat);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCache::GetFileAtID(NcaID id) const {
|
||||
VirtualFile file;
|
||||
// Try all four modes of file storage:
|
||||
// (bit 1 = uppercase/lower, bit 0 = within a two-digit dir)
|
||||
// 00: /000000**/{:032X}.nca
|
||||
// 01: /{:032X}.nca
|
||||
// 10: /000000**/{:032x}.nca
|
||||
// 11: /{:032x}.nca
|
||||
for (u8 i = 0; i < 4; ++i) {
|
||||
const auto path = GetRelativePathFromNcaID(id, (i & 0b10) == 0, (i & 0b01) == 0);
|
||||
file = OpenFileOrDirectoryConcat(dir, path);
|
||||
if (file != nullptr)
|
||||
return file;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
static boost::optional<NcaID> CheckMapForContentRecord(
|
||||
const boost::container::flat_map<u64, CNMT>& map, u64 title_id, ContentRecordType type) {
|
||||
if (map.find(title_id) == map.end())
|
||||
return boost::none;
|
||||
|
||||
const auto& cnmt = map.at(title_id);
|
||||
|
||||
const auto iter = std::find_if(cnmt.GetContentRecords().begin(), cnmt.GetContentRecords().end(),
|
||||
[type](const ContentRecord& rec) { return rec.type == type; });
|
||||
if (iter == cnmt.GetContentRecords().end())
|
||||
return boost::none;
|
||||
|
||||
return boost::make_optional(iter->nca_id);
|
||||
}
|
||||
|
||||
boost::optional<NcaID> RegisteredCache::GetNcaIDFromMetadata(u64 title_id,
|
||||
ContentRecordType type) const {
|
||||
if (type == ContentRecordType::Meta && meta_id.find(title_id) != meta_id.end())
|
||||
return meta_id.at(title_id);
|
||||
|
||||
const auto res1 = CheckMapForContentRecord(yuzu_meta, title_id, type);
|
||||
if (res1 != boost::none)
|
||||
return res1;
|
||||
return CheckMapForContentRecord(meta, title_id, type);
|
||||
}
|
||||
|
||||
std::vector<NcaID> RegisteredCache::AccumulateFiles() const {
|
||||
std::vector<NcaID> ids;
|
||||
for (const auto& d2_dir : dir->GetSubdirectories()) {
|
||||
if (FollowsNcaIdFormat(d2_dir->GetName())) {
|
||||
ids.push_back(Common::HexStringToArray<0x10, true>(d2_dir->GetName().substr(0, 0x20)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!FollowsTwoDigitDirFormat(d2_dir->GetName()))
|
||||
continue;
|
||||
|
||||
for (const auto& nca_dir : d2_dir->GetSubdirectories()) {
|
||||
if (!FollowsNcaIdFormat(nca_dir->GetName()))
|
||||
continue;
|
||||
|
||||
ids.push_back(Common::HexStringToArray<0x10, true>(nca_dir->GetName().substr(0, 0x20)));
|
||||
}
|
||||
|
||||
for (const auto& nca_file : d2_dir->GetFiles()) {
|
||||
if (!FollowsNcaIdFormat(nca_file->GetName()))
|
||||
continue;
|
||||
|
||||
ids.push_back(
|
||||
Common::HexStringToArray<0x10, true>(nca_file->GetName().substr(0, 0x20)));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& d2_file : dir->GetFiles()) {
|
||||
if (FollowsNcaIdFormat(d2_file->GetName()))
|
||||
ids.push_back(Common::HexStringToArray<0x10, true>(d2_file->GetName().substr(0, 0x20)));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
void RegisteredCache::ProcessFiles(const std::vector<NcaID>& ids) {
|
||||
for (const auto& id : ids) {
|
||||
const auto file = GetFileAtID(id);
|
||||
|
||||
if (file == nullptr)
|
||||
continue;
|
||||
const auto nca = std::make_shared<NCA>(parser(file, id));
|
||||
if (nca->GetStatus() != Loader::ResultStatus::Success ||
|
||||
nca->GetType() != NCAContentType::Meta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto section0 = nca->GetSubdirectories()[0];
|
||||
|
||||
for (const auto& section0_file : section0->GetFiles()) {
|
||||
if (section0_file->GetExtension() != "cnmt")
|
||||
continue;
|
||||
|
||||
meta.insert_or_assign(nca->GetTitleId(), CNMT(section0_file));
|
||||
meta_id.insert_or_assign(nca->GetTitleId(), id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RegisteredCache::AccumulateYuzuMeta() {
|
||||
const auto dir = this->dir->GetSubdirectory("yuzu_meta");
|
||||
if (dir == nullptr)
|
||||
return;
|
||||
|
||||
for (const auto& file : dir->GetFiles()) {
|
||||
if (file->GetExtension() != "cnmt")
|
||||
continue;
|
||||
|
||||
CNMT cnmt(file);
|
||||
yuzu_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt));
|
||||
}
|
||||
}
|
||||
|
||||
void RegisteredCache::Refresh() {
|
||||
if (dir == nullptr)
|
||||
return;
|
||||
const auto ids = AccumulateFiles();
|
||||
ProcessFiles(ids);
|
||||
AccumulateYuzuMeta();
|
||||
}
|
||||
|
||||
RegisteredCache::RegisteredCache(VirtualDir dir_, RegisteredCacheParsingFunction parsing_function)
|
||||
: dir(std::move(dir_)), parser(std::move(parsing_function)) {
|
||||
Refresh();
|
||||
}
|
||||
|
||||
RegisteredCache::~RegisteredCache() = default;
|
||||
|
||||
bool RegisteredCache::HasEntry(u64 title_id, ContentRecordType type) const {
|
||||
return GetEntryRaw(title_id, type) != nullptr;
|
||||
}
|
||||
|
||||
bool RegisteredCache::HasEntry(RegisteredCacheEntry entry) const {
|
||||
return GetEntryRaw(entry) != nullptr;
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCache::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
|
||||
const auto id = GetNcaIDFromMetadata(title_id, type);
|
||||
if (id == boost::none)
|
||||
return nullptr;
|
||||
|
||||
return GetFileAtID(id.get());
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCache::GetEntryUnparsed(RegisteredCacheEntry entry) const {
|
||||
return GetEntryUnparsed(entry.title_id, entry.type);
|
||||
}
|
||||
|
||||
boost::optional<u32> RegisteredCache::GetEntryVersion(u64 title_id) const {
|
||||
const auto meta_iter = meta.find(title_id);
|
||||
if (meta_iter != meta.end())
|
||||
return meta_iter->second.GetTitleVersion();
|
||||
|
||||
const auto yuzu_meta_iter = yuzu_meta.find(title_id);
|
||||
if (yuzu_meta_iter != yuzu_meta.end())
|
||||
return yuzu_meta_iter->second.GetTitleVersion();
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const {
|
||||
const auto id = GetNcaIDFromMetadata(title_id, type);
|
||||
if (id == boost::none)
|
||||
return nullptr;
|
||||
|
||||
return parser(GetFileAtID(id.get()), id.get());
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCache::GetEntryRaw(RegisteredCacheEntry entry) const {
|
||||
return GetEntryRaw(entry.title_id, entry.type);
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> RegisteredCache::GetEntry(u64 title_id, ContentRecordType type) const {
|
||||
const auto raw = GetEntryRaw(title_id, type);
|
||||
if (raw == nullptr)
|
||||
return nullptr;
|
||||
return std::make_shared<NCA>(raw);
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> RegisteredCache::GetEntry(RegisteredCacheEntry entry) const {
|
||||
return GetEntry(entry.title_id, entry.type);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void RegisteredCache::IterateAllMetadata(
|
||||
std::vector<T>& out, std::function<T(const CNMT&, const ContentRecord&)> proc,
|
||||
std::function<bool(const CNMT&, const ContentRecord&)> filter) const {
|
||||
for (const auto& kv : meta) {
|
||||
const auto& cnmt = kv.second;
|
||||
if (filter(cnmt, EMPTY_META_CONTENT_RECORD))
|
||||
out.push_back(proc(cnmt, EMPTY_META_CONTENT_RECORD));
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) {
|
||||
out.push_back(proc(cnmt, rec));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& kv : yuzu_meta) {
|
||||
const auto& cnmt = kv.second;
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
if (GetFileAtID(rec.nca_id) != nullptr && filter(cnmt, rec)) {
|
||||
out.push_back(proc(cnmt, rec));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RegisteredCacheEntry> RegisteredCache::ListEntries() const {
|
||||
std::vector<RegisteredCacheEntry> out;
|
||||
IterateAllMetadata<RegisteredCacheEntry>(
|
||||
out,
|
||||
[](const CNMT& c, const ContentRecord& r) {
|
||||
return RegisteredCacheEntry{c.GetTitleID(), r.type};
|
||||
},
|
||||
[](const CNMT& c, const ContentRecord& r) { return true; });
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<RegisteredCacheEntry> RegisteredCache::ListEntriesFilter(
|
||||
boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type,
|
||||
boost::optional<u64> title_id) const {
|
||||
std::vector<RegisteredCacheEntry> out;
|
||||
IterateAllMetadata<RegisteredCacheEntry>(
|
||||
out,
|
||||
[](const CNMT& c, const ContentRecord& r) {
|
||||
return RegisteredCacheEntry{c.GetTitleID(), r.type};
|
||||
},
|
||||
[&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) {
|
||||
if (title_type != boost::none && title_type.get() != c.GetType())
|
||||
return false;
|
||||
if (record_type != boost::none && record_type.get() != r.type)
|
||||
return false;
|
||||
if (title_id != boost::none && title_id.get() != c.GetTitleID())
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::shared_ptr<NCA> GetNCAFromNSPForID(std::shared_ptr<NSP> nsp, const NcaID& id) {
|
||||
const auto file = nsp->GetFile(fmt::format("{}.nca", Common::HexArrayToString(id, false)));
|
||||
if (file == nullptr)
|
||||
return nullptr;
|
||||
return std::make_shared<NCA>(file);
|
||||
}
|
||||
|
||||
InstallResult RegisteredCache::InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists,
|
||||
const VfsCopyFunction& copy) {
|
||||
return InstallEntry(xci->GetSecurePartitionNSP(), overwrite_if_exists, copy);
|
||||
}
|
||||
|
||||
InstallResult RegisteredCache::InstallEntry(std::shared_ptr<NSP> nsp, bool overwrite_if_exists,
|
||||
const VfsCopyFunction& copy) {
|
||||
const auto& ncas = nsp->GetNCAsCollapsed();
|
||||
const auto& meta_iter = std::find_if(ncas.begin(), ncas.end(), [](std::shared_ptr<NCA> nca) {
|
||||
return nca->GetType() == NCAContentType::Meta;
|
||||
});
|
||||
|
||||
if (meta_iter == ncas.end()) {
|
||||
LOG_ERROR(Loader, "The XCI you are attempting to install does not have a metadata NCA and "
|
||||
"is therefore malformed. Double check your encryption keys.");
|
||||
return InstallResult::ErrorMetaFailed;
|
||||
}
|
||||
|
||||
// Install Metadata File
|
||||
const auto meta_id_raw = (*meta_iter)->GetName().substr(0, 32);
|
||||
const auto meta_id = Common::HexStringToArray<16>(meta_id_raw);
|
||||
|
||||
const auto res = RawInstallNCA(*meta_iter, copy, overwrite_if_exists, meta_id);
|
||||
if (res != InstallResult::Success)
|
||||
return res;
|
||||
|
||||
// Install all the other NCAs
|
||||
const auto section0 = (*meta_iter)->GetSubdirectories()[0];
|
||||
const auto cnmt_file = section0->GetFiles()[0];
|
||||
const CNMT cnmt(cnmt_file);
|
||||
for (const auto& record : cnmt.GetContentRecords()) {
|
||||
const auto nca = GetNCAFromNSPForID(nsp, record.nca_id);
|
||||
if (nca == nullptr)
|
||||
return InstallResult::ErrorCopyFailed;
|
||||
const auto res2 = RawInstallNCA(nca, copy, overwrite_if_exists, record.nca_id);
|
||||
if (res2 != InstallResult::Success)
|
||||
return res2;
|
||||
}
|
||||
|
||||
Refresh();
|
||||
return InstallResult::Success;
|
||||
}
|
||||
|
||||
InstallResult RegisteredCache::InstallEntry(std::shared_ptr<NCA> nca, TitleType type,
|
||||
bool overwrite_if_exists, const VfsCopyFunction& copy) {
|
||||
CNMTHeader header{
|
||||
nca->GetTitleId(), ///< Title ID
|
||||
0, ///< Ignore/Default title version
|
||||
type, ///< Type
|
||||
{}, ///< Padding
|
||||
0x10, ///< Default table offset
|
||||
1, ///< 1 Content Entry
|
||||
0, ///< No Meta Entries
|
||||
{}, ///< Padding
|
||||
};
|
||||
OptionalHeader opt_header{0, 0};
|
||||
ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca->GetType()), {}};
|
||||
const auto& data = nca->GetBaseFile()->ReadBytes(0x100000);
|
||||
mbedtls_sha256(data.data(), data.size(), c_rec.hash.data(), 0);
|
||||
memcpy(&c_rec.nca_id, &c_rec.hash, 16);
|
||||
const CNMT new_cnmt(header, opt_header, {c_rec}, {});
|
||||
if (!RawInstallYuzuMeta(new_cnmt))
|
||||
return InstallResult::ErrorMetaFailed;
|
||||
return RawInstallNCA(nca, copy, overwrite_if_exists, c_rec.nca_id);
|
||||
}
|
||||
|
||||
InstallResult RegisteredCache::RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy,
|
||||
bool overwrite_if_exists,
|
||||
boost::optional<NcaID> override_id) {
|
||||
const auto in = nca->GetBaseFile();
|
||||
Core::Crypto::SHA256Hash hash{};
|
||||
|
||||
// Calculate NcaID
|
||||
// NOTE: Because computing the SHA256 of an entire NCA is quite expensive (especially if the
|
||||
// game is massive), we're going to cheat and only hash the first MB of the NCA.
|
||||
// Also, for XCIs the NcaID matters, so if the override id isn't none, use that.
|
||||
NcaID id{};
|
||||
if (override_id == boost::none) {
|
||||
const auto& data = in->ReadBytes(0x100000);
|
||||
mbedtls_sha256(data.data(), data.size(), hash.data(), 0);
|
||||
memcpy(id.data(), hash.data(), 16);
|
||||
} else {
|
||||
id = override_id.get();
|
||||
}
|
||||
|
||||
std::string path = GetRelativePathFromNcaID(id, false, true);
|
||||
|
||||
if (GetFileAtID(id) != nullptr && !overwrite_if_exists) {
|
||||
LOG_WARNING(Loader, "Attempting to overwrite existing NCA. Skipping...");
|
||||
return InstallResult::ErrorAlreadyExists;
|
||||
}
|
||||
|
||||
if (GetFileAtID(id) != nullptr) {
|
||||
LOG_WARNING(Loader, "Overwriting existing NCA...");
|
||||
VirtualDir c_dir;
|
||||
{ c_dir = dir->GetFileRelative(path)->GetContainingDirectory(); }
|
||||
c_dir->DeleteFile(FileUtil::GetFilename(path));
|
||||
}
|
||||
|
||||
auto out = dir->CreateFileRelative(path);
|
||||
if (out == nullptr)
|
||||
return InstallResult::ErrorCopyFailed;
|
||||
return copy(in, out) ? InstallResult::Success : InstallResult::ErrorCopyFailed;
|
||||
}
|
||||
|
||||
bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
|
||||
// Reasoning behind this method can be found in the comment for InstallEntry, NCA overload.
|
||||
const auto dir = this->dir->CreateDirectoryRelative("yuzu_meta");
|
||||
const auto filename = GetCNMTName(cnmt.GetType(), cnmt.GetTitleID());
|
||||
if (dir->GetFile(filename) == nullptr) {
|
||||
auto out = dir->CreateFile(filename);
|
||||
const auto buffer = cnmt.Serialize();
|
||||
out->Resize(buffer.size());
|
||||
out->WriteBytes(buffer);
|
||||
} else {
|
||||
auto out = dir->GetFile(filename);
|
||||
CNMT old_cnmt(out);
|
||||
// Returns true on change
|
||||
if (old_cnmt.UnionRecords(cnmt)) {
|
||||
out->Resize(0);
|
||||
const auto buffer = old_cnmt.Serialize();
|
||||
out->Resize(buffer.size());
|
||||
out->WriteBytes(buffer);
|
||||
}
|
||||
}
|
||||
Refresh();
|
||||
return std::find_if(yuzu_meta.begin(), yuzu_meta.end(),
|
||||
[&cnmt](const std::pair<u64, CNMT>& kv) {
|
||||
return kv.second.GetType() == cnmt.GetType() &&
|
||||
kv.second.GetTitleID() == cnmt.GetTitleID();
|
||||
}) != yuzu_meta.end();
|
||||
}
|
||||
|
||||
RegisteredCacheUnion::RegisteredCacheUnion(std::vector<std::shared_ptr<RegisteredCache>> caches)
|
||||
: caches(std::move(caches)) {}
|
||||
|
||||
void RegisteredCacheUnion::Refresh() {
|
||||
for (const auto& c : caches)
|
||||
c->Refresh();
|
||||
}
|
||||
|
||||
bool RegisteredCacheUnion::HasEntry(u64 title_id, ContentRecordType type) const {
|
||||
return std::any_of(caches.begin(), caches.end(), [title_id, type](const auto& cache) {
|
||||
return cache->HasEntry(title_id, type);
|
||||
});
|
||||
}
|
||||
|
||||
bool RegisteredCacheUnion::HasEntry(RegisteredCacheEntry entry) const {
|
||||
return HasEntry(entry.title_id, entry.type);
|
||||
}
|
||||
|
||||
boost::optional<u32> RegisteredCacheUnion::GetEntryVersion(u64 title_id) const {
|
||||
for (const auto& c : caches) {
|
||||
const auto res = c->GetEntryVersion(title_id);
|
||||
if (res != boost::none)
|
||||
return res;
|
||||
}
|
||||
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCacheUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
|
||||
for (const auto& c : caches) {
|
||||
const auto res = c->GetEntryUnparsed(title_id, type);
|
||||
if (res != nullptr)
|
||||
return res;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCacheUnion::GetEntryUnparsed(RegisteredCacheEntry entry) const {
|
||||
return GetEntryUnparsed(entry.title_id, entry.type);
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCacheUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const {
|
||||
for (const auto& c : caches) {
|
||||
const auto res = c->GetEntryRaw(title_id, type);
|
||||
if (res != nullptr)
|
||||
return res;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VirtualFile RegisteredCacheUnion::GetEntryRaw(RegisteredCacheEntry entry) const {
|
||||
return GetEntryRaw(entry.title_id, entry.type);
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> RegisteredCacheUnion::GetEntry(u64 title_id, ContentRecordType type) const {
|
||||
const auto raw = GetEntryRaw(title_id, type);
|
||||
if (raw == nullptr)
|
||||
return nullptr;
|
||||
return std::make_shared<NCA>(raw);
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> RegisteredCacheUnion::GetEntry(RegisteredCacheEntry entry) const {
|
||||
return GetEntry(entry.title_id, entry.type);
|
||||
}
|
||||
|
||||
std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntries() const {
|
||||
std::vector<RegisteredCacheEntry> out;
|
||||
for (const auto& c : caches) {
|
||||
c->IterateAllMetadata<RegisteredCacheEntry>(
|
||||
out,
|
||||
[](const CNMT& c, const ContentRecord& r) {
|
||||
return RegisteredCacheEntry{c.GetTitleID(), r.type};
|
||||
},
|
||||
[](const CNMT& c, const ContentRecord& r) { return true; });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<RegisteredCacheEntry> RegisteredCacheUnion::ListEntriesFilter(
|
||||
boost::optional<TitleType> title_type, boost::optional<ContentRecordType> record_type,
|
||||
boost::optional<u64> title_id) const {
|
||||
std::vector<RegisteredCacheEntry> out;
|
||||
for (const auto& c : caches) {
|
||||
c->IterateAllMetadata<RegisteredCacheEntry>(
|
||||
out,
|
||||
[](const CNMT& c, const ContentRecord& r) {
|
||||
return RegisteredCacheEntry{c.GetTitleID(), r.type};
|
||||
},
|
||||
[&title_type, &record_type, &title_id](const CNMT& c, const ContentRecord& r) {
|
||||
if (title_type != boost::none && title_type.get() != c.GetType())
|
||||
return false;
|
||||
if (record_type != boost::none && record_type.get() != r.type)
|
||||
return false;
|
||||
if (title_id != boost::none && title_id.get() != c.GetTitleID())
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace FileSys
|
||||
174
src/core/file_sys/registered_cache.h
Normal file
174
src/core/file_sys/registered_cache.h
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
class CNMT;
|
||||
class NCA;
|
||||
class NSP;
|
||||
class XCI;
|
||||
|
||||
enum class ContentRecordType : u8;
|
||||
enum class TitleType : u8;
|
||||
|
||||
struct ContentRecord;
|
||||
|
||||
using NcaID = std::array<u8, 0x10>;
|
||||
using RegisteredCacheParsingFunction = std::function<VirtualFile(const VirtualFile&, const NcaID&)>;
|
||||
using VfsCopyFunction = std::function<bool(VirtualFile, VirtualFile)>;
|
||||
|
||||
enum class InstallResult {
|
||||
Success,
|
||||
ErrorAlreadyExists,
|
||||
ErrorCopyFailed,
|
||||
ErrorMetaFailed,
|
||||
};
|
||||
|
||||
struct RegisteredCacheEntry {
|
||||
u64 title_id;
|
||||
ContentRecordType type;
|
||||
|
||||
std::string DebugInfo() const;
|
||||
};
|
||||
|
||||
constexpr u64 GetUpdateTitleID(u64 base_title_id) {
|
||||
return base_title_id | 0x800;
|
||||
}
|
||||
|
||||
// boost flat_map requires operator< for O(log(n)) lookups.
|
||||
bool operator<(const RegisteredCacheEntry& lhs, const RegisteredCacheEntry& rhs);
|
||||
|
||||
/*
|
||||
* A class that catalogues NCAs in the registered directory structure.
|
||||
* Nintendo's registered format follows this structure:
|
||||
*
|
||||
* Root
|
||||
* | 000000XX <- XX is the ____ two digits of the NcaID
|
||||
* | <hash>.nca <- hash is the NcaID (first half of SHA256 over entire file) (folder)
|
||||
* | 00
|
||||
* | 01 <- Actual content split along 4GB boundaries. (optional)
|
||||
*
|
||||
* (This impl also supports substituting the nca dir for an nca file, as that's more convenient when
|
||||
* 4GB splitting can be ignored.)
|
||||
*/
|
||||
class RegisteredCache {
|
||||
friend class RegisteredCacheUnion;
|
||||
|
||||
public:
|
||||
// Parsing function defines the conversion from raw file to NCA. If there are other steps
|
||||
// besides creating the NCA from the file (e.g. NAX0 on SD Card), that should go in a custom
|
||||
// parsing function.
|
||||
explicit RegisteredCache(VirtualDir dir,
|
||||
RegisteredCacheParsingFunction parsing_function =
|
||||
[](const VirtualFile& file, const NcaID& id) { return file; });
|
||||
~RegisteredCache();
|
||||
|
||||
void Refresh();
|
||||
|
||||
bool HasEntry(u64 title_id, ContentRecordType type) const;
|
||||
bool HasEntry(RegisteredCacheEntry entry) const;
|
||||
|
||||
boost::optional<u32> GetEntryVersion(u64 title_id) const;
|
||||
|
||||
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const;
|
||||
VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const;
|
||||
|
||||
VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const;
|
||||
VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const;
|
||||
|
||||
std::shared_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const;
|
||||
std::shared_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const;
|
||||
|
||||
std::vector<RegisteredCacheEntry> ListEntries() const;
|
||||
// If a parameter is not boost::none, it will be filtered for from all entries.
|
||||
std::vector<RegisteredCacheEntry> ListEntriesFilter(
|
||||
boost::optional<TitleType> title_type = boost::none,
|
||||
boost::optional<ContentRecordType> record_type = boost::none,
|
||||
boost::optional<u64> title_id = boost::none) const;
|
||||
|
||||
// Raw copies all the ncas from the xci/nsp to the csache. Does some quick checks to make sure
|
||||
// there is a meta NCA and all of them are accessible.
|
||||
InstallResult InstallEntry(std::shared_ptr<XCI> xci, bool overwrite_if_exists = false,
|
||||
const VfsCopyFunction& copy = &VfsRawCopy);
|
||||
InstallResult InstallEntry(std::shared_ptr<NSP> nsp, bool overwrite_if_exists = false,
|
||||
const VfsCopyFunction& copy = &VfsRawCopy);
|
||||
|
||||
// Due to the fact that we must use Meta-type NCAs to determine the existance of files, this
|
||||
// poses quite a challenge. Instead of creating a new meta NCA for this file, yuzu will create a
|
||||
// dir inside the NAND called 'yuzu_meta' and store the raw CNMT there.
|
||||
// TODO(DarkLordZach): Author real meta-type NCAs and install those.
|
||||
InstallResult InstallEntry(std::shared_ptr<NCA> nca, TitleType type,
|
||||
bool overwrite_if_exists = false,
|
||||
const VfsCopyFunction& copy = &VfsRawCopy);
|
||||
|
||||
private:
|
||||
template <typename T>
|
||||
void IterateAllMetadata(std::vector<T>& out,
|
||||
std::function<T(const CNMT&, const ContentRecord&)> proc,
|
||||
std::function<bool(const CNMT&, const ContentRecord&)> filter) const;
|
||||
std::vector<NcaID> AccumulateFiles() const;
|
||||
void ProcessFiles(const std::vector<NcaID>& ids);
|
||||
void AccumulateYuzuMeta();
|
||||
boost::optional<NcaID> GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const;
|
||||
VirtualFile GetFileAtID(NcaID id) const;
|
||||
VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& dir, std::string_view path) const;
|
||||
InstallResult RawInstallNCA(std::shared_ptr<NCA> nca, const VfsCopyFunction& copy,
|
||||
bool overwrite_if_exists,
|
||||
boost::optional<NcaID> override_id = boost::none);
|
||||
bool RawInstallYuzuMeta(const CNMT& cnmt);
|
||||
|
||||
VirtualDir dir;
|
||||
RegisteredCacheParsingFunction parser;
|
||||
// maps tid -> NcaID of meta
|
||||
boost::container::flat_map<u64, NcaID> meta_id;
|
||||
// maps tid -> meta
|
||||
boost::container::flat_map<u64, CNMT> meta;
|
||||
// maps tid -> meta for CNMT in yuzu_meta
|
||||
boost::container::flat_map<u64, CNMT> yuzu_meta;
|
||||
};
|
||||
|
||||
// Combines multiple RegisteredCaches (i.e. SysNAND, UserNAND, SDMC) into one interface.
|
||||
class RegisteredCacheUnion {
|
||||
public:
|
||||
explicit RegisteredCacheUnion(std::vector<std::shared_ptr<RegisteredCache>> caches);
|
||||
|
||||
void Refresh();
|
||||
|
||||
bool HasEntry(u64 title_id, ContentRecordType type) const;
|
||||
bool HasEntry(RegisteredCacheEntry entry) const;
|
||||
|
||||
boost::optional<u32> GetEntryVersion(u64 title_id) const;
|
||||
|
||||
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const;
|
||||
VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const;
|
||||
|
||||
VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const;
|
||||
VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const;
|
||||
|
||||
std::shared_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const;
|
||||
std::shared_ptr<NCA> GetEntry(RegisteredCacheEntry entry) const;
|
||||
|
||||
std::vector<RegisteredCacheEntry> ListEntries() const;
|
||||
// If a parameter is not boost::none, it will be filtered for from all entries.
|
||||
std::vector<RegisteredCacheEntry> ListEntriesFilter(
|
||||
boost::optional<TitleType> title_type = boost::none,
|
||||
boost::optional<ContentRecordType> record_type = boost::none,
|
||||
boost::optional<u64> title_id = boost::none) const;
|
||||
|
||||
private:
|
||||
std::vector<std::shared_ptr<RegisteredCache>> caches;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
@@ -65,7 +65,7 @@ void ProcessFile(VirtualFile file, size_t file_offset, size_t data_offset, u32 t
|
||||
auto entry = GetEntry<FileEntry>(file, file_offset + this_file_offset);
|
||||
|
||||
parent->AddFile(std::make_shared<OffsetVfsFile>(
|
||||
file, entry.first.size, entry.first.offset + data_offset, entry.second, parent));
|
||||
file, entry.first.size, entry.first.offset + data_offset, entry.second));
|
||||
|
||||
if (entry.first.sibling == ROMFS_ENTRY_EMPTY)
|
||||
break;
|
||||
@@ -79,7 +79,7 @@ void ProcessDirectory(VirtualFile file, size_t dir_offset, size_t file_offset, s
|
||||
while (true) {
|
||||
auto entry = GetEntry<DirectoryEntry>(file, dir_offset + this_dir_offset);
|
||||
auto current = std::make_shared<VectorVfsDirectory>(
|
||||
std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parent, entry.second);
|
||||
std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, entry.second);
|
||||
|
||||
if (entry.first.child_file != ROMFS_ENTRY_EMPTY) {
|
||||
ProcessFile(file, file_offset, data_offset, entry.first.child_file, current);
|
||||
@@ -108,9 +108,9 @@ VirtualDir ExtractRomFS(VirtualFile file) {
|
||||
const u64 file_offset = header.file_meta.offset;
|
||||
const u64 dir_offset = header.directory_meta.offset + 4;
|
||||
|
||||
const auto root =
|
||||
auto root =
|
||||
std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{}, std::vector<VirtualDir>{},
|
||||
file->GetContainingDirectory(), file->GetName());
|
||||
file->GetName(), file->GetContainingDirectory());
|
||||
|
||||
ProcessDirectory(file, dir_offset, file_offset, header.data_offset, 0, root);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <array>
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
|
||||
@@ -2,24 +2,71 @@
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include "common/assert.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/romfs_factory.h"
|
||||
#include "core/hle/kernel/process.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
RomFSFactory::RomFSFactory(Loader::AppLoader& app_loader) {
|
||||
// Load the RomFS from the app
|
||||
if (Loader::ResultStatus::Success != app_loader.ReadRomFS(file)) {
|
||||
if (app_loader.ReadRomFS(file) != Loader::ResultStatus::Success) {
|
||||
LOG_ERROR(Service_FS, "Unable to read RomFS!");
|
||||
}
|
||||
|
||||
updatable = app_loader.IsRomFSUpdatable();
|
||||
ivfc_offset = app_loader.ReadRomFSIVFCOffset();
|
||||
}
|
||||
|
||||
ResultVal<VirtualFile> RomFSFactory::OpenCurrentProcess() {
|
||||
if (!updatable)
|
||||
return MakeResult<VirtualFile>(file);
|
||||
|
||||
const PatchManager patch_manager(Core::CurrentProcess()->program_id);
|
||||
return MakeResult<VirtualFile>(patch_manager.PatchRomFS(file, ivfc_offset));
|
||||
}
|
||||
|
||||
ResultVal<VirtualFile> RomFSFactory::Open(u64 title_id, StorageId storage, ContentRecordType type) {
|
||||
switch (storage) {
|
||||
case StorageId::NandSystem: {
|
||||
const auto res = Service::FileSystem::GetSystemNANDContents()->GetEntry(title_id, type);
|
||||
if (res == nullptr) {
|
||||
// TODO(DarkLordZach): Find the right error code to use here
|
||||
return ResultCode(-1);
|
||||
}
|
||||
const auto romfs = res->GetRomFS();
|
||||
if (romfs == nullptr) {
|
||||
// TODO(DarkLordZach): Find the right error code to use here
|
||||
return ResultCode(-1);
|
||||
}
|
||||
return MakeResult<VirtualFile>(romfs);
|
||||
}
|
||||
case StorageId::NandUser: {
|
||||
const auto res = Service::FileSystem::GetUserNANDContents()->GetEntry(title_id, type);
|
||||
if (res == nullptr) {
|
||||
// TODO(DarkLordZach): Find the right error code to use here
|
||||
return ResultCode(-1);
|
||||
}
|
||||
const auto romfs = res->GetRomFS();
|
||||
if (romfs == nullptr) {
|
||||
// TODO(DarkLordZach): Find the right error code to use here
|
||||
return ResultCode(-1);
|
||||
}
|
||||
return MakeResult<VirtualFile>(romfs);
|
||||
}
|
||||
default:
|
||||
UNIMPLEMENTED_MSG("Unimplemented storage_id={:02X}", static_cast<u8>(storage));
|
||||
}
|
||||
}
|
||||
|
||||
ResultVal<VirtualFile> RomFSFactory::Open(u64 title_id) {
|
||||
// TODO(DarkLordZach): Use title id.
|
||||
return MakeResult<VirtualFile>(file);
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -6,20 +6,38 @@
|
||||
|
||||
#include <memory>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/hle/result.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace Loader {
|
||||
class AppLoader;
|
||||
} // namespace Loader
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
enum class ContentRecordType : u8;
|
||||
|
||||
enum class StorageId : u8 {
|
||||
None = 0,
|
||||
Host = 1,
|
||||
GameCard = 2,
|
||||
NandSystem = 3,
|
||||
NandUser = 4,
|
||||
SdCard = 5,
|
||||
};
|
||||
|
||||
/// File system interface to the RomFS archive
|
||||
class RomFSFactory {
|
||||
public:
|
||||
explicit RomFSFactory(Loader::AppLoader& app_loader);
|
||||
|
||||
ResultVal<VirtualFile> Open(u64 title_id);
|
||||
ResultVal<VirtualFile> OpenCurrentProcess();
|
||||
ResultVal<VirtualFile> Open(u64 title_id, StorageId storage, ContentRecordType type);
|
||||
|
||||
private:
|
||||
VirtualFile file;
|
||||
bool updatable;
|
||||
u64 ivfc_offset;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <memory>
|
||||
#include "common/assert.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/savedata_factory.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/hle/kernel/process.h"
|
||||
|
||||
namespace FileSys {
|
||||
@@ -73,7 +75,7 @@ ResultVal<VirtualDir> SaveDataFactory::Open(SaveDataSpaceId space, SaveDataDescr
|
||||
}
|
||||
|
||||
std::string SaveDataFactory::GetFullPath(SaveDataSpaceId space, SaveDataType type, u64 title_id,
|
||||
u128 user_id, u64 save_id) const {
|
||||
u128 user_id, u64 save_id) {
|
||||
// According to switchbrew, if a save is of type SaveData and the title id field is 0, it should
|
||||
// be interpreted as the title id of the current process.
|
||||
if (type == SaveDataType::SaveData && title_id == 0)
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace FileSys {
|
||||
@@ -49,11 +51,11 @@ public:
|
||||
|
||||
ResultVal<VirtualDir> Open(SaveDataSpaceId space, SaveDataDescriptor meta);
|
||||
|
||||
static std::string GetFullPath(SaveDataSpaceId space, SaveDataType type, u64 title_id,
|
||||
u128 user_id, u64 save_id);
|
||||
|
||||
private:
|
||||
VirtualDir dir;
|
||||
|
||||
std::string GetFullPath(SaveDataSpaceId space, SaveDataType type, u64 title_id, u128 user_id,
|
||||
u64 save_id) const;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -3,15 +3,27 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <memory>
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/sdmc_factory.h"
|
||||
#include "core/file_sys/xts_archive.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
SDMCFactory::SDMCFactory(VirtualDir dir) : dir(std::move(dir)) {}
|
||||
SDMCFactory::SDMCFactory(VirtualDir dir_)
|
||||
: dir(std::move(dir_)), contents(std::make_shared<RegisteredCache>(
|
||||
GetOrCreateDirectoryRelative(dir, "/Nintendo/Contents/registered"),
|
||||
[](const VirtualFile& file, const NcaID& id) {
|
||||
return std::make_shared<NAX>(file, id)->GetDecrypted();
|
||||
})) {}
|
||||
|
||||
SDMCFactory::~SDMCFactory() = default;
|
||||
|
||||
ResultVal<VirtualDir> SDMCFactory::Open() {
|
||||
return MakeResult<VirtualDir>(dir);
|
||||
}
|
||||
|
||||
std::shared_ptr<RegisteredCache> SDMCFactory::GetSDMCContents() const {
|
||||
return contents;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -4,19 +4,27 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class RegisteredCache;
|
||||
|
||||
/// File system interface to the SDCard archive
|
||||
class SDMCFactory {
|
||||
public:
|
||||
explicit SDMCFactory(VirtualDir dir);
|
||||
~SDMCFactory();
|
||||
|
||||
ResultVal<VirtualDir> Open();
|
||||
std::shared_ptr<RegisteredCache> GetSDMCContents() const;
|
||||
|
||||
private:
|
||||
VirtualDir dir;
|
||||
|
||||
std::shared_ptr<RegisteredCache> contents;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
245
src/core/file_sys/submission_package.cpp
Normal file
245
src/core/file_sys/submission_package.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
#include <fmt/ostream.h>
|
||||
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/partition_filesystem.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace FileSys {
|
||||
NSP::NSP(VirtualFile file_)
|
||||
: file(std::move(file_)), status{Loader::ResultStatus::Success},
|
||||
pfs(std::make_shared<PartitionFilesystem>(file)) {
|
||||
if (pfs->GetStatus() != Loader::ResultStatus::Success) {
|
||||
status = pfs->GetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDirectoryExeFS(pfs)) {
|
||||
extracted = true;
|
||||
exefs = pfs;
|
||||
|
||||
const auto& files = pfs->GetFiles();
|
||||
const auto romfs_iter =
|
||||
std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& file) {
|
||||
return file->GetName().find(".romfs") != std::string::npos;
|
||||
});
|
||||
if (romfs_iter != files.end())
|
||||
romfs = *romfs_iter;
|
||||
return;
|
||||
}
|
||||
|
||||
extracted = false;
|
||||
const auto files = pfs->GetFiles();
|
||||
|
||||
Core::Crypto::KeyManager keys;
|
||||
for (const auto& ticket_file : files) {
|
||||
if (ticket_file->GetExtension() == "tik") {
|
||||
if (ticket_file == nullptr ||
|
||||
ticket_file->GetSize() <
|
||||
Core::Crypto::TICKET_FILE_TITLEKEY_OFFSET + sizeof(Core::Crypto::Key128)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Core::Crypto::Key128 key{};
|
||||
ticket_file->Read(key.data(), key.size(), Core::Crypto::TICKET_FILE_TITLEKEY_OFFSET);
|
||||
std::string_view name_only(ticket_file->GetName());
|
||||
name_only.remove_suffix(4);
|
||||
const auto rights_id_raw = Common::HexStringToArray<16>(name_only);
|
||||
u128 rights_id;
|
||||
std::memcpy(rights_id.data(), rights_id_raw.data(), sizeof(u128));
|
||||
keys.SetKey(Core::Crypto::S128KeyType::Titlekey, key, rights_id[1], rights_id[0]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& outer_file : files) {
|
||||
if (outer_file->GetName().substr(outer_file->GetName().size() - 9) == ".cnmt.nca") {
|
||||
const auto nca = std::make_shared<NCA>(outer_file);
|
||||
if (nca->GetStatus() != Loader::ResultStatus::Success) {
|
||||
program_status[nca->GetTitleId()] = nca->GetStatus();
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto section0 = nca->GetSubdirectories()[0];
|
||||
|
||||
for (const auto& inner_file : section0->GetFiles()) {
|
||||
if (inner_file->GetExtension() != "cnmt")
|
||||
continue;
|
||||
|
||||
const CNMT cnmt(inner_file);
|
||||
auto& ncas_title = ncas[cnmt.GetTitleID()];
|
||||
|
||||
ncas_title[ContentRecordType::Meta] = nca;
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto id_string = Common::HexArrayToString(rec.nca_id, false);
|
||||
const auto next_file = pfs->GetFile(fmt::format("{}.nca", id_string));
|
||||
if (next_file == nullptr) {
|
||||
LOG_WARNING(Service_FS,
|
||||
"NCA with ID {}.nca is listed in content metadata, but cannot "
|
||||
"be found in PFS. NSP appears to be corrupted.",
|
||||
id_string);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto next_nca = std::make_shared<NCA>(next_file);
|
||||
if (next_nca->GetType() == NCAContentType::Program)
|
||||
program_status[cnmt.GetTitleID()] = next_nca->GetStatus();
|
||||
if (next_nca->GetStatus() == Loader::ResultStatus::Success)
|
||||
ncas_title[rec.type] = std::move(next_nca);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSP::~NSP() = default;
|
||||
|
||||
Loader::ResultStatus NSP::GetStatus() const {
|
||||
return status;
|
||||
}
|
||||
|
||||
Loader::ResultStatus NSP::GetProgramStatus(u64 title_id) const {
|
||||
const auto iter = program_status.find(title_id);
|
||||
if (iter == program_status.end())
|
||||
return Loader::ResultStatus::ErrorNSPMissingProgramNCA;
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
u64 NSP::GetFirstTitleID() const {
|
||||
if (program_status.empty())
|
||||
return 0;
|
||||
return program_status.begin()->first;
|
||||
}
|
||||
|
||||
u64 NSP::GetProgramTitleID() const {
|
||||
const auto out = GetFirstTitleID();
|
||||
if ((out & 0x800) == 0)
|
||||
return out;
|
||||
|
||||
const auto ids = GetTitleIDs();
|
||||
const auto iter =
|
||||
std::find_if(ids.begin(), ids.end(), [](u64 tid) { return (tid & 0x800) == 0; });
|
||||
return iter == ids.end() ? out : *iter;
|
||||
}
|
||||
|
||||
std::vector<u64> NSP::GetTitleIDs() const {
|
||||
std::vector<u64> out;
|
||||
out.reserve(ncas.size());
|
||||
for (const auto& kv : ncas)
|
||||
out.push_back(kv.first);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool NSP::IsExtractedType() const {
|
||||
return extracted;
|
||||
}
|
||||
|
||||
VirtualFile NSP::GetRomFS() const {
|
||||
return romfs;
|
||||
}
|
||||
|
||||
VirtualDir NSP::GetExeFS() const {
|
||||
return exefs;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<NCA>> NSP::GetNCAsCollapsed() const {
|
||||
if (extracted)
|
||||
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||
std::vector<std::shared_ptr<NCA>> out;
|
||||
for (const auto& map : ncas) {
|
||||
for (const auto& inner_map : map.second)
|
||||
out.push_back(inner_map.second);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::multimap<u64, std::shared_ptr<NCA>> NSP::GetNCAsByTitleID() const {
|
||||
if (extracted)
|
||||
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||
std::multimap<u64, std::shared_ptr<NCA>> out;
|
||||
for (const auto& map : ncas) {
|
||||
for (const auto& inner_map : map.second)
|
||||
out.emplace(map.first, inner_map.second);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::map<u64, std::map<ContentRecordType, std::shared_ptr<NCA>>> NSP::GetNCAs() const {
|
||||
return ncas;
|
||||
}
|
||||
|
||||
std::shared_ptr<NCA> NSP::GetNCA(u64 title_id, ContentRecordType type) const {
|
||||
if (extracted)
|
||||
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||
|
||||
const auto title_id_iter = ncas.find(title_id);
|
||||
if (title_id_iter == ncas.end())
|
||||
return nullptr;
|
||||
|
||||
const auto type_iter = title_id_iter->second.find(type);
|
||||
if (type_iter == title_id_iter->second.end())
|
||||
return nullptr;
|
||||
|
||||
return type_iter->second;
|
||||
}
|
||||
|
||||
VirtualFile NSP::GetNCAFile(u64 title_id, ContentRecordType type) const {
|
||||
if (extracted)
|
||||
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||
const auto nca = GetNCA(title_id, type);
|
||||
if (nca != nullptr)
|
||||
return nca->GetBaseFile();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<Core::Crypto::Key128> NSP::GetTitlekey() const {
|
||||
if (extracted)
|
||||
LOG_WARNING(Service_FS, "called on an NSP that is of type extracted.");
|
||||
std::vector<Core::Crypto::Key128> out;
|
||||
for (const auto& ticket_file : ticket_files) {
|
||||
if (ticket_file == nullptr ||
|
||||
ticket_file->GetSize() <
|
||||
Core::Crypto::TICKET_FILE_TITLEKEY_OFFSET + sizeof(Core::Crypto::Key128)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.emplace_back();
|
||||
ticket_file->Read(out.back().data(), out.back().size(),
|
||||
Core::Crypto::TICKET_FILE_TITLEKEY_OFFSET);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<VirtualFile> NSP::GetFiles() const {
|
||||
return pfs->GetFiles();
|
||||
}
|
||||
|
||||
std::vector<VirtualDir> NSP::GetSubdirectories() const {
|
||||
return pfs->GetSubdirectories();
|
||||
}
|
||||
|
||||
std::string NSP::GetName() const {
|
||||
return file->GetName();
|
||||
}
|
||||
|
||||
VirtualDir NSP::GetParentDirectory() const {
|
||||
return file->GetContainingDirectory();
|
||||
}
|
||||
|
||||
bool NSP::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
|
||||
return false;
|
||||
}
|
||||
} // namespace FileSys
|
||||
76
src/core/file_sys/submission_package.h
Normal file
76
src/core/file_sys/submission_package.h
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace Loader {
|
||||
enum class ResultStatus : u16;
|
||||
}
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class NCA;
|
||||
class PartitionFilesystem;
|
||||
|
||||
enum class ContentRecordType : u8;
|
||||
|
||||
class NSP : public ReadOnlyVfsDirectory {
|
||||
public:
|
||||
explicit NSP(VirtualFile file);
|
||||
~NSP();
|
||||
|
||||
Loader::ResultStatus GetStatus() const;
|
||||
Loader::ResultStatus GetProgramStatus(u64 title_id) const;
|
||||
// Should only be used when one title id can be assured.
|
||||
u64 GetFirstTitleID() const;
|
||||
u64 GetProgramTitleID() const;
|
||||
std::vector<u64> GetTitleIDs() const;
|
||||
|
||||
bool IsExtractedType() const;
|
||||
|
||||
// Common (Can be safely called on both types)
|
||||
VirtualFile GetRomFS() const;
|
||||
VirtualDir GetExeFS() const;
|
||||
|
||||
// Type 0 Only (Collection of NCAs + Certificate + Ticket + Meta XML)
|
||||
std::vector<std::shared_ptr<NCA>> GetNCAsCollapsed() const;
|
||||
std::multimap<u64, std::shared_ptr<NCA>> GetNCAsByTitleID() const;
|
||||
std::map<u64, std::map<ContentRecordType, std::shared_ptr<NCA>>> GetNCAs() const;
|
||||
std::shared_ptr<NCA> GetNCA(u64 title_id, ContentRecordType type) const;
|
||||
VirtualFile GetNCAFile(u64 title_id, ContentRecordType type) const;
|
||||
std::vector<Core::Crypto::Key128> GetTitlekey() const;
|
||||
|
||||
std::vector<VirtualFile> GetFiles() const override;
|
||||
|
||||
std::vector<VirtualDir> GetSubdirectories() const override;
|
||||
|
||||
std::string GetName() const override;
|
||||
|
||||
VirtualDir GetParentDirectory() const override;
|
||||
|
||||
protected:
|
||||
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
|
||||
|
||||
private:
|
||||
VirtualFile file;
|
||||
|
||||
bool extracted;
|
||||
Loader::ResultStatus status;
|
||||
std::map<u64, Loader::ResultStatus> program_status;
|
||||
|
||||
std::shared_ptr<PartitionFilesystem> pfs;
|
||||
// Map title id -> {map type -> NCA}
|
||||
std::map<u64, std::map<ContentRecordType, std::shared_ptr<NCA>>> ncas;
|
||||
std::vector<VirtualFile> ticket_files;
|
||||
|
||||
VirtualFile romfs;
|
||||
VirtualDir exefs;
|
||||
};
|
||||
} // namespace FileSys
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "common/common_paths.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/backend.h"
|
||||
#include "core/file_sys/mode.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
@@ -461,4 +462,11 @@ bool VfsRawCopy(VirtualFile src, VirtualFile dest) {
|
||||
std::vector<u8> data = src->ReadAllBytes();
|
||||
return dest->WriteBytes(data, 0) == data.size();
|
||||
}
|
||||
|
||||
VirtualDir GetOrCreateDirectoryRelative(const VirtualDir& rel, std::string_view path) {
|
||||
const auto res = rel->GetDirectoryRelative(path);
|
||||
if (res == nullptr)
|
||||
return rel->CreateDirectoryRelative(path);
|
||||
return res;
|
||||
}
|
||||
} // namespace FileSys
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include "boost/optional.hpp"
|
||||
#include <boost/optional.hpp>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/mode.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
@@ -19,6 +18,8 @@ class VfsDirectory;
|
||||
class VfsFile;
|
||||
class VfsFilesystem;
|
||||
|
||||
enum class Mode : u32;
|
||||
|
||||
// Convenience typedefs to use Vfs* interfaces
|
||||
using VirtualFilesystem = std::shared_ptr<VfsFilesystem>;
|
||||
using VirtualDir = std::shared_ptr<VfsDirectory>;
|
||||
@@ -317,4 +318,8 @@ bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block
|
||||
// directory of src/dest.
|
||||
bool VfsRawCopy(VirtualFile src, VirtualFile dest);
|
||||
|
||||
// Checks if the directory at path relative to rel exists. If it does, returns that. If it does not
|
||||
// it attempts to create it and returns the new dir or nullptr on failure.
|
||||
VirtualDir GetOrCreateDirectoryRelative(const VirtualDir& rel, std::string_view path);
|
||||
|
||||
} // namespace FileSys
|
||||
|
||||
94
src/core/file_sys/vfs_concat.cpp
Normal file
94
src/core/file_sys/vfs_concat.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "core/file_sys/vfs_concat.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name) {
|
||||
if (files.empty())
|
||||
return nullptr;
|
||||
if (files.size() == 1)
|
||||
return files[0];
|
||||
|
||||
return std::shared_ptr<VfsFile>(new ConcatenatedVfsFile(std::move(files), std::move(name)));
|
||||
}
|
||||
|
||||
ConcatenatedVfsFile::ConcatenatedVfsFile(std::vector<VirtualFile> files_, std::string name)
|
||||
: name(std::move(name)) {
|
||||
size_t next_offset = 0;
|
||||
for (const auto& file : files_) {
|
||||
files[next_offset] = file;
|
||||
next_offset += file->GetSize();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConcatenatedVfsFile::GetName() const {
|
||||
if (files.empty())
|
||||
return "";
|
||||
if (!name.empty())
|
||||
return name;
|
||||
return files.begin()->second->GetName();
|
||||
}
|
||||
|
||||
size_t ConcatenatedVfsFile::GetSize() const {
|
||||
if (files.empty())
|
||||
return 0;
|
||||
return files.rbegin()->first + files.rbegin()->second->GetSize();
|
||||
}
|
||||
|
||||
bool ConcatenatedVfsFile::Resize(size_t new_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<VfsDirectory> ConcatenatedVfsFile::GetContainingDirectory() const {
|
||||
if (files.empty())
|
||||
return nullptr;
|
||||
return files.begin()->second->GetContainingDirectory();
|
||||
}
|
||||
|
||||
bool ConcatenatedVfsFile::IsWritable() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ConcatenatedVfsFile::IsReadable() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ConcatenatedVfsFile::Read(u8* data, size_t length, size_t offset) const {
|
||||
auto entry = files.end();
|
||||
for (auto iter = files.begin(); iter != files.end(); ++iter) {
|
||||
if (iter->first > offset) {
|
||||
entry = --iter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the entry should be the last one. The loop above will make it end().
|
||||
if (entry == files.end() && offset < files.rbegin()->first + files.rbegin()->second->GetSize())
|
||||
--entry;
|
||||
|
||||
if (entry == files.end())
|
||||
return 0;
|
||||
|
||||
const auto remaining = entry->second->GetSize() + offset - entry->first;
|
||||
if (length > remaining) {
|
||||
return entry->second->Read(data, remaining, offset - entry->first) +
|
||||
Read(data + remaining, length - remaining, offset + remaining);
|
||||
}
|
||||
|
||||
return entry->second->Read(data, length, offset - entry->first);
|
||||
}
|
||||
|
||||
size_t ConcatenatedVfsFile::Write(const u8* data, size_t length, size_t offset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ConcatenatedVfsFile::Rename(std::string_view name) {
|
||||
return false;
|
||||
}
|
||||
} // namespace FileSys
|
||||
41
src/core/file_sys/vfs_concat.h
Normal file
41
src/core/file_sys/vfs_concat.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2018 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include "core/file_sys/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
// Wrapper function to allow for more efficient handling of files.size() == 0, 1 cases.
|
||||
VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name = "");
|
||||
|
||||
// Class that wraps multiple vfs files and concatenates them, making reads seamless. Currently
|
||||
// read-only.
|
||||
class ConcatenatedVfsFile : public VfsFile {
|
||||
friend VirtualFile ConcatenateFiles(std::vector<VirtualFile> files, std::string name);
|
||||
|
||||
ConcatenatedVfsFile(std::vector<VirtualFile> files, std::string name);
|
||||
|
||||
public:
|
||||
std::string GetName() const override;
|
||||
size_t GetSize() const override;
|
||||
bool Resize(size_t new_size) override;
|
||||
std::shared_ptr<VfsDirectory> GetContainingDirectory() const override;
|
||||
bool IsWritable() const override;
|
||||
bool IsReadable() const override;
|
||||
size_t Read(u8* data, size_t length, size_t offset) const override;
|
||||
size_t Write(const u8* data, size_t length, size_t offset) override;
|
||||
bool Rename(std::string_view name) override;
|
||||
|
||||
private:
|
||||
// Maps starting offset to file -- more efficient.
|
||||
boost::container::flat_map<u64, VirtualFile> files;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <utility>
|
||||
#include "common/assert.h"
|
||||
#include "common/common_paths.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/file_sys/vfs_real.h"
|
||||
|
||||
@@ -39,6 +40,7 @@ static std::string ModeFlagsToString(Mode mode) {
|
||||
}
|
||||
|
||||
RealVfsFilesystem::RealVfsFilesystem() : VfsFilesystem(nullptr) {}
|
||||
RealVfsFilesystem::~RealVfsFilesystem() = default;
|
||||
|
||||
std::string RealVfsFilesystem::GetName() const {
|
||||
return "Real";
|
||||
@@ -83,8 +85,12 @@ VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) {
|
||||
|
||||
VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) {
|
||||
const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault);
|
||||
if (!FileUtil::Exists(path) && !FileUtil::CreateEmptyFile(path))
|
||||
return nullptr;
|
||||
const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash);
|
||||
if (!FileUtil::Exists(path)) {
|
||||
FileUtil::CreateFullPath(path_fwd);
|
||||
if (!FileUtil::CreateEmptyFile(path))
|
||||
return nullptr;
|
||||
}
|
||||
return OpenFile(path, perms);
|
||||
}
|
||||
|
||||
@@ -140,8 +146,12 @@ VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms)
|
||||
|
||||
VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms) {
|
||||
const auto path = FileUtil::SanitizePath(path_, FileUtil::DirectorySeparator::PlatformDefault);
|
||||
if (!FileUtil::Exists(path) && !FileUtil::CreateDir(path))
|
||||
return nullptr;
|
||||
const auto path_fwd = FileUtil::SanitizePath(path, FileUtil::DirectorySeparator::ForwardSlash);
|
||||
if (!FileUtil::Exists(path)) {
|
||||
FileUtil::CreateFullPath(path_fwd);
|
||||
if (!FileUtil::CreateDir(path))
|
||||
return nullptr;
|
||||
}
|
||||
// Cannot use make_shared as RealVfsDirectory constructor is private
|
||||
return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
|
||||
}
|
||||
@@ -211,6 +221,8 @@ RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::shared_ptr<FileUtil::IOF
|
||||
parent_components(FileUtil::SliceVector(path_components, 0, path_components.size() - 1)),
|
||||
perms(perms_) {}
|
||||
|
||||
RealVfsFile::~RealVfsFile() = default;
|
||||
|
||||
std::string RealVfsFile::GetName() const {
|
||||
return path_components.back();
|
||||
}
|
||||
@@ -304,16 +316,18 @@ RealVfsDirectory::RealVfsDirectory(RealVfsFilesystem& base_, const std::string&
|
||||
FileUtil::CreateDir(path);
|
||||
}
|
||||
|
||||
RealVfsDirectory::~RealVfsDirectory() = default;
|
||||
|
||||
std::shared_ptr<VfsFile> RealVfsDirectory::GetFileRelative(std::string_view path) const {
|
||||
const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path));
|
||||
if (!FileUtil::Exists(full_path))
|
||||
if (!FileUtil::Exists(full_path) || FileUtil::IsDirectory(full_path))
|
||||
return nullptr;
|
||||
return base.OpenFile(full_path, perms);
|
||||
}
|
||||
|
||||
std::shared_ptr<VfsDirectory> RealVfsDirectory::GetDirectoryRelative(std::string_view path) const {
|
||||
const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path));
|
||||
if (!FileUtil::Exists(full_path))
|
||||
if (!FileUtil::Exists(full_path) || !FileUtil::IsDirectory(full_path))
|
||||
return nullptr;
|
||||
return base.OpenDirectory(full_path, perms);
|
||||
}
|
||||
@@ -333,7 +347,6 @@ std::shared_ptr<VfsFile> RealVfsDirectory::CreateFileRelative(std::string_view p
|
||||
|
||||
std::shared_ptr<VfsDirectory> RealVfsDirectory::CreateDirectoryRelative(std::string_view path) {
|
||||
const auto full_path = FileUtil::SanitizePath(this->path + DIR_SEP + std::string(path));
|
||||
auto parent = std::string(FileUtil::GetParentPath(full_path));
|
||||
return base.CreateDirectory(full_path, perms);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user