diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index c6b67eebb..c1ec8ce6e 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); @@ -96,6 +113,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; } @@ -217,6 +241,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)); @@ -235,6 +296,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 @@ -299,6 +363,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; } @@ -315,6 +383,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); @@ -324,24 +396,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 d7a888886..0c468dcbb 100644 --- a/port/include/video.h +++ b/port/include/video.h @@ -11,11 +11,17 @@ #define VIDEO_MAX_FPS 240 #endif +typedef struct { + s32 width; + s32 height; +} displaymode; + s32 videoInit(void); void videoStartFrame(void); void videoSubmitCommands(Gfx *cmds); void videoClearScreen(void); void videoEndFrame(void); +f64 videoGetDiffTime(void); void *videoGetWindowHandle(void); @@ -27,17 +33,35 @@ 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 videoGetDisplayFPSDivisor(void); +s32 videoGetMSAA(void); 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 videoSetDisplayMode(const s32 index); +void videoSetVsync(const s32 vsync); +void videoSetFramerateLimit(const s32 limit); +void videoSetDisplayFPS(const s32 displayfps); +void videoSetDisplayFPSDivisor(const s32 divisor); +void videoSetMSAA(const s32 msaa); s32 videoCreateFramebuffer(u32 w, u32 h, s32 upscale, s32 autoresize); void videoSetFramebuffer(s32 target); @@ -49,4 +73,6 @@ s32 videoFramebuffersSupported(void); void videoResetTextureCache(void); void videoFreeCachedTexture(const void *texptr); +void videoShutdown(void); + #endif diff --git a/port/src/main.c b/port/src/main.c index 5e404426d..c745c0efb 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -36,6 +36,7 @@ u32 g_VmNumPageReplaces = 0; u8 g_VmShowStats = 0; s32 g_TickRateDiv = 1; +s32 g_TickRateDivOverride = 0; s32 g_TickExtraSleep = true; s32 g_SkipIntro = false; @@ -88,6 +89,7 @@ static void cleanup(void) sysLogPrintf(LOG_NOTE, "shutdown"); inputSaveBinds(); configSave(CONFIG_PATH); + videoShutdown(); crashShutdown(); // TODO: actually shut down all subsystems } @@ -164,6 +166,7 @@ PD_CONSTRUCTOR static void gameConfigInit(void) configRegisterInt("Game.MenuMouseControl", &g_MenuMouseControl, 0, 1); configRegisterFloat("Game.ScreenShakeIntensity", &g_ViShakeIntensityMult, 0.f, 10.f); configRegisterInt("Game.TickRateDivisor", &g_TickRateDiv, 0, 10); + configRegisterInt("Game.TickRateDivisorOverride", &g_TickRateDivOverride, 0, 1); configRegisterInt("Game.ExtraSleep", &g_TickExtraSleep, 0, 1); configRegisterInt("Game.SkipIntro", &g_SkipIntro, 0, 1); configRegisterInt("Game.DisableMpDeathMusic", &g_MusicDisableMpDeath, 0, 1); diff --git a/port/src/optionsmenu.c b/port/src/optionsmenu.c index 2b845ebe0..1d03bc169 100644 --- a/port/src/optionsmenu.c +++ b/port/src/optionsmenu.c @@ -687,6 +687,29 @@ static MenuItemHandlerResult menuhandlerFullScreen(s32 operation, struct menuite return 0; } +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 menuhandlerMaximizeWindow(s32 operation, struct menuitem *item, union handlerdata *data) { switch (operation) { @@ -700,6 +723,146 @@ static MenuItemHandlerResult menuhandlerMaximizeWindow(s32 operation, struct men return 0; } +static MenuItemHandlerResult menuhandlerCenterWindow(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GET: + return videoGetCenterWindow(); + case MENUOP_SET: + videoSetCenterWindow(data->checkbox.value); + break; + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerVsync(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static const char *opts[] = { + "Adaptive", + "Off", + "On (Every Frame)", + "On (Every 2 Frames)", + "On (Every 3 Frames)", + "On (Every 4 Frames)", + "On (Every 5 Frames)", + "On (Every 6 Frames)", + "On (Every 7 Frames)", + "On (Every 8 Frames)", + "On (Every 9 Frames)", + "On (Every 10 Frames)" + }; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = ARRAYCOUNT(opts); + break; + case MENUOP_GETOPTIONTEXT: + return (intptr_t)opts[data->dropdown.value]; + 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: + if (!g_TickRateDivOverride) { + g_TickRateDiv = (data->slider.value == 0 || data->slider.value > 60) ? 0 : 1; + } + 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[] = { @@ -744,12 +907,46 @@ 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 menuhandlerDisplayFPSInterval(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GETSLIDER: + data->slider.value = videoGetDisplayFPSDivisor() - 1; + break; + case MENUOP_SET: + videoSetDisplayFPSDivisor(data->dropdown.value + 1); + break; + case MENUOP_GETSLIDERLABEL: + // NOTE: data->slider.label length must not exceed 15. + if (data->slider.value == 0) { + strcpy(data->slider.label, "1 Sec"); + } else { + sprintf(data->slider.label, "1/%d Secs", data->slider.value + 1); + } + } + + return 0; +} + static MenuItemHandlerResult menuhandlerGeMuzzleFlashes(s32 operation, struct menuitem *item, union handlerdata *data) { switch (operation) { @@ -763,6 +960,49 @@ static MenuItemHandlerResult menuhandlerGeMuzzleFlashes(s32 operation, struct me return 0; } +static MenuItemHandlerResult menuhandlerTickrateDivisorOverride(s32 operation, struct menuitem *item, union handlerdata *data) +{ + s32 framerateLimit; + + switch (operation) { + case MENUOP_GET: + return g_TickRateDivOverride; + case MENUOP_SET: + g_TickRateDivOverride = data->checkbox.value; + if (!g_TickRateDivOverride) { + framerateLimit = videoGetFramerateLimit(); + g_TickRateDiv = (framerateLimit == 0 || framerateLimit > 60) ? 0 : 1; + } + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerTickrateDivisor(s32 operation, struct menuitem *item, union handlerdata *data) +{ + s32 framerateLimit; + + switch (operation) { + case MENUOP_CHECKHIDDEN: + if (!g_TickRateDivOverride) { + return true; + } + break; + case MENUOP_GETSLIDER: + data->slider.value = g_TickRateDiv; + break; + case MENUOP_SET: + if (g_TickRateDivOverride) { + g_TickRateDiv = data->slider.value; + } else { + framerateLimit = videoGetFramerateLimit(); + g_TickRateDiv = (framerateLimit == 0 || framerateLimit > 60) ? 0 : 1; + } + } + + return 0; +} + static MenuItemHandlerResult menuhandlerCenterHUD(s32 operation, struct menuitem *item, union handlerdata *data) { static const char *opts[] = { @@ -820,6 +1060,22 @@ struct menuitem g_ExtendedVideoMenuItems[] = { 0, menuhandlerFullScreen, }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Full Screen Mode", + 0, + menuhandlerFullScreenMode, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Resolution", + 0, + menuhandlerResolution, + }, { MENUITEMTYPE_CHECKBOX, 0, @@ -832,9 +1088,73 @@ struct menuitem g_ExtendedVideoMenuItems[] = { 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)"HUD Centering", + 0, + menuhandlerCenterHUD, + }, + { + 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)"Display FPS", + 0, + menuhandlerDisplayFPS, + }, + { + MENUITEMTYPE_SLIDER, + 0, + MENUITEMFLAG_LITERAL_TEXT | MENUITEMFLAG_SLIDER_WIDE, + (uintptr_t)"Display FPS Interval", + 31, + menuhandlerDisplayFPSInterval, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Anti-aliasing", + 0, + menuhandlerMSAA, }, { MENUITEMTYPE_DROPDOWN, @@ -852,21 +1172,29 @@ struct menuitem g_ExtendedVideoMenuItems[] = { 0, menuhandlerTexFilter2D, }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, + }, { MENUITEMTYPE_CHECKBOX, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"GE64-style Muzzle Flashes", + (uintptr_t)"Detail Textures", 0, - menuhandlerGeMuzzleFlashes, + menuhandlerTexDetail, }, { - MENUITEMTYPE_DROPDOWN, + MENUITEMTYPE_CHECKBOX, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"HUD Centering", + (uintptr_t)"GE64-style Muzzle Flashes", 0, - menuhandlerCenterHUD, + menuhandlerGeMuzzleFlashes, }, { MENUITEMTYPE_SLIDER, @@ -884,6 +1212,30 @@ struct menuitem g_ExtendedVideoMenuItems[] = { 0, NULL, }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Override Tickrate Divisor", + 0, + menuhandlerTickrateDivisorOverride, + }, + { + MENUITEMTYPE_SLIDER, + 0, + MENUITEMFLAG_LITERAL_TEXT | MENUITEMFLAG_SLIDER_WIDE, + (uintptr_t)"Tickrate Divisor", + 10, + menuhandlerTickrateDivisor, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, + }, { MENUITEMTYPE_SELECTABLE, 0, diff --git a/port/src/video.c b/port/src/video.c index 52eafc228..7430204e8 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,10 +36,17 @@ 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 s32 vidDisplayFPSDivisor = 16; +static displaymode vidModeDefault; +static s32 vidNumModes = 1; +static displaymode *vidModes = &vidModeDefault; +static f64 vidDiffTime; static s32 texFilter = FILTER_LINEAR; static s32 texFilter2D = true; @@ -46,8 +54,9 @@ static s32 texDetail = true; static u32 dlcount = 0; static u32 frames = 0; -static u32 framesPerSec = 0; -static f64 startTime, endTime, fpsTime; +static f64 startTime, endTime; + +static s32 videoInitDisplayModes(void); s32 videoInit(void) { @@ -73,22 +82,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 +105,10 @@ void videoStartFrame(void) startTime = wmAPI->get_time(); gfx_start_frame(); } + + // Synchronize with their backend counterparts. + vidFullscreen = videoGetFullscreen(); + vidMaximize = videoGetMaximizeWindow(); } void videoSubmitCommands(Gfx *cmds) @@ -113,26 +121,33 @@ void videoSubmitCommands(Gfx *cmds) void videoEndFrame(void) { + static f64 accumTime = 0.0; + if (!initDone) { return; } gfx_end_frame(); - endTime = wmAPI->get_time(); - ++frames; - ++framesPerSec; - if (endTime >= fpsTime) { + endTime = wmAPI->get_time(); + accumTime += endTime - startTime; + + if (accumTime >= 1.0 / (f64)vidDisplayFPSDivisor) { char tmp[128]; - snprintf(tmp, sizeof(tmp), "fps %3u frt %lf frm %u", framesPerSec, endTime - startTime, frames); + vidDiffTime = endTime - startTime; + accumTime = 0.0; + snprintf(tmp, sizeof(tmp), "fps %3u frt %lf frm %u", (u32)(1.0 / vidDiffTime), vidDiffTime, frames); wmAPI->set_window_title(tmp); - framesPerSec = 0; - fpsTime = endTime + 1.0; } } +f64 videoGetDiffTime(void) +{ + return vidDiffTime; +} + void videoClearScreen(void) { videoStartFrame(); @@ -177,19 +192,164 @@ 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; +} + +s32 videoGetDisplayFPSDivisor(void) +{ + return vidDisplayFPSDivisor; +} + +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 +375,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 +399,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); } } @@ -251,6 +443,38 @@ 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 videoSetDisplayFPSDivisor(const s32 divisor) +{ + vidDisplayFPSDivisor = divisor; +} + void videoSetFramebuffer(s32 target) { return gfx_set_framebuffer(target, 1.f); @@ -287,6 +511,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); @@ -294,10 +523,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); + configRegisterInt("Video.DisplayFPSDivisor", &vidDisplayFPSDivisor, 1, 32); 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 22f3993a8..bd2a37c2f 100644 --- a/src/game/lv.c +++ b/src/game/lv.c @@ -96,6 +96,9 @@ #include "lib/vars.h" #include "lib/vi.h" #include "types.h" +#ifndef PLATFORM_N64 +#include "video.h" +#endif struct sndstate *g_MiscSfxAudioHandles[3]; u32 var800aa5bc; @@ -951,6 +954,35 @@ void lvFindThreats(void) } } +#ifndef PLATFORM_N64 +Gfx *lvRenderFPS(Gfx *gdl) +{ + s32 x = 27, y = 13; + f32 fps = 1.0f / videoGetDiffTime(); + s32 ifps = (s32)fps; + u8 r = (ifps <= 30) ? 255 : (ifps <= 60) ? roundf(255 * (1.0f - (ifps - 30.0f) / 30.0f)) : 0; + u8 g = (ifps <= 30) ? roundf(255 * (ifps - 1.0f) / 29.0f) : 255; + u8 b = (ifps > 60 && ifps <= 90) ? roundf(255 * (ifps - 60.0f) / 30.0f) : (ifps > 90) ? 255 : 0; + u8 a = 160; + u32 color = r << 24 | g << 16 | b << 8 | a; + char buffer[16]; + + 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. @@ -1745,6 +1777,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 c95acb89e..59eec1160 100644 --- a/src/game/menuitem.c +++ b/src/game/menuitem.c @@ -44,6 +44,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) { @@ -2171,7 +2173,7 @@ Gfx* menuitemColorBoxRender(Gfx *gdl, struct menurendercontext *context) return gdl; } -#endif +#endif Gfx *menuitemSelectableRender(Gfx *gdl, struct menurendercontext *context) { @@ -2332,8 +2334,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; } @@ -2472,9 +2481,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; } @@ -2484,8 +2498,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; } @@ -2565,11 +2588,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 { @@ -2609,7 +2642,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 777fbed56..110e88f5b 100644 --- a/src/include/constants.h +++ b/src/include/constants.h @@ -1666,6 +1666,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 @@ -1696,7 +1697,7 @@ #ifndef PLATFORM_N64 #define MENUITEMTYPE_COLORBOX 0x1b -#endif +#endif #define MENUMODELFLAG_HASSCALE 0x01 #define MENUMODELFLAG_HASPOSITION 0x02 diff --git a/src/include/data.h b/src/include/data.h index 24c106292..6a5148ca0 100644 --- a/src/include/data.h +++ b/src/include/data.h @@ -550,6 +550,7 @@ extern s32 g_BlurFb; extern s32 g_BlurFbCapTimer; extern bool g_BlurFbDirty; extern s32 g_TickRateDiv; +extern s32 g_TickRateDivOverride; extern s32 g_TickExtraSleep; extern s32 g_MusicDisableMpDeath; extern s32 g_BgunGeMuzzleFlashes;