mirror of
https://github.com/izzy2lost/WeeU.git
synced 2026-07-06 00:19:59 -07:00
Add icon & title loader classes and JNI project
This commit is contained in:
+21
-12
@@ -43,6 +43,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
add_subdirectory(Common)
|
||||
add_subdirectory(gui)
|
||||
if(ANDROID)
|
||||
add_subdirectory(android/app/src/main/cpp)
|
||||
endif()
|
||||
add_subdirectory(Cafe)
|
||||
add_subdirectory(Cemu)
|
||||
add_subdirectory(config)
|
||||
@@ -53,21 +56,27 @@ add_subdirectory(imgui)
|
||||
add_subdirectory(resource)
|
||||
add_subdirectory(asm)
|
||||
|
||||
add_executable(CemuBin
|
||||
main.cpp
|
||||
mainLLE.cpp
|
||||
)
|
||||
if(ANDROID)
|
||||
add_library(CemuBin
|
||||
main.cpp
|
||||
mainLLE.cpp
|
||||
)
|
||||
else()
|
||||
add_executable(CemuBin
|
||||
main.cpp
|
||||
mainLLE.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(CemuBin PRIVATE
|
||||
resource/cemu.rc
|
||||
)
|
||||
if(WIN32)
|
||||
target_sources(CemuBin PRIVATE
|
||||
resource/cemu.rc
|
||||
)
|
||||
endif()
|
||||
set_property(TARGET CemuBin PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
set_property(TARGET CemuBin PROPERTY WIN32_EXECUTABLE $<NOT:$<CONFIG:Debug>>)
|
||||
set(OUTPUT_NAME "Cemu_$<LOWER_CASE:$<CONFIG>>")
|
||||
endif()
|
||||
|
||||
set_property(TARGET CemuBin PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
set_property(TARGET CemuBin PROPERTY WIN32_EXECUTABLE $<NOT:$<CONFIG:Debug>>)
|
||||
set(OUTPUT_NAME "Cemu_$<LOWER_CASE:$<CONFIG>>")
|
||||
|
||||
if (MACOS_BUNDLE)
|
||||
set_property(TARGET CemuBin PROPERTY MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/resource/MacOSXBundleInfo.plist.in")
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace 'info.cemu.Cemu'
|
||||
compileSdk 33
|
||||
compileSdk 34
|
||||
ndkVersion '25.2.9519653'
|
||||
defaultConfig {
|
||||
applicationId "info.cemu.Cemu"
|
||||
minSdk 30
|
||||
targetSdk 33
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
ndk {
|
||||
@@ -17,20 +17,6 @@ android {
|
||||
abiFilters("x86_64")
|
||||
}
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments(
|
||||
'-DENABLE_VCPKG=ON',
|
||||
'-DVCPKG_TARGET_ANDROID=ON',
|
||||
'-DENABLE_SDL=OFF',
|
||||
'-DENABLE_OPENGL=OFF',
|
||||
'-DENABLE_WXWIDGETS=OFF',
|
||||
'-DBUNDLE_SPEEX=ON',
|
||||
'-DENABLE_DISCORD_RPC=OFF',
|
||||
'-DENABLE_WAYLAND=OFF'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -45,8 +31,25 @@ android {
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('../../../CMakeLists.txt')
|
||||
version '3.22.1'
|
||||
path file('../../../CMakeLists.txt')
|
||||
}
|
||||
}
|
||||
defaultConfig {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments(
|
||||
'-DENABLE_VCPKG=ON',
|
||||
'-DVCPKG_TARGET_ANDROID=ON',
|
||||
'-DENABLE_SDL=OFF',
|
||||
'-DENABLE_WXWIDGETS=OFF',
|
||||
'-DENABLE_OPENGL=OFF',
|
||||
'-DBUNDLE_SPEEX=ON',
|
||||
'-DENABLE_DISCORD_RPC=OFF',
|
||||
'-DENABLE_WAYLAND=OFF'
|
||||
)
|
||||
abiFilters("x86_64")
|
||||
}
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
@@ -55,7 +58,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
add_library(
|
||||
CemuAndroid
|
||||
SHARED
|
||||
native-lib.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
CemuAndroid
|
||||
PRIVATE
|
||||
CemuCommon
|
||||
CemuAndroidGui
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_example_myapplication_MainActivity_stringFromJNI(
|
||||
JNIEnv *env,
|
||||
jobject /* this */) {
|
||||
std::string hello = "Hello from C++";
|
||||
return env->NewStringUTF(hello.c_str());
|
||||
}
|
||||
@@ -9,9 +9,8 @@ import info.cemu.Cemu.databinding.ActivityMainBinding;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
// Used to load the 'Cemu' library on application startup.
|
||||
static {
|
||||
System.loadLibrary("Cemu");
|
||||
System.loadLibrary("CemuAndroid");
|
||||
}
|
||||
|
||||
private ActivityMainBinding binding;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
add_library(CemuAndroidGui
|
||||
AndroidGuiWrapper.cpp
|
||||
GameIconLoader.cpp
|
||||
GameIconLoader.h
|
||||
GameTitleLoader.cpp
|
||||
GameTitleLoader.h
|
||||
OpenGLCanvas.cpp
|
||||
Image.cpp
|
||||
)
|
||||
|
||||
target_include_directories(CemuAndroidGui PUBLIC "../")
|
||||
|
||||
target_link_libraries(CemuAndroidGui PRIVATE
|
||||
CemuAudio
|
||||
CemuCafe
|
||||
CemuCommon
|
||||
CemuComponents
|
||||
CemuConfig
|
||||
CemuInput
|
||||
CemuResource
|
||||
CemuUtil
|
||||
Boost::headers
|
||||
CURL::libcurl
|
||||
libzip::zip
|
||||
OpenSSL::Crypto
|
||||
pugixml::pugixml
|
||||
ZArchive::zarchive
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "GameIconLoader.h"
|
||||
|
||||
#include "Cafe/TitleList/TitleInfo.h"
|
||||
#include "Cafe/TitleList/TitleList.h"
|
||||
|
||||
GameIconLoader::GameIconLoader() {
|
||||
m_loaderThread = std::thread(&GameIconLoader::loadGameIcons, this);
|
||||
}
|
||||
|
||||
GameIconLoader::~GameIconLoader() {
|
||||
m_continueLoading = false;
|
||||
m_condVar.notify_one();
|
||||
m_loaderThread.join();
|
||||
}
|
||||
|
||||
void GameIconLoader::requestIcon(TitleId titleId) {
|
||||
{
|
||||
std::lock_guard lock(m_threadMutex);
|
||||
m_iconsToLoad.emplace_front(titleId);
|
||||
}
|
||||
m_condVar.notify_one();
|
||||
}
|
||||
|
||||
void GameIconLoader::loadGameIcons() {
|
||||
while (m_continueLoading) {
|
||||
TitleId titleId = 0;
|
||||
{
|
||||
std::unique_lock lock(m_threadMutex);
|
||||
m_condVar.wait(lock, [this] {
|
||||
return !m_iconsToLoad.empty() || !m_continueLoading;
|
||||
});
|
||||
if (!m_continueLoading)
|
||||
return;
|
||||
titleId = m_iconsToLoad.front();
|
||||
m_iconsToLoad.pop_front();
|
||||
}
|
||||
TitleInfo titleInfo;
|
||||
if (!CafeTitleList::GetFirstByTitleId(titleId, titleInfo))
|
||||
continue;
|
||||
|
||||
if (auto iconIt = m_iconCache.find(titleId); iconIt != m_iconCache.end()) {
|
||||
if (m_onIconLoaded) m_onIconLoaded(titleId, iconIt->second);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string tempMountPath = TitleInfo::GetUniqueTempMountingPath();
|
||||
if (!titleInfo.Mount(tempMountPath, "", FSC_PRIORITY_BASE))
|
||||
continue;
|
||||
auto tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga").c_str());
|
||||
if (tgaData && tgaData->size() > 16) {
|
||||
int width, height, channels;
|
||||
Image image(tgaData.value());
|
||||
if (image.isOk()) {
|
||||
if (m_onIconLoaded) m_onIconLoaded(titleId, image);
|
||||
m_iconCache.emplace(titleId, std::move(image));
|
||||
}
|
||||
} else {
|
||||
cemuLog_log(LogType::Force, "Failed to load icon for title {:016x}", titleId);
|
||||
}
|
||||
titleInfo.Unmount(tempMountPath);
|
||||
}
|
||||
}
|
||||
|
||||
void GameIconLoader::setOnIconLoaded(const std::function<void(TitleId, const Image &)> &onIconLoaded) {
|
||||
{
|
||||
std::lock_guard lock(m_threadMutex);
|
||||
m_onIconLoaded = onIconLoaded;
|
||||
}
|
||||
m_condVar.notify_one();
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "Cafe/TitleList/TitleId.h"
|
||||
#include "Image.h"
|
||||
|
||||
class GameIconLoader {
|
||||
std::atomic_bool m_continueLoading = true;
|
||||
std::condition_variable m_condVar;
|
||||
std::thread m_loaderThread;
|
||||
std::mutex m_threadMutex;
|
||||
std::deque <TitleId> m_iconsToLoad;
|
||||
std::map <TitleId, Image> m_iconCache;
|
||||
std::function<void(TitleId, const Image &)> m_onIconLoaded = nullptr;
|
||||
|
||||
void loadGameIcons();
|
||||
|
||||
public:
|
||||
GameIconLoader();
|
||||
|
||||
~GameIconLoader();
|
||||
|
||||
void setOnIconLoaded(const std::function<void(TitleId, const Image &)> &onIconLoaded);
|
||||
|
||||
void requestIcon(TitleId titleId);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "GameTitleLoader.h"
|
||||
|
||||
GameTitleLoader::GameTitleLoader() {
|
||||
m_loaderThread = std::thread(&GameTitleLoader::loadGameTitles, this);
|
||||
}
|
||||
|
||||
void GameTitleLoader::queueTitle(TitleId titleId) {
|
||||
{
|
||||
std::lock_guard lock(m_threadMutex);
|
||||
m_titlesToLoad.emplace_front(titleId);
|
||||
}
|
||||
m_condVar.notify_one();
|
||||
}
|
||||
|
||||
void GameTitleLoader::setOnTitleLoaded(const std::function<void(const Game &)> &onTitleLoaded) {
|
||||
{
|
||||
std::lock_guard lock(m_threadMutex);
|
||||
m_onTitleLoaded = onTitleLoaded;
|
||||
}
|
||||
m_condVar.notify_one();
|
||||
}
|
||||
|
||||
void GameTitleLoader::reloadGameTitles() {
|
||||
if (m_callbackIdTitleList.has_value()) {
|
||||
CafeTitleList::Refresh();
|
||||
CafeTitleList::UnregisterCallback(m_callbackIdTitleList.value());
|
||||
}
|
||||
registerCallback();
|
||||
}
|
||||
|
||||
GameTitleLoader::~GameTitleLoader() {
|
||||
m_continueLoading = false;
|
||||
m_condVar.notify_one();
|
||||
m_loaderThread.join();
|
||||
if (m_callbackIdTitleList.has_value())
|
||||
CafeTitleList::UnregisterCallback(m_callbackIdTitleList.value());
|
||||
}
|
||||
|
||||
void GameTitleLoader::titleRefresh(TitleId titleId) {
|
||||
GameInfo2 gameInfo = CafeTitleList::GetGameInfo(titleId);
|
||||
if (!gameInfo.IsValid()) {
|
||||
return;
|
||||
}
|
||||
TitleId baseTitleId = gameInfo.GetBaseTitleId();
|
||||
bool isNewEntry = false;
|
||||
if (auto gameIndex = m_gameInfos.find(baseTitleId); gameIndex == m_gameInfos.end()) {
|
||||
isNewEntry = true;
|
||||
m_titleIds.push_back(baseTitleId);
|
||||
m_gameInfos[baseTitleId] = Game();
|
||||
}
|
||||
Game &game = m_gameInfos[baseTitleId];
|
||||
game.titleId = baseTitleId;
|
||||
game.name = GetNameByTitleId(baseTitleId);
|
||||
game.version = gameInfo.GetVersion();
|
||||
game.region = gameInfo.GetRegion();
|
||||
if (gameInfo.HasAOC()) {
|
||||
game.dlc = gameInfo.GetAOCVersion();
|
||||
}
|
||||
if (isNewEntry) {
|
||||
iosu::pdm::GameListStat playTimeStat;
|
||||
if (iosu::pdm::GetStatForGamelist(baseTitleId, playTimeStat))
|
||||
game.secondsPlayed = playTimeStat.numMinutesPlayed * 60;
|
||||
if (m_onTitleLoaded) m_onTitleLoaded(game);
|
||||
} else {
|
||||
// TODO: Update
|
||||
}
|
||||
}
|
||||
|
||||
void GameTitleLoader::loadGameTitles() {
|
||||
while (m_continueLoading) {
|
||||
TitleId titleId = 0;
|
||||
{
|
||||
std::unique_lock lock(m_threadMutex);
|
||||
m_condVar.wait(lock, [this] {
|
||||
return (!m_titlesToLoad.empty()) ||
|
||||
!m_continueLoading;
|
||||
});
|
||||
if (!m_continueLoading)
|
||||
return;
|
||||
titleId = m_titlesToLoad.front();
|
||||
m_titlesToLoad.pop_front();
|
||||
}
|
||||
titleRefresh(titleId);
|
||||
}
|
||||
}
|
||||
|
||||
std::string GameTitleLoader::GetNameByTitleId(uint64 titleId) {
|
||||
auto it = m_name_cache.find(titleId);
|
||||
if (it != m_name_cache.end())
|
||||
return it->second;
|
||||
TitleInfo titleInfo;
|
||||
if (!CafeTitleList::GetFirstByTitleId(titleId, titleInfo))
|
||||
return "Unknown title";
|
||||
std::string name;
|
||||
if (!GetConfig().GetGameListCustomName(titleId, name))
|
||||
name = titleInfo.GetTitleName();
|
||||
m_name_cache.emplace(titleId, name);
|
||||
return name;
|
||||
}
|
||||
|
||||
void GameTitleLoader::registerCallback() {
|
||||
m_callbackIdTitleList = CafeTitleList::RegisterCallback(
|
||||
[](CafeTitleListCallbackEvent *evt,
|
||||
void *ctx) {
|
||||
static_cast<GameTitleLoader *>(ctx)->HandleTitleListCallback(evt);
|
||||
}, this);
|
||||
}
|
||||
|
||||
void GameTitleLoader::HandleTitleListCallback(CafeTitleListCallbackEvent *evt) {
|
||||
if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED ||
|
||||
evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) {
|
||||
queueTitle(evt->titleInfo->GetAppTitleId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "Cafe/IOSU/PDM/iosu_pdm.h"
|
||||
#include "Cafe/TitleList/TitleId.h"
|
||||
#include "Cafe/TitleList/TitleList.h"
|
||||
|
||||
struct Game {
|
||||
std::string name;
|
||||
uint32 secondsPlayed;
|
||||
uint16 dlc;
|
||||
uint16 version;
|
||||
TitleId titleId;
|
||||
CafeConsoleRegion region;
|
||||
};
|
||||
|
||||
class GameTitleLoader {
|
||||
std::atomic_bool m_continueLoading = true;
|
||||
std::mutex m_threadMutex;
|
||||
std::condition_variable m_condVar;
|
||||
std::thread m_loaderThread;
|
||||
std::deque<TitleId> m_titlesToLoad;
|
||||
std::optional<uint64> m_callbackIdTitleList;
|
||||
std::vector<TitleId> m_titleIds;
|
||||
std::map<TitleId, Game> m_gameInfos;
|
||||
std::map<TitleId, std::string> m_name_cache;
|
||||
std::function<void(const Game &)> m_onTitleLoaded = nullptr;
|
||||
public:
|
||||
GameTitleLoader();
|
||||
|
||||
void queueTitle(TitleId titleId);
|
||||
|
||||
void setOnTitleLoaded(const std::function<void(const Game &)> &onTitleLoaded);
|
||||
|
||||
void reloadGameTitles();
|
||||
|
||||
~GameTitleLoader();
|
||||
|
||||
void titleRefresh(TitleId titleId);
|
||||
|
||||
private:
|
||||
void loadGameTitles();
|
||||
|
||||
std::string GetNameByTitleId(uint64 titleId);
|
||||
|
||||
void registerCallback();
|
||||
|
||||
void HandleTitleListCallback(CafeTitleListCallbackEvent *evt);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "Image.h"
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_TGA
|
||||
|
||||
#include "stb_image.h"
|
||||
|
||||
Image::Image(Image &&image) {
|
||||
this->m_image = image.m_image;
|
||||
this->m_width = image.m_width;
|
||||
this->m_height = image.m_height;
|
||||
this->m_channels = image.m_channels;
|
||||
image.m_image = nullptr;
|
||||
}
|
||||
|
||||
Image::Image(const std::vector<uint8> &imageBytes) {
|
||||
m_image =
|
||||
stbi_load_from_memory(imageBytes.data(), imageBytes.size(), &m_width,
|
||||
&m_height, &m_channels, STBI_rgb_alpha);
|
||||
if (m_image) {
|
||||
for (int i = 0; i < m_width * m_height * 4; i += 4) {
|
||||
uint8 r = m_image[i];
|
||||
uint8 g = m_image[i + 1];
|
||||
uint8 b = m_image[i + 2];
|
||||
uint8 a = m_image[i + 3];
|
||||
m_image[i] = b;
|
||||
m_image[i + 1] = g;
|
||||
m_image[i + 2] = r;
|
||||
m_image[i + 3] = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Image::isOk() const { return m_image != nullptr; }
|
||||
|
||||
int *Image::intColors() const { return reinterpret_cast<int *>(m_image); }
|
||||
|
||||
Image::~Image() {
|
||||
if (m_image)
|
||||
stbi_image_free(m_image);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
struct Image {
|
||||
uint8_t *m_image = nullptr;
|
||||
int m_width = 0;
|
||||
int m_height = 0;
|
||||
int m_channels = 0;
|
||||
|
||||
Image(Image &&image);
|
||||
|
||||
Image(const std::vector <uint8> &imageBytes);
|
||||
|
||||
bool isOk() const;
|
||||
|
||||
int *intColors() const;
|
||||
|
||||
~Image();
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
bool GLCanvas_MakeCurrent(bool padView) {}
|
||||
void GLCanvas_SwapBuffers(bool swapTV, bool swapDRC) {}
|
||||
bool GLCanvas_HasPadViewOpen() {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,11 @@ add_library(CemuGui
|
||||
guiWrapper.h
|
||||
)
|
||||
|
||||
if(ANDROID)
|
||||
add_subdirectory(AndroidGui)
|
||||
target_link_libraries(CemuGui PRIVATE CemuAndroidGui)
|
||||
endif()
|
||||
|
||||
if(ENABLE_WXWIDGETS)
|
||||
add_subdirectory(wxGui)
|
||||
target_link_libraries(CemuGui PRIVATE CemuwxGui)
|
||||
|
||||
+2
-1
@@ -327,11 +327,12 @@ int main(int argc, char* argv[])
|
||||
return 0;
|
||||
}
|
||||
|
||||
#elif __ANDROID__
|
||||
#else
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#if BOOST_OS_LINUX && !__ANDROID__
|
||||
#if BOOST_OS_LINUX
|
||||
XInitThreads();
|
||||
#endif
|
||||
if (!LaunchSettings::HandleCommandline(argc, argv))
|
||||
|
||||
Reference in New Issue
Block a user