graphics: rewrite auto-rotate and refresh rate options to be more reliable (#607)

## Description of change

Rotate monitor & change the refresh rate **well before** the game is
launched.

This is much faster and more reliable compared to what we do now, which
is trying to do monitor changes during CreateWindow hooks. For example,
SDVX makes a bunch of OS / NVAPI calls to figure out monitor geometry
while it's booting, and by the time CreateWindow is called, the game
already made up its mind, which leads to weird graphical issues.

This affects both windowed mode and full screen.

In addition, add options for normal orientation (landscape) and upside
down (landscape, flipped) options. This can be used if the monitor is
normally rotated to portrait, but the game needs to be in landscape.
This commit is contained in:
bicarus
2026-04-04 03:23:28 -07:00
committed by GitHub
parent e0f6cb2cbd
commit a4c3eddc2f
5 changed files with 133 additions and 102 deletions
@@ -552,18 +552,6 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
modified = true; modified = true;
} }
} }
if (!modified &&
(width > height) &&
(GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ||
GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CCW)) {
log_misc(
"graphics::d3d9", "swapping width and height for mode {}, {}x{} @ {}Hz (-autoorientation)",
Mode, width, height, refresh);
pMode->Height = width;
pMode->Width = height;
modified = true;
}
} }
CHECK_RESULT(ret); CHECK_RESULT(ret);
+116 -80
View File
@@ -301,30 +301,6 @@ static BOOL WINAPI ClipCursor_hook(const RECT *lpRect) {
return ClipCursor_orig(lpRect); return ClipCursor_orig(lpRect);
} }
static void fix_up_rb_auto_rotate_dimensions(DWORD &w, DWORD &h) {
// reflec beat
// for whatever reason, rb creates the first window it weird dimensions (774x1389 for reflesia, for example)
// so doing ChangeDisplaySettings as-is ends up with DISP_CHANGE_BADMODE
// while a second window (with title D3DProxyWindow) does get created with correct dimensions, it's
// too late to rotate the window then; as a workaround force the display change now with patched up dimensions
bool changed = false;
if (w == 774) {
// LBR, MBR reflesia
w = 768;
h = 1360;
changed = true;
} else if (w == 1086) {
// MBR (before reflesia)
w = 1080;
h = 1920;
changed = true;
}
if (changed) {
log_misc("graphics", "fix auto-rotate dimensions for reflec beat: w={}, h={}", w, h);
}
}
static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName, static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight, DWORD dwStyle, int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
@@ -346,62 +322,6 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
fmt::ptr(hInstance), fmt::ptr(hInstance),
fmt::ptr(lpParam)); fmt::ptr(lpParam));
// set display orientation and/or refresh rate
// only set orientation when the target window is portrait
// (avoid doing this for SDVX subscreen which is in landscape, for example)
auto adjust_orientation =
(GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ||
GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CCW);
// reflec beat
if (avs::game::is_model("KBR")) {
// KBR (rb1) launches landscape and draws rotated by itself, don't do any rotation
adjust_orientation = false;
} else if (avs::game::is_model({"LBR", "MBR"}) && window_name == "D3DProxyWindow") {
// do not auto-rotate for second window (D3DProxyWindow)
adjust_orientation = false;
}
if ((nHeight > nWidth && adjust_orientation) || GRAPHICS_FORCE_REFRESH > 0) {
DEVMODE mode {};
// get display settings
if (EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &mode)) {
if (adjust_orientation) {
DWORD orientation = GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ? DMDO_90 : DMDO_270;
log_misc(
"graphics",
"auto-rotate: call ChangeDisplaySettings and rotate display to DMDO_xx mode {}, w={}, h={}",
orientation, nWidth, nHeight);
mode.dmPelsWidth = nWidth;
mode.dmPelsHeight = nHeight;
mode.dmDisplayOrientation = orientation;
// reflec beat
if (avs::game::is_model({"LBR", "MBR"})) {
fix_up_rb_auto_rotate_dimensions(mode.dmPelsWidth, mode.dmPelsHeight);
}
}
if (GRAPHICS_FORCE_REFRESH > 0) {
log_info(
"graphics",
"call ChangeDisplaySettings to force refresh rate: {} => {} Hz (-graphics-force-refresh)",
mode.dmDisplayFrequency,
GRAPHICS_FORCE_REFRESH);
mode.dmDisplayFrequency = GRAPHICS_FORCE_REFRESH;
}
const auto disp_res = ChangeDisplaySettings(&mode, CDS_FULLSCREEN);
if (disp_res != DISP_CHANGE_SUCCESSFUL) {
log_warning("graphics", "ChangeDisplaySettings failed: {}", disp_res);
}
} else {
log_warning("graphics", "failed to get display settings");
}
}
// gfdm // gfdm
if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) { if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) {
// set window name // set window name
@@ -1149,3 +1069,119 @@ std::string graphics_screenshot_genpath() {
id++; id++;
} }
} }
static std::string get_dmdo_string(DWORD dmdo) {
switch (dmdo) {
case DMDO_DEFAULT:
return "DMDO_DEFAULT";
case DMDO_90:
return "DMDO_90";
case DMDO_180:
return "DMDO_180";
case DMDO_270:
return "DMDO_270";
default:
return fmt::format("Unknown ({})", dmdo);
}
}
void update_monitor_on_boot() {
// note: all of this is only being done for the primary motnior
// get current settings
DEVMODEA dm = {};
dm.dmSize = sizeof(dm);
if (!EnumDisplaySettingsExA(NULL, ENUM_CURRENT_SETTINGS, &dm, 0)) {
log_info("graphics", "EnumDisplaySettingsExa failed {}", get_last_error_string());
return;
}
bool needs_update = false;
// convert orientation values, and figure out if resolution needs to be swapped
bool rotate_resolution = false;
DWORD orientation = DMDO_DEFAULT;
switch (GRAPHICS_ADJUST_ORIENTATION) {
case ORIENTATION_CW:
orientation = DMDO_90;
if (dm.dmDisplayOrientation == DMDO_DEFAULT || dm.dmDisplayOrientation == DMDO_180) {
rotate_resolution = true;
}
break;
case ORIENTATION_CCW:
orientation = DMDO_270;
if (dm.dmDisplayOrientation == DMDO_DEFAULT || dm.dmDisplayOrientation == DMDO_180) {
rotate_resolution = true;
}
break;
case ORIENTATION_FLIPPED:
orientation = DMDO_180;
if (dm.dmDisplayOrientation == DMDO_90 || dm.dmDisplayOrientation == DMDO_270) {
rotate_resolution = true;
}
break;
default:
orientation = DMDO_DEFAULT;
if (dm.dmDisplayOrientation == DMDO_90 || dm.dmDisplayOrientation == DMDO_270) {
rotate_resolution = true;
}
break;
}
// update orientation (and resolution if it must be swapped)
if (dm.dmDisplayOrientation != orientation) {
log_misc("graphics",
"current orientation {} => desired orientation {}",
get_dmdo_string(dm.dmDisplayOrientation), get_dmdo_string(orientation));
const DWORD originalWidth = dm.dmPelsWidth;
const DWORD originalHeight = dm.dmPelsHeight;
// change orientation
dm.dmDisplayOrientation = orientation;
dm.dmFields |= DM_DISPLAYORIENTATION;
// rotate resolution
if (rotate_resolution) {
dm.dmPelsWidth = originalHeight;
dm.dmPelsHeight = originalWidth;
dm.dmFields |= DM_PELSHEIGHT | DM_PELSWIDTH;
}
needs_update = true;
}
// update refresh rate
if (GRAPHICS_FORCE_REFRESH > 0) {
log_misc("graphics",
"current refresh rate {} => desired refresh rate {}",
dm.dmDisplayFrequency, GRAPHICS_FORCE_REFRESH);
dm.dmDisplayFrequency = GRAPHICS_FORCE_REFRESH;
dm.dmFields |= DM_DISPLAYFREQUENCY;
needs_update = true;
}
if (!needs_update) {
return;
}
const auto result = ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
if (result != DISP_CHANGE_SUCCESSFUL) {
log_fatal(
"graphics",
"failed to update display settings ({}px x {}px @ {}Hz): {}",
dm.dmPelsWidth,
dm.dmPelsHeight,
dm.dmDisplayFrequency,
get_last_error_string());
} else {
log_info("graphics", "display settings updated successfully ({}px x {}px @ {}Hz)",
dm.dmPelsWidth,
dm.dmPelsHeight,
dm.dmDisplayFrequency);
}
// sleep for a little bit after changing monitor settings to delay game launch
Sleep(1000);
}
+3
View File
@@ -15,6 +15,7 @@ enum graphics_orientation {
ORIENTATION_CW = 0, ORIENTATION_CW = 0,
ORIENTATION_CCW = 1, ORIENTATION_CCW = 1,
ORIENTATION_NORMAL = 2, ORIENTATION_NORMAL = 2,
ORIENTATION_FLIPPED = 3
}; };
enum graphics_dx9on12_state { enum graphics_dx9on12_state {
@@ -107,3 +108,5 @@ bool graphics_window_decoration_change_crashes_game();
bool graphics_window_resize_breaks_game(); bool graphics_window_resize_breaks_game();
bool graphics_window_move_and_resize_breaks_game(); bool graphics_window_move_and_resize_breaks_game();
void graphics_load_windowed_subscreen_parameters(); void graphics_load_windowed_subscreen_parameters();
void update_monitor_on_boot();
+3
View File
@@ -2133,6 +2133,9 @@ int main_implementation(int argc, char *argv[]) {
sysutils::print_gpus(); sysutils::print_gpus();
} }
// fix up monitor
update_monitor_on_boot();
// initialize raw input // initialize raw input
RI_MGR = std::make_unique<rawinput::RawInputManager>(); RI_MGR = std::make_unique<rawinput::RawInputManager>();
for (const auto &device : sextet_devices) { for (const auto &device : sextet_devices) {
+11 -10
View File
@@ -224,11 +224,9 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Graphics (Full Screen)", .category = "Graphics (Full Screen)",
}, },
{ {
.title = "Force Monitor Refresh Rate", .title = "Monitor Refresh Rate",
.name = "graphics-force-refresh", .name = "graphics-force-refresh",
.desc = .desc = "Change the refresh rate for the primary monitor before launching the game. It will be restored on exit.",
"Attempt to change the refresh rate for the primary monitor before the game boots; "
"works in both full screen and windowed modes, but known to fail for some games.",
.type = OptionType::Integer, .type = OptionType::Integer,
.category = "Graphics (Common)", .category = "Graphics (Common)",
}, },
@@ -1847,17 +1845,20 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// spice2x_AutoOrientation // spice2x_AutoOrientation
.title = "Auto-rotate Display", .title = "Rotate Monitor",
.name = "sp2x-autoorientation", .name = "sp2x-autoorientation",
.display_name = "autoorientation", .display_name = "autoorientation",
.aliases= "autoorientation", .aliases= "autoorientation",
.desc = "Automatically adjust the orientation of your display when launched. " .desc = "Change the orientation of the primary display before launching the game. It will be restored on exit",
"WARNING: game may launch at incorrect refresh rate! Use in combination with "
"-graphics-force-refresh and potentially either -9on12 or full-screen optimizations (FSO) to fix.",
.type = OptionType::Enum, .type = OptionType::Enum,
.category = "Graphics (Common)", .category = "Graphics (Common)",
// match graphics_orientation // match graphics_orientation enum
.elements = {{"0", "90 (CW)"}, {"1", "270 (CCW)"}}, .elements = {
{"0", "Portrait"},
{"1", "Portrait, Flipped"},
{"2", "Landscape"},
{"3", "Landscape, Flipped"}
},
}, },
{ {
.title = "AVS Log Level", .title = "AVS Log Level",