mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-16 07:50:28 +02:00
The core and the headless tools no longer use mutexes, condition variables, spinlocks, std::atomic, std::thread or volatile flags. Threads hand off through libretro-common's retro_atomic and rthreads plus a few small pieces on top of them in Common/Thread: Thread, MpscQueue, EventCounter, ParkingLot waits and ReaderGate. Every wait blocks until it's woken; nothing polls on a timer. State that only one thread touches just loses its lock. The PSP filesystem, sceIo and the savedata dialog now belong to the emulation thread: async reads and writes run when they're queued and complete later in emulated time, and the IO and savedata threads are gone. The friend finder only talks to the adhoc server and hands what it gets to the emulation thread. GPU init always runs on the thread that starts the game. The Vulkan render and compile queues, pipeline and render pass caches, delete list and SPIR-V cache, the GL render queue, the software renderer's jit caches, the file loader caches, the web debugger, UPnP, input, savestates, logging and the GE debugger all hand off through queues and atomics. The IR jit's spinning profiler thread goes. A CPU thread with nothing to run now sleeps until work is queued instead of spinning. Headless with --debugger waits for a client while stopped, and ends the run once every client that connected has left. Web server shutdown keeps servicing the CPU queue until the server has finished.
248 lines
6.6 KiB
C++
248 lines
6.6 KiB
C++
#include <cstring>
|
|
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Data/Format/IniFile.h"
|
|
#include "Common/File/VFS/VFS.h"
|
|
#include "Common/Log.h"
|
|
|
|
#include "Common/StringUtils.h"
|
|
|
|
// Don't forget to update the constants in the header file if you change this.
|
|
static const char * const g_categoryNames[(size_t)I18NCat::CATEGORY_COUNT] = {
|
|
"Audio",
|
|
"Controls",
|
|
"CwCheats",
|
|
"DesktopUI",
|
|
"Developer",
|
|
"Dialog",
|
|
"Error",
|
|
"Game",
|
|
"Graphics",
|
|
"InstallZip",
|
|
"KeyMapping",
|
|
"MainMenu",
|
|
"MainSettings",
|
|
"MappableControls",
|
|
"Networking",
|
|
"Pause",
|
|
"PostShaders",
|
|
"PSPCredits",
|
|
"MemStick",
|
|
"RemoteISO",
|
|
"Reporting",
|
|
"Savedata",
|
|
"Screen",
|
|
"Search",
|
|
"Store",
|
|
"SysInfo",
|
|
"System",
|
|
"TextureShaders",
|
|
"Themes",
|
|
"UI Elements",
|
|
"VR",
|
|
"Achievements",
|
|
"PSPSettings",
|
|
};
|
|
|
|
I18NRepo g_i18nrepo;
|
|
|
|
std::string I18NRepo::LanguageID() {
|
|
return languageID_;
|
|
}
|
|
|
|
I18NRepo::I18NRepo() {
|
|
for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++)
|
|
retro_atomic_ptr_init(&cats_[i], nullptr);
|
|
Clear();
|
|
}
|
|
|
|
void I18NRepo::Publish(size_t index, I18NCategory *cat) {
|
|
owned_.emplace_back(cat);
|
|
retro_atomic_store_release_ptr(&cats_[index], cat);
|
|
}
|
|
|
|
void I18NRepo::Clear() {
|
|
for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) {
|
|
// Initialize with empty categories, so that early lookups don't crash.
|
|
Publish(i, new I18NCategory());
|
|
}
|
|
}
|
|
|
|
I18NCategory::I18NCategory() {
|
|
for (int i = 0; i < MISSED_SLOTS; i++)
|
|
retro_atomic_ptr_init(&missedHashes_[i], nullptr);
|
|
mpsc_stack_init(&missed_);
|
|
}
|
|
|
|
I18NCategory::I18NCategory(const Section §ion) : I18NCategory() {
|
|
std::map<std::string, std::string> sectionMap = section.ToMap();
|
|
SetMap(sectionMap);
|
|
name_ = section.name().c_str();
|
|
}
|
|
|
|
I18NCategory::~I18NCategory() {
|
|
ClearMissed();
|
|
}
|
|
|
|
void I18NCategory::ClearMissed() {
|
|
mpsc_stack_node_t *node = mpsc_stack_drain(&missed_);
|
|
while (node) {
|
|
mpsc_stack_node_t *next = node->next;
|
|
delete ((MissedLink *)node)->self;
|
|
node = next;
|
|
}
|
|
for (int i = 0; i < MISSED_SLOTS; i++)
|
|
retro_atomic_store_relaxed_ptr(&missedHashes_[i], nullptr);
|
|
}
|
|
|
|
void I18NCategory::NoteMissed(std::string_view key, std::string_view def) {
|
|
// FNV-1a, never zero, which marks a free slot.
|
|
uintptr_t h = (uintptr_t)14695981039346656037ULL;
|
|
for (char c : key)
|
|
h = (h ^ (unsigned char)c) * (uintptr_t)1099511628211ULL;
|
|
if (h == 0)
|
|
h = 1;
|
|
for (int probe = 0; probe < MISSED_SLOTS; probe++) {
|
|
retro_atomic_ptr_t *slot = &missedHashes_[(h + probe) & (MISSED_SLOTS - 1)];
|
|
const uintptr_t seen = (uintptr_t)retro_atomic_load_acquire_ptr(slot);
|
|
if (seen == h)
|
|
return; // already recorded
|
|
if (seen == 0) {
|
|
if (!retro_atomic_cas_ptr(slot, nullptr, (void *)h)) {
|
|
if ((uintptr_t)retro_atomic_load_acquire_ptr(slot) == h)
|
|
return;
|
|
continue;
|
|
}
|
|
MissedKey *missed = new MissedKey{ {}, std::string(key), std::string(!def.empty() ? def : key) };
|
|
missed->link.self = missed;
|
|
mpsc_stack_push(&missed_, &missed->link.node);
|
|
return;
|
|
}
|
|
}
|
|
// Full: stop recording.
|
|
}
|
|
|
|
std::string_view I18NCategory::T(std::string_view key, std::string_view def) {
|
|
auto iter = map_.find(key);
|
|
if (iter != map_.end()) {
|
|
return iter->second.text.c_str();
|
|
} else {
|
|
if (map_.empty()) {
|
|
// Too early. This is probably in desktop-ui translation.
|
|
return !def.empty() ? def : key;
|
|
}
|
|
if (key != "Font") {
|
|
// Font is allowed to be missing.
|
|
DEBUG_LOG(Log::UI, "Missing translation [%s] %.*s (%.*s)", name_.c_str(), STR_VIEW(key), STR_VIEW(def));
|
|
NoteMissed(key, def);
|
|
}
|
|
return !def.empty() ? def : key;
|
|
}
|
|
}
|
|
|
|
const char *I18NCategory::T_cstr(const char *key, const char *def) {
|
|
auto iter = map_.find(key);
|
|
if (iter != map_.end()) {
|
|
return iter->second.text.c_str();
|
|
} else {
|
|
if (map_.empty()) {
|
|
// Too early. This is probably in desktop-ui translation.
|
|
return def ? def : key;
|
|
}
|
|
if (strcmp(key, "Font") != 0) {
|
|
DEBUG_LOG(Log::UI, "Missing translation %s (%s)", key, def);
|
|
NoteMissed(key, def ? def : "");
|
|
}
|
|
return def ? def : key;
|
|
}
|
|
}
|
|
|
|
void I18NCategory::SetMap(const std::map<std::string, std::string> &m) {
|
|
for (const auto &[key, value] : m) {
|
|
if (map_.find(key) == map_.end()) {
|
|
std::string text = ReplaceAll(value, "\\n", "\n");
|
|
_dbg_assert_(key.find('\n') == std::string::npos);
|
|
map_[key] = I18NEntry(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::map<std::string, std::string, std::less<>> I18NCategory::Missed() const {
|
|
// Pushes only add in front of what is already there, so the walk is safe.
|
|
std::map<std::string, std::string, std::less<>> missed;
|
|
const mpsc_stack_node_t *node = (const mpsc_stack_node_t *)retro_atomic_load_acquire_ptr(const_cast<retro_atomic_ptr_t *>(&missed_.head));
|
|
for (; node; node = node->next) {
|
|
const MissedKey *m = ((const MissedLink *)node)->self;
|
|
missed[m->key] = m->def;
|
|
}
|
|
return missed;
|
|
}
|
|
|
|
I18NCategory *I18NRepo::GetCategory(I18NCat category) {
|
|
if (category != I18NCat::NONE)
|
|
return Cat(category);
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
Path I18NRepo::GetIniPath(const std::string &languageID) const {
|
|
return Path("lang") / (languageID + ".ini");
|
|
}
|
|
|
|
bool I18NRepo::IniExists(const std::string &languageID) const {
|
|
File::FileInfo info;
|
|
if (!g_VFS.Exists(GetIniPath(languageID).ToString().c_str()))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool I18NRepo::LoadIni(const std::string &languageID, const Path &overridePath) {
|
|
IniFile ini;
|
|
Path iniPath;
|
|
|
|
// INFO_LOG(Log::UI, "Loading lang ini %s", iniPath.c_str());
|
|
if (!overridePath.empty()) {
|
|
iniPath = overridePath / (languageID + ".ini");
|
|
} else {
|
|
iniPath = GetIniPath(languageID);
|
|
}
|
|
|
|
if (!ini.LoadFromVFS(g_VFS, iniPath.ToString()))
|
|
return false;
|
|
|
|
// Build every category first, then publish: a lookup sees either language, whole.
|
|
const std::vector<std::unique_ptr<Section>> §ions = ini.Sections();
|
|
I18NCategory *built[(size_t)I18NCat::CATEGORY_COUNT]{};
|
|
for (auto §ion : sections) {
|
|
for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) {
|
|
if (!strcmp(section->name().c_str(), g_categoryNames[i])) {
|
|
delete built[i];
|
|
built[i] = new I18NCategory(*section.get());
|
|
}
|
|
}
|
|
}
|
|
for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++)
|
|
Publish(i, built[i] ? built[i] : new I18NCategory());
|
|
|
|
languageID_ = languageID;
|
|
return true;
|
|
}
|
|
|
|
void I18NRepo::LogMissingKeys() const {
|
|
for (size_t i = 0; i < (size_t)I18NCat::CATEGORY_COUNT; i++) {
|
|
const I18NCategory *cat = Cat((I18NCat)i);
|
|
for (auto &key : cat->Missed()) {
|
|
INFO_LOG(Log::UI, "Missing translation [%s]: %s (%s)", g_categoryNames[i], key.first.c_str(), key.second.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
I18NCategory *GetI18NCategory(I18NCat category) {
|
|
if (category == I18NCat::NONE) {
|
|
return nullptr;
|
|
}
|
|
I18NCategory *cat = g_i18nrepo.GetCategory(category);
|
|
_dbg_assert_(cat);
|
|
return cat;
|
|
}
|