diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 4f0f7475b..02775cf7b 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -282,7 +282,7 @@ jobs: bundle: io.github.fgsfdsfgs.perfect_dark.flatpak manifest-path: dist/linux/io.github.fgsfdsfgs.perfect_dark.yaml cache-key: flatpak-builder-${{ github.sha }} - upload-artifact: true + upload-artifact: false # - name: Build Flatpak bundle # run: | @@ -302,7 +302,7 @@ jobs: publish-latest-build: runs-on: ubuntu-latest if: github.ref == 'refs/heads/port' - needs: [build-i686-windows, build-i686-linux, build-x86_64-windows, build-x86_64-linux, build-arm64-nswitch, build-x86_64-osx, build-arm64-osx] + needs: [build-i686-windows, build-i686-linux, build-x86_64-windows, build-x86_64-linux, build-arm64-nswitch, build-x86_64-osx, build-arm64-osx, build-flatpak-bundle] permissions: contents: write steps: @@ -323,6 +323,7 @@ jobs: zip -r ../ci-release/pd-i686-windows.zip pd-i686-windows zip -r ../ci-release/pd-x86_64-windows.zip pd-x86_64-windows zip -r ../ci-release/pd-arm64-nswitch.zip pd-arm64-nswitch + mv io.github.fgsfdsfgs.perfect_dark.flatpak/io.github.fgsfdsfgs.perfect_dark.flatpak ../ci-release/ popd git tag --force ci-dev-build port git push --force origin ci-dev-build diff --git a/README.md b/README.md index 7eca4f416..168e8af41 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ There are minor graphics- and gameplay-related issues, and possibly occasional c * basic mod support, currently enough to load a few custom levels; * slightly expanded memory heap size; * experimental high framerate support (up to 240 FPS): - * set `Game.TickRateDivisor` to `0` in `pd.ini` to activate; + * enable `Uncap Tickrate` in `Extended Video Options` to activate; * in practice the game will have issues running faster than ~165 FPS, so use VSync or `Video.FramerateLimit` to cap it. * emulate the Transfer Pak functionality the game has on the Nintendo 64 to unlock some cheats automatically. diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index 043a7e2f2..e974d4fd4 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -30,10 +30,30 @@ static uint64_t qpc_freq; #define FRAME_INTERVAL_US_NUMERATOR 1000000 #define FRAME_INTERVAL_US_DENOMINATOR (target_fps) -static void set_fullscreen(bool on, bool call_callback) { - if (fullscreen_state == on) { - return; +static int32_t gfx_sdl_get_maximized_state(void) { + return (int32_t)maximized_state; +} + +static int32_t gfx_sdl_get_fullscreen_state(void) { + return (int32_t)fullscreen_state; +} + +static int32_t gfx_sdl_get_fullscreen_flag_mode(void) { + return fullscreen_flag == SDL_WINDOW_FULLSCREEN_DESKTOP ? 0 : 1; +} + +static void gfx_sdl_set_fullscreen_flag(int32_t mode) { + switch (mode) { + case 0: { + fullscreen_flag = SDL_WINDOW_FULLSCREEN_DESKTOP; + } break; + case 1: { + fullscreen_flag = SDL_WINDOW_FULLSCREEN; + } break; } +} + +static void set_fullscreen(bool on, bool call_callback) { fullscreen_state = on; SDL_SetWindowFullscreen(wnd, on ? fullscreen_flag : 0); if (call_callback && on_fullscreen_changed_callback) { @@ -42,9 +62,6 @@ static void set_fullscreen(bool on, bool call_callback) { } static void set_maximize_window(bool on) { - if (maximized_state == on) { - return; - } maximized_state = on; if (on) { SDL_MaximizeWindow(wnd); @@ -95,6 +112,13 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) { posY = 100; } + if (set->centered) { + SDL_DisplayMode mode = {}; + SDL_GetCurrentDisplayMode(0, &mode); + posX = mode.w / 2 - window_width / 2; + posY = mode.h / 2 - window_height / 2; + } + if (set->fullscreen_is_exclusive) { fullscreen_flag = SDL_WINDOW_FULLSCREEN; } @@ -222,6 +246,43 @@ static void gfx_sdl_set_cursor_visibility(bool visible) { } } +static void get_centered_positions_native(int32_t width, int32_t height, int32_t *posX, int32_t *posY) { + const int disp_idx = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode mode = {}; + SDL_GetDesktopDisplayMode(disp_idx, &mode); + *posX = mode.w / 2 - width / 2; + *posY = mode.h / 2 - height / 2; +} + +static void gfx_sdl_get_centered_positions(int32_t width, int32_t height, int32_t *posX, int32_t *posY) { + const int disp_idx = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode mode = {}; + SDL_GetCurrentDisplayMode(disp_idx, &mode); + *posX = mode.w / 2 - width / 2; + *posY = mode.h / 2 - height / 2; +} + +static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool should_center) { + const SDL_DisplayMode mode = {.w = width, .h = height}; + const int disp_idx = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode closest = {}; + if (SDL_GetClosestDisplayMode(disp_idx, &mode, &closest)) { + SDL_SetWindowDisplayMode(wnd, &closest); + SDL_SetWindowSize(wnd, closest.w, closest.h); + if (should_center) { + int32_t posX = 0; + int32_t posY = 0; + get_centered_positions_native(closest.w, closest.h, &posX, &posY); + SDL_SetWindowPosition(wnd, posX, posY); + } + } +} + +static void gfx_sdl_set_dimensions(uint32_t width, uint32_t height, int32_t posX, int32_t posY) { + SDL_SetWindowSize(wnd, width, height); + SDL_SetWindowPosition(wnd, posX, posY); +} + static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY) { SDL_GL_GetDrawableSize(wnd, static_cast((void*)width), static_cast((void*)height)); SDL_GetWindowPosition(wnd, static_cast(posX), static_cast(posY)); @@ -240,6 +301,9 @@ static void gfx_sdl_handle_events(void) { case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { SDL_GL_GetDrawableSize(wnd, &window_width, &window_height); + if (!fullscreen_state) { + maximized_state = SDL_GetWindowFlags(wnd) & SDL_WINDOW_MAXIMIZED ? true : false; + } } else if (event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(wnd)) { // We listen specifically for main window close because closing main window @@ -304,6 +368,10 @@ static double gfx_sdl_get_time(void) { return SDL_GetPerformanceCounter() / (double)qpc_freq; } +static int32_t gfx_sdl_get_target_fps(void) { + return target_fps; +} + static void gfx_sdl_set_target_fps(int fps) { target_fps = fps; } @@ -320,6 +388,10 @@ static void gfx_sdl_set_window_title(const char *title) { SDL_SetWindowTitle(wnd, title); } +static int gfx_sdl_get_swap_interval(void) { + return SDL_GL_GetSwapInterval(); +} + static bool gfx_sdl_set_swap_interval(int interval) { const bool success = SDL_GL_SetSwapInterval(interval) >= 0; vsync_enabled = success && (interval != 0); @@ -329,24 +401,63 @@ static bool gfx_sdl_set_swap_interval(int interval) { return success; } -struct GfxWindowManagerAPI gfx_sdl = { +int gfx_sdl_get_display_mode(int modenum, int *out_w, int *out_h) { + const int display_in_use = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode sdlmode; + if (SDL_GetDisplayMode(display_in_use, modenum, &sdlmode) == 0) { + *out_w = sdlmode.w; + *out_h = sdlmode.h; + return 1; + } + return 0; +} + +int gfx_sdl_get_current_display_mode(int *out_w, int *out_h) { + const int display_in_use = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode sdlmode; + if (SDL_GetCurrentDisplayMode(display_in_use, &sdlmode) == 0) { + *out_w = sdlmode.w; + *out_h = sdlmode.h; + return 1; + } + return 0; +} + +int gfx_sdl_get_num_display_modes(void) { + const int display_in_use = SDL_GetWindowDisplayIndex(wnd); + return SDL_GetNumDisplayModes(display_in_use); +} + +struct GfxWindowManagerAPI gfx_sdl = { gfx_sdl_init, gfx_sdl_close, + gfx_sdl_get_display_mode, + gfx_sdl_get_current_display_mode, + gfx_sdl_get_num_display_modes, + gfx_sdl_get_fullscreen_state, gfx_sdl_set_fullscreen_changed_callback, gfx_sdl_set_fullscreen, gfx_sdl_set_fullscreen_exclusive, + gfx_sdl_set_fullscreen_flag, + gfx_sdl_get_fullscreen_flag_mode, + gfx_sdl_get_maximized_state, gfx_sdl_set_maximize_window, gfx_sdl_get_active_window_refresh_rate, gfx_sdl_set_cursor_visibility, + gfx_sdl_set_closest_resolution, + gfx_sdl_set_dimensions, gfx_sdl_get_dimensions, + gfx_sdl_get_centered_positions, gfx_sdl_handle_events, gfx_sdl_start_frame, gfx_sdl_swap_buffers_begin, gfx_sdl_swap_buffers_end, gfx_sdl_get_time, + gfx_sdl_get_target_fps, gfx_sdl_set_target_fps, gfx_sdl_can_disable_vsync, gfx_sdl_get_window_handle, gfx_sdl_set_window_title, + gfx_sdl_get_swap_interval, gfx_sdl_set_swap_interval, }; diff --git a/port/fast3d/gfx_window_manager_api.h b/port/fast3d/gfx_window_manager_api.h index 61b05315f..d10fc45cf 100644 --- a/port/fast3d/gfx_window_manager_api.h +++ b/port/fast3d/gfx_window_manager_api.h @@ -13,28 +13,41 @@ struct GfxWindowInitSettings { bool fullscreen; bool fullscreen_is_exclusive; bool maximized; + bool centered; bool allow_hidpi; }; struct GfxWindowManagerAPI { void (*init)(const struct GfxWindowInitSettings *settings); void (*close)(void); + int (*get_display_mode)(int modenum, int *out_w, int *out_h); + int (*get_current_display_mode)(int *out_w, int *out_h); + int (*get_num_display_modes)(void); + int32_t (*get_fullscreen_state)(void); void (*set_fullscreen_changed_callback)(void (*on_fullscreen_changed)(bool is_now_fullscreen)); void (*set_fullscreen)(bool enable); void (*set_fullscreen_exclusive)(bool exc); + void (*set_fullscreen_flag)(int32_t mode); + int32_t (*get_fullscreen_flag_mode)(void); + int32_t (*get_maximized_state)(void); void (*set_maximize)(bool enable); void (*get_active_window_refresh_rate)(uint32_t* refresh_rate); void (*set_cursor_visibility)(bool visible); + void (*set_closest_resolution)(int32_t width, int32_t height, bool should_center); + void (*set_dimensions)(uint32_t width, uint32_t height, int32_t posX, int32_t posY); void (*get_dimensions)(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY); + void (*get_centered_positions)(int32_t width, int32_t height, int32_t *posX, int32_t *posY); void (*handle_events)(void); bool (*start_frame)(void); void (*swap_buffers_begin)(void); void (*swap_buffers_end)(void); double (*get_time)(void); // For debug + int32_t (*get_target_fps)(void); void (*set_target_fps)(int fps); bool (*can_disable_vsync)(void); void *(*get_window_handle)(void); void (*set_window_title)(const char *); + int (*get_swap_interval)(void); bool (*set_swap_interval)(int); }; diff --git a/port/include/video.h b/port/include/video.h index 70e8bf0e8..7a15ffc68 100644 --- a/port/include/video.h +++ b/port/include/video.h @@ -11,6 +11,11 @@ #define VIDEO_MAX_FPS 240 #endif +typedef struct { + s32 width; + s32 height; +} displaymode; + s32 videoInit(void); void videoStartFrame(void); void videoSubmitCommands(Gfx *cmds); @@ -27,18 +32,38 @@ s32 videoGetWidth(void); s32 videoGetHeight(void); f32 videoGetAspect(void); s32 videoGetFullscreen(void); +s32 videoGetFullscreenMode(void); s32 videoGetMaximizeWindow(void); void videoSetMaximizeWindow(s32 fs); +s32 videoGetCenterWindow(void); +void videoSetCenterWindow(s32 center); u32 videoGetTextureFilter(void); s32 videoGetTextureFilter2D(void); s32 videoGetDetailTextures(void); +s32 videoGetDisplayModeIndex(void); +s32 videoGetDisplayMode(displaymode *out, const s32 index); +s32 videoGetNumDisplayModes(void); +s32 videoGetVsync(void); +s32 videoGetFramerateLimit(void); +s32 videoGetDisplayFPS(void); +s32 videoGetMSAA(void); + +f32 videoGetAverageFPS(void); +f64 videoGetLastRenderTime(void); + +void videoCapFramerate(s32 limit); void videoSetWindowOffset(s32 x, s32 y); void videoSetFullscreen(s32 fs); +void videoSetFullscreenMode(s32 mode); void videoSetTextureFilter(u32 filter); void videoSetTextureFilter2D(s32 filter); void videoSetDetailTextures(s32 detail); -void videoCapFramerate(s32 limit); +void videoSetDisplayMode(const s32 index); +void videoSetVsync(const s32 vsync); +void videoSetFramerateLimit(const s32 limit); +void videoSetDisplayFPS(const s32 displayfps); +void videoSetMSAA(const s32 msaa); s32 videoCreateFramebuffer(u32 w, u32 h, s32 upscale, s32 autoresize); void videoSetFramebuffer(s32 target); @@ -50,4 +75,6 @@ s32 videoFramebuffersSupported(void); void videoResetTextureCache(void); void videoFreeCachedTexture(const void *texptr); +void videoShutdown(void); + #endif diff --git a/port/src/config.c b/port/src/config.c index 6be6f0d9f..596cf3a5a 100644 --- a/port/src/config.c +++ b/port/src/config.c @@ -10,7 +10,7 @@ #define CONFIG_MAX_SECNAME 128 #define CONFIG_MAX_KEYNAME 256 -#define CONFIG_MAX_SETTINGS 256 +#define CONFIG_MAX_SETTINGS 300 typedef enum { CFG_NONE, @@ -34,6 +34,7 @@ struct configentry { } settings[CONFIG_MAX_SETTINGS]; static s32 numSettings = 0; +static u8 configMaxWarningLogged = 0; static inline s32 configClampInt(s32 val, s32 min, s32 max) { @@ -64,11 +65,15 @@ static inline struct configentry *configAddEntry(const char *key) { if (numSettings < CONFIG_MAX_SETTINGS) { struct configentry *cfg = &settings[numSettings++]; - strncpy(cfg->key, key, CONFIG_MAX_KEYNAME); + snprintf(cfg->key, CONFIG_MAX_KEYNAME, "%s", key); const char *delim = strrchr(cfg->key, '.'); cfg->seclen = delim ? (delim - cfg->key) : 0; return cfg; } + if (!configMaxWarningLogged) { + sysLogPrintf(LOG_WARNING, "Maximum number of configuration entries exceeded: %d", CONFIG_MAX_SETTINGS); + configMaxWarningLogged = 1; + } return NULL; } diff --git a/port/src/main.c b/port/src/main.c index 8ddff3e61..dc4d6a722 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -91,6 +91,7 @@ static void cleanup(void) netDisconnect(); inputSaveBinds(); configSave(CONFIG_PATH); + videoShutdown(); crashShutdown(); // TODO: actually shut down all subsystems } diff --git a/port/src/optionsmenu.c b/port/src/optionsmenu.c index aa7f501b3..4771fc28b 100644 --- a/port/src/optionsmenu.c +++ b/port/src/optionsmenu.c @@ -698,19 +698,164 @@ static MenuItemHandlerResult menuhandlerFullScreen(s32 operation, struct menuite return 0; } -static MenuItemHandlerResult menuhandlerMaximizeWindow(s32 operation, struct menuitem *item, union handlerdata *data) +static MenuItemHandlerResult menuhandlerFullScreenMode(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static const char *opts[] = { + "Borderless", + "Exclusive" + }; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = ARRAYCOUNT(opts); + break; + case MENUOP_GETOPTIONTEXT: + return (intptr_t)opts[data->dropdown.value]; + case MENUOP_SET: + videoSetFullscreenMode(data->dropdown.value); + break; + case MENUOP_GETSELECTEDINDEX: + data->dropdown.value = videoGetFullscreenMode(); + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerCenterWindow(s32 operation, struct menuitem *item, union handlerdata *data) { switch (operation) { case MENUOP_GET: - return videoGetMaximizeWindow(); + return videoGetCenterWindow(); case MENUOP_SET: - videoSetMaximizeWindow(data->checkbox.value); + videoSetCenterWindow(data->checkbox.value); break; } return 0; } +static MenuItemHandlerResult menuhandlerVsync(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static const s32 numOpts = 10; + static const char *constOpts[] = { + "Adaptive", + "Off", + "On" + }; + static char dynOpt[20]; + s32 vblanks; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = numOpts; + break; + case MENUOP_GETOPTIONTEXT: + if (data->dropdown.value < ARRAYCOUNT(constOpts)) + return (intptr_t)constOpts[data->dropdown.value]; + vblanks = (s32)data->dropdown.value - 1; + snprintf(dynOpt, sizeof(dynOpt), "On (%d frames)", vblanks); + return (intptr_t)dynOpt; + case MENUOP_SET: + videoSetVsync(data->dropdown.value - 1); + case MENUOP_GETSELECTEDINDEX: + data->dropdown.value = videoGetVsync() + 1; + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerFramerateLimit(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GETSLIDER: + data->slider.value = videoGetFramerateLimit(); + break; + case MENUOP_SET: + videoSetFramerateLimit(data->slider.value); + break; + case MENUOP_GETSLIDERLABEL: + // NOTE: data->slider.label length must not exceed 15. + if (data->slider.value == 0) { + strcpy(data->slider.label, "Off"); + } else { + sprintf(data->slider.label, "%d FPS", data->slider.value); + } + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerMSAA(s32 operation, struct menuitem *item, union handlerdata *data) +{ + s32 msaa; + static const char *opts[] = { + "Off", + "2x (MSAA)", + "4x (MSAA)", + "8x (MSAA)", + "16x (MSAA)" + }; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = ARRAYCOUNT(opts); + break; + case MENUOP_GETOPTIONTEXT: + return (intptr_t)opts[data->dropdown.value]; + case MENUOP_SET: + videoSetMSAA(1 << data->dropdown.value); + break; + case MENUOP_GETSELECTEDINDEX: + msaa = videoGetMSAA(); + if (msaa < 2) { + data->dropdown.value = 0; + } else if (msaa < 4) { + data->dropdown.value = 1; + } else if (msaa < 8) { + data->dropdown.value = 2; + } else if (msaa < 16) { + data->dropdown.value = 3; + } else { + data->dropdown.value = 4; + } + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerResolution(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static char resstring[32]; + static const char *rescustom = "Custom"; + displaymode mode; + + switch (operation) { + case MENUOP_CHECKDISABLED: + if (videoGetFullscreen() && videoGetFullscreenMode() == 0) { + return true; + } + break; + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = videoGetNumDisplayModes(); + break; + case MENUOP_GETOPTIONTEXT: + videoGetDisplayMode(&mode, data->dropdown.value); + if (mode.width == 0 && mode.height == 0) { + return (intptr_t)rescustom; + } else { + snprintf(resstring, sizeof(resstring), "%dx%d", mode.width, mode.height); + } + return (intptr_t)resstring; + case MENUOP_SET: + videoSetDisplayMode(data->dropdown.value); + break; + case MENUOP_GETSELECTEDINDEX: + data->dropdown.value = videoGetDisplayModeIndex(); + } + + return 0; +} + static MenuItemHandlerResult menuhandlerTexFilter(s32 operation, struct menuitem *item, union handlerdata *data) { static const char *opts[] = { @@ -755,12 +900,25 @@ static MenuItemHandlerResult menuhandlerTexFilter2D(s32 operation, struct menuit return videoGetTextureFilter2D(); case MENUOP_SET: videoSetTextureFilter2D(data->checkbox.value); + g_TexFilter2D = videoGetTextureFilter2D() ? G_TF_BILERP : G_TF_POINT; break; } return 0; } +static MenuItemHandlerResult menuhandlerDisplayFPS(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GET: + return videoGetDisplayFPS(); + case MENUOP_SET: + videoSetDisplayFPS(data->checkbox.value); + } + + return 0; +} + static MenuItemHandlerResult menuhandlerGeMuzzleFlashes(s32 operation, struct menuitem *item, union handlerdata *data) { switch (operation) { @@ -774,6 +932,19 @@ static MenuItemHandlerResult menuhandlerGeMuzzleFlashes(s32 operation, struct me return 0; } +static MenuItemHandlerResult menuhandlerUncapTickrate(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GET: + return (g_TickRateDiv == 0); + case MENUOP_SET: + g_TickRateDiv = !data->checkbox.value; + break; + } + + return 0; +} + static MenuItemHandlerResult menuhandlerCenterHUD(s32 operation, struct menuitem *item, union handlerdata *data) { static const char *opts[] = { @@ -832,20 +1003,84 @@ struct menuitem g_ExtendedVideoMenuItems[] = { menuhandlerFullScreen, }, { - MENUITEMTYPE_CHECKBOX, + MENUITEMTYPE_DROPDOWN, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"Maximize Window", + (uintptr_t)"Full Screen Mode", 0, - menuhandlerMaximizeWindow, + menuhandlerFullScreenMode, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Resolution", + 0, + menuhandlerResolution, }, { MENUITEMTYPE_CHECKBOX, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"Detail Textures", + (uintptr_t)"Center Window", 0, - menuhandlerTexDetail, + menuhandlerCenterWindow, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Anti-aliasing", + 0, + menuhandlerMSAA, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Vsync", + 0, + menuhandlerVsync, + }, + { + MENUITEMTYPE_SLIDER, + 0, + MENUITEMFLAG_LITERAL_TEXT | MENUITEMFLAG_SLIDER_WIDE | MENUITEMFLAG_SLIDER_DEFERRED, + (uintptr_t)"Framerate Limit", + VIDEO_MAX_FPS, + menuhandlerFramerateLimit, + }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Uncap Tickrate", + 0, + menuhandlerUncapTickrate, + }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Display FPS", + 0, + menuhandlerDisplayFPS, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, }, { MENUITEMTYPE_DROPDOWN, @@ -867,9 +1102,17 @@ struct menuitem g_ExtendedVideoMenuItems[] = { MENUITEMTYPE_CHECKBOX, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"GE64-style Muzzle Flashes", + (uintptr_t)"Detail Textures", 0, - menuhandlerGeMuzzleFlashes, + menuhandlerTexDetail, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, }, { MENUITEMTYPE_DROPDOWN, @@ -879,6 +1122,14 @@ struct menuitem g_ExtendedVideoMenuItems[] = { 0, menuhandlerCenterHUD, }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"GE64-style Muzzle Flashes", + 0, + menuhandlerGeMuzzleFlashes, + }, { MENUITEMTYPE_SLIDER, 0, diff --git a/port/src/video.c b/port/src/video.c index 4ebe4af87..61c711695 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -6,6 +6,7 @@ #include #include "platform.h" #include "config.h" +#include "system.h" #include "video.h" #include "../fast3d/gfx_api.h" @@ -35,19 +36,33 @@ static s32 vidFramebuffers = true; static s32 vidFullscreen = DEFAULT_VID_FULLSCREEN; static s32 vidFullscreenExclusive = DEFAULT_VID_FULLSCREEN_EXCLUSIVE; static s32 vidMaximize = false; +static s32 vidCenter = false; static s32 vidAllowHiDpi = false; static s32 vidVsync = 1; static s32 vidMSAA = 1; static s32 vidFramerateLimit = 0; +static s32 vidDisplayFPS = 0; +static f32 vidDisplayFPSInterval = 1.f; +static f32 vidAvgFPS = 0; +static f64 vidLastRenderTime; + +static s32 vidNumModes = 1; +static displaymode vidModeDefault; +static displaymode *vidModes = &vidModeDefault; + static s32 texFilter = FILTER_LINEAR; static s32 texFilter2D = true; -static s32 texDetail = true; +static s32 texDetail = false; static u32 dlcount = 0; static u32 frames = 0; -static u32 framesPerSec = 0; -static f64 startTime, endTime, fpsTime; +static f64 startTime, endTime; +static f64 accumDelta = 0.0; +static f64 fpsTime = 0.0; +static s32 fpsNumFrames = 0; + +static s32 videoInitDisplayModes(void); s32 videoInit(void) { @@ -73,22 +88,17 @@ s32 videoInit(void) .fullscreen = vidFullscreen, .fullscreen_is_exclusive = vidFullscreenExclusive, .maximized = vidMaximize, + .centered = vidCenter, .allow_hidpi = vidAllowHiDpi } }; gfx_init(&set); - if (!wmAPI->set_swap_interval(vidVsync)) { - vidVsync = 0; - } + videoInitDisplayModes(); + videoSetVsync(vidVsync); + videoSetFramerateLimit(vidFramerateLimit); - if (vidVsync == 0 && vidFramerateLimit == 0) { - // cap FPS if there's no vsync to prevent the game from exploding - vidFramerateLimit = VIDEO_MAX_FPS; - } - - wmAPI->set_target_fps(vidFramerateLimit); // disabled because vsync is on gfx_set_texture_filter((enum FilteringMode)texFilter); initDone = true; @@ -101,6 +111,10 @@ void videoStartFrame(void) startTime = wmAPI->get_time(); gfx_start_frame(); } + + // Synchronize with their backend counterparts. + vidFullscreen = videoGetFullscreen(); + vidMaximize = videoGetMaximizeWindow(); } void videoSubmitCommands(Gfx *cmds) @@ -119,20 +133,35 @@ void videoEndFrame(void) gfx_end_frame(); - endTime = wmAPI->get_time(); - ++frames; - ++framesPerSec; + ++fpsNumFrames; + + const f64 flipTime = wmAPI->get_time(); + accumDelta += flipTime - endTime; + endTime = flipTime; + vidLastRenderTime = endTime - startTime; if (endTime >= fpsTime) { char tmp[128]; - snprintf(tmp, sizeof(tmp), "fps %3u frt %lf frm %u", framesPerSec, endTime - startTime, frames); + vidAvgFPS = fpsNumFrames ? ((f64)fpsNumFrames / accumDelta) : 0.f; + fpsNumFrames = 0; + accumDelta = 0.0; + snprintf(tmp, sizeof(tmp), "fps %4.1f frt %lf frm %u", vidAvgFPS, vidLastRenderTime, frames); wmAPI->set_window_title(tmp); - framesPerSec = 0; - fpsTime = endTime + 1.0; + fpsTime = endTime + vidDisplayFPSInterval; } } +f64 videoGetLastRenderTime(void) +{ + return vidLastRenderTime; +} + +f32 videoGetAverageFPS(void) +{ + return vidAvgFPS; +} + void videoClearScreen(void) { videoStartFrame(); @@ -177,19 +206,159 @@ s32 videoGetHeight(void) s32 videoGetFullscreen(void) { + vidFullscreen = wmAPI->get_fullscreen_state(); return vidFullscreen; } +s32 videoGetFullscreenMode(void) +{ + vidFullscreenExclusive = wmAPI->get_fullscreen_flag_mode(); + return vidFullscreenExclusive; +} + s32 videoGetMaximizeWindow(void) { + vidMaximize = wmAPI->get_maximized_state(); return vidMaximize; } +s32 videoGetCenterWindow(void) +{ + return vidCenter; +} + f32 videoGetAspect(void) { return gfx_current_dimensions.aspect_ratio; } +s32 videoGetDisplayModeIndex(void) +{ + for (s32 i = 1; i < vidNumModes; ++i) { + if (vidModes[i].width == gfx_current_dimensions.width && + vidModes[i].height == gfx_current_dimensions.height) { + return i; + } + } + // Current dimensions don't match any known mode, so return index 0, "Custom". + return 0; +} + +s32 videoGetMSAA(void) +{ + vidMSAA = (s32)gfx_msaa_level; + return vidMSAA; +} + +s32 videoGetVsync(void) +{ + vidVsync = wmAPI->get_swap_interval(); + return vidVsync; +} + +s32 videoGetFramerateLimit(void) +{ + vidFramerateLimit = wmAPI->get_target_fps(); + return vidFramerateLimit; +} + +s32 videoGetDisplayFPS(void) +{ + return vidDisplayFPS; +} + +static s32 videoInitDisplayModes(void) +{ + if (!wmAPI->get_current_display_mode(&vidModeDefault.width, &vidModeDefault.height)) { + vidModeDefault.width = 640; + vidModeDefault.height = 480; + return false; + } + + const s32 numBaseModes = wmAPI->get_num_display_modes(); + if (!numBaseModes) { + return false; + } + + const s32 numCustomModes = 1; + displaymode *modeList = sysMemZeroAlloc((numBaseModes + numCustomModes) * sizeof(displaymode)); + if (!modeList) { + return false; + } + + modeList[0].width = 0; + modeList[0].height = 0; + + s32 numModes = 1; + s32 w = -1, h = w, neww = w, newh = w; + + // SDL modes are guaranteed to be sorted high to low + for (s32 i = 0; i < numBaseModes; ++i) { + wmAPI->get_display_mode(i, &neww, &newh); + + if (neww != w || newh != h) { + w = neww; + h = newh; + modeList[numModes].width = w; + modeList[numModes].height = h; + ++numModes; + } + } + + modeList = sysMemRealloc(modeList, numModes * sizeof(displaymode)); + if (!modeList) { + return false; + } + + vidModes = modeList; + vidNumModes = numModes; + + return true; +} + +s32 videoGetDisplayMode(displaymode *out, const s32 index) +{ + if (index >= 0 && index < vidNumModes) { + *out = vidModes[index]; + return true; + } + return false; +} + +s32 videoGetNumDisplayModes(void) +{ + return vidNumModes; +} + +void videoSetDisplayMode(const s32 index) +{ + const displaymode dm = vidModes[index]; + + if (index == 0) { + // "Custom" video mode. + return; + } + + vidWidth = dm.width; + vidHeight = dm.height; + + s32 posX = 100; + s32 posY = 100; + if (vidCenter) { + wmAPI->get_centered_positions(vidWidth, vidHeight, &posX, &posY); + } + + if (vidFullscreen) { + wmAPI->set_closest_resolution(vidWidth, vidHeight, vidCenter); + } else { + if (vidMaximize) { + videoSetMaximizeWindow(false); + } else { + wmAPI->set_dimensions(vidWidth, vidHeight, posX, posY); + } + } +} + s32 videoGetTextureFilter2D(void) { return texFilter2D; @@ -215,7 +384,22 @@ void videoSetFullscreen(s32 fs) { if (fs != vidFullscreen) { vidFullscreen = !!fs; + wmAPI->set_closest_resolution(vidWidth, vidHeight, vidCenter); wmAPI->set_fullscreen(vidFullscreen); + if (!vidFullscreen && vidMaximize) { + wmAPI->set_maximize(false); + wmAPI->set_maximize(true); + } + } +} + +void videoSetFullscreenMode(s32 mode) +{ + vidFullscreenExclusive = mode; + wmAPI->set_fullscreen_flag(mode); + if (vidFullscreen) { + wmAPI->set_fullscreen(false); + wmAPI->set_fullscreen(true); } } @@ -224,6 +408,23 @@ void videoSetMaximizeWindow(s32 fs) if (fs != vidMaximize) { vidMaximize = !!fs; wmAPI->set_maximize(vidMaximize); + if (vidCenter && !vidMaximize) { + s32 posX = 0; + s32 posY = 0; + wmAPI->get_centered_positions(vidWidth, vidHeight, &posX, &posY); + wmAPI->set_dimensions(vidWidth, vidHeight, posX, posY); + } + } +} + +void videoSetCenterWindow(s32 center) +{ + vidCenter = center; + if (vidCenter && !vidMaximize) { + s32 posX = 0; + s32 posY = 0; + wmAPI->get_centered_positions(vidWidth, vidHeight, &posX, &posY); + wmAPI->set_dimensions(vidWidth, vidHeight, posX, posY); } } @@ -259,6 +460,33 @@ s32 videoCreateFramebuffer(u32 w, u32 h, s32 upscale, s32 autoresize) return gfx_create_framebuffer(w, h, upscale, autoresize); } +void videoSetMSAA(const s32 msaa) +{ + vidMSAA = msaa; + gfx_msaa_level = (u32)vidMSAA; +} + +void videoSetVsync(const s32 vsync) +{ + vidVsync = wmAPI->set_swap_interval(vsync) ? vsync : 0; + + if (vidVsync == 0 && vidFramerateLimit == 0) { + // cap FPS if there's no vsync to prevent the game from exploding + videoSetFramerateLimit(VIDEO_MAX_FPS); + } +} + +void videoSetFramerateLimit(const s32 limit) +{ + vidFramerateLimit = (vidVsync == 0 && limit == 0) ? VIDEO_MAX_FPS : limit; + wmAPI->set_target_fps(vidFramerateLimit); +} + +void videoSetDisplayFPS(const s32 displayfps) +{ + vidDisplayFPS = displayfps; +} + void videoSetFramebuffer(s32 target) { return gfx_set_framebuffer(target, 1.f); @@ -295,6 +523,11 @@ void videoFreeCachedTexture(const void *texptr) gfx_texture_cache_delete(texptr); } +void videoShutdown(void) +{ + free(vidModes); +} + PD_CONSTRUCTOR static void videoConfigInit(void) { configRegisterInt("Video.DefaultFullscreen", &vidFullscreen, 0, 1); @@ -302,10 +535,13 @@ PD_CONSTRUCTOR static void videoConfigInit(void) configRegisterInt("Video.DefaultWidth", &vidWidth, 0, 32767); configRegisterInt("Video.DefaultHeight", &vidHeight, 0, 32767); configRegisterInt("Video.ExclusiveFullscreen", &vidFullscreenExclusive, 0, 1); + configRegisterInt("Video.CenterWindow", &vidCenter, 0, 1); configRegisterInt("Video.AllowHiDpi", &vidAllowHiDpi, 0, 1); configRegisterInt("Video.VSync", &vidVsync, -1, 10); configRegisterInt("Video.FramebufferEffects", &vidFramebuffers, 0, 1); configRegisterInt("Video.FramerateLimit", &vidFramerateLimit, 0, VIDEO_MAX_FPS); + configRegisterInt("Video.DisplayFPS", &vidDisplayFPS, 0, 1); + configRegisterFloat("Video.DisplayFPSInterval", &vidDisplayFPSInterval, 0.01f, 32.f); configRegisterInt("Video.MSAA", &vidMSAA, 1, 16); configRegisterInt("Video.TextureFilter", &texFilter, 0, 2); configRegisterInt("Video.TextureFilter2D", &texFilter2D, 0, 1); diff --git a/src/game/lv.c b/src/game/lv.c index ba1daa5b0..4c36e36ae 100644 --- a/src/game/lv.c +++ b/src/game/lv.c @@ -99,6 +99,7 @@ #ifndef PLATFORM_N64 #include "net/net.h" #include "net/netmsg.h" +#include "video.h" #endif struct sndstate *g_MiscSfxAudioHandles[3]; @@ -971,6 +972,45 @@ void lvFindThreats(void) } } +#ifndef PLATFORM_N64 +Gfx *lvRenderFPS(Gfx *gdl) +{ + const f32 fps = videoGetAverageFPS(); + const u8 a = 160; + s32 x = 27, y = 13; + u32 color; + char buffer[16]; + + if (fps <= 30.f) { + // red -> yellow + color = 0xff000000 | a | ((u32)((fps / 30.f) * 255.f) << 16); + } else if (fps <= 60.f) { + // yellow -> green + color = 0x00ff0000 | a | ((u32)((1.f - (fps - 30.f) / 30.f) * 255.f) << 24); + } else if (fps <= 90.f) { + // green -> cyan + color = 0x00ff0000 | a | ((u32)(((fps - 60.f) / 30.f) * 255.f) << 8); + } else { + // cyan + color = 0x00ffff00 | a; + } + + if (g_CharsNumeric && g_FontNumeric) { + snprintf(buffer, sizeof buffer, "%.2f", fps); + + gSPSetExtraGeometryModeEXT(gdl++, g_HudAlignModeL); + + gdl = text0f153628(gdl); + gdl = textRender(gdl, &x, &y, buffer, g_CharsNumeric, g_FontNumeric, color, 0x000000a0, viGetWidth(), viGetHeight(), 0, 0); + gdl = text0f153780(gdl); + + gSPClearExtraGeometryModeEXT(gdl++, g_HudAlignModeL); + } + + return gdl; +} +#endif + /** * Renders a complete frame for all players, and also does some other game logic * that really doesn't belong here. @@ -1781,6 +1821,12 @@ Gfx *lvRender(Gfx *gdl) gDPSetScissor(gdl++, G_SC_NON_INTERLACE, 0, 0, viGetWidth(), viGetHeight()); +#ifndef PLATFORM_N64 + if (videoGetDisplayFPS()) { + gdl = lvRenderFPS(gdl); + } +#endif + #if VERSION < VERSION_NTSC_1_0 if ((uintptr_t)gdl < (uintptr_t)g_GfxBuffers[g_GfxActiveBufferIndex] || (uintptr_t)gdl > (uintptr_t)g_GfxBuffers[g_GfxActiveBufferIndex + 1]) { diff --git a/src/game/menuitem.c b/src/game/menuitem.c index c79500d5a..035bfd0fb 100644 --- a/src/game/menuitem.c +++ b/src/game/menuitem.c @@ -51,6 +51,8 @@ u8 g_KeyboardKeys[5][10] = { { '1','2','1','2','1','2','3','1','2','3' }, }; +static s32 deferredindex = -1; + s32 func0f0e5ce0(s32 value) { if (value < var800711a4) { @@ -2269,7 +2271,7 @@ Gfx* menuitemColorBoxRender(Gfx *gdl, struct menurendercontext *context) return gdl; } -#endif +#endif Gfx *menuitemSelectableRender(Gfx *gdl, struct menurendercontext *context) { @@ -2430,8 +2432,15 @@ Gfx *menuitemSliderRender(Gfx *gdl, struct menurendercontext *context) extray = 0; if (context->item->handler != NULL) { - context->item->handler(MENUOP_GETSLIDER, context->item, &data); - slidervalue = (s16) data.slider.value; + if ((context->item->flags & MENUITEMFLAG_SLIDER_DEFERRED) && + deferredindex != -1 && + context->dialog->dimmed && + context->focused) { + slidervalue = (s16) deferredindex; + } else { + context->item->handler(MENUOP_GETSLIDER, context->item, &data); + slidervalue = (s16) data.slider.value; + } } else { slidervalue = 0; } @@ -2570,9 +2579,14 @@ bool menuitemSliderTick(struct menuitem *item, struct menudialog *dialog, struct index = item->param3; } if (item->handler) { - item->handler(MENUOP_GET, item, &handlerdata); - handlerdata.slider.value = index; - item->handler(MENUOP_SET, item, &handlerdata); + if ((item->flags & MENUITEMFLAG_SLIDER_DEFERRED) && + (tickflags & MENUTICKFLAG_DIALOGISDIMMED)) { + deferredindex = index; + } else { + item->handler(MENUOP_GET, item, &handlerdata); + handlerdata.slider.value = index; + item->handler(MENUOP_SET, item, &handlerdata); + } } return true; } @@ -2582,8 +2596,17 @@ bool menuitemSliderTick(struct menuitem *item, struct menudialog *dialog, struct if (tickflags & MENUTICKFLAG_DIALOGISDIMMED) { if (item->handler) { - item->handler(MENUOP_GETSLIDER, item, &handlerdata); - index = (s16) handlerdata.slider.value; + if (item->flags & MENUITEMFLAG_SLIDER_DEFERRED) { + if (deferredindex == -1) { + item->handler(MENUOP_GETSLIDER, item, &handlerdata); + deferredindex = (s16) handlerdata.slider.value; + } + index = deferredindex; + } else { + item->handler(MENUOP_GETSLIDER, item, &handlerdata); + index = (s16) handlerdata.slider.value; + } + } else { index = 0; } @@ -2663,11 +2686,21 @@ bool menuitemSliderTick(struct menuitem *item, struct menudialog *dialog, struct inputs->leftright = 0; handlerdata.slider.value = index; - if (item->handler) { - item->handler(MENUOP_SET, item, &handlerdata); + if (item->flags & MENUITEMFLAG_SLIDER_DEFERRED) { + deferredindex = index; + } else { + if (item->handler) { + item->handler(MENUOP_SET, item, &handlerdata); + } } if (inputs->select) { + if (item->flags & MENUITEMFLAG_SLIDER_DEFERRED) { + deferredindex = -1; + if (item->handler) { + item->handler(MENUOP_SET, item, &handlerdata); + } + } dialog->dimmed = false; } } else { @@ -2707,7 +2740,7 @@ Gfx *menuitemCarouselRender(Gfx *gdl, struct menurendercontext *context) #ifdef PLATFORM_N64 s16 chevronOffset = 0; -#else +#else s16 chevronOffset = 3; #endif diff --git a/src/include/constants.h b/src/include/constants.h index 54cfee47d..41598eed8 100644 --- a/src/include/constants.h +++ b/src/include/constants.h @@ -1690,6 +1690,7 @@ #define MENUITEMFLAG_CAROUSEL_04000000 0x04000000 #define MENUITEMFLAG_LITERAL_TEXT 0x08000000 #define MENUITEMFLAG_SLIDER_WIDE 0x10000000 +#define MENUITEMFLAG_SLIDER_DEFERRED 0x20000000 #define MENUITEMTYPE_LABEL 0x01 #define MENUITEMTYPE_LIST 0x02 @@ -1720,7 +1721,7 @@ #ifndef PLATFORM_N64 #define MENUITEMTYPE_COLORBOX 0x1b -#endif +#endif #define MENUMODELFLAG_HASSCALE 0x01 #define MENUMODELFLAG_HASPOSITION 0x02