feat(switch): 添加 Nintendo Switch 平台支持

- 添加 Switch 平台专用的日志输出和初始化处理
- 修改窗口模块以支持 OpenGL ES 3.2 渲染
- 更新构建系统以支持 Switch 平台编译
- 添加 Switch 平台文件系统操作支持
- 移除 Vulkan 相关依赖,改用 OpenGL ES
- 更新插件加载器以适配 Switch 平台限制
This commit is contained in:
ChestnutYueyue 2026-03-01 03:48:51 +08:00
parent fb11f2a71e
commit 6717015e28
11 changed files with 4474 additions and 27686 deletions

View File

@ -20,9 +20,9 @@ using ResizeCb = std::function<void(int32 w, int32 h)>;
using CloseCb = std::function<void()>; using CloseCb = std::function<void()>;
/** /**
* @brief - Vulkan * @brief - OpenGL ES 3.2
* *
* SDL2 Vulkan * SDL2 OpenGL ES 3.2
* 使 Module * 使 Module
*/ */
class WindowModule : public Module { class WindowModule : public Module {
@ -122,18 +122,26 @@ public:
bool shouldClose() const { return shouldClose_; } bool shouldClose() const { return shouldClose_; }
/** /**
* @brief Vulkan * @brief OpenGL ES
* @return
*/
std::vector<const char *> getVulkanExtensions() const;
/**
* @brief Vulkan
* @param instance Vulkan
* @param surface
* @return * @return
*/ */
bool createVulkanSurface(void *instance, void **surface) const; bool createGLContext();
/**
* @brief OpenGL ES
*/
void destroyGLContext();
/**
* @brief
*/
void swapBuffers();
/**
* @brief OpenGL ES
* @return SDL_GLContext
*/
void* getGLContext() const;
private: private:
void handleWindowEvent(const SDL_WindowEvent &evt); void handleWindowEvent(const SDL_WindowEvent &evt);
@ -147,6 +155,7 @@ private:
void onModuleConfig(const AppConfig &config); void onModuleConfig(const AppConfig &config);
SDL_Window *window_ = nullptr; SDL_Window *window_ = nullptr;
SDL_GLContext glContext_ = nullptr;
bool shouldClose_ = false; bool shouldClose_ = false;
CloseCb onClose_; CloseCb onClose_;

View File

@ -6,6 +6,11 @@
#include <type_traits> #include <type_traits>
#include <types/base/types.h> #include <types/base/types.h>
// Switch 平台支持
#ifdef __SWITCH__
#include <switch.h>
#endif
#ifndef TEST_BUILD #ifndef TEST_BUILD
// SDL2 日志头文件(测试构建时不使用) // SDL2 日志头文件(测试构建时不使用)
#include <SDL.h> #include <SDL.h>

View File

@ -9,6 +9,11 @@
#include <direct.h> #include <direct.h>
#include <windows.h> #include <windows.h>
#define mkdir_impl(path, mode) _mkdir(path) #define mkdir_impl(path, mode) _mkdir(path)
#elif defined(__SWITCH__)
#include <dirent.h>
#include <unistd.h>
// Switch 使用 ::mkdir 避免与类成员函数冲突
#define mkdir_impl(path, mode) ::mkdir(path, mode)
#else #else
#include <dirent.h> #include <dirent.h>
#include <unistd.h> #include <unistd.h>

View File

@ -1,41 +1,38 @@
#include <platform/window_module.h>
#include <config/app_config.h> #include <config/app_config.h>
#include <event/events.h> #include <event/events.h>
#include <memory> #include <memory>
#include <platform/window_module.h>
#include <utils/logger.h> #include <utils/logger.h>
// SDL Vulkan support // OpenGL ES 3.2 with GLAD
#include <SDL_vulkan.h> #include <glad/glad.h>
// Vulkan
#include <vulkan/vulkan.h>
namespace extra2d { namespace extra2d {
WindowModule::WindowModule() = default; WindowModule::WindowModule() = default;
WindowModule::~WindowModule() { WindowModule::~WindowModule() { shutdown(); }
shutdown();
}
WindowModule::WindowModule(WindowModule &&other) noexcept WindowModule::WindowModule(WindowModule &&other) noexcept
: window_(other.window_) : window_(other.window_), glContext_(other.glContext_),
, shouldClose_(other.shouldClose_) shouldClose_(other.shouldClose_), onClose_(std::move(other.onClose_)),
, onClose_(std::move(other.onClose_)) onResize_(std::move(other.onResize_)),
, onResize_(std::move(other.onResize_)) configListener_(std::move(other.configListener_)) {
, configListener_(std::move(other.configListener_)) {
other.window_ = nullptr; other.window_ = nullptr;
other.glContext_ = nullptr;
} }
WindowModule &WindowModule::operator=(WindowModule &&other) noexcept { WindowModule &WindowModule::operator=(WindowModule &&other) noexcept {
if (this != &other) { if (this != &other) {
shutdown(); shutdown();
window_ = other.window_; window_ = other.window_;
glContext_ = other.glContext_;
shouldClose_ = other.shouldClose_; shouldClose_ = other.shouldClose_;
onClose_ = std::move(other.onClose_); onClose_ = std::move(other.onClose_);
onResize_ = std::move(other.onResize_); onResize_ = std::move(other.onResize_);
configListener_ = std::move(other.configListener_); configListener_ = std::move(other.configListener_);
other.window_ = nullptr; other.window_ = nullptr;
other.glContext_ = nullptr;
} }
return *this; return *this;
} }
@ -47,15 +44,16 @@ bool WindowModule::init() {
} }
// 监听模块配置事件 // 监听模块配置事件
configListener_ = std::make_unique<events::OnModuleConfig<AppConfig>::Listener>(); configListener_ =
configListener_->bind([this](const AppConfig& config) { std::make_unique<events::OnModuleConfig<AppConfig>::Listener>();
this->onModuleConfig(config); configListener_->bind(
}); [this](const AppConfig &config) { this->onModuleConfig(config); });
return true; return true;
} }
void WindowModule::shutdown() { void WindowModule::shutdown() {
destroyGLContext();
if (window_) { if (window_) {
SDL_DestroyWindow(window_); SDL_DestroyWindow(window_);
window_ = nullptr; window_ = nullptr;
@ -66,7 +64,7 @@ bool WindowModule::create(const WindowCfg& cfg) {
if (window_) if (window_)
return true; return true;
uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN; uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL;
if (cfg.resizable) { if (cfg.resizable) {
flags |= SDL_WINDOW_RESIZABLE; flags |= SDL_WINDOW_RESIZABLE;
} }
@ -74,7 +72,8 @@ bool WindowModule::create(const WindowCfg& cfg) {
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
} }
window_ = SDL_CreateWindow(cfg.title.c_str(), SDL_WINDOWPOS_CENTERED, window_ =
SDL_CreateWindow(cfg.title.c_str(), SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, cfg.width, cfg.height, flags); SDL_WINDOWPOS_CENTERED, cfg.width, cfg.height, flags);
if (!window_) { if (!window_) {
@ -108,6 +107,12 @@ bool WindowModule::pollEvents() {
break; break;
} }
} }
#ifdef __SWITCH__
// Switch 平台:每帧更新控制台,确保日志实时显示
consoleUpdate(NULL);
#endif
return !shouldClose_; return !shouldClose_;
} }
@ -222,25 +227,52 @@ bool WindowModule::isVisible() const {
return (flags & SDL_WINDOW_SHOWN) != 0; return (flags & SDL_WINDOW_SHOWN) != 0;
} }
std::vector<const char*> WindowModule::getVulkanExtensions() const { bool WindowModule::createGLContext() {
std::vector<const char*> extensions;
if (!window_) { if (!window_) {
return extensions;
}
uint32 count = 0;
SDL_Vulkan_GetInstanceExtensions(window_, &count, nullptr);
extensions.resize(count);
SDL_Vulkan_GetInstanceExtensions(window_, &count, extensions.data());
return extensions;
}
bool WindowModule::createVulkanSurface(void* instance, void** surface) const {
if (!window_ || !instance || !surface) {
return false; return false;
} }
return SDL_Vulkan_CreateSurface(window_, static_cast<VkInstance>(instance), reinterpret_cast<VkSurfaceKHR*>(surface));
// 创建 OpenGL ES 3.2 上下文
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
glContext_ = SDL_GL_CreateContext(window_);
if (!glContext_) {
E2D_LOG_ERROR("Failed to create OpenGL ES context: {}", SDL_GetError());
return false;
} }
// 初始化 GLAD
if (!gladLoadGLES2Loader((GLADloadproc)SDL_GL_GetProcAddress)) {
E2D_LOG_ERROR("Failed to initialize GLAD");
SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr;
return false;
}
E2D_LOG_INFO("OpenGL ES context created: {}.{}", GLVersion.major,
GLVersion.minor);
return true;
}
void WindowModule::destroyGLContext() {
if (glContext_) {
SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr;
}
}
void WindowModule::swapBuffers() {
if (window_) {
SDL_GL_SwapWindow(window_);
}
}
void *WindowModule::getGLContext() const { return glContext_; }
} // namespace extra2d } // namespace extra2d

View File

@ -1,10 +1,11 @@
#include <plugin/plugin_loader.h>
#include <event/event_bus.h> #include <event/event_bus.h>
#include <algorithm> #include <plugin/plugin_loader.h>
#include <queue> #include <queue>
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
#elif defined(__SWITCH__)
// Nintendo Switch 平台使用 libnx不支持传统动态库加载
#else #else
#include <dlfcn.h> #include <dlfcn.h>
#endif #endif
@ -48,9 +49,8 @@ bool PluginLoader::loadFromLibrary(const char* path) {
// 获取创建函数 // 获取创建函数
using CreateFunc = IPlugin *(*)(); using CreateFunc = IPlugin *(*)();
CreateFunc createFunc = reinterpret_cast<CreateFunc>( CreateFunc createFunc =
getSymbol(handle, "extra2d_create_plugin") reinterpret_cast<CreateFunc>(getSymbol(handle, "extra2d_create_plugin"));
);
if (!createFunc) { if (!createFunc) {
unloadDynamicLibrary(handle); unloadDynamicLibrary(handle);
@ -83,7 +83,8 @@ bool PluginLoader::loadFromLibrary(const char* path) {
sortedPlugins_.clear(); sortedPlugins_.clear();
// 插件加载事件(暂不发送,避免依赖 events.h // 插件加载事件(暂不发送,避免依赖 events.h
// event::broadcast<events::OnPluginLoaded>(name, plugin->getInfo().version.c_str()); // event::broadcast<events::OnPluginLoaded>(name,
// plugin->getInfo().version.c_str());
// 如果已经初始化,立即加载插件 // 如果已经初始化,立即加载插件
if (inited_) { if (inited_) {
@ -121,7 +122,8 @@ void PluginLoader::registerPlugin(IPlugin* plugin) {
sortedPlugins_.clear(); sortedPlugins_.clear();
// 插件加载事件(暂不发送,避免依赖 events.h // 插件加载事件(暂不发送,避免依赖 events.h
// event::broadcast<events::OnPluginLoaded>(name, plugin->getInfo().version.c_str()); // event::broadcast<events::OnPluginLoaded>(name,
// plugin->getInfo().version.c_str());
// 如果已经初始化,立即加载插件 // 如果已经初始化,立即加载插件
if (inited_) { if (inited_) {
@ -193,7 +195,8 @@ bool PluginLoader::initAll() {
// 检查所有插件的依赖是否满足 // 检查所有插件的依赖是否满足
for (const auto &pair : plugins_) { for (const auto &pair : plugins_) {
if (pair.second.plugin && !checkDependencies(pair.second.plugin->getDependencies())) { if (pair.second.plugin &&
!checkDependencies(pair.second.plugin->getDependencies())) {
return false; return false;
} }
} }
@ -229,9 +232,7 @@ void PluginLoader::shutdownAll() {
inited_ = false; inited_ = false;
} }
size_t PluginLoader::getPluginCount() const { size_t PluginLoader::getPluginCount() const { return plugins_.size(); }
return plugins_.size();
}
void PluginLoader::addSearchPath(const char *path) { void PluginLoader::addSearchPath(const char *path) {
if (path) { if (path) {
@ -253,7 +254,8 @@ bool PluginLoader::resolveDependencies(IPlugin* plugin) {
return checkDependencies(plugin->getDependencies()); return checkDependencies(plugin->getDependencies());
} }
bool PluginLoader::checkDependencies(const std::vector<std::string>& dependencies) { bool PluginLoader::checkDependencies(
const std::vector<std::string> &dependencies) {
for (const auto &dep : dependencies) { for (const auto &dep : dependencies) {
if (!hasPlugin(dep.c_str())) { if (!hasPlugin(dep.c_str())) {
return false; return false;
@ -278,7 +280,8 @@ void PluginLoader::sortPluginsByDependencies() {
// 构建图 // 构建图
for (const auto &pair : plugins_) { for (const auto &pair : plugins_) {
if (!pair.second.plugin) continue; if (!pair.second.plugin)
continue;
for (const auto &dep : pair.second.plugin->getDependencies()) { for (const auto &dep : pair.second.plugin->getDependencies()) {
graph[dep].push_back(pair.first); graph[dep].push_back(pair.first);
@ -321,26 +324,41 @@ void PluginLoader::sortPluginsByDependencies() {
void *PluginLoader::loadDynamicLibrary(const char *path) { void *PluginLoader::loadDynamicLibrary(const char *path) {
#ifdef _WIN32 #ifdef _WIN32
return LoadLibraryA(path); return LoadLibraryA(path);
#elif defined(__SWITCH__)
// Nintendo Switch 平台不支持动态库加载
(void)path;
return nullptr;
#else #else
return dlopen(path, RTLD_LAZY); return dlopen(path, RTLD_LAZY);
#endif #endif
} }
void PluginLoader::unloadDynamicLibrary(void *handle) { void PluginLoader::unloadDynamicLibrary(void *handle) {
if (!handle) return; if (!handle)
return;
#ifdef _WIN32 #ifdef _WIN32
FreeLibrary(static_cast<HMODULE>(handle)); FreeLibrary(static_cast<HMODULE>(handle));
#elif defined(__SWITCH__)
// Nintendo Switch 平台不支持动态库加载
(void)handle;
#else #else
dlclose(handle); dlclose(handle);
#endif #endif
} }
void *PluginLoader::getSymbol(void *handle, const char *name) { void *PluginLoader::getSymbol(void *handle, const char *name) {
if (!handle || !name) return nullptr; if (!handle || !name)
return nullptr;
#ifdef _WIN32 #ifdef _WIN32
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(handle), name)); return reinterpret_cast<void *>(
GetProcAddress(static_cast<HMODULE>(handle), name));
#elif defined(__SWITCH__)
// Nintendo Switch 平台不支持动态库加载
(void)handle;
(void)name;
return nullptr;
#else #else
return dlsym(handle, name); return dlsym(handle, name);
#endif #endif

View File

@ -9,6 +9,25 @@ bool Logger::consoleOutput_ = true;
bool Logger::fileOutput_ = false; bool Logger::fileOutput_ = false;
std::string Logger::logFile_; std::string Logger::logFile_;
// Switch 平台:自定义 SDL 日志输出函数
#ifdef __SWITCH__
/**
* @brief Switch
* @param userdata
* @param category
* @param priority
* @param message
*/
static void SwitchLogOutput(void *userdata, int category,
SDL_LogPriority priority, const char *message) {
(void)userdata;
(void)category;
(void)priority;
// 输出到 Switch 控制台
printf("%s\n", message);
}
#endif
/** /**
* @brief * @brief
* @param level * @param level
@ -41,11 +60,17 @@ void Logger::init() {
return; return;
} }
// 设置 SDL 日志级别为详细模式(允许所有级别的日志) #ifdef __SWITCH__
// Switch 平台:初始化控制台并设置 SDL 日志重定向
consoleInit(NULL);
SDL_LogSetOutputFunction(SwitchLogOutput, nullptr);
#else
// 其他平台:设置 SDL 日志级别为详细模式
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_VERBOSE); SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_VERBOSE);
#endif
initialized_ = true; initialized_ = true;
log(LogLevel::Info, "Logger initialized with SDL2"); log(LogLevel::Info, "Logger initialized");
} }
/** /**
@ -54,6 +79,9 @@ void Logger::init() {
void Logger::shutdown() { void Logger::shutdown() {
if (initialized_) { if (initialized_) {
log(LogLevel::Info, "Logger shutting down"); log(LogLevel::Info, "Logger shutting down");
#ifdef __SWITCH__
consoleExit(NULL);
#endif
} }
initialized_ = false; initialized_ = false;
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -78,7 +78,7 @@ end
-- ============================================== -- ==============================================
if target_plat == "mingw" then if target_plat == "mingw" then
add_requires("glm", "libsdl2", "libsdl2_mixer", "vulkansdk") add_requires("glm", "libsdl2", "libsdl2_mixer")
end end
-- ============================================== -- ==============================================

View File

@ -13,24 +13,27 @@ function define_extra2d_engine()
target("extra2d") target("extra2d")
set_kind("static") set_kind("static")
add_files("src/**.cpp|core/*.cpp|module/module_manager.cpp|plugin/plugin_manager.cpp|platform/window.cpp|platform/input.cpp") add_files("src/**.cpp")
add_files("third_party/glad/src/glad.c")
add_includedirs("include", {public = true}) add_includedirs("include", {public = true})
add_includedirs("third_party", {public = true}) add_includedirs("third_party", {public = true})
add_includedirs("third_party/glad/include", {public = true})
local plat = get_current_plat() local plat = get_current_plat()
if plat == "mingw" then if plat == "mingw" then
add_defines("_UNICODE", "UNICODE") add_defines("_UNICODE", "UNICODE")
-- 使用 xmake 官方包 -- 使用 xmake 官方包
add_packages("glm", "libsdl2", "libsdl2_mixer", "vulkansdk", {public = true}) add_packages("glm", "libsdl2", "libsdl2_mixer", {public = true})
add_syslinks("winmm", "imm32", "version", "setupapi", {public = true}) add_syslinks("winmm", "imm32", "version", "setupapi", {public = true})
elseif plat == "switch" then elseif plat == "switch" then
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro" local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true}) add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true})
add_linkdirs(devkitPro .. "/portlibs/switch/lib") add_linkdirs(devkitPro .. "/portlibs/switch/lib")
-- Switch 平台可能需要不同的图形库 -- Switch 平台使用 OpenGL ES + EGL + Mesa 驱动
-- 注意:链接顺序很重要,被依赖的库要放在后面
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg", add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
"modplug", "mpg123", "FLAC", "drm_nouveau", "modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
{public = true}) {public = true})
end end

View File

@ -34,7 +34,7 @@ function define_switch_toolchain()
add_includedirs(path.join(devkitPro, "libnx/include")) add_includedirs(path.join(devkitPro, "libnx/include"))
add_linkdirs(path.join(devkitPro, "libnx/lib")) add_linkdirs(path.join(devkitPro, "libnx/lib"))
-- portlibs 路径(EGL + 桌面 OpenGL + SDL2 -- portlibs 路径(OpenGL ES + EGL + SDL2
add_includedirs(path.join(devkitPro, "portlibs/switch/include")) add_includedirs(path.join(devkitPro, "portlibs/switch/include"))
add_includedirs(path.join(devkitPro, "portlibs/switch/include/SDL2")) add_includedirs(path.join(devkitPro, "portlibs/switch/include/SDL2"))
add_linkdirs(path.join(devkitPro, "portlibs/switch/lib")) add_linkdirs(path.join(devkitPro, "portlibs/switch/lib"))