mkxp-z/src/main.cpp
刘皓 f162e8a494
Replace xxd with a custom executable
This removes the need to have xxd installed and provides a portable way
to specify the name of the output array (xxd has an `-n` option for
this, but it isn't present in older versions of xxd), which helps reduce
the possibility of symbol conflicts in libretro builds and also prevents
portability issues since the name of xxd's output array depends on the
relative path to the input file, which can break if Meson changes the
structure of the build directory or if the user sets the build directory
to a different location.

Also, this custom executable declares the array as const so that it goes
into the read-only data section of the binary instead of the data
section.
2025-02-27 22:55:22 -05:00

547 lines
14 KiB
C++

/*
** main.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2013 - 2021 Amaryllis Kulla <ancurio@mapleshrine.eu>
**
** mkxp is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 2 of the License, or
** (at your option) any later version.
**
** mkxp is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with mkxp. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MKXPZ_BUILD_XCODE
#include "icon.png.xxd"
#endif
#include <alc.h>
#include <SDL.h>
#include <SDL_image.h>
#include <SDL_sound.h>
#include <SDL_ttf.h>
#include <assert.h>
#include <string.h>
#include <string>
#include <unistd.h>
#include <regex>
#include "binding.h"
#include "sharedstate.h"
#include "eventthread.h"
#include "util/debugwriter.h"
#include "util/exception.h"
#include "display/gl/gl-debug.h"
#include "display/gl/gl-fun.h"
#include "filesystem/filesystem.h"
#include "system/system.h"
#if defined(__WIN32__)
#include "resource.h"
#include <Winsock2.h>
#include "util/win-consoleutils.h"
// Try to work around buggy GL drivers that tend to be in Optimus laptops
// by forcing MKXP to use the dedicated card instead of the integrated one
#include <windows.h>
extern "C" {
__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif
#ifdef MKXPZ_STEAM
#include "steamshim_child.h"
#endif
#ifdef MKXPZ_BUILD_XCODE
#include <Availability.h>
#include "TouchBar.h"
#if !defined(__MAC_10_15) || __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_15
#define MKXPZ_INIT_GL_LATER
#endif
#endif
#ifndef MKXPZ_INIT_GL_LATER
#define GLINIT_SHOWERROR(s) showInitError(s)
#else
#define GLINIT_SHOWERROR(s) rgssThreadError(threadData, s)
#endif
static void rgssThreadError(RGSSThreadData *rtData, const std::string &msg);
static void showInitError(const std::string &msg);
static inline const char *glGetStringInt(GLenum name) {
return (const char *)gl.GetString(name);
}
static void printGLInfo() {
const std::string renderer(glGetStringInt(GL_RENDERER));
const std::string version(glGetStringInt(GL_VERSION));
std::regex rgx("ANGLE \\((.+), ANGLE Metal Renderer: (.+), Version (.+)\\)");
std::smatch matches;
if (std::regex_search(renderer, matches, rgx)) {
Debug() << "Backend :" << "Metal";
Debug() << "Metal Device :" << matches[2] << "(" + matches[1].str() + ")";
Debug() << "Renderer Version :" << matches[3].str();
std::smatch vmatches;
if (std::regex_search(version, vmatches, std::regex("\\(ANGLE (.+) git hash: .+\\)"))) {
Debug() << "ANGLE Version :" << vmatches[1].str();
}
return;
}
Debug() << "Backend :" << "OpenGL";
Debug() << "GL Vendor :" << glGetStringInt(GL_VENDOR);
Debug() << "GL Renderer :" << renderer;
Debug() << "GL Version :" << version;
Debug() << "GLSL Version :" << glGetStringInt(GL_SHADING_LANGUAGE_VERSION);
}
static SDL_GLContext initGL(SDL_Window *win, Config &conf,
RGSSThreadData *threadData);
int rgssThreadFun(void *userdata) {
RGSSThreadData *threadData = static_cast<RGSSThreadData *>(userdata);
#ifdef MKXPZ_INIT_GL_LATER
threadData->glContext =
initGL(threadData->window, threadData->config, threadData);
if (!threadData->glContext)
return 0;
#else
SDL_GL_MakeCurrent(threadData->window, threadData->glContext);
#endif
/* Setup AL context */
ALCcontext *alcCtx = alcCreateContext(threadData->alcDev, 0);
if (!alcCtx) {
rgssThreadError(threadData, "Error creating OpenAL context");
return 0;
}
alcMakeContextCurrent(alcCtx);
try {
SharedState::initInstance(threadData);
} catch (const Exception &exc) {
rgssThreadError(threadData, exc.msg);
alcDestroyContext(alcCtx);
return 0;
}
/* Start script execution */
scriptBinding->execute();
threadData->rqTermAck.set();
threadData->ethread->requestTerminate();
SharedState::finiInstance();
alcDestroyContext(alcCtx);
return 0;
}
static void printRgssVersion(int ver) {
const char *const makers[] = {"", "XP", "VX", "VX Ace"};
char buf[128];
snprintf(buf, sizeof(buf), "RGSS version %d (RPG Maker %s)", ver,
makers[ver]);
Debug() << buf;
}
static void rgssThreadError(RGSSThreadData *rtData, const std::string &msg) {
rtData->rgssErrorMsg = msg;
rtData->ethread->requestTerminate();
rtData->rqTermAck.set();
}
static void showInitError(const std::string &msg) {
Debug() << msg;
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "mkxp-z", msg.c_str(), 0);
}
static void setupWindowIcon(const Config &conf, SDL_Window *win) {
SDL_RWops *iconSrc;
if (conf.iconPath.empty())
#ifndef MKXPZ_BUILD_XCODE
iconSrc = SDL_RWFromConstMem(mkxp_assets_icon_png, sizeof mkxp_assets_icon_png);
#else
iconSrc = SDL_RWFromFile(mkxp_fs::getPathForAsset("icon", "png").c_str(), "rb");
#endif
else
iconSrc = SDL_RWFromFile(conf.iconPath.c_str(), "rb");
SDL_Surface *iconImg = IMG_Load_RW(iconSrc, SDL_TRUE);
if (iconImg) {
SDL_SetWindowIcon(win, iconImg);
SDL_FreeSurface(iconImg);
}
}
int main(int argc, char *argv[]) {
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
#ifdef GLES2_HEADER
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
#endif
/* initialize SDL first */
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_TIMER) < 0) {
showInitError(std::string("Error initializing SDL: ") + SDL_GetError());
return 0;
}
if (!EventThread::allocUserEvents()) {
showInitError("Error allocating SDL user events");
return 0;
}
#ifndef WORKDIR_CURRENT
char dataDir[512]{};
#if defined(__linux__)
char *tmp{};
tmp = getenv("SRCDIR");
if (tmp) {
strncpy(dataDir, tmp, sizeof(dataDir));
}
#endif
if (!dataDir[0]) {
strncpy(dataDir, mkxp_fs::getDefaultGameRoot().c_str(), sizeof(dataDir));
}
mkxp_fs::setCurrentDirectory(dataDir);
#endif
/* now we load the config */
Config conf;
conf.read(argc, argv);
#if defined(__WIN32__)
// Create a debug console in debug mode
if (conf.winConsole) {
if (setupWindowsConsole()) {
reopenWindowsStreams();
} else {
char buf[200];
snprintf(buf, sizeof(buf), "Error allocating console: %lu",
GetLastError());
showInitError(std::string(buf));
}
}
#endif
#ifdef MKXPZ_STEAM
if (!STEAMSHIM_init()) {
showInitError("Failed to initialize Steamworks. The application cannot "
"continue launching.");
SDL_Quit();
return 0;
}
#endif
if (conf.windowTitle.empty())
conf.windowTitle = conf.game.title;
assert(conf.rgssVersion >= 1 && conf.rgssVersion <= 3);
printRgssVersion(conf.rgssVersion);
int imgFlags = IMG_INIT_PNG | IMG_INIT_JPG;
if (IMG_Init(imgFlags) != imgFlags) {
showInitError(std::string("Error initializing SDL_image: ") +
SDL_GetError());
SDL_Quit();
#ifdef MKXPZ_STEAM
STEAMSHIM_deinit();
#endif
return 0;
}
if (TTF_Init() < 0) {
showInitError(std::string("Error initializing SDL_ttf: ") +
SDL_GetError());
IMG_Quit();
SDL_Quit();
#ifdef MKXPZ_STEAM
STEAMSHIM_deinit();
#endif
return 0;
}
if (Sound_Init() == 0) {
showInitError(std::string("Error initializing SDL_sound: ") +
Sound_GetError());
TTF_Quit();
IMG_Quit();
SDL_Quit();
#ifdef MKXPZ_STEAM
STEAMSHIM_deinit();
#endif
return 0;
}
#if defined(__WIN32__)
WSAData wsadata = {0};
if (WSAStartup(0x101, &wsadata) || wsadata.wVersion != 0x101) {
char buf[200];
snprintf(buf, sizeof(buf), "Error initializing winsock: %08X",
WSAGetLastError());
showInitError(
std::string(buf)); // Not an error worth ending the program over
}
#endif
SDL_Window *win;
Uint32 winFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_ALLOW_HIGHDPI;
if (conf.winResizable)
winFlags |= SDL_WINDOW_RESIZABLE;
if (conf.fullscreen)
winFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
#ifdef GLES2_HEADER
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
// LoadLibrary properly initializes EGL, it won't work otherwise.
// Doesn't completely do it though, needs a small patch to SDL
#ifdef MKXPZ_BUILD_XCODE
SDL_setenv("ANGLE_DEFAULT_PLATFORM", (conf.preferMetalRenderer) ? "metal" : "opengl", true);
SDL_GL_LoadLibrary("@rpath/libEGL.dylib");
#endif
#endif
win = SDL_CreateWindow(conf.windowTitle.c_str(), SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, conf.defScreenW,
conf.defScreenH, winFlags);
if (!win) {
showInitError(std::string("Error creating window: ") + SDL_GetError());
#ifdef MKXPZ_STEAM
STEAMSHIM_deinit();
#endif
return 0;
}
#ifdef MKXPZ_BUILD_XCODE
{
std::string downloadsPath = "/Users/" + mkxp_sys::getUserName() + "/Downloads";
if (mkxp_fs::getCurrentDirectory().find(downloadsPath) == 0) {
showInitError(conf.game.title +
" cannot run from the Downloads directory.\n\n" +
"Please move the application to the Applications folder (or anywhere else) " +
"and try again.");
#ifdef MKXPZ_STEAM
STEAMSHIM_deinit();
#endif
return 0;
}
}
#endif
#if defined(MKXPZ_BUILD_XCODE)
#define DEBUG_FSELECT_MSG "Select the folder from which to load game files. This is the folder containing the game's INI."
#define DEBUG_FSELECT_PROMPT "Load Game"
if (conf.manualFolderSelect) {
std::string dataDirStr = mkxp_fs::selectPath(win, DEBUG_FSELECT_MSG, DEBUG_FSELECT_PROMPT);
if (!dataDirStr.empty()) {
conf.gameFolder = dataDirStr;
mkxp_fs::setCurrentDirectory(dataDirStr.c_str());
Debug() << "Current directory set to" << dataDirStr;
conf.read(argc, argv);
conf.readGameINI();
}
}
#endif
/* OSX and Windows have their own native ways of
* dealing with icons; don't interfere with them */
#ifdef __LINUX__
setupWindowIcon(conf, win);
#else
(void)setupWindowIcon;
#endif
ALCdevice *alcDev = alcOpenDevice(0);
if (!alcDev) {
showInitError("Could not detect an available audio device.");
SDL_DestroyWindow(win);
TTF_Quit();
IMG_Quit();
SDL_Quit();
#ifdef MKXPZ_STEAM
STEAMSHIM_deinit();
#endif
return 0;
}
SDL_DisplayMode mode;
SDL_GetDisplayMode(0, 0, &mode);
/* Can't sync to display refresh rate if its value is unknown */
if (!mode.refresh_rate)
conf.syncToRefreshrate = false;
EventThread eventThread;
#ifndef MKXPZ_INIT_GL_LATER
SDL_GLContext glCtx = initGL(win, conf, 0);
#else
SDL_GLContext glCtx = NULL;
#endif
RGSSThreadData rtData(&eventThread, argv[0], win, alcDev, mode.refresh_rate,
mkxp_sys::getScalingFactor(), conf, glCtx);
int winW, winH, drwW, drwH;
SDL_GetWindowSize(win, &winW, &winH);
rtData.windowSizeMsg.post(Vec2i(winW, winH));
SDL_GL_GetDrawableSize(win, &drwW, &drwH);
rtData.drawableSizeMsg.post(Vec2i(drwW, drwH));
/* Load and post key bindings */
rtData.bindingUpdateMsg.post(loadBindings(conf));
#ifdef MKXPZ_BUILD_XCODE
// Create Touch Bar
initTouchBar(win, conf);
#endif
/* Start RGSS thread */
SDL_Thread *rgssThread = SDL_CreateThread(rgssThreadFun, "rgss", &rtData);
/* Start event processing */
eventThread.process(rtData);
/* Request RGSS thread to stop */
rtData.rqTerm.set();
/* Wait for RGSS thread response */
for (int i = 0; i < 1000; ++i) {
/* We can stop waiting when the request was ack'd */
if (rtData.rqTermAck) {
Debug() << "RGSS thread ack'd request after" << i * 10 << "ms";
break;
}
/* Give RGSS thread some time to respond */
SDL_Delay(10);
}
/* If RGSS thread ack'd request, wait for it to shutdown,
* otherwise abandon hope and just end the process as is. */
if (rtData.rqTermAck)
SDL_WaitThread(rgssThread, 0);
else
SDL_ShowSimpleMessageBox(
SDL_MESSAGEBOX_ERROR, conf.game.title.c_str(),
std::string("The RGSS script seems to be stuck. "+conf.game.title+" will now force quit.").c_str(),
win);
if (!rtData.rgssErrorMsg.empty()) {
Debug() << rtData.rgssErrorMsg;
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, conf.game.title.c_str(),
rtData.rgssErrorMsg.c_str(), win);
}
if (rtData.glContext)
SDL_GL_DeleteContext(rtData.glContext);
/* Clean up any remainin events */
eventThread.cleanup();
Debug() << "Shutting down.";
alcCloseDevice(alcDev);
SDL_DestroyWindow(win);
#if defined(__WIN32__)
if (wsadata.wVersion)
WSACleanup();
#endif
#ifdef MKXPZ_STEAM
STEAMSHIM_deinit();
#endif
Sound_Quit();
TTF_Quit();
IMG_Quit();
SDL_Quit();
return 0;
}
static SDL_GLContext initGL(SDL_Window *win, Config &conf,
RGSSThreadData *threadData) {
SDL_GLContext glCtx{};
/* Setup GL context. Must be done in main thread since macOS 10.15 */
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
if (conf.debugMode)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
glCtx = SDL_GL_CreateContext(win);
if (!glCtx) {
GLINIT_SHOWERROR(std::string("Could not create OpenGL context: ") + SDL_GetError());
return 0;
}
try {
initGLFunctions();
} catch (const Exception &exc) {
GLINIT_SHOWERROR(exc.msg);
SDL_GL_DeleteContext(glCtx);
return 0;
}
if (!conf.enableBlitting)
gl.BlitFramebuffer = 0;
gl.ClearColor(0, 0, 0, 1);
gl.Clear(GL_COLOR_BUFFER_BIT);
SDL_GL_SwapWindow(win);
printGLInfo();
bool vsync = conf.vsync || conf.syncToRefreshrate;
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
// GLDebugLogger dLogger;
return glCtx;
}