First Commit
This commit is contained in:
59
src/core/hw/aes/arithmetic128.cpp
Normal file
59
src/core/hw/aes/arithmetic128.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include "core/hw/aes/arithmetic128.h"
|
||||
|
||||
namespace HW::AES {
|
||||
|
||||
AESKey Lrot128(const AESKey& in, u32 rot) {
|
||||
AESKey out;
|
||||
rot %= 128;
|
||||
const u32 byte_shift = rot / 8;
|
||||
const u32 bit_shift = rot % 8;
|
||||
|
||||
for (u32 i = 0; i < 16; i++) {
|
||||
const u32 wrap_index_a = (i + byte_shift) % 16;
|
||||
const u32 wrap_index_b = (i + byte_shift + 1) % 16;
|
||||
out[i] = ((in[wrap_index_a] << bit_shift) | (in[wrap_index_b] >> (8 - bit_shift))) & 0xFF;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
AESKey Add128(const AESKey& a, const AESKey& b) {
|
||||
AESKey out;
|
||||
u32 carry = 0;
|
||||
u32 sum = 0;
|
||||
|
||||
for (int i = 15; i >= 0; i--) {
|
||||
sum = a[i] + b[i] + carry;
|
||||
carry = sum >> 8;
|
||||
out[i] = static_cast<u8>(sum & 0xff);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
AESKey Add128(const AESKey& a, u64 b) {
|
||||
AESKey out = a;
|
||||
u32 carry = 0;
|
||||
u32 sum = 0;
|
||||
|
||||
for (int i = 15; i >= 8; i--) {
|
||||
sum = a[i] + static_cast<u8>((b >> ((15 - i) * 8)) & 0xff) + carry;
|
||||
carry = sum >> 8;
|
||||
out[i] = static_cast<u8>(sum & 0xff);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
AESKey Xor128(const AESKey& a, const AESKey& b) {
|
||||
AESKey out;
|
||||
std::transform(a.cbegin(), a.cend(), b.cbegin(), out.begin(), std::bit_xor<>());
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace HW::AES
|
||||
16
src/core/hw/aes/arithmetic128.h
Normal file
16
src/core/hw/aes/arithmetic128.h
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2017 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "core/hw/aes/key.h"
|
||||
|
||||
namespace HW::AES {
|
||||
AESKey Lrot128(const AESKey& in, u32 rot);
|
||||
AESKey Add128(const AESKey& a, const AESKey& b);
|
||||
AESKey Add128(const AESKey& a, u64 b);
|
||||
AESKey Xor128(const AESKey& a, const AESKey& b);
|
||||
|
||||
} // namespace HW::AES
|
||||
93
src/core/hw/aes/ccm.cpp
Normal file
93
src/core/hw/aes/ccm.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2017 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cryptopp/aes.h>
|
||||
#include <cryptopp/ccm.h>
|
||||
#include <cryptopp/cryptlib.h>
|
||||
#include <cryptopp/filters.h>
|
||||
#include "common/alignment.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/hw/aes/ccm.h"
|
||||
#include "core/hw/aes/key.h"
|
||||
|
||||
namespace HW::AES {
|
||||
|
||||
namespace {
|
||||
|
||||
// 3DS uses a non-standard AES-CCM algorithm, so we need to derive a sub class from the standard one
|
||||
// and override with the non-standard part.
|
||||
using CryptoPP::AES;
|
||||
using CryptoPP::CCM_Base;
|
||||
using CryptoPP::CCM_Final;
|
||||
using CryptoPP::lword;
|
||||
template <bool T_IsEncryption>
|
||||
class CCM_3DSVariant_Final : public CCM_Final<AES, CCM_MAC_SIZE, T_IsEncryption> {
|
||||
public:
|
||||
void UncheckedSpecifyDataLengths(lword header_length, lword message_length,
|
||||
lword footer_length) override {
|
||||
// 3DS uses the aligned size to generate B0 for authentication, instead of the original size
|
||||
lword aligned_message_length = Common::AlignUp(message_length, AES_BLOCK_SIZE);
|
||||
CCM_Base::UncheckedSpecifyDataLengths(header_length, aligned_message_length, footer_length);
|
||||
CCM_Base::m_messageLength = message_length; // restore the actual message size
|
||||
}
|
||||
};
|
||||
|
||||
class CCM_3DSVariant {
|
||||
public:
|
||||
using Encryption = CCM_3DSVariant_Final<true>;
|
||||
using Decryption = CCM_3DSVariant_Final<false>;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<u8> EncryptSignCCM(std::span<const u8> pdata, const CCMNonce& nonce,
|
||||
std::size_t slot_id) {
|
||||
if (!IsNormalKeyAvailable(slot_id)) {
|
||||
LOG_ERROR(HW_AES, "Key slot {} not available. Will use zero key.", slot_id);
|
||||
}
|
||||
const AESKey normal = GetNormalKey(slot_id);
|
||||
std::vector<u8> cipher(pdata.size() + CCM_MAC_SIZE);
|
||||
|
||||
try {
|
||||
CCM_3DSVariant::Encryption e;
|
||||
e.SetKeyWithIV(normal.data(), AES_BLOCK_SIZE, nonce.data(), CCM_NONCE_SIZE);
|
||||
e.SpecifyDataLengths(0, pdata.size(), 0);
|
||||
CryptoPP::ArraySource as(pdata.data(), pdata.size(), true,
|
||||
new CryptoPP::AuthenticatedEncryptionFilter(
|
||||
e, new CryptoPP::ArraySink(cipher.data(), cipher.size())));
|
||||
} catch (const CryptoPP::Exception& e) {
|
||||
LOG_ERROR(HW_AES, "FAILED with: {}", e.what());
|
||||
}
|
||||
return cipher;
|
||||
}
|
||||
|
||||
std::vector<u8> DecryptVerifyCCM(std::span<const u8> cipher, const CCMNonce& nonce,
|
||||
std::size_t slot_id) {
|
||||
if (!IsNormalKeyAvailable(slot_id)) {
|
||||
LOG_ERROR(HW_AES, "Key slot {} not available. Will use zero key.", slot_id);
|
||||
}
|
||||
const AESKey normal = GetNormalKey(slot_id);
|
||||
const std::size_t pdata_size = cipher.size() - CCM_MAC_SIZE;
|
||||
std::vector<u8> pdata(pdata_size);
|
||||
|
||||
try {
|
||||
CCM_3DSVariant::Decryption d;
|
||||
d.SetKeyWithIV(normal.data(), AES_BLOCK_SIZE, nonce.data(), CCM_NONCE_SIZE);
|
||||
d.SpecifyDataLengths(0, pdata_size, 0);
|
||||
CryptoPP::AuthenticatedDecryptionFilter df(
|
||||
d, new CryptoPP::ArraySink(pdata.data(), pdata_size));
|
||||
CryptoPP::ArraySource as(cipher.data(), cipher.size(), true, new CryptoPP::Redirector(df));
|
||||
if (!df.GetLastResult()) {
|
||||
LOG_ERROR(HW_AES, "FAILED");
|
||||
return {};
|
||||
}
|
||||
} catch (const CryptoPP::Exception& e) {
|
||||
LOG_ERROR(HW_AES, "FAILED with: {}", e.what());
|
||||
return {};
|
||||
}
|
||||
return pdata;
|
||||
}
|
||||
|
||||
} // namespace HW::AES
|
||||
40
src/core/hw/aes/ccm.h
Normal file
40
src/core/hw/aes/ccm.h
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2017 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace HW::AES {
|
||||
|
||||
constexpr std::size_t CCM_NONCE_SIZE = 12;
|
||||
constexpr std::size_t CCM_MAC_SIZE = 16;
|
||||
|
||||
using CCMNonce = std::array<u8, CCM_NONCE_SIZE>;
|
||||
|
||||
/**
|
||||
* Encrypts and adds a MAC to the given data using AES-CCM algorithm.
|
||||
* @param pdata The plain text data to encrypt
|
||||
* @param nonce The nonce data to use for encryption
|
||||
* @param slot_id The slot ID of the key to use for encryption
|
||||
* @returns a vector of u8 containing the encrypted data with MAC at the end
|
||||
*/
|
||||
std::vector<u8> EncryptSignCCM(std::span<const u8> pdata, const CCMNonce& nonce,
|
||||
std::size_t slot_id);
|
||||
|
||||
/**
|
||||
* Decrypts and verify the MAC of the given data using AES-CCM algorithm.
|
||||
* @param cipher The cipher text data to decrypt, with MAC at the end to verify
|
||||
* @param nonce The nonce data to use for decryption
|
||||
* @param slot_id The slot ID of the key to use for decryption
|
||||
* @returns a vector of u8 containing the decrypted data; an empty vector if the verification fails
|
||||
*/
|
||||
std::vector<u8> DecryptVerifyCCM(std::span<const u8> cipher, const CCMNonce& nonce,
|
||||
std::size_t slot_id);
|
||||
|
||||
} // namespace HW::AES
|
||||
374
src/core/hw/aes/key.cpp
Normal file
374
src/core/hw/aes/key.cpp
Normal file
@@ -0,0 +1,374 @@
|
||||
// Copyright 2017 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <boost/iostreams/device/file_descriptor.hpp>
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
#include "common/common_paths.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/hle/service/fs/archive.h"
|
||||
#include "core/hw/aes/arithmetic128.h"
|
||||
#include "core/hw/aes/key.h"
|
||||
#include "core/hw/rsa/rsa.h"
|
||||
|
||||
namespace HW::AES {
|
||||
|
||||
namespace {
|
||||
|
||||
// The generator constant was calculated using the 0x39 KeyX and KeyY retrieved from a 3DS and the
|
||||
// normal key dumped from a Wii U solving the equation:
|
||||
// NormalKey = (((KeyX ROL 2) XOR KeyY) + constant) ROL 87
|
||||
// On a real 3DS the generation for the normal key is hardware based, and thus the constant can't
|
||||
// get dumped. Generated normal keys are also not accessible on a 3DS. The used formula for
|
||||
// calculating the constant is a software implementation of what the hardware generator does.
|
||||
constexpr AESKey generator_constant = {{0x1F, 0xF9, 0xE9, 0xAA, 0xC5, 0xFE, 0x04, 0x08, 0x02, 0x45,
|
||||
0x91, 0xDC, 0x5D, 0x52, 0x76, 0x8A}};
|
||||
|
||||
AESKey HexToKey(const std::string& hex) {
|
||||
if (hex.size() < 32) {
|
||||
throw std::invalid_argument("hex string is too short");
|
||||
}
|
||||
|
||||
AESKey key;
|
||||
for (std::size_t i = 0; i < key.size(); ++i) {
|
||||
key[i] = static_cast<u8>(std::stoi(hex.substr(i * 2, 2), nullptr, 16));
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
std::vector<u8> HexToVector(const std::string& hex) {
|
||||
std::vector<u8> vector(hex.size() / 2);
|
||||
for (std::size_t i = 0; i < vector.size(); ++i) {
|
||||
vector[i] = static_cast<u8>(std::stoi(hex.substr(i * 2, 2), nullptr, 16));
|
||||
}
|
||||
|
||||
return vector;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> ParseCommonKeyName(const std::string& full_name) {
|
||||
std::size_t index;
|
||||
int end;
|
||||
if (std::sscanf(full_name.c_str(), "common%zd%n", &index, &end) == 1 &&
|
||||
end == static_cast<int>(full_name.size())) {
|
||||
return index;
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::pair<std::size_t, std::string>> ParseNfcSecretName(
|
||||
const std::string& full_name) {
|
||||
std::size_t index;
|
||||
int end;
|
||||
if (std::sscanf(full_name.c_str(), "nfcSecret%zd%n", &index, &end) == 1) {
|
||||
return std::make_pair(index, full_name.substr(end));
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::pair<std::size_t, char>> ParseKeySlotName(const std::string& full_name) {
|
||||
std::size_t slot;
|
||||
char type;
|
||||
int end;
|
||||
if (std::sscanf(full_name.c_str(), "slot0x%zXKey%c%n", &slot, &type, &end) == 2 &&
|
||||
end == static_cast<int>(full_name.size())) {
|
||||
return std::make_pair(slot, type);
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
struct KeySlot {
|
||||
std::optional<AESKey> x;
|
||||
std::optional<AESKey> y;
|
||||
std::optional<AESKey> normal;
|
||||
|
||||
void SetKeyX(std::optional<AESKey> key) {
|
||||
x = key;
|
||||
GenerateNormalKey();
|
||||
}
|
||||
|
||||
void SetKeyY(std::optional<AESKey> key) {
|
||||
y = key;
|
||||
GenerateNormalKey();
|
||||
}
|
||||
|
||||
void SetNormalKey(std::optional<AESKey> key) {
|
||||
normal = key;
|
||||
}
|
||||
|
||||
void GenerateNormalKey() {
|
||||
if (x && y) {
|
||||
normal = Lrot128(Add128(Xor128(Lrot128(*x, 2), *y), generator_constant), 87);
|
||||
} else {
|
||||
normal.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
x.reset();
|
||||
y.reset();
|
||||
normal.reset();
|
||||
}
|
||||
};
|
||||
|
||||
std::array<KeySlot, KeySlotID::MaxKeySlotID> key_slots;
|
||||
std::array<std::optional<AESKey>, MaxCommonKeySlot> common_key_y_slots;
|
||||
std::array<std::optional<AESKey>, NumDlpNfcKeyYs> dlp_nfc_key_y_slots;
|
||||
std::array<NfcSecret, NumNfcSecrets> nfc_secrets;
|
||||
AESIV nfc_iv;
|
||||
|
||||
struct KeyDesc {
|
||||
char key_type;
|
||||
std::size_t slot_id;
|
||||
// This key is identical to the key with the same key_type and slot_id -1
|
||||
bool same_as_before;
|
||||
};
|
||||
|
||||
void LoadBootromKeys() {
|
||||
constexpr std::array<KeyDesc, 80> keys = {
|
||||
{{'X', 0x2C, false}, {'X', 0x2D, true}, {'X', 0x2E, true}, {'X', 0x2F, true},
|
||||
{'X', 0x30, false}, {'X', 0x31, true}, {'X', 0x32, true}, {'X', 0x33, true},
|
||||
{'X', 0x34, false}, {'X', 0x35, true}, {'X', 0x36, true}, {'X', 0x37, true},
|
||||
{'X', 0x38, false}, {'X', 0x39, true}, {'X', 0x3A, true}, {'X', 0x3B, true},
|
||||
{'X', 0x3C, false}, {'X', 0x3D, false}, {'X', 0x3E, false}, {'X', 0x3F, false},
|
||||
{'Y', 0x4, false}, {'Y', 0x5, false}, {'Y', 0x6, false}, {'Y', 0x7, false},
|
||||
{'Y', 0x8, false}, {'Y', 0x9, false}, {'Y', 0xA, false}, {'Y', 0xB, false},
|
||||
{'N', 0xC, false}, {'N', 0xD, true}, {'N', 0xE, true}, {'N', 0xF, true},
|
||||
{'N', 0x10, false}, {'N', 0x11, true}, {'N', 0x12, true}, {'N', 0x13, true},
|
||||
{'N', 0x14, false}, {'N', 0x15, false}, {'N', 0x16, false}, {'N', 0x17, false},
|
||||
{'N', 0x18, false}, {'N', 0x19, true}, {'N', 0x1A, true}, {'N', 0x1B, true},
|
||||
{'N', 0x1C, false}, {'N', 0x1D, true}, {'N', 0x1E, true}, {'N', 0x1F, true},
|
||||
{'N', 0x20, false}, {'N', 0x21, true}, {'N', 0x22, true}, {'N', 0x23, true},
|
||||
{'N', 0x24, false}, {'N', 0x25, true}, {'N', 0x26, true}, {'N', 0x27, true},
|
||||
{'N', 0x28, true}, {'N', 0x29, false}, {'N', 0x2A, false}, {'N', 0x2B, false},
|
||||
{'N', 0x2C, false}, {'N', 0x2D, true}, {'N', 0x2E, true}, {'N', 0x2F, true},
|
||||
{'N', 0x30, false}, {'N', 0x31, true}, {'N', 0x32, true}, {'N', 0x33, true},
|
||||
{'N', 0x34, false}, {'N', 0x35, true}, {'N', 0x36, true}, {'N', 0x37, true},
|
||||
{'N', 0x38, false}, {'N', 0x39, true}, {'N', 0x3A, true}, {'N', 0x3B, true},
|
||||
{'N', 0x3C, true}, {'N', 0x3D, false}, {'N', 0x3E, false}, {'N', 0x3F, false}}};
|
||||
|
||||
// Bootrom sets all these keys when executed, but later some of the normal keys get overwritten
|
||||
// by other applications e.g. process9. These normal keys thus aren't used by any application
|
||||
// and have no value for emulation
|
||||
|
||||
const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
|
||||
auto file = FileUtil::IOFile(filepath, "rb");
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t length = file.GetSize();
|
||||
if (length != 65536) {
|
||||
LOG_ERROR(HW_AES, "Bootrom9 size is wrong: {}", length);
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr std::size_t KEY_SECTION_START = 55760;
|
||||
file.Seek(KEY_SECTION_START, SEEK_SET); // Jump to the key section
|
||||
|
||||
AESKey new_key;
|
||||
for (const auto& key : keys) {
|
||||
if (!key.same_as_before) {
|
||||
file.ReadArray(new_key.data(), new_key.size());
|
||||
if (!file) {
|
||||
LOG_ERROR(HW_AES, "Reading from Bootrom9 failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(HW_AES, "Loaded Slot{:#02x} Key{} from Bootrom9.", key.slot_id, key.key_type);
|
||||
|
||||
switch (key.key_type) {
|
||||
case 'X':
|
||||
key_slots.at(key.slot_id).SetKeyX(new_key);
|
||||
break;
|
||||
case 'Y':
|
||||
key_slots.at(key.slot_id).SetKeyY(new_key);
|
||||
break;
|
||||
case 'N':
|
||||
key_slots.at(key.slot_id).SetNormalKey(new_key);
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(HW_AES, "Invalid key type {}", key.key_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LoadPresetKeys() {
|
||||
const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + AES_KEYS;
|
||||
FileUtil::CreateFullPath(filepath); // Create path if not already created
|
||||
|
||||
boost::iostreams::stream<boost::iostreams::file_descriptor_source> file;
|
||||
FileUtil::OpenFStream<std::ios_base::in>(file, filepath);
|
||||
if (!file.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!file.eof()) {
|
||||
std::string line;
|
||||
std::getline(file, line);
|
||||
|
||||
// Ignore empty or commented lines.
|
||||
if (line.empty() || line.starts_with("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto parts = Common::SplitString(line, '=');
|
||||
if (parts.size() != 2) {
|
||||
LOG_ERROR(HW_AES, "Failed to parse {}", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string& name = parts[0];
|
||||
|
||||
const auto nfc_secret = ParseNfcSecretName(name);
|
||||
if (nfc_secret) {
|
||||
auto value = HexToVector(parts[1]);
|
||||
if (nfc_secret->first >= nfc_secrets.size()) {
|
||||
LOG_ERROR(HW_AES, "Invalid NFC secret index {}", nfc_secret->first);
|
||||
} else if (nfc_secret->second == "Phrase") {
|
||||
nfc_secrets[nfc_secret->first].phrase = value;
|
||||
} else if (nfc_secret->second == "Seed") {
|
||||
nfc_secrets[nfc_secret->first].seed = value;
|
||||
} else if (nfc_secret->second == "HmacKey") {
|
||||
nfc_secrets[nfc_secret->first].hmac_key = value;
|
||||
} else {
|
||||
LOG_ERROR(HW_AES, "Invalid NFC secret '{}'", name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
AESKey key;
|
||||
try {
|
||||
key = HexToKey(parts[1]);
|
||||
} catch (const std::logic_error& e) {
|
||||
LOG_ERROR(HW_AES, "Invalid key {}: {}", parts[1], e.what());
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto common_key = ParseCommonKeyName(name);
|
||||
if (common_key) {
|
||||
if (common_key >= common_key_y_slots.size()) {
|
||||
LOG_ERROR(HW_AES, "Invalid common key index {}", common_key.value());
|
||||
} else {
|
||||
common_key_y_slots[common_key.value()] = key;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name == "dlpKeyY") {
|
||||
dlp_nfc_key_y_slots[DlpNfcKeyY::Dlp] = key;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name == "nfcKeyY") {
|
||||
dlp_nfc_key_y_slots[DlpNfcKeyY::Nfc] = key;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name == "nfcIv") {
|
||||
nfc_iv = key;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto key_slot = ParseKeySlotName(name);
|
||||
if (!key_slot) {
|
||||
LOG_ERROR(HW_AES, "Invalid key name '{}'", name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key_slot->first >= MaxKeySlotID) {
|
||||
LOG_ERROR(HW_AES, "Out of range key slot ID {:#X}", key_slot->first);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key_slot->second) {
|
||||
case 'X':
|
||||
key_slots.at(key_slot->first).SetKeyX(key);
|
||||
break;
|
||||
case 'Y':
|
||||
key_slots.at(key_slot->first).SetKeyY(key);
|
||||
break;
|
||||
case 'N':
|
||||
key_slots.at(key_slot->first).SetNormalKey(key);
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(HW_AES, "Invalid key type '{}'", key_slot->second);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InitKeys(bool force) {
|
||||
static bool initialized = false;
|
||||
if (initialized && !force) {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
HW::RSA::InitSlots();
|
||||
LoadBootromKeys();
|
||||
LoadPresetKeys();
|
||||
}
|
||||
|
||||
void SetKeyX(std::size_t slot_id, const AESKey& key) {
|
||||
key_slots.at(slot_id).SetKeyX(key);
|
||||
}
|
||||
|
||||
void SetKeyY(std::size_t slot_id, const AESKey& key) {
|
||||
key_slots.at(slot_id).SetKeyY(key);
|
||||
}
|
||||
|
||||
void SetNormalKey(std::size_t slot_id, const AESKey& key) {
|
||||
key_slots.at(slot_id).SetNormalKey(key);
|
||||
}
|
||||
|
||||
bool IsKeyXAvailable(std::size_t slot_id) {
|
||||
return key_slots.at(slot_id).x.has_value();
|
||||
}
|
||||
|
||||
bool IsNormalKeyAvailable(std::size_t slot_id) {
|
||||
return key_slots.at(slot_id).normal.has_value();
|
||||
}
|
||||
|
||||
AESKey GetNormalKey(std::size_t slot_id) {
|
||||
return key_slots.at(slot_id).normal.value_or(AESKey{});
|
||||
}
|
||||
|
||||
void SelectCommonKeyIndex(u8 index) {
|
||||
key_slots[KeySlotID::TicketCommonKey].SetKeyY(common_key_y_slots.at(index));
|
||||
}
|
||||
|
||||
void SelectDlpNfcKeyYIndex(u8 index) {
|
||||
key_slots[KeySlotID::DLPNFCDataKey].SetKeyY(dlp_nfc_key_y_slots.at(index));
|
||||
}
|
||||
|
||||
bool NfcSecretsAvailable() {
|
||||
auto missing_secret =
|
||||
std::find_if(nfc_secrets.begin(), nfc_secrets.end(), [](auto& nfc_secret) {
|
||||
return nfc_secret.phrase.empty() || nfc_secret.seed.empty() ||
|
||||
nfc_secret.hmac_key.empty();
|
||||
});
|
||||
SelectDlpNfcKeyYIndex(DlpNfcKeyY::Nfc);
|
||||
return IsNormalKeyAvailable(KeySlotID::DLPNFCDataKey) && missing_secret == nfc_secrets.end();
|
||||
}
|
||||
|
||||
const NfcSecret& GetNfcSecret(NfcSecretId secret_id) {
|
||||
return nfc_secrets[secret_id];
|
||||
}
|
||||
|
||||
const AESIV& GetNfcIv() {
|
||||
return nfc_iv;
|
||||
}
|
||||
|
||||
} // namespace HW::AES
|
||||
94
src/core/hw/aes/key.h
Normal file
94
src/core/hw/aes/key.h
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2017 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace HW::AES {
|
||||
|
||||
enum KeySlotID : std::size_t {
|
||||
|
||||
// Used to decrypt the SSL client cert/private-key stored in ClCertA.
|
||||
SSLKey = 0x0D,
|
||||
|
||||
// AES keyslots used to decrypt NCCH
|
||||
NCCHSecure1 = 0x2C,
|
||||
NCCHSecure2 = 0x25,
|
||||
NCCHSecure3 = 0x18,
|
||||
NCCHSecure4 = 0x1B,
|
||||
|
||||
// AES Keyslot used to generate the UDS data frame CCMP key.
|
||||
UDSDataKey = 0x2D,
|
||||
|
||||
// AES Keyslot used to encrypt the BOSS container data.
|
||||
BOSSDataKey = 0x38,
|
||||
|
||||
// AES Keyslot used to calculate DLP data frame checksum and encrypt Amiibo key data.
|
||||
DLPNFCDataKey = 0x39,
|
||||
|
||||
// AES Keyslot used to generate the StreetPass CCMP key.
|
||||
CECDDataKey = 0x2E,
|
||||
|
||||
// AES Keyslot used by the friends module.
|
||||
FRDKey = 0x36,
|
||||
|
||||
// AES keyslot used for APT:Wrap/Unwrap functions
|
||||
APTWrap = 0x31,
|
||||
|
||||
// AES keyslot used for decrypting ticket title key
|
||||
TicketCommonKey = 0x3D,
|
||||
|
||||
MaxKeySlotID = 0x40,
|
||||
};
|
||||
|
||||
enum DlpNfcKeyY : std::size_t {
|
||||
// Download Play KeyY
|
||||
Dlp = 0,
|
||||
|
||||
// NFC (Amiibo) KeyY
|
||||
Nfc = 1
|
||||
};
|
||||
|
||||
struct NfcSecret {
|
||||
std::vector<u8> phrase;
|
||||
std::vector<u8> seed;
|
||||
std::vector<u8> hmac_key;
|
||||
};
|
||||
|
||||
enum NfcSecretId : std::size_t {
|
||||
UnfixedInfo = 0,
|
||||
LockedSecret = 1,
|
||||
};
|
||||
|
||||
constexpr std::size_t MaxCommonKeySlot = 6;
|
||||
constexpr std::size_t NumDlpNfcKeyYs = 2;
|
||||
constexpr std::size_t NumNfcSecrets = 2;
|
||||
|
||||
constexpr std::size_t AES_BLOCK_SIZE = 16;
|
||||
|
||||
using AESKey = std::array<u8, AES_BLOCK_SIZE>;
|
||||
using AESIV = std::array<u8, AES_BLOCK_SIZE>;
|
||||
|
||||
void InitKeys(bool force = false);
|
||||
|
||||
void SetKeyX(std::size_t slot_id, const AESKey& key);
|
||||
void SetKeyY(std::size_t slot_id, const AESKey& key);
|
||||
void SetNormalKey(std::size_t slot_id, const AESKey& key);
|
||||
|
||||
bool IsKeyXAvailable(std::size_t slot_id);
|
||||
bool IsNormalKeyAvailable(std::size_t slot_id);
|
||||
AESKey GetNormalKey(std::size_t slot_id);
|
||||
|
||||
void SelectCommonKeyIndex(u8 index);
|
||||
void SelectDlpNfcKeyYIndex(u8 index);
|
||||
|
||||
bool NfcSecretsAvailable();
|
||||
const NfcSecret& GetNfcSecret(NfcSecretId secret_id);
|
||||
const AESIV& GetNfcIv();
|
||||
|
||||
} // namespace HW::AES
|
||||
113
src/core/hw/rsa/rsa.cpp
Normal file
113
src/core/hw/rsa/rsa.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <sstream>
|
||||
#include <cryptopp/hex.h>
|
||||
#include <cryptopp/integer.h>
|
||||
#include <cryptopp/nbtheory.h>
|
||||
#include <cryptopp/sha.h>
|
||||
#include <fmt/format.h>
|
||||
#include "common/common_paths.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/hw/rsa/rsa.h"
|
||||
|
||||
namespace HW::RSA {
|
||||
|
||||
namespace {
|
||||
std::vector<u8> HexToBytes(const std::string& hex) {
|
||||
std::vector<u8> bytes;
|
||||
|
||||
for (unsigned int i = 0; i < hex.length(); i += 2) {
|
||||
std::string byteString = hex.substr(i, 2);
|
||||
u8 byte = static_cast<u8>(std::strtol(byteString.c_str(), nullptr, 16));
|
||||
bytes.push_back(byte);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
constexpr std::size_t SlotSize = 4;
|
||||
std::array<RsaSlot, SlotSize> rsa_slots;
|
||||
|
||||
std::vector<u8> RsaSlot::GetSignature(std::span<const u8> message) const {
|
||||
CryptoPP::Integer sig =
|
||||
CryptoPP::ModularExponentiation(CryptoPP::Integer(message.data(), message.size()),
|
||||
CryptoPP::Integer(exponent.data(), exponent.size()),
|
||||
CryptoPP::Integer(modulus.data(), modulus.size()));
|
||||
std::stringstream ss;
|
||||
ss << std::hex << sig;
|
||||
CryptoPP::HexDecoder decoder;
|
||||
decoder.Put(reinterpret_cast<unsigned char*>(ss.str().data()), ss.str().size());
|
||||
decoder.MessageEnd();
|
||||
std::vector<u8> result(decoder.MaxRetrievable());
|
||||
decoder.Get(result.data(), result.size());
|
||||
return HexToBytes(ss.str());
|
||||
}
|
||||
|
||||
void InitSlots() {
|
||||
static bool initialized = false;
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
|
||||
const std::string filepath = FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir) + BOOTROM9;
|
||||
FileUtil::IOFile file(filepath, "rb");
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t length = file.GetSize();
|
||||
if (length != 65536) {
|
||||
LOG_ERROR(HW_AES, "Bootrom9 size is wrong: {}", length);
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr std::size_t RSA_MODULUS_POS = 0xB3E0;
|
||||
file.Seek(RSA_MODULUS_POS, SEEK_SET);
|
||||
std::vector<u8> modulus(256);
|
||||
file.ReadArray(modulus.data(), modulus.size());
|
||||
|
||||
constexpr std::size_t RSA_EXPONENT_POS = 0xB4E0;
|
||||
file.Seek(RSA_EXPONENT_POS, SEEK_SET);
|
||||
std::vector<u8> exponent(256);
|
||||
file.ReadArray(exponent.data(), exponent.size());
|
||||
|
||||
rsa_slots[0] = RsaSlot(std::move(exponent), std::move(modulus));
|
||||
// TODO(B3N30): Initalize the other slots. But since they aren't used at all, we can skip them
|
||||
// for now
|
||||
}
|
||||
|
||||
RsaSlot GetSlot(std::size_t slot_id) {
|
||||
if (slot_id >= rsa_slots.size())
|
||||
return RsaSlot{};
|
||||
return rsa_slots[slot_id];
|
||||
}
|
||||
|
||||
std::vector<u8> CreateASN1Message(std::span<const u8> data) {
|
||||
static constexpr std::array<u8, 224> asn1_header = {
|
||||
{0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x31, 0x30, 0x0D, 0x06,
|
||||
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}};
|
||||
|
||||
std::vector<u8> message(asn1_header.begin(), asn1_header.end());
|
||||
CryptoPP::SHA256 sha;
|
||||
message.resize(message.size() + CryptoPP::SHA256::DIGESTSIZE);
|
||||
sha.CalculateDigest(message.data() + asn1_header.size(), data.data(), data.size());
|
||||
return message;
|
||||
}
|
||||
|
||||
} // namespace HW::RSA
|
||||
37
src/core/hw/rsa/rsa.h
Normal file
37
src/core/hw/rsa/rsa.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace HW::RSA {
|
||||
|
||||
class RsaSlot {
|
||||
public:
|
||||
RsaSlot() = default;
|
||||
RsaSlot(std::vector<u8> exponent, std::vector<u8> modulus)
|
||||
: init(true), exponent(std::move(exponent)), modulus(std::move(modulus)) {}
|
||||
std::vector<u8> GetSignature(std::span<const u8> message) const;
|
||||
|
||||
explicit operator bool() const {
|
||||
// TODO(B3N30): Maybe check if exponent and modulus are vailid
|
||||
return init;
|
||||
}
|
||||
|
||||
private:
|
||||
bool init = false;
|
||||
std::vector<u8> exponent;
|
||||
std::vector<u8> modulus;
|
||||
};
|
||||
|
||||
void InitSlots();
|
||||
|
||||
RsaSlot GetSlot(std::size_t slot_id);
|
||||
|
||||
std::vector<u8> CreateASN1Message(std::span<const u8> data);
|
||||
|
||||
} // namespace HW::RSA
|
||||
417
src/core/hw/y2r.cpp
Normal file
417
src/core/hw/y2r.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
// Copyright 2015 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include "common/assert.h"
|
||||
#include "common/color.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/microprofile.h"
|
||||
#include "common/vector_math.h"
|
||||
#include "core/core.h"
|
||||
#include "core/hle/service/cam/y2r_u.h"
|
||||
#include "core/hw/y2r.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
namespace HW::Y2R {
|
||||
|
||||
using namespace Service::Y2R;
|
||||
|
||||
static const std::size_t MAX_TILES = 1024 / 8;
|
||||
static const std::size_t TILE_SIZE = 8 * 8;
|
||||
using ImageTile = std::array<u32, TILE_SIZE>;
|
||||
|
||||
/// Converts a image strip from the source YUV format into individual 8x8 RGB32 tiles.
|
||||
template <InputFormat input_format>
|
||||
static void ConvertYUVToRGB(const u8* input_Y, const u8* input_U, const u8* input_V,
|
||||
ImageTile output[], unsigned int width, unsigned int height,
|
||||
const CoefficientSet& coefficients) {
|
||||
|
||||
for (unsigned int y = 0; y < height; ++y) {
|
||||
for (unsigned int x = 0; x < width; ++x) {
|
||||
s32 Y;
|
||||
s32 U;
|
||||
s32 V;
|
||||
if constexpr (input_format == InputFormat::YUV422_Indiv8 ||
|
||||
input_format == InputFormat::YUV422_Indiv16) {
|
||||
Y = input_Y[y * width + x];
|
||||
U = input_U[(y * width + x) / 2];
|
||||
V = input_V[(y * width + x) / 2];
|
||||
} else if constexpr (input_format == InputFormat::YUV420_Indiv8 ||
|
||||
input_format == InputFormat::YUV420_Indiv16) {
|
||||
Y = input_Y[y * width + x];
|
||||
U = input_U[((y / 2) * width + x) / 2];
|
||||
V = input_V[((y / 2) * width + x) / 2];
|
||||
} else if constexpr (input_format == InputFormat::YUYV422_Interleaved) {
|
||||
Y = input_Y[(y * width + x) * 2];
|
||||
U = input_Y[(y * width + (x / 2) * 2) * 2 + 1];
|
||||
V = input_Y[(y * width + (x / 2) * 2) * 2 + 3];
|
||||
} else {
|
||||
UNREACHABLE_MSG("Unknown Y2R input format {}", input_format);
|
||||
return;
|
||||
}
|
||||
|
||||
// This conversion process is bit-exact with hardware, as far as could be tested.
|
||||
auto& c = coefficients;
|
||||
s32 cY = c[0] * Y;
|
||||
|
||||
s32 r = cY + c[1] * V;
|
||||
s32 g = cY - c[2] * V - c[3] * U;
|
||||
s32 b = cY + c[4] * U;
|
||||
|
||||
const s32 rounding_offset = 0x18;
|
||||
r = (r >> 3) + c[5] + rounding_offset;
|
||||
g = (g >> 3) + c[6] + rounding_offset;
|
||||
b = (b >> 3) + c[7] + rounding_offset;
|
||||
|
||||
unsigned int tile = x / 8;
|
||||
unsigned int tile_x = x % 8;
|
||||
u32* out = &output[tile][y * 8 + tile_x];
|
||||
*out = ((u32)std::clamp(r >> 5, 0, 0xFF) << 24) |
|
||||
((u32)std::clamp(g >> 5, 0, 0xFF) << 16) |
|
||||
((u32)std::clamp(b >> 5, 0, 0xFF) << 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulates an incoming CDMA transfer. The N parameter is used to automatically convert 16-bit
|
||||
/// formats to 8-bit.
|
||||
template <std::size_t N>
|
||||
static void ReceiveData(Memory::MemorySystem& memory, u8* output, ConversionBuffer& buf,
|
||||
std::size_t amount_of_data) {
|
||||
const u8* input = memory.GetPointer(buf.address);
|
||||
|
||||
std::size_t output_unit = buf.transfer_unit / N;
|
||||
ASSERT(amount_of_data % output_unit == 0);
|
||||
|
||||
while (amount_of_data > 0) {
|
||||
for (std::size_t i = 0; i < output_unit; ++i) {
|
||||
output[i] = input[i * N];
|
||||
}
|
||||
|
||||
output += output_unit;
|
||||
input += buf.transfer_unit + buf.gap;
|
||||
|
||||
buf.address += buf.transfer_unit + buf.gap;
|
||||
buf.image_size -= buf.transfer_unit;
|
||||
amount_of_data -= output_unit;
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert intermediate RGB32 format to the final output format while simulating an outgoing CDMA
|
||||
/// transfer.
|
||||
template <OutputFormat output_format>
|
||||
static void SendData(Memory::MemorySystem& memory, const u32* input, ConversionBuffer& buf,
|
||||
int amount_of_data, u8 alpha) {
|
||||
|
||||
u8* output = memory.GetPointer(buf.address);
|
||||
|
||||
while (amount_of_data > 0) {
|
||||
u8* unit_end = output + buf.transfer_unit;
|
||||
while (output < unit_end) {
|
||||
u32 color = *input++;
|
||||
Common::Vec4<u8> col_vec{(u8)(color >> 24), (u8)(color >> 16), (u8)(color >> 8), alpha};
|
||||
|
||||
if constexpr (output_format == OutputFormat::RGBA8) {
|
||||
Common::Color::EncodeRGBA8(col_vec, output);
|
||||
output += 4;
|
||||
} else if constexpr (output_format == OutputFormat::RGB8) {
|
||||
Common::Color::EncodeRGB8(col_vec, output);
|
||||
output += 3;
|
||||
} else if constexpr (output_format == OutputFormat::RGB5A1) {
|
||||
Common::Color::EncodeRGB5A1(col_vec, output);
|
||||
output += 2;
|
||||
} else if constexpr (output_format == OutputFormat::RGB565) {
|
||||
Common::Color::EncodeRGB565(col_vec, output);
|
||||
output += 2;
|
||||
} else {
|
||||
UNREACHABLE_MSG("Unknown Y2R output format {}", output_format);
|
||||
}
|
||||
|
||||
amount_of_data -= 1;
|
||||
}
|
||||
|
||||
output += buf.gap;
|
||||
buf.address += buf.transfer_unit + buf.gap;
|
||||
buf.image_size -= buf.transfer_unit;
|
||||
}
|
||||
}
|
||||
|
||||
static const u8 linear_lut[TILE_SIZE] = {
|
||||
// clang-format off
|
||||
0, 1, 2, 3, 4, 5, 6, 7,
|
||||
8, 9, 10, 11, 12, 13, 14, 15,
|
||||
16, 17, 18, 19, 20, 21, 22, 23,
|
||||
24, 25, 26, 27, 28, 29, 30, 31,
|
||||
32, 33, 34, 35, 36, 37, 38, 39,
|
||||
40, 41, 42, 43, 44, 45, 46, 47,
|
||||
48, 49, 50, 51, 52, 53, 54, 55,
|
||||
56, 57, 58, 59, 60, 61, 62, 63,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static const u8 morton_lut[TILE_SIZE] = {
|
||||
// clang-format off
|
||||
0, 1, 4, 5, 16, 17, 20, 21,
|
||||
2, 3, 6, 7, 18, 19, 22, 23,
|
||||
8, 9, 12, 13, 24, 25, 28, 29,
|
||||
10, 11, 14, 15, 26, 27, 30, 31,
|
||||
32, 33, 36, 37, 48, 49, 52, 53,
|
||||
34, 35, 38, 39, 50, 51, 54, 55,
|
||||
40, 41, 44, 45, 56, 57, 60, 61,
|
||||
42, 43, 46, 47, 58, 59, 62, 63,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static void RotateTile0(const ImageTile& input, ImageTile& output, int height,
|
||||
const u8 out_map[64]) {
|
||||
for (int i = 0; i < height * 8; ++i) {
|
||||
output[out_map[i]] = input[i];
|
||||
}
|
||||
}
|
||||
|
||||
static void RotateTile90(const ImageTile& input, ImageTile& output, int height,
|
||||
const u8 out_map[64]) {
|
||||
int out_i = 0;
|
||||
for (int x = 0; x < 8; ++x) {
|
||||
for (int y = height - 1; y >= 0; --y) {
|
||||
output[out_map[out_i++]] = input[y * 8 + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void RotateTile180(const ImageTile& input, ImageTile& output, int height,
|
||||
const u8 out_map[64]) {
|
||||
int out_i = 0;
|
||||
for (int i = height * 8 - 1; i >= 0; --i) {
|
||||
output[out_map[out_i++]] = input[i];
|
||||
}
|
||||
}
|
||||
|
||||
static void RotateTile270(const ImageTile& input, ImageTile& output, int height,
|
||||
const u8 out_map[64]) {
|
||||
int out_i = 0;
|
||||
for (int x = 8 - 1; x >= 0; --x) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
output[out_map[out_i++]] = input[y * 8 + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void WriteTileToOutput(u32* output, const ImageTile& tile, int height, int line_stride) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < 8; ++x) {
|
||||
output[y * line_stride + x] = tile[y * 8 + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MICROPROFILE_DEFINE(Y2R_PerformConversion, "Y2R", "PerformConversion", MP_RGB(185, 66, 245));
|
||||
|
||||
/**
|
||||
* Performs a Y2R colorspace conversion.
|
||||
*
|
||||
* The Y2R hardware implements hardware-accelerated YUV to RGB colorspace conversions. It is most
|
||||
* commonly used for video playback or to display camera input to the screen.
|
||||
*
|
||||
* The conversion process is quite configurable, and can be divided in distinct steps. From
|
||||
* observation, it appears that the hardware buffers a single 8-pixel tall strip of image data
|
||||
* internally and converts it in one go before writing to the output and loading the next strip.
|
||||
*
|
||||
* The steps taken to convert one strip of image data are:
|
||||
*
|
||||
* - The hardware receives data via CDMA (http://3dbrew.org/wiki/Corelink_DMA_Engines), which is
|
||||
* presumably stored in one or more internal buffers. This process can be done in several separate
|
||||
* transfers, as long as they don't exceed the size of the internal image buffer. This allows
|
||||
* flexibility in input strides.
|
||||
* - The input data is decoded into a YUV tuple. Several formats are suported, see the `InputFormat`
|
||||
* enum.
|
||||
* - The YUV tuple is converted, using fixed point calculations, to RGB. This step can be configured
|
||||
* using a set of coefficients to support different colorspace standards. See `CoefficientSet`.
|
||||
* - The strip can be optionally rotated 90, 180 or 270 degrees. Since each strip is processed
|
||||
* independently, this notably rotates each *strip*, not the entire image. This means that for 90
|
||||
* or 270 degree rotations, the output will be in terms of several 8 x height images, and for any
|
||||
* non-zero rotation the strips will have to be re-arranged so that the parts of the image will
|
||||
* not be shuffled together. This limitation makes this a feature of somewhat dubious utility. 90
|
||||
* or 270 degree rotations in images with non-even height don't seem to work properly.
|
||||
* - The data is converted to the output RGB format. See the `OutputFormat` enum.
|
||||
* - The data can be output either linearly line-by-line or in the swizzled 8x8 tile format used by
|
||||
* the PICA. This is decided by the `BlockAlignment` enum. If 8x8 alignment is used, then the
|
||||
* image must have a height divisible by 8. The image width must always be divisible by 8.
|
||||
* - The final data is then CDMAed out to main memory and the next image strip is processed. This
|
||||
* offers the same flexibility as the input stage.
|
||||
*
|
||||
* In this implementation, to avoid the combinatorial explosion of parameter combinations, common
|
||||
* intermediate formats are used and where possible tables or parameters are used instead of
|
||||
* diverging code paths to keep the amount of branches in check. Some steps are also merged to
|
||||
* increase efficiency.
|
||||
*
|
||||
* Output for all valid settings combinations matches hardware, however output in some edge-cases
|
||||
* differs:
|
||||
*
|
||||
* - `Block8x8` alignment with non-mod8 height produces different garbage patterns on the last
|
||||
* strip, especially when combined with rotation.
|
||||
* - Hardware, when using `Linear` alignment with a non-even height and 90 or 270 degree rotation
|
||||
* produces misaligned output on the last strip. This implmentation produces output with the
|
||||
* correct "expected" alignment.
|
||||
*
|
||||
* Hardware behaves strangely (doesn't fire the completion interrupt, for example) in these cases,
|
||||
* so they are believed to be invalid configurations anyway.
|
||||
*/
|
||||
void PerformConversion(Memory::MemorySystem& memory, ConversionConfiguration cvt) {
|
||||
MICROPROFILE_SCOPE(Y2R_PerformConversion);
|
||||
|
||||
ASSERT(cvt.input_line_width % 8 == 0);
|
||||
ASSERT(cvt.block_alignment != BlockAlignment::Block8x8 || cvt.input_lines % 8 == 0);
|
||||
// Tiles per row
|
||||
std::size_t num_tiles = cvt.input_line_width / 8;
|
||||
ASSERT(num_tiles <= MAX_TILES);
|
||||
|
||||
// Buffer used as a CDMA source/target.
|
||||
std::unique_ptr<u8[]> data_buffer(new u8[cvt.input_line_width * 8 * 4]);
|
||||
// Intermediate storage for decoded 8x8 image tiles. Always stored as RGB32.
|
||||
std::unique_ptr<ImageTile[]> tiles(new ImageTile[num_tiles]);
|
||||
ImageTile tmp_tile;
|
||||
|
||||
// LUT used to remap writes to a tile. Used to allow linear or swizzled output without
|
||||
// requiring two different code paths.
|
||||
const u8* tile_remap = nullptr;
|
||||
switch (cvt.block_alignment) {
|
||||
case BlockAlignment::Linear:
|
||||
tile_remap = linear_lut;
|
||||
break;
|
||||
case BlockAlignment::Block8x8:
|
||||
tile_remap = morton_lut;
|
||||
break;
|
||||
}
|
||||
|
||||
for (unsigned int y = 0; y < cvt.input_lines; y += 8) {
|
||||
unsigned int row_height = std::min(cvt.input_lines - y, 8u);
|
||||
|
||||
// Total size in pixels of incoming data required for this strip.
|
||||
const std::size_t row_data_size = row_height * cvt.input_line_width;
|
||||
|
||||
u8* input_Y = data_buffer.get();
|
||||
u8* input_U = input_Y + 8 * cvt.input_line_width;
|
||||
u8* input_V = input_U + 8 * cvt.input_line_width / 2;
|
||||
|
||||
switch (cvt.input_format) {
|
||||
case InputFormat::YUV422_Indiv8:
|
||||
ReceiveData<1>(memory, input_Y, cvt.src_Y, row_data_size);
|
||||
ReceiveData<1>(memory, input_U, cvt.src_U, row_data_size / 2);
|
||||
ReceiveData<1>(memory, input_V, cvt.src_V, row_data_size / 2);
|
||||
ConvertYUVToRGB<InputFormat::YUV422_Indiv8>(input_Y, input_U, input_V, tiles.get(),
|
||||
cvt.input_line_width, row_height,
|
||||
cvt.coefficients);
|
||||
break;
|
||||
case InputFormat::YUV420_Indiv8:
|
||||
ReceiveData<1>(memory, input_Y, cvt.src_Y, row_data_size);
|
||||
ReceiveData<1>(memory, input_U, cvt.src_U, row_data_size / 4);
|
||||
ReceiveData<1>(memory, input_V, cvt.src_V, row_data_size / 4);
|
||||
ConvertYUVToRGB<InputFormat::YUV420_Indiv8>(input_Y, input_U, input_V, tiles.get(),
|
||||
cvt.input_line_width, row_height,
|
||||
cvt.coefficients);
|
||||
break;
|
||||
case InputFormat::YUV422_Indiv16:
|
||||
ReceiveData<2>(memory, input_Y, cvt.src_Y, row_data_size);
|
||||
ReceiveData<2>(memory, input_U, cvt.src_U, row_data_size / 2);
|
||||
ReceiveData<2>(memory, input_V, cvt.src_V, row_data_size / 2);
|
||||
ConvertYUVToRGB<InputFormat::YUV422_Indiv16>(input_Y, input_U, input_V, tiles.get(),
|
||||
cvt.input_line_width, row_height,
|
||||
cvt.coefficients);
|
||||
break;
|
||||
case InputFormat::YUV420_Indiv16:
|
||||
ReceiveData<2>(memory, input_Y, cvt.src_Y, row_data_size);
|
||||
ReceiveData<2>(memory, input_U, cvt.src_U, row_data_size / 4);
|
||||
ReceiveData<2>(memory, input_V, cvt.src_V, row_data_size / 4);
|
||||
ConvertYUVToRGB<InputFormat::YUV420_Indiv16>(input_Y, input_U, input_V, tiles.get(),
|
||||
cvt.input_line_width, row_height,
|
||||
cvt.coefficients);
|
||||
break;
|
||||
case InputFormat::YUYV422_Interleaved:
|
||||
input_U = nullptr;
|
||||
input_V = nullptr;
|
||||
ReceiveData<1>(memory, input_Y, cvt.src_YUYV, row_data_size * 2);
|
||||
ConvertYUVToRGB<InputFormat::YUYV422_Interleaved>(input_Y, input_U, input_V,
|
||||
tiles.get(), cvt.input_line_width,
|
||||
row_height, cvt.coefficients);
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown Y2R input format {}", cvt.input_format);
|
||||
return;
|
||||
}
|
||||
|
||||
u32* output_buffer = reinterpret_cast<u32*>(data_buffer.get());
|
||||
|
||||
for (std::size_t i = 0; i < num_tiles; ++i) {
|
||||
int image_strip_width = 0;
|
||||
int output_stride = 0;
|
||||
|
||||
switch (cvt.rotation) {
|
||||
case Rotation::None:
|
||||
RotateTile0(tiles[i], tmp_tile, row_height, tile_remap);
|
||||
image_strip_width = cvt.input_line_width;
|
||||
output_stride = 8;
|
||||
break;
|
||||
case Rotation::Clockwise_90:
|
||||
RotateTile90(tiles[i], tmp_tile, row_height, tile_remap);
|
||||
image_strip_width = 8;
|
||||
output_stride = 8 * row_height;
|
||||
break;
|
||||
case Rotation::Clockwise_180:
|
||||
// For 180 and 270 degree rotations we also invert the order of tiles in the strip,
|
||||
// since the rotates are done individually on each tile.
|
||||
RotateTile180(tiles[num_tiles - i - 1], tmp_tile, row_height, tile_remap);
|
||||
image_strip_width = cvt.input_line_width;
|
||||
output_stride = 8;
|
||||
break;
|
||||
case Rotation::Clockwise_270:
|
||||
RotateTile270(tiles[num_tiles - i - 1], tmp_tile, row_height, tile_remap);
|
||||
image_strip_width = 8;
|
||||
output_stride = 8 * row_height;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (cvt.block_alignment) {
|
||||
case BlockAlignment::Linear:
|
||||
WriteTileToOutput(output_buffer, tmp_tile, row_height, image_strip_width);
|
||||
output_buffer += output_stride;
|
||||
break;
|
||||
case BlockAlignment::Block8x8:
|
||||
WriteTileToOutput(output_buffer, tmp_tile, 8, 8);
|
||||
output_buffer += TILE_SIZE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (cvt.output_format) {
|
||||
case OutputFormat::RGBA8:
|
||||
SendData<OutputFormat::RGBA8>(memory, reinterpret_cast<u32*>(data_buffer.get()),
|
||||
cvt.dst, static_cast<int>(row_data_size),
|
||||
static_cast<u8>(cvt.alpha));
|
||||
break;
|
||||
case OutputFormat::RGB8:
|
||||
SendData<OutputFormat::RGB8>(memory, reinterpret_cast<u32*>(data_buffer.get()), cvt.dst,
|
||||
static_cast<int>(row_data_size),
|
||||
static_cast<u8>(cvt.alpha));
|
||||
break;
|
||||
case OutputFormat::RGB5A1:
|
||||
SendData<OutputFormat::RGB5A1>(memory, reinterpret_cast<u32*>(data_buffer.get()),
|
||||
cvt.dst, static_cast<int>(row_data_size),
|
||||
static_cast<u8>(cvt.alpha));
|
||||
break;
|
||||
case OutputFormat::RGB565:
|
||||
SendData<OutputFormat::RGB565>(memory, reinterpret_cast<u32*>(data_buffer.get()),
|
||||
cvt.dst, static_cast<int>(row_data_size),
|
||||
static_cast<u8>(cvt.alpha));
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown Y2R output format {}", cvt.output_format);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace HW::Y2R
|
||||
17
src/core/hw/y2r.h
Normal file
17
src/core/hw/y2r.h
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2015 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Memory {
|
||||
class MemorySystem;
|
||||
}
|
||||
|
||||
namespace Service::Y2R {
|
||||
struct ConversionConfiguration;
|
||||
} // namespace Service::Y2R
|
||||
|
||||
namespace HW::Y2R {
|
||||
void PerformConversion(Memory::MemorySystem& memory, Service::Y2R::ConversionConfiguration cvt);
|
||||
} // namespace HW::Y2R
|
||||
Reference in New Issue
Block a user