diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml index b66f0660..d8aa1c70 100644 --- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml +++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml @@ -14,13 +14,15 @@ - - - - - - - + + + + + + + + + @@ -37,20 +39,38 @@ - - - + + + - + - + + + + + + + + + + + + + + + + + + + - + @@ -59,7 +79,7 @@ - + @@ -68,7 +88,7 @@ - + @@ -77,7 +97,7 @@ - + @@ -86,7 +106,7 @@ - + @@ -95,7 +115,7 @@ - + @@ -112,7 +132,9 @@ + + @@ -122,8 +144,10 @@ - - + + + + diff --git a/port/linux/README.md b/port/linux/README.md index da997a9b..4c6f29d2 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -220,16 +220,19 @@ the setting for one start of the game. It has priority over the file. | Setting | Default | Environment variable | Function | | --- | --- | --- | --- | -| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at its desktop resolution. `"borderless"`: a window over the whole desktop. Both draw at the resolution of the display, the picture 480 lines of the game and the width of the display. `"windowed"`: a window with the 640x480 picture of the Xbox, scaled. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. | -| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: fullscreen at the resolution of the display. The picture has 480 lines of the game and the width of the display. `false`: a window with the 640x480 picture of the Xbox. Used when `display.mode` is empty. | -| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | The size of the window, as a multiple of 640x480. You can change the size of the window. | +| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at the mode of `display.resolution` (the nearest the display has), or at its desktop mode. `"borderless"`: a window over the whole desktop, whose mode does not change. `"windowed"`: a window of `display.window_size`. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. | +| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: borderless, as `display.mode = "borderless"`. `false`: a window, as `display.mode = "windowed"`. Used when `display.mode` is empty. | +| `display.resolution` | `"native"` | `HALO_RESOLUTION` | What fullscreen and borderless draw at: `"native"`, the display's own resolution, or `"x"`, such as `"1920x1080"`, 640x480 or more. Fullscreen sets the display to it. Borderless draws at it and scales the picture to the display, where the display has room for it. The picture has 480 lines of the game and the width of the resolution's shape. A window draws at its own size instead. Video Setup's Resolution sets it, from the display's modes; it shows with Fullscreen and Borderless. | +| `display.resolution_scaling` | `"native"` | `HALO_RESOLUTION_SCALING` | `"native"`: the game draws at the resolution of the window, or of the display (or `display.resolution`) fullscreen. `"original"`: the game draws the 640x480 picture of the Xbox and scales it up to the window or the display, whatever `display.mode` is. Video Setup sets it. | +| `display.window_size` | `""` | `HALO_WINDOW_SIZE` | The size of the window, as `"x"`, such as `"1920x1080"`, 640x480 or more. You can change the size of the window; the game's picture takes its shape. Empty: `display.window_scale` decides. Video Setup's Window Size sets it, from sizes of each shape (4:3, 16:10, 16:9 and 21:9) that fit the desktop; it shows with Windowed. | +| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | Used when `display.window_size` is empty: the size of the window, as a multiple of 640x480. | | `display.vsync` | `true` | `HALO_NO_VSYNC=1` sets `false` | `true`: each frame waits for the display. | | `display.max_fps` | `0` | `HALO_MAX_FPS` | With vsync off, the most frames each second. `0`: twice the display's refresh rate. `-1`: no limit, which can hang some Intel graphics (Raptor Lake), resetting the desktop's graphics too. | | `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. | | `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". | | `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". | | `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. | -| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the display's resolution, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. | +| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. | | `display.menus` | `"pc"` | `HALO_MENUS` | `"pc"`: the PC version's menus, from the files in `port/assets/menus` and a `menus` folder next to `config.toml`. Refer to "Menus". `"xbox"`: the Xbox's menus. | | `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only within the motion sensor's reach, while the enemy is in sight and not camouflaged, so it never shows where an enemy hides. The gametype's motion tracker setting also applies: no names if it shows no players, only allies' if it shows only friends. | | `display.player_name_scale` | `1.0` | `HALO_PLAYER_NAME_SCALE` | How large the players' names are drawn: `1.0` is three quarters of the size of the HUD's text, from `0.25` to `4`. With high-res text, larger names are rasterized at their size, so they stay sharp. | @@ -615,7 +618,7 @@ The game sends the activity only to a Discord client of the same user. | Game code | All 466 C files of the game. The changes are in "Game source changes". | | Graphics | Direct3D 8 on OpenGL 4.5 core through SDL3 (`src/d3d8_gl.c`). The port translates the NV2A vertex shaders and register combiners to GLSL. It decodes all the Xbox texture formats. The vertex and index buffers come from a GL copy of the Xbox memory. | | High-res HUD | The HUD is drawn from high-res assets: redraws at 8x the size of the maps' bitmaps (4x for the largest), in `port/assets/hud`. They cover the meters, counters, panels and their outlines, the motion sensor, reticles, waypoints and scopes, but no bitmap with English text. `tools/hud_assets.py` makes them from the SVG redraws, and the build embeds them in the executable. When the game uploads one of those bitmaps, `src/hud_hires.c` gives the high-res texture in its place, if the bitmap's pixels are those of the English maps: another language's maps keep their own. The game sizes and places the HUD from its tags as before. `display.high_res_hud = false` turns this off. | -| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the display's resolution, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. | +| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the resolution the game draws at, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. | | Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz, with volume, pitch, mix bins, distance, stereo pan, occlusion and obstruction. There is no Doppler effect, no cones and no reverb. | | Input | XInput on SDL3 (`src/xinput_sdl.c`): keyboard, mouse, gamepads with rumble, and the debug keyboard for the console. | | Files | The Win32 file functions and the MSVC file functions on POSIX, with the translation of Xbox paths. | @@ -705,7 +708,7 @@ Other changes: | `input/input_abstraction.c`, `game/player_control.c`, `game/players.c`, `game/player_queues_new.c`, `units/units.h` | The keyboard and mouse's actions (`src/xinput_sdl.c`, `include/halo_keyboard.h`) join controller 1's game controls; their reload key reloads on its own, and their action key only acts (a control flag of the port's, sent with the player's action, stops the reload the controller's X falls back to). | | `sound/sound_manager.c`, `interface/hud.c`, `game/game_engine.c` | The music's and the other sounds' volumes; the HUD's and the scoreboard's settings are read again when Settings changes them. | | `interface/hud.c` | In multiplayer, players' names are drawn above their heads (`display.player_names`, `display.player_name_scale`). | -| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the display's resolution (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. | +| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the resolution the game draws at (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. | The x86 inline assembly of the game is replaced by C. Thus the compiler can optimize that code for each processor: diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c index de267f17..49601bcf 100644 --- a/port/linux/game/menu_functions.c +++ b/port/linux/game/menu_functions.c @@ -25,7 +25,8 @@ calls them from PC_MENU_FUNCTION_BASE on): - the settings screens' (tools/port_settings.py): "port settings save" (OK) writes those changed and applies the window's (the rest follow config.toml themselves), "port settings defaults" shows the defaults, - "port settings help" the help of the row chosen; + "port settings help" the help of the row chosen (and Video Setup's + Resolution or Window Size, for the display mode shown); - Controls Setup's: the keyboard and mouse's controls, two bindings each, shown a group at a time ("controls update menu"); "controls begin binding" takes the next key or button pressed for the binding left and @@ -314,6 +315,22 @@ static boolean setting_text(char const *name, char *text, unsigned int size, boo sdl_platform.c) */ if (!strcmp(name, "display.mode") && !text[0]) snprintf(text, size, "%s", default_value || config_boolean("display.fullscreen") ? "borderless" : "windowed"); + /* (display.window_size empty: 640x480 times display.window_scale, as + older versions set it) */ + if (!strcmp(name, "display.window_size") && !text[0]) + { + char scale_text[32] = ""; + long scale; + + if (default_value) + config_default("display.window_scale", scale_text, sizeof(scale_text)); + else + config_text("display.window_scale", scale_text, sizeof(scale_text)); + scale = atol(scale_text); + if (scale < 1) + scale = 1; + snprintf(text, size, "%ldx%ld", 640 * scale, 480 * scale); + } return TRUE; } for (index = 0; index < NUMBEROF(profile_settings); index++) @@ -1137,6 +1154,37 @@ static void settings_help(struct widget_instance *list) help->parameters.text_box.string_list_index = index; } +/* Video Setup's Resolution and Window Size, in the one row's place +(tools/port_settings.py): the one the display mode shown uses (Window Size +the window's, Resolution fullscreen's and borderless's) is shown, and the +list passes over the other, which takes no focus while it is hidden */ +static void video_rows_show(struct widget_instance *list) +{ + struct widget_instance *mode = named(list, "mode_spinner", 0); + struct widget_instance *resolution = named(list, "op_resolution", 0); + struct widget_instance *window_size = named(list, "op_window_size", 0); + struct pc_menu_setting *setting = mode ? pc_menu_setting_get(mode->definition_tag_index) : NULL; + struct widget_instance *shown, *child; + short index; + + if (!setting || !resolution || !window_size) + return; + index = mode->parameters.list.selected_index; + shown = index >= 0 && index < setting->value_count && !strcmp(setting->values[index], "windowed") ? + window_size : resolution; + resolution->visible = shown == resolution; + window_size->visible = shown == window_size; + /* (the focus on the one hidden goes to the one shown: Defaults can + change the mode while it has it) */ + if (list->focused_child == (shown == resolution ? window_size : resolution)) + { + for (index = 0, child = list->child; child && child != shown; child = child->next) + index++; + list->focused_child = shown; + list->parameters.list.selected_index = index; + } +} + /* ---------- Change Color: the profile's colour, from a list of the game's colours (more than its rows: it scrolls), by the Xbox's names for its spinner's functions */ @@ -4564,7 +4612,10 @@ void pc_menu_game_data_function_invoke( else if (!strcmp(name, "port lobby update")) lobby_update(widget); else if (!strcmp(name, "port settings help")) + { + video_rows_show(widget); settings_help(widget); + } else if (!strcmp(name, "controls update menu")) controls_update(widget); else if (!strcmp(name, "color picker update")) diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c index 4d3e69e4..8617d1bc 100644 --- a/port/linux/game/menu_tags.c +++ b/port/linux/game/menu_tags.c @@ -44,6 +44,8 @@ no screen pauses the game (a network game's clock runs on). /* the platform layer's (port/linux/src) */ void platform_log(char const *format, ...); char const *config_string(char const *name); +int platform_display_resolutions(long *widths, long *heights, int maximum); +int platform_window_sizes(long *widths, long *heights, int maximum); /* the game's network server (this machine hosts: network_game_globals.c) */ void *global_network_game_server_get(void); @@ -700,6 +702,41 @@ static long split(char const *text, char const **pieces) return count; } +/* a spinner's strings, or its values, split: its own, but Video Setup's +sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set +"1920x1080": its resolutions after Resolution's own (NATIVE), and the +window sizes that fit it in place of Window Size's own */ +static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces) +{ + long count = split(values ? widget->values : widget->strings, pieces); + long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS]; + long added, index; + + if (widget->setting && !strcmp(widget->setting, "display.resolution")) + added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count)); + else if (widget->setting && !strcmp(widget->setting, "display.window_size")) + { + added = platform_window_sizes(widths, heights, MAXIMUM_STRINGS); + if (added > 0) + count = 0; + } + else + return count; + for (index = 0; index < added; index++) + { + char text[32]; + char *piece; + + snprintf(text, sizeof(text), values ? "%ldx%ld" : "%ld x %ld", widths[index], heights[index]); + piece = allocate((long)strlen(text) + 1); + if (!piece) + break; + strcpy(piece, text); + pieces[count++] = piece; + } + return count; +} + static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index) { struct bitmap_group *group = allocate(sizeof(struct bitmap_group)); @@ -914,7 +951,7 @@ static void setting_add(struct halo_menu_widget const *source, long definition_i setting->definition_index = definition_index; setting->setting = source->setting; setting->loaded_index = NONE; - setting->value_count = split(source->values, setting->values); + setting->value_count = spinner_split(source, TRUE, setting->values); } /* a font: large, small, terminal, or the map's by its path */ @@ -1639,7 +1676,7 @@ void menu_tags_loaded( if (widget->strings) { char const *pieces[MAXIMUM_STRINGS]; - long count = split(widget->strings, pieces); + long count = spinner_split(widget, FALSE, pieces); instance_set(instances, UNICODE_STRING_LIST_TAG, build.spinner_tags[index], widget->name, " strings", string_list_build(pieces, count)); diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c index c3b92a04..41f11475 100644 --- a/port/linux/src/d3d8_gl.c +++ b/port/linux/src/d3d8_gl.c @@ -60,17 +60,19 @@ struct xgpu_capabilities xgpu_capabilities; The Xbox screen is 640x480. The native ports can draw a wider one: 480 lines, and as many columns as the display's shape gives. On Android that is display.screen_width (port_config.c; 640 keeps 4:3); on the desktop, the -display's shape while the game is fullscreen, and 640 in a window. The -game's camera derives its horizontal field of view from the viewport, so the -3D view simply widens. The menus and full-screen overlays are laid out for -640 columns; while they draw (halo_screen_ui_offset), everything shifts right -to center them. +shape of the window (of the display while the game is fullscreen, or of +display.resolution), and 640 where display.resolution_scaling is "original". +The game's camera derives its horizontal field of view from the viewport, so +the 3D view simply widens. The menus and full-screen overlays are laid out +for 640 columns; while they draw (halo_screen_ui_offset), everything shifts +right to center them. -Fullscreen on the desktop also draws at the display's resolution: render +The desktop also draws at that resolution (platform_screen_mode): render targets the size of the screen get that many pixels (screen_scale), and viewports, clears and visibility counts are scaled to match, so the game -still works in its 480 lines. The width and the scale change only between -frames, after one is presented (halo_screen_commit). */ +still works in its 480 lines; "original" draws 640x480, scaled up at +presentation. The width and the scale change only between frames, after one +is presented (halo_screen_commit). */ #define SCREEN_HEIGHT 480 #define SCREEN_MAXIMUM_WIDTH 1920 diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 419a08e9..96dba105 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -66,15 +66,28 @@ struct config_setting static const struct config_setting config_settings[] = { { "display.fullscreen", _config_boolean, "true", "HALO_FULLSCREEN", _environment_value, _platform_desktop, - "Start fullscreen, drawing at the display's resolution and shape; false\n" - "starts in a window, which draws the Xbox's 640x480. F11 switches." }, + "Where display.mode is empty: start borderless over the whole display;\n" + "false starts in a window. F11 switches." }, { "display.mode", _config_string, "\"\"", "HALO_DISPLAY_MODE", _environment_value, _platform_desktop, - "\"fullscreen\" takes the display at its desktop resolution, \"borderless\"\n" - "is a window over the whole desktop (both draw at the display's\n" - "resolution), \"windowed\" a window of the Xbox's 640x480, scaled. Empty:\n" - "display.fullscreen's (true: borderless). F11 switches to the window and back." }, + "\"fullscreen\" takes the display (at display.resolution's mode),\n" + "\"borderless\" is a window over the whole desktop, \"windowed\" a window\n" + "(display.window_size). Empty: display.fullscreen's (true: borderless).\n" + "F11 switches to the window and back." }, + { "display.resolution", _config_string, "\"native\"", "HALO_RESOLUTION", _environment_value, _platform_desktop, + "What fullscreen and borderless draw at: \"native\", the display's own, or\n" + "\"x\" (\"1920x1080\"), 640x480 or more. Fullscreen sets the\n" + "display to it; borderless draws it scaled to the display." }, + { "display.resolution_scaling", _config_string, "\"native\"", "HALO_RESOLUTION_SCALING", _environment_value, + _platform_desktop, + "\"native\" draws at the window's resolution (fullscreen, the display's or\n" + "display.resolution); \"original\" draws the Xbox's 640x480 and scales it\n" + "up." }, + { "display.window_size", _config_string, "\"\"", "HALO_WINDOW_SIZE", _environment_value, _platform_desktop, + "The window's size, \"x\" (\"1920x1080\"), 640x480 or more (it\n" + "can be resized). Empty: display.window_scale's." }, { "display.window_scale", _config_integer, "2", "HALO_WINDOW_SCALE", _environment_value, _platform_desktop, - "The window's size as a multiple of 640x480 (it can be resized)." }, + "Where display.window_size is empty: the window's size as a multiple of\n" + "640x480." }, { "display.screen_width", _config_integer, "0", "HALO_SCREEN_WIDTH", _environment_value, _platform_android, "Columns of the 480-line picture: 0 for the display's shape, 640 for the\n" "Xbox's 4:3." }, @@ -96,8 +109,8 @@ static const struct config_setting config_settings[] = "false draws the maps' own bitmaps." }, { "display.high_res_text", _config_boolean, "true", "HALO_HIGH_RES_TEXT", _environment_value, _platform_all, "Draw the menus' and HUD's text with the fonts in port/assets/fonts\n" - "(Overpass) at the display's resolution, and the menus' titles from\n" - "port/assets/titles; false draws the maps' bitmap fonts and titles." }, + "(Overpass) at the resolution the game draws at, and the menus' titles\n" + "from port/assets/titles; false draws the maps' bitmap fonts and titles." }, { "display.menus", _config_string, "\"pc\"", "HALO_MENUS", _environment_value, _platform_all, "The menus: \"pc\" for the PC version's main menu (port/assets/menus,\n" "and a menus folder here for your own), \"xbox\" for the Xbox's." }, diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c index 56bf2212..58130ee4 100644 --- a/port/linux/src/sdl_platform.c +++ b/port/linux/src/sdl_platform.c @@ -378,22 +378,82 @@ static BOOL platform_fullscreen_setting(void) return !config_boolean("debug.hidden_window") && platform_display_mode() != _display_mode_windowed; } +/* a size as a setting has it, "x": whether it is one, and +the Xbox's 640x480 or more */ +static BOOL platform_size_parse(const char *text, long *width, long *height) +{ + char *end; + + *width = strtol(text, &end, 10); + *height = *end == 'x' || *end == 'X' ? strtol(end + 1, &end, 10) : 0; + return !*end && *width >= 640 && *height >= 480; +} + +/* display.resolution in pixels, or 0x0 for the display's own ("native"), +as for one the game cannot draw at */ +static void platform_resolution_setting(long *width, long *height) +{ + if (!platform_size_parse(config_string("display.resolution"), width, height)) + *width = *height = 0; +} + +/* the window's size (display.window_size), else the Xbox's 640x480 times +display.window_scale, as older versions set it */ +static void platform_window_size_setting(long *width, long *height) +{ + long scale = config_integer("display.window_scale"); + + if (!platform_size_parse(config_string("display.window_size"), width, height)) + { + *width = 640 * (scale < 1 ? 1 : scale); + *height = 480 * (scale < 1 ? 1 : scale); + } +} + +/* a display's own size in pixels (its desktop mode) */ +static BOOL platform_display_size(SDL_DisplayID display, long *width, long *height) +{ + const SDL_DisplayMode *mode = display ? SDL_GetDesktopDisplayMode(display) : NULL; + + if (!mode) + return FALSE; + *width = (long)(mode->w * mode->pixel_density + 0.5f); + *height = (long)(mode->h * mode->pixel_density + 0.5f); + return TRUE; +} + /* the window's fullscreen kind (display.mode): borderless, a window over the whole desktop (SDL's fullscreen without a mode), or fullscreen, the -display taken at its desktop resolution. Either draws at the display's -resolution (platform_screen_mode); F11 switches to the kind set. */ +display taken at display.resolution's mode (the one nearest it), else at its +desktop one. F11 switches to the kind set. */ static void platform_fullscreen_kind_apply(void) { static int applied = -1; + static long applied_width, applied_height; int exclusive = platform_display_mode() == _display_mode_fullscreen ? 1 : 0; + long width = 0, height = 0; SDL_DisplayID display; + SDL_DisplayMode closest; + const SDL_DisplayMode *mode = NULL; - if (!platform_window || exclusive == applied) + if (!platform_window) + return; + if (exclusive) + platform_resolution_setting(&width, &height); + if (exclusive == applied && width == applied_width && height == applied_height) return; applied = exclusive; + applied_width = width; + applied_height = height; display = SDL_GetDisplayForWindow(platform_window); - SDL_SetWindowFullscreenMode(platform_window, - exclusive && display ? SDL_GetDesktopDisplayMode(display) : NULL); + if (exclusive && display) + { + if (width && SDL_GetClosestFullscreenDisplayMode(display, (int)width, (int)height, 0.0f, false, &closest)) + mode = &closest; + else + mode = SDL_GetDesktopDisplayMode(display); + } + SDL_SetWindowFullscreenMode(platform_window, mode); } /* whether the game last asked for the window to be fullscreen */ @@ -428,44 +488,197 @@ static BOOL platform_window_fullscreen(void) height >= (int)(mode->h * mode->pixel_density + 0.5f); } -/* whether the game is, or is to be, fullscreen, and if so the size in -pixels of the display it fills (d3d8_gl.c draws at that resolution) */ +/* the size in pixels the game draws its picture at (d3d8_gl.c): the +window's, the display's while fullscreen, or display.resolution's while +fullscreen (either kind) where the display has room for it; before the +window opens, what it will be. FALSE where display.resolution_scaling is +"original": the Xbox's 640x480, scaled to the window. */ BOOL platform_screen_mode(long *width, long *height) { - SDL_DisplayID display; - const SDL_DisplayMode *mode; + /* (the size last given, for a window that has none: minimized) */ + static long last_width, last_height; + long resolution_width, resolution_height; + BOOL fullscreen; - if (platform_window ? !platform_window_fullscreen() : - !platform_fullscreen_setting() || !platform_sdl_initialize()) + if (!strcmp(config_string("display.resolution_scaling"), "original")) + return FALSE; + if (platform_window) { - return FALSE; + int pixel_width = 0, pixel_height = 0; + + SDL_GetWindowSizeInPixels(platform_window, &pixel_width, &pixel_height); + if (pixel_width <= 0 || pixel_height <= 0) + { + *width = last_width; + *height = last_height; + return last_width > 0; + } + *width = pixel_width; + *height = pixel_height; + fullscreen = platform_window_fullscreen(); } - display = platform_window ? SDL_GetDisplayForWindow(platform_window) : SDL_GetPrimaryDisplay(); - mode = display ? SDL_GetDesktopDisplayMode(display) : NULL; - if (!mode) - return FALSE; - *width = (long)(mode->w * mode->pixel_density + 0.5f); - *height = (long)(mode->h * mode->pixel_density + 0.5f); + else + { + if (!platform_sdl_initialize()) + return FALSE; + fullscreen = platform_fullscreen_setting(); + platform_window_size_setting(width, height); + if (fullscreen && !platform_display_size(SDL_GetPrimaryDisplay(), width, height)) + return FALSE; + } + /* (fullscreen's display is at the resolution already, where it has that + mode: platform_fullscreen_kind_apply; borderless's is scaled to) */ + platform_resolution_setting(&resolution_width, &resolution_height); + if (fullscreen && resolution_width && resolution_width <= *width && resolution_height <= *height) + { + *width = resolution_width; + *height = resolution_height; + } + last_width = *width; + last_height = *height; return TRUE; } +/* the size added to the list unless it has it already; the count */ +static int platform_resolution_add(long *widths, long *heights, int count, int maximum, long width, long height) +{ + int index; + + for (index = 0; index < count; index++) + { + if (widths[index] == width && heights[index] == height) + return count; + } + if (count < maximum) + { + widths[count] = width; + heights[count] = height; + count++; + } + return count; +} + +int platform_display_resolutions(long *widths, long *heights, int maximum) +{ + SDL_DisplayID display; + SDL_DisplayMode **modes; + long display_width, display_height, width, height; + int mode_count = 0, count = 0, index; + + if (maximum < 1 || !platform_sdl_initialize()) + return 0; + display = platform_window ? SDL_GetDisplayForWindow(platform_window) : 0; + if (!display) + display = SDL_GetPrimaryDisplay(); + if (!platform_display_size(display, &display_width, &display_height)) + return 0; + modes = SDL_GetFullscreenDisplayModes(display, &mode_count); + for (index = 0; modes && index < mode_count; index++) + { + width = (long)(modes[index]->w * modes[index]->pixel_density + 0.5f); + height = (long)(modes[index]->h * modes[index]->pixel_density + 0.5f); + if (width >= 640 && height >= 480 && width <= display_width && height <= display_height && + (width != display_width || height != display_height)) + { + count = platform_resolution_add(widths, heights, count, maximum, width, height); + } + } + SDL_free(modes); + /* (the one set, though this display has no such mode, so that Video + Setup shows it) */ + platform_resolution_setting(&width, &height); + if (width && (width != display_width || height != display_height)) + count = platform_resolution_add(widths, heights, count, maximum, width, height); + /* largest first */ + for (index = 1; index < count; index++) + { + int place; + + width = widths[index]; + height = heights[index]; + for (place = index; place > 0 && (widths[place - 1] < width || + (widths[place - 1] == width && heights[place - 1] < height)); place--) + { + widths[place] = widths[place - 1]; + heights[place] = heights[place - 1]; + } + widths[place] = width; + heights[place] = height; + } + return count; +} + +/* Video Setup's window sizes, by shape (4:3, 16:10, 16:9, 21:9), each from +the smallest */ +static const short platform_window_sizes_offered[][2] = +{ + { 640, 480 }, { 800, 600 }, { 1024, 768 }, { 1280, 960 }, { 1600, 1200 }, { 1920, 1440 }, { 2560, 1920 }, + { 1280, 800 }, { 1440, 900 }, { 1680, 1050 }, { 1920, 1200 }, { 2560, 1600 }, + { 1280, 720 }, { 1600, 900 }, { 1920, 1080 }, { 2560, 1440 }, { 3840, 2160 }, + { 2560, 1080 }, { 3440, 1440 }, { 3840, 1600 }, { 5120, 2160 }, +}; + +int platform_window_sizes(long *widths, long *heights, int maximum) +{ + SDL_DisplayID display; + SDL_Rect usable; + long width, height; + int count = 0, index; + + if (maximum < 1 || !platform_sdl_initialize()) + return 0; + display = platform_window ? SDL_GetDisplayForWindow(platform_window) : 0; + if (!display) + display = SDL_GetPrimaryDisplay(); + if (!display || !SDL_GetDisplayUsableBounds(display, &usable)) + usable.w = usable.h = 0; + for (index = 0; index < (int)(sizeof(platform_window_sizes_offered) / sizeof(*platform_window_sizes_offered)); + index++) + { + width = platform_window_sizes_offered[index][0]; + height = platform_window_sizes_offered[index][1]; + /* (the Xbox's own whatever the desktop's size) */ + if (!index || (width <= usable.w && height <= usable.h)) + count = platform_resolution_add(widths, heights, count, maximum, width, height); + } + /* (the one set, though it is none of them or too big for this desktop, + so that Video Setup shows it) */ + platform_window_size_setting(&width, &height); + return platform_resolution_add(widths, heights, count, maximum, width, height); +} + +#else +int platform_display_resolutions(long *widths, long *heights, int maximum) +{ + (void)widths; + (void)heights; + (void)maximum; + return 0; +} + +int platform_window_sizes(long *widths, long *heights, int maximum) +{ + (void)widths; + (void)heights; + (void)maximum; + return 0; +} + +#endif +#ifndef HALO_ANDROID +/* the window's size (platform_window_size_setting), as the window was made +or last resized: platform_display_apply */ +static long platform_window_width = -1, platform_window_height = -1; #endif -/* the window's scale (display.window_scale, as the window was made or last -resized: platform_display_apply) */ -static long platform_window_scale = -1; BOOL platform_video_initialize(unsigned long width, unsigned long height) { - int scale = (int)config_integer("display.window_scale"); int version; if (platform_window) return TRUE; if (!platform_sdl_initialize()) return FALSE; - if (scale < 1) - scale = 1; - platform_window_scale = scale; #ifdef HALO_ANDROID SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); @@ -490,14 +703,23 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) #endif #ifdef HALO_ANDROID - platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale), - SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN); + { + int scale = (int)config_integer("display.window_scale"); + + if (scale < 1) + scale = 1; + platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale), + SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN); + } #else - /* fullscreen at the desktop's resolution unless display.fullscreen is - false, where the game draws the display's shape at its resolution - (d3d8_gl.c); the window size is the windowed mode F11 switches to and - from, where it draws 640x480 */ - platform_window = SDL_CreateWindow("Halo", (int)(width * scale), (int)(height * scale), + /* fullscreen (either kind) unless display.mode is the window, which F11 + switches to and from: display.window_size, whatever shape the fullscreen + picture has. The game draws at the size platform_screen_mode gives + (d3d8_gl.c). */ + (void)width; + (void)height; + platform_window_size_setting(&platform_window_width, &platform_window_height); + platform_window = SDL_CreateWindow("Halo", (int)platform_window_width, (int)platform_window_height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | (config_boolean("debug.hidden_window") ? SDL_WINDOW_HIDDEN : 0) | (platform_fullscreen_setting() ? SDL_WINDOW_FULLSCREEN : 0)); @@ -539,23 +761,28 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height) return TRUE; } -/* display.fullscreen, display.window_scale (when it changes: the window can -be resized) and display.vsync, as Settings has written them */ +/* display.mode, display.resolution (fullscreen's display mode), +display.window_size (when it changes: the window can be resized) and +display.vsync, as Settings has written them; display.resolution_scaling +and borderless's resolution are taken up between frames +(halo_screen_commit) */ void platform_display_apply(void) { #ifndef HALO_ANDROID BOOL fullscreen = platform_fullscreen_setting(); - long scale = config_integer("display.window_scale"); + long width, height; if (!platform_window) return; platform_fullscreen_kind_apply(); if (platform_window_fullscreen() != (fullscreen != FALSE)) platform_window_set_fullscreen(fullscreen); - if (scale != platform_window_scale && scale >= 1) + platform_window_size_setting(&width, &height); + if (width != platform_window_width || height != platform_window_height) { - platform_window_scale = scale; - SDL_SetWindowSize(platform_window, (int)(640 * scale), (int)(480 * scale)); + platform_window_width = width; + platform_window_height = height; + SDL_SetWindowSize(platform_window, (int)width, (int)height); } #else if (!platform_window) diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h index 6ecd3b36..1c4978be 100644 --- a/port/linux/src/sdl_platform.h +++ b/port/linux/src/sdl_platform.h @@ -62,6 +62,15 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height); #ifndef HALO_ANDROID BOOL platform_screen_mode(long *width, long *height); #endif +/* Video Setup's resolutions (port/linux/game/menu_tags.c), in pixels: the +display's modes the game can draw at, largest first, each once, but the +display's own (Native), and display.resolution's where it is none of them; +how many. None on Android. */ +int platform_display_resolutions(long *widths, long *heights, int maximum); +/* Video Setup's window sizes: those of each shape (4:3, 16:10, 16:9, 21:9) +that fit the desktop, and display.window_size's; how many. None on +Android. */ +int platform_window_sizes(long *widths, long *heights, int maximum); void platform_video_drawable_size(int *width, int *height); /* the window's mode and size and V-Sync, from config.toml as Settings has just written it (the main thread's) */ diff --git a/tools/port_settings.py b/tools/port_settings.py index 36cfe890..d79da606 100644 --- a/tools/port_settings.py +++ b/tools/port_settings.py @@ -29,14 +29,25 @@ SCREENS = { "screen": "video_settings_screen", "header": ("header_profile_video_settings", f"{PE}/video_settings/header_profile_video_settings"), "spacing": 26, + # (rows in the place of the row before them: Window Size in + # Resolution's, port/linux/game/menu_functions.c showing the one the + # display mode chosen uses) + "same_place": ["display.window_size"], "rows": [ ("DISPLAY MODE:", "display.mode", [("FULLSCREEN", "fullscreen"), ("BORDERLESS", "borderless"), ("WINDOWED", "windowed")], - "Fullscreen and borderless draw at the display's\nresolution; windowed, 640x480 scaled. F11: window.", + "Fullscreen takes the display; borderless covers it\nwith a window. F11 switches to the window.", "desktop"), - ("WINDOW SIZE:", "display.window_scale", - [("640 x 480", "1"), ("1280 x 960", "2"), ("1920 x 1440", "3"), ("2560 x 1920", "4")], - "The window's size when windowed (its edges can\nalso be dragged).", "desktop"), + # (port/linux/game/menu_tags.c adds the display's resolutions) + ("RESOLUTION:", "display.resolution", [("NATIVE", "native")], + "What fullscreen and borderless draw at. Fullscreen\nsets the display to it; borderless scales it.", + "desktop"), + # (port/linux/game/menu_tags.c puts the sizes that fit the desktop + # in place of this one) + ("WINDOW SIZE:", "display.window_size", [("1280 x 960", "1280x960")], + "The window's size: 4:3, then 16:10, 16:9 and 21:9\n(its edges can also be dragged).", "desktop"), + ("RESOLUTION SCALING:", "display.resolution_scaling", [("NATIVE", "native"), ("ORIGINAL", "original")], + "Native draws at the resolution; Original draws\nthe Xbox's 640x480 and scales it up.", "desktop"), ("V-SYNC:", "display.vsync", ON_OFF, "Wait for the display between frames, so that the\npicture never tears.", None), ("FRAME RATE LIMIT:", "display.max_fps", @@ -191,8 +202,10 @@ def _screen(folder: str, spec: dict, rows: list, list_inputs: list, list_handler ("height", 60), ("string_list", f"{base}/help_strings"), ("font", "ui\\large_ui"), ("color", "#FFFFFFFF")], []) children = [f'' for name in list_inputs] + list_handlers - for index, (row, platform) in enumerate(rows): - children.append(f'') + # (each row: its widget, platform, and the place it is in, else the next) + for index, (row, platform, *place) in enumerate(rows): + y = 73 + (place[0] if place else index) * spec["spacing"] + children.append(f'') children.append(f'') lines += _widget(f"{base}/options_menu", [("type", "column_list"), ("width", 640), ("height", 480), @@ -211,10 +224,13 @@ def _screen(folder: str, spec: dict, rows: list, list_inputs: list, list_handler def _setting_screen(folder: str, spec: dict) -> list: base = f"{PE}/{folder}" rows, extra = [], [] + place = -1 for index, (label, setting, choices, _, platform) in enumerate(spec["rows"]): key = setting.split(".", 1)[1] row = f"{base}/op_{key}" - rows.append((row, platform)) + if setting not in spec.get("same_place", ()): + place += 1 + rows.append((row, platform, place)) extra += _widget(row, [("width", 512), ("height", 28), ("flags", "pass_unhandled_to_focused_child"), ("bitmap", "bitmaps/option_bkds"), ("color", "#FF2896FF"), ("platform", platform)], [f'',