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()>;
/**
* @brief - Vulkan
* @brief - OpenGL ES 3.2
*
* SDL2 Vulkan
* SDL2 OpenGL ES 3.2
* 使 Module
*/
class WindowModule : public Module {
@ -122,18 +122,26 @@ public:
bool shouldClose() const { return shouldClose_; }
/**
* @brief Vulkan
* @return
*/
std::vector<const char *> getVulkanExtensions() const;
/**
* @brief Vulkan
* @param instance Vulkan
* @param surface
* @brief OpenGL ES
* @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:
void handleWindowEvent(const SDL_WindowEvent &evt);
@ -147,6 +155,7 @@ private:
void onModuleConfig(const AppConfig &config);
SDL_Window *window_ = nullptr;
SDL_GLContext glContext_ = nullptr;
bool shouldClose_ = false;
CloseCb onClose_;

View File

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

View File

@ -9,6 +9,11 @@
#include <direct.h>
#include <windows.h>
#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
#include <dirent.h>
#include <unistd.h>

View File

@ -1,41 +1,38 @@
#include <platform/window_module.h>
#include <config/app_config.h>
#include <event/events.h>
#include <memory>
#include <platform/window_module.h>
#include <utils/logger.h>
// SDL Vulkan support
#include <SDL_vulkan.h>
// Vulkan
#include <vulkan/vulkan.h>
// OpenGL ES 3.2 with GLAD
#include <glad/glad.h>
namespace extra2d {
WindowModule::WindowModule() = default;
WindowModule::~WindowModule() {
shutdown();
}
WindowModule::~WindowModule() { shutdown(); }
WindowModule::WindowModule(WindowModule &&other) noexcept
: window_(other.window_)
, shouldClose_(other.shouldClose_)
, onClose_(std::move(other.onClose_))
, onResize_(std::move(other.onResize_))
, configListener_(std::move(other.configListener_)) {
: window_(other.window_), glContext_(other.glContext_),
shouldClose_(other.shouldClose_), onClose_(std::move(other.onClose_)),
onResize_(std::move(other.onResize_)),
configListener_(std::move(other.configListener_)) {
other.window_ = nullptr;
other.glContext_ = nullptr;
}
WindowModule &WindowModule::operator=(WindowModule &&other) noexcept {
if (this != &other) {
shutdown();
window_ = other.window_;
glContext_ = other.glContext_;
shouldClose_ = other.shouldClose_;
onClose_ = std::move(other.onClose_);
onResize_ = std::move(other.onResize_);
configListener_ = std::move(other.configListener_);
other.window_ = nullptr;
other.glContext_ = nullptr;
}
return *this;
}
@ -47,15 +44,16 @@ bool WindowModule::init() {
}
// 监听模块配置事件
configListener_ = std::make_unique<events::OnModuleConfig<AppConfig>::Listener>();
configListener_->bind([this](const AppConfig& config) {
this->onModuleConfig(config);
});
configListener_ =
std::make_unique<events::OnModuleConfig<AppConfig>::Listener>();
configListener_->bind(
[this](const AppConfig &config) { this->onModuleConfig(config); });
return true;
}
void WindowModule::shutdown() {
destroyGLContext();
if (window_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
@ -66,7 +64,7 @@ bool WindowModule::create(const WindowCfg& cfg) {
if (window_)
return true;
uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN;
uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL;
if (cfg.resizable) {
flags |= SDL_WINDOW_RESIZABLE;
}
@ -74,7 +72,8 @@ bool WindowModule::create(const WindowCfg& cfg) {
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);
if (!window_) {
@ -108,6 +107,12 @@ bool WindowModule::pollEvents() {
break;
}
}
#ifdef __SWITCH__
// Switch 平台:每帧更新控制台,确保日志实时显示
consoleUpdate(NULL);
#endif
return !shouldClose_;
}
@ -222,25 +227,52 @@ bool WindowModule::isVisible() const {
return (flags & SDL_WINDOW_SHOWN) != 0;
}
std::vector<const char*> WindowModule::getVulkanExtensions() const {
std::vector<const char*> extensions;
bool WindowModule::createGLContext() {
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 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

View File

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

View File

@ -9,6 +9,25 @@ bool Logger::consoleOutput_ = true;
bool Logger::fileOutput_ = false;
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
* @param level
@ -41,11 +60,17 @@ void Logger::init() {
return;
}
// 设置 SDL 日志级别为详细模式(允许所有级别的日志)
#ifdef __SWITCH__
// Switch 平台:初始化控制台并设置 SDL 日志重定向
consoleInit(NULL);
SDL_LogSetOutputFunction(SwitchLogOutput, nullptr);
#else
// 其他平台:设置 SDL 日志级别为详细模式
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_VERBOSE);
#endif
initialized_ = true;
log(LogLevel::Info, "Logger initialized with SDL2");
log(LogLevel::Info, "Logger initialized");
}
/**
@ -54,6 +79,9 @@ void Logger::init() {
void Logger::shutdown() {
if (initialized_) {
log(LogLevel::Info, "Logger shutting down");
#ifdef __SWITCH__
consoleExit(NULL);
#endif
}
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
add_requires("glm", "libsdl2", "libsdl2_mixer", "vulkansdk")
add_requires("glm", "libsdl2", "libsdl2_mixer")
end
-- ==============================================

View File

@ -13,24 +13,27 @@ function define_extra2d_engine()
target("extra2d")
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("third_party", {public = true})
add_includedirs("third_party/glad/include", {public = true})
local plat = get_current_plat()
if plat == "mingw" then
add_defines("_UNICODE", "UNICODE")
-- 使用 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})
elseif plat == "switch" then
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true})
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
-- Switch 平台可能需要不同的图形库
-- Switch 平台使用 OpenGL ES + EGL + Mesa 驱动
-- 注意:链接顺序很重要,被依赖的库要放在后面
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
"modplug", "mpg123", "FLAC", "drm_nouveau",
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
{public = true})
end

View File

@ -34,7 +34,7 @@ function define_switch_toolchain()
add_includedirs(path.join(devkitPro, "libnx/include"))
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/SDL2"))
add_linkdirs(path.join(devkitPro, "portlibs/switch/lib"))