From 9d5371009cb54d7637a31154e5f9db6eba6fa45f Mon Sep 17 00:00:00 2001 From: XProger Date: Sat, 5 Jan 2019 04:09:22 +0300 Subject: [PATCH] =?UTF-8?q?=E2=84=9615=20Nintendo=20Switch=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gapi_gl.h | 4 + src/platform/nx/Makefile | 184 ++++++++++++++++++++++ src/platform/nx/deploy.bat | 1 + src/platform/nx/main.cpp | 306 +++++++++++++++++++++++++++++++++++++ src/utils.h | 2 +- 5 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 src/platform/nx/Makefile create mode 100644 src/platform/nx/deploy.bat create mode 100644 src/platform/nx/main.cpp diff --git a/src/gapi_gl.h b/src/gapi_gl.h index d703680..01bc311 100644 --- a/src/gapi_gl.h +++ b/src/gapi_gl.h @@ -1070,6 +1070,10 @@ namespace GAPI { support.texHalf = support.texHalfLinear || extSupport(ext, "_texture_half_float"); support.clipDist = false; // TODO + #ifdef _OS_NX + support.shaderBinary = false; // TODO: check GPU crash for current switchbrew mesa libs + #endif + #ifdef PROFILE support.profMarker = extSupport(ext, "_KHR_debug"); support.profTiming = extSupport(ext, "_timer_query"); diff --git a/src/platform/nx/Makefile b/src/platform/nx/Makefile new file mode 100644 index 0000000..76d7cda --- /dev/null +++ b/src/platform/nx/Makefile @@ -0,0 +1,184 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITPRO)),) +$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITPRO)/libnx/switch_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# EXEFS_SRC is the optional input directory containing data copied into exefs, if anything this normally should only contain "main.npdm". +# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) +# +# NO_ICON: if set to anything, do not use icon. +# NO_NACP: if set to anything, no .nacp file is generated. +# APP_TITLE is the name of the app stored in the .nacp file (Optional) +# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) +# APP_VERSION is the version of the app stored in the .nacp file (Optional) +# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) +# ICON is the filename of the icon (.jpg), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .jpg +# - icon.jpg +# - /default_icon.jpg +#--------------------------------------------------------------------------------- +TARGET := OpenLara +BUILD := build +SOURCES := ./ ./../../libs/stb_vorbis/ ./../../libs/tinf/ ./../../libs/minimp3/ +DATA := data +INCLUDES := ../../ +EXEFS_SRC := exefs_src +#ROMFS := romfs + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv8-a -mtune=cortex-a57 -mtp=soft -fPIE -mcpu=cortex-a57+crc+fp+simd + +CFLAGS := -g -O -ffast-math -fno-strict-aliasing -fomit-frame-pointer -ffunction-sections \ + $(ARCH) $(DEFINES) + +CFLAGS += $(INCLUDE) -D__SWITCH__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lEGL -lGLESv2 -lglapi -ldrm_nouveau -lnx -lm + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(PORTLIBS) $(LIBNX) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects +#--------------------------------------------------------------------------------- +export LD := $(CXX) + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export BUILD_EXEFS_SRC := $(TOPDIR)/$(EXEFS_SRC) + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.jpg) + ifneq (,$(findstring $(TARGET).jpg,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).jpg + else + ifneq (,$(findstring icon.jpg,$(icons))) + export APP_ICON := $(TOPDIR)/icon.jpg + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_ICON)),) + export NROFLAGS += --icon=$(APP_ICON) +endif + +ifeq ($(strip $(NO_NACP)),) + export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp +endif + +ifneq ($(APP_TITLEID),) + export NACPFLAGS += --titleid=$(APP_TITLEID) +endif + +ifneq ($(ROMFS),) + export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) +endif + +.PHONY: $(BUILD) clean all + +#--------------------------------------------------------------------------------- +all: $(BUILD) + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).pfs0 $(TARGET).nso $(TARGET).nro $(TARGET).nacp $(TARGET).elf + + +#--------------------------------------------------------------------------------- +else +.PHONY: all + +DEPENDS := $(OFILES:.o=.d) + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +all : $(OUTPUT).pfs0 $(OUTPUT).nro + +$(OUTPUT).pfs0 : $(OUTPUT).nso + +$(OUTPUT).nso : $(OUTPUT).elf + +ifeq ($(strip $(NO_NACP)),) +$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp +else +$(OUTPUT).nro : $(OUTPUT).elf +endif + +$(OUTPUT).elf : $(OFILES) + +$(OFILES_SRC) : $(HFILES_BIN) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/src/platform/nx/deploy.bat b/src/platform/nx/deploy.bat new file mode 100644 index 0000000..f3b3592 --- /dev/null +++ b/src/platform/nx/deploy.bat @@ -0,0 +1 @@ +C:\devkitPro\tools\bin\nxlink.exe -a 192.168.1.46 OpenLara.nro \ No newline at end of file diff --git a/src/platform/nx/main.cpp b/src/platform/nx/main.cpp new file mode 100644 index 0000000..012ed71 --- /dev/null +++ b/src/platform/nx/main.cpp @@ -0,0 +1,306 @@ +#include +#include +#include + +#include +#include "game.h" + +// multi-threading +void* osMutexInit() { + RMutex *mutex = new RMutex(); + rmutexInit(mutex); + return mutex; +} + +void osMutexFree(void *obj) { + delete ((RMutex*)obj); +} + +void osMutexLock(void *obj) { + rmutexLock((RMutex*)obj); +} + +void osMutexUnlock(void *obj) { + rmutexUnlock((RMutex*)obj); +} + +// timing +unsigned int startTime; + +int osGetTime() { + u64 tick = armGetSystemTick(); + return (tick * 625 / 12) / 1000000; +} + +// sound +#define SND_RESAMPLE (44100.0f / 48000.0f) +#define SND_FRAMES_FILL 2352 +#define SND_FRAMES_OUT int(SND_FRAMES_FILL / SND_RESAMPLE) + +Thread sndThread; +AudioOutBuffer sndOutBuffer[2]; +Sound::Frame *sndFrames; + +void sndFill(void *args) { + Sound::Frame *frames = new Sound::Frame[SND_FRAMES_FILL]; // some decoders + + while (!Core::isQuit) { + AudioOutBuffer *chunk; + audoutWaitPlayFinish(&chunk, NULL, U64_MAX); + + Sound::fill(frames, SND_FRAMES_FILL); + + for (int j = 0; j < SND_FRAMES_OUT; j++) { // resample 44100 -> 48000 + ((Sound::Frame*)chunk->buffer)[j] = frames[int(j * SND_RESAMPLE)]; + } + + audoutAppendAudioOutBuffer(chunk); + } + + delete[] frames; +} + +bool sndInit() { + audoutInitialize(); + + memset(sndOutBuffer, 0, sizeof(sndOutBuffer)); + + for (int i = 0; i < COUNT(sndOutBuffer); i++) { + AudioOutBuffer &chunk = sndOutBuffer[i]; + chunk.data_size = SND_FRAMES_OUT * sizeof(Sound::Frame); + chunk.buffer_size = (chunk.data_size + 0xFFF) & ~0xFFF; + chunk.buffer = memalign(0x1000, chunk.buffer_size); + memset(chunk.buffer, 0, chunk.buffer_size); + audoutAppendAudioOutBuffer(&chunk); + } + + audoutStartAudioOut(); + + threadCreate(&sndThread, sndFill, NULL, 0x4000, 0x2B, 2); + threadStart(&sndThread); + + return true; +} + +void sndFree() { + threadWaitForExit(&sndThread); + threadClose(&sndThread); + audoutStopAudioOut(); + audoutExit(); +} + +// Window +EGLDisplay display; +EGLSurface surface; +EGLContext context; + +bool eglInit() { + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) + return false; + + if (eglInitialize(display, NULL, NULL) == EGL_FALSE) + return false; + + static const EGLint eglAttr[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_NONE + }; + + static const EGLint ctxAttr[] = { + EGL_CONTEXT_MAJOR_VERSION, 2, + EGL_CONTEXT_MINOR_VERSION, 0, + EGL_NONE + }; + + EGLConfig config; + EGLint configCount = 0; + + if (eglChooseConfig(display, eglAttr, &config, 1, &configCount) == EGL_FALSE) + return false; + + surface = eglCreateWindowSurface(display, config, (char*)"", NULL); + + if (surface == EGL_NO_SURFACE) + return false; + + if (eglBindAPI(EGL_OPENGL_API) == EGL_FALSE) + return false; + + context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr); + if (context == EGL_NO_CONTEXT) + return false; + + if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) + return false; + + return true; +} + +void eglFree() { + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroySurface(display, surface); + eglDestroyContext(display, context); + eglTerminate(display); + eglReleaseThread(); +} + +// Input +bool osJoyReady(int index) { + return index == 0; +} + +void osJoyVibrate(int index, float L, float R) { + // TODO +} + +bool joyIsSplit; +int joySplitTime; + +void joySplit(bool split) { + joyIsSplit = split; + joySplitTime = osGetTime(); + if (split) { + hidSetNpadJoyHoldType(HidJoyHoldType_Horizontal); + hidSetNpadJoyAssignmentModeSingleByDefault(CONTROLLER_PLAYER_1); + hidSetNpadJoyAssignmentModeSingleByDefault(CONTROLLER_PLAYER_2); + } else { + hidSetNpadJoyHoldType(HidJoyHoldType_Default); + hidSetNpadJoyAssignmentModeDual(CONTROLLER_PLAYER_1); + hidSetNpadJoyAssignmentModeDual(CONTROLLER_PLAYER_2); + hidMergeSingleJoyAsDualJoy(CONTROLLER_PLAYER_1, CONTROLLER_PLAYER_2); + + if (Game::level && Game::level.players[1]) { + Game::level->addPlayer(1); // add existing player == remove player + } + } +} + +void joyInit() { + joySplit(false); +} + +void joyUpdate() { + const static u64 keys[jkMAX] = { 0, + KEY_B, KEY_A, KEY_Y, KEY_X, KEY_L, KEY_R, KEY_PLUS, KEY_MINUS, + 0, 0, KEY_ZL, KEY_ZR, + KEY_DLEFT, KEY_DRIGHT, KEY_DUP, KEY_DDOWN, + }; + + if (hidGetHandheldMode() && joyIsSplit) { + joySplit(false); + } + + hidScanInput(); + + u64 mDown = 0; + + for (int i = 0; i < (joyIsSplit ? 2 : 1); i++) { + HidControllerID ctrl = (i == 0 ? CONTROLLER_P1_AUTO : CONTROLLER_PLAYER_2); + + u64 mask = hidKeysDown(ctrl) | hidKeysHeld(ctrl); + mDown |= mask; + + for (int j = 1; j < jkMAX; j++) { + if (j != jkSelect && j != jkStart) { + Input::setJoyDown(i, JoyKey(jkNone + j), (mask & keys[j]) != 0); + } + } + + Input::setJoyDown(i, jkSelect, (mask & (KEY_MINUS | KEY_PLUS)) != 0); + Input::setJoyDown(i, jkStart, (mask & (KEY_LSTICK | KEY_RSTICK)) != 0); + + JoystickPosition sL, sR; + + hidJoystickRead(&sL, ctrl, JOYSTICK_LEFT); + hidJoystickRead(&sR, ctrl, JOYSTICK_RIGHT); + Input::setJoyPos(i, jkL, vec2(float(sL.dx), float(-sL.dy)) / 32767.0f); + Input::setJoyPos(i, jkR, vec2(float(sR.dx), float(-sR.dy)) / 32767.0f); + } + + if ((mDown & (KEY_L | KEY_R)) == (KEY_L | KEY_R)) { // hold L and R to split/merge joy-con's + if (joySplitTime + 1000 < osGetTime()) { // 1 sec timer + joySplit(!joyIsSplit); + } + } else { + joySplitTime = osGetTime(); + } +} + +void touchUpdate() { + int touchesCount = hidTouchCount(); + + bool touchState[COUNT(Input::touch)]; + + for (int i = 0; i < COUNT(Input::touch); i++) { + touchState[i] = Input::down[ikTouchA + i]; + } + + for (int i = 0; i < touchesCount; i++) { + touchPosition touch; + hidTouchRead(&touch, i); + + InputKey key = Input::getTouch(i /*touch.id*/); // require libnx with https://github.com/switchbrew/libnx/pull/228 + if (key == ikNone) continue; + + Input::setPos(key, vec2(float(touch.px), float(touch.py))); + Input::setDown(key, true); + + touchState[key - ikTouchA] = false; + } + + for (int i = 0; i < COUNT(Input::touch); i++) { + if (touchState[i]) { + Input::setDown(InputKey(ikTouchA + i), false); + } + } +} + +int main(int argc, char* argv[]) { + Core::width = 1280; + Core::height = 720; + + if (!eglInit()) { + LOG("! can't initialize EGL context\n"); + return EXIT_FAILURE; + } + + cacheDir[0] = saveDir[0] = contentDir[0] = 0; + + strcat(contentDir, "/switch/OpenLara/"); + strcat(cacheDir, "/switch/OpenLara/cache/"); + strcat(saveDir, "/switch/OpenLara/"); + + //mkdir("/switch/OpenLara/cache/"); + + startTime = osGetTime(); + + sndInit(); + joyInit(); + + Game::init(); + + while (appletMainLoop() && !Core::isQuit) { + joyUpdate(); + touchUpdate(); + + if (Game::update()) { + Game::render(); + Core::waitVBlank(); + eglSwapBuffers(display, surface); + } + }; + + sndFree(); + Game::deinit(); + + eglFree(); + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/src/utils.h b/src/utils.h index 25deea3..6239ef2 100644 --- a/src/utils.h +++ b/src/utils.h @@ -24,7 +24,7 @@ #else //#define ASSERT(expr) if (expr) {} else { LOG("ASSERT:\n %s:%d\n %s => %s\n", __FILE__, __LINE__, __FUNCTION__, #expr); } #define ASSERT(expr) - #define ASSERTV(expr) (expr) ? 0 : 1 + #define ASSERTV(expr) (expr) #ifdef _OS_LINUX #define LOG(...) printf(__VA_ARGS__); fflush(stdout)