Implement high dpi awareness into the client and all renderers.

Over the last years high dpi displays became increasingly popular. We
already implemented very basic high dpi support for Windows several
years ago by setting the "please don't scale us" option. Linux (and
all other unixoid platforms) hadn't a real understandig of high dpi
and everything involved until the advent of Wayland and finally the
*fractional-scale-v1* protocol last autumn.

Since then SDL (even bevore the protocol was finalized) and all three
common Wayland compositors (Gnome, KDE and wlroots) have grown support
for the protocol and are now high dpi aware. In a high dpi aware world
the choice is scale yourself or get scaled by the compositor. The later
option isn't so good for fast paced games like Quake II, it introduces
blur and slugishness. Therefore implement hig dpi awareness through SDL.

This isn't perfect right now:
* SDL is still getting high dpi awareness fixes with every release. High
  dpi awareness in YQ2 is currently limited to at least SDL 2.26 and we
  may rise the required version even more if necessary.
* There are stll bugs in the compositors. For example Gnome 44.1 has a
  tedency to misscalculate the space occupied by the fullscreen window,
  the game ends up wit a white bar on top. sway even misscalculates
  some scaling factors: https://github.com/swaywm/sway/issues/7463
* SDL hasn't got a reliable way to get the real non scales desktop
  resolution.

Because of this:
* High dpi awareness is currently restricted to Wayland. Let's get one
  platform right before we implement it for everything else.
* It's hidden behind `vid_highdpiawareness` and disabled by default.

High dpi awareness is implemented by setting `SDL_WINDOW_ALLOW_HIGHDPI`
on the window. After the window and rendering context are created
`SDL_GL_GetDrawableSize()` or `SDL_GetRendererOutputSize()` are used
to query the actual drawable size and the internal representation is
overwritten with it.

This would scale the fullscreen window over the full screen, no matter
what resolution was selected. Some work arounds are implemented to
(mostly) keep the old behaviour were resoltions lower than the desktop
resolution weren't scaled. There is one inconsistency: While the non
high dpi aware soft renderer always scaled over the full screen, the
high dpi aware variant doesn't. This is a restriction by SDL Renderer.

Setting native fullscreen was broken before when running on high dpi
displays and it's not fixed. This is caused by SDL being unable to
determine the real resolution (or whatever the compositor thinks the
real resolution is). Depending on the compositor or if the client is
high dpi aware the correct resolution must be set by hand **or** auto
setting with `r_mode -2` must be used. Resolution detection was switched
to `SDL_GetCurrentDisplayMode()` because it's somewhat less problematic
than `SDL_GetCDesktopDisplayMode()`.

The renderer API was extended by one function pointer `*GetDrawableSize`
used to communicate the actual drawable size between renderer and
client. The API version was bumped to 6. I'll fix ref_vk before pushing
this change to master.
This commit is contained in:
Yamagi 2023-05-11 19:18:24 +02:00
parent e0dd7c9d00
commit a5560ff3a2
10 changed files with 295 additions and 8 deletions

View file

@ -386,6 +386,16 @@ Set `0` by default.
It's recommended to use the displays native resolution with the
fullscreen window, use `r_mode -2` to switch to it.
* **vid_highdpiaware**: When set to `1` the client is high DPI aware
and scales the window (and thus the requested resolution) by the
scaling factor of the underlying display. Example: The displays
scaling factor is 1.25 and the user requests 1920x1080. The client
will render at 1920\*1.25x1080\*1.25=2400x1350.
When set to `0` (the default) the client leaves the decision if the
window should be scaled to the underlying compositor. Scaling applied
by the compositor may introduce blur and sluggishness.
Currently high dpi awareness is only supported under Wayland.
* **vid_maxfps**: The maximum framerate. *Note* that vsync (`r_vsync`)
also restricts the framerate to the monitor refresh rate, so if vsync
is enabled, the game won't render more than frame than the display can

View file

@ -1281,6 +1281,48 @@ SetMode_impl(int *pwidth, int *pheight, int mode, int fullscreen)
return rserr_invalid_mode;
}
/* This is totaly obscure: For some strange reasons the renderer
maintains two(!) repesentations of the resolution. One comes
from the client and is saved in r_newrefdef. The other one
is determined here and saved in vid. Several calculations take
both representations into account.
The values will always be the same. The GLimp_InitGraphics()
call above communicates the requested resolution to the client
where it ends up in the vid subsystem and the vid system writes
it into r_newrefdef.
We can't avoid the client roundtrip, because we can get the
real size of the drawable (which can differ from the resolution
due to high dpi awareness) only after the render context was
created by GLimp_InitGraphics() and need to communicate it
somehow to the client. So we just overwrite the values saved
in vid with a call to RI_GetDrawableSize(), just like the
client does. This makes sure that both values are the same
and everything is okay.
We also need to take the special case fullscreen window into
account. With the fullscreen windows we cannot use the
drawable size, it would scale all cases to the size of the
window. Instead use the drawable size when the user wants
native resolution (the fullscreen window fills the screen)
and use the requested resolution in all other cases. */
if (IsHighDPIaware)
{
if (vid_fullscreen->value != 2)
{
RI_GetDrawableSize(pwidth, pheight);
}
else
{
if (r_mode->value == -2)
{
/* User requested native resolution. */
RI_GetDrawableSize(pwidth, pheight);
}
}
}
return rserr_ok;
}
@ -1868,6 +1910,7 @@ GetRefAPI(refimport_t imp)
re.Shutdown = RI_Shutdown;
re.PrepareForWindow = RI_PrepareForWindow;
re.InitContext = RI_InitContext;
re.GetDrawableSize = RI_GetDrawableSize;
re.ShutdownContext = RI_ShutdownContext;
re.IsVSyncActive = RI_IsVSyncActive;
re.BeginRegistration = RI_BeginRegistration;

View file

@ -37,6 +37,7 @@
static SDL_Window* window = NULL;
static SDL_GLContext context = NULL;
qboolean IsHighDPIaware = false;
static qboolean vsyncActive = false;
// ----
@ -247,9 +248,23 @@ int RI_InitContext(void* win)
snprintf(title, sizeof(title), "Yamagi Quake II %s - OpenGL 1.4", YQ2VERSION);
SDL_SetWindowTitle(window, title);
#if SDL_VERSION_ATLEAST(2, 26, 0)
// Figure out if we are high dpi aware.
int flags = SDL_GetWindowFlags(win);
IsHighDPIaware = (flags & SDL_WINDOW_ALLOW_HIGHDPI) ? true : false;
#endif
return true;
}
/*
* Fills the actual size of the drawable into width and height.
*/
void RI_GetDrawableSize(int* width, int* height)
{
SDL_GL_GetDrawableSize(window, width, height);
}
/*
* Shuts the GL context down.
*/

View file

@ -161,6 +161,8 @@ extern vec3_t r_origin;
extern refdef_t r_newrefdef;
extern int r_viewcluster, r_viewcluster2, r_oldviewcluster, r_oldviewcluster2;
extern qboolean IsHighDPIaware;
extern cvar_t *r_norefresh;
extern cvar_t *gl_lefthand;
extern cvar_t *r_gunfov;
@ -391,6 +393,11 @@ void RI_ShutdownContext(void);
*/
void *RI_GetProcAddress (const char* proc);
/*
* Fills the actual size of the drawable into width and height.
*/
void RI_GetDrawableSize(int* width, int* height);
/* g11_draw */
extern image_t * RDraw_FindPic(char *name);
extern void RDraw_GetPicSize(int *w, int *h, char *pic);

View file

@ -385,6 +385,48 @@ SetMode_impl(int *pwidth, int *pheight, int mode, int fullscreen)
return rserr_invalid_mode;
}
/* This is totaly obscure: For some strange reasons the renderer
maintains two(!) repesentations of the resolution. One comes
from the client and is saved in gl3_newrefdef. The other one
is determined here and saved in vid. Several calculations take
both representations into account.
The values will always be the same. The GLimp_InitGraphics()
call above communicates the requested resolution to the client
where it ends up in the vid subsystem and the vid system writes
it into gl3_newrefdef.
We can't avoid the client roundtrip, because we can get the
real size of the drawable (which can differ from the resolution
due to high dpi awareness) only after the render context was
created by GLimp_InitGraphics() and need to communicate it
somehow to the client. So we just overwrite the values saved
in vid with a call to GL3_GetDrawableSize(), just like the
client does. This makes sure that both values are the same
and everything is okay.
We also need to take the special case fullscreen window into
account. With the fullscreen windows we cannot use the
drawable size, it would scale all cases to the size of the
window. Instead use the drawable size when the user wants
native resolution (the fullscreen window fills the screen)
and use the requested resolution in all other cases. */
if (IsHighDPIaware)
{
if (vid_fullscreen->value != 2)
{
GL3_GetDrawableSize(pwidth, pheight);
}
else
{
if (r_mode->value == -2)
{
/* User requested native resolution. */
GL3_GetDrawableSize(pwidth, pheight);
}
}
}
return rserr_ok;
}
@ -1960,6 +2002,7 @@ GetRefAPI(refimport_t imp)
re.Shutdown = GL3_Shutdown;
re.PrepareForWindow = GL3_PrepareForWindow;
re.InitContext = GL3_InitContext;
re.GetDrawableSize = GL3_GetDrawableSize;
re.ShutdownContext = GL3_ShutdownContext;
re.IsVSyncActive = GL3_IsVsyncActive;

View file

@ -34,6 +34,7 @@
static SDL_Window* window = NULL;
static SDL_GLContext context = NULL;
static qboolean vsyncActive = false;
qboolean IsHighDPIaware = false;
// --------
@ -402,9 +403,23 @@ int GL3_InitContext(void* win)
#endif
SDL_SetWindowTitle(window, title);
#if SDL_VERSION_ATLEAST(2, 26, 0)
// Figure out if we are high dpi aware.
int flags = SDL_GetWindowFlags(win);
IsHighDPIaware = (flags & SDL_WINDOW_ALLOW_HIGHDPI) ? true : false;
#endif
return true;
}
/*
* Fills the actual size of the drawable into width and height.
*/
void GL3_GetDrawableSize(int* width, int* height)
{
SDL_GL_GetDrawableSize(window, width, height);
}
/*
* Shuts the GL context down.
*/

View file

@ -286,6 +286,8 @@ extern int gl3_viewcluster, gl3_viewcluster2, gl3_oldviewcluster, gl3_oldviewclu
extern int c_brush_polys, c_alias_polys;
extern qboolean IsHighDPIaware;
/* NOTE: struct image_s* is what re.RegisterSkin() etc return so no gl3image_s!
* (I think the client only passes the pointer around and doesn't know the
* definition of this struct, so this being different from struct image_s
@ -385,6 +387,7 @@ extern void GL3_RotateForEntity(entity_t *e);
// gl3_sdl.c
extern int GL3_InitContext(void* win);
extern void GL3_GetDrawableSize(int* width, int* height);
extern int GL3_PrepareForWindow(void);
extern qboolean GL3_IsVsyncActive(void);
extern void GL3_EndFrame(void);

View file

@ -43,6 +43,7 @@ pixel_t *vid_alphamap = NULL;
light_t vid_lightthreshold = 0;
static int vid_minu, vid_minv, vid_maxu, vid_maxv;
static int vid_zminu, vid_zminv, vid_zmaxu, vid_zmaxv;
static qboolean IsHighDPIaware;
// last position on map
static vec3_t lastvieworg;
@ -442,6 +443,7 @@ R_UnRegister (void)
static void RE_ShutdownContext(void);
static void SWimp_CreateRender(int width, int height);
static int RE_InitContext(void *win);
static void RE_GetDrawableSize(int* width, int* height);
static qboolean RE_SetMode(void);
/*
@ -1853,6 +1855,7 @@ GetRefAPI(refimport_t imp)
refexport.IsVSyncActive = RE_IsVsyncActive;
refexport.Shutdown = RE_Shutdown;
refexport.InitContext = RE_InitContext;
refexport.GetDrawableSize = RE_GetDrawableSize;
refexport.ShutdownContext = RE_ShutdownContext;
refexport.PrepareForWindow = RE_PrepareForWindow;
@ -1927,8 +1930,24 @@ RE_InitContext(void *win)
This will show the new, black contents of the window. */
SDL_RenderPresent(renderer);
vid_buffer_height = vid.height;
vid_buffer_width = vid.width;
#if SDL_VERSION_ATLEAST(2, 26, 0)
// Figure out if we are high dpi aware.
int flags = SDL_GetWindowFlags(win);
IsHighDPIaware = (flags & SDL_WINDOW_ALLOW_HIGHDPI) ? true : false;
#endif
/* We can't rely on vid, because the context is created
before we had a chance to overwrite it with the drawable
size. */
if (IsHighDPIaware)
{
RE_GetDrawableSize(&vid_buffer_width, &vid_buffer_height);
}
else
{
vid_buffer_height = vid.height;
vid_buffer_width = vid.width;
}
texture = SDL_CreateTexture(renderer,
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
@ -1945,6 +1964,15 @@ RE_InitContext(void *win)
return true;
}
/*
* Fills the actual size of the drawable into width and height.
*/
void RE_GetDrawableSize(int* width, int* height)
{
SDL_GetRendererOutputSize(renderer, width, height);
}
static void
RE_ShutdownContext(void)
{
@ -2324,6 +2352,48 @@ SWimp_SetMode(int *pwidth, int *pheight, int mode, int fullscreen )
return rserr_invalid_mode;
}
/* This is totaly obscure: For some strange reasons the renderer
maintains three(!) repesentations of the resolution. One comes
from the client and is saved in r_newrefdef. The other one
is determined here and saved in vid. The third one is used by
the backbuffer. Some calculations take all three representations
into account.
The values will always be the same. The GLimp_InitGraphics()
call above communicates the requested resolution to the client
where it ends up in the vid subsystem and the vid system writes
it into r_newrefdef. The backbuffer is derived from vid.
We can't avoid the client roundtrip, because we can get the
real size of the drawable (which can differ from the resolution
due to high dpi awareness) only after the render context was
created by GLimp_InitGraphics() and need to communicate it
somehow to the client. So we just overwrite the values saved
in vid with a call to RE_GetDrawableSize(), just like the
client does. This makes sure that both values are the same
and everything is okay.
We also need to take the special case fullscreen window into
account. With the fullscreen windows we cannot use the
drawable size, it would scale all cases to the size of the
window. Instead use the drawable size when the user wants
native resolution (the fullscreen window fills the screen)
and use the requested resolution in all other cases. */
if (IsHighDPIaware)
{
if (vid_fullscreen->value != 2)
{
RE_GetDrawableSize(pwidth, pheight);
}
else
{
if (r_mode->value == -2)
{
/* User requested native resolution. */
RE_GetDrawableSize(pwidth, pheight);
}
}
}
return retval;
}

View file

@ -37,6 +37,7 @@ int glimp_refreshRate = -1;
static cvar_t *vid_displayrefreshrate;
static cvar_t *vid_displayindex;
static cvar_t *vid_highdpiaware;
static cvar_t *vid_rate;
static int last_flags = 0;
@ -401,6 +402,7 @@ GLimp_Init(void)
{
vid_displayrefreshrate = Cvar_Get("vid_displayrefreshrate", "-1", CVAR_ARCHIVE);
vid_displayindex = Cvar_Get("vid_displayindex", "0", CVAR_ARCHIVE);
vid_highdpiaware = Cvar_Get("vid_highdpiaware", "0", CVAR_ARCHIVE);
vid_rate = Cvar_Get("vid_rate", "-1", CVAR_ARCHIVE);
if (!SDL_WasInit(SDL_INIT_VIDEO))
@ -455,6 +457,35 @@ GLimp_Shutdown(void)
ClearDisplayIndices();
}
/*
* Determine if we want to be high dpi aware. If
* we are we must scale ourself. If we are not the
* compositor might scale us.
*/
static int
Glimp_DetermineHighDPISupport(int flags)
{
#if SDL_VERSION_ATLEAST(2, 26, 0)
/* Make sure that high dpi is never set when we don't want it. */
flags &= ~SDL_WINDOW_ALLOW_HIGHDPI;
if (vid_highdpiaware->value == 0)
{
return flags;
}
/* Handle high dpi awareness based on the render backend.
SDL doesn't support high dpi awareness for all backends
and the quality and behavior differs between them. */
if ((strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0))
{
flags |= SDL_WINDOW_ALLOW_HIGHDPI;
}
return flags;
#endif
}
/*
* (Re)initializes the actual window.
*/
@ -506,10 +537,6 @@ GLimp_InitGraphics(int fullscreen, int *pwidth, int *pheight)
window = NULL;
}
/* We need the window size for the menu, the HUD, etc. */
viddef.width = width;
viddef.height = height;
if(last_flags != -1 && (last_flags & SDL_WINDOW_OPENGL))
{
/* Reset SDL. */
@ -533,6 +560,9 @@ GLimp_InitGraphics(int fullscreen, int *pwidth, int *pheight)
flags |= fs_flag;
}
/* Check for high dpi support. */
flags = Glimp_DetermineHighDPISupport(flags);
/* Mkay, now the hard work. Let's create the window. */
cvar_t *gl_msaa_samples = Cvar_Get("r_msaa_samples", "0", CVAR_ARCHIVE);
@ -618,6 +648,51 @@ GLimp_InitGraphics(int fullscreen, int *pwidth, int *pheight)
return false;
}
/* We need the actual drawable size for things like the
console, the menus, etc. This might be different to
the resolution due to high dpi awareness.
The fullscreen window is special. We want it to fill
the screen when native resolution is requestes, all
other cases should look broken. */
if (flags & SDL_WINDOW_ALLOW_HIGHDPI)
{
if (fullscreen != 2)
{
re.GetDrawableSize(&viddef.width, &viddef.height);
}
else
{
cvar_t *r_mode = Cvar_Get("r_mode", "4", 0);
if (r_mode->value == -2 )
{
re.GetDrawableSize(&viddef.width, &viddef.height);
}
else
{
/* User likes it broken. */
viddef.width = *pwidth;
viddef.height = *pheight;
}
}
}
else
{
/* Another bug or design failure in SDL: When we are
not high dpi aware the drawable size returned by
SDL may be too small. It seems like the window
decoration are taken into account when they shouldn't.
It can be seen when creating a fullscreen window.
Work around that by always using the resolution and
not the drawable size when we are not high dpi aware. */
viddef.width = *pwidth;
viddef.height = *pheight;
}
Com_Printf("Drawable size: %ix%i\n", viddef.width, viddef.height);
/* Set the window icon - For SDL2, this must be done after creating the window */
SetSDLIcon();
@ -731,7 +806,7 @@ GLimp_GetDesktopMode(int *pwidth, int *pheight)
}
// We can't get desktop where we start, so use first desktop
if(SDL_GetDesktopDisplayMode(last_display, &mode) != 0)
if(SDL_GetCurrentDisplayMode(last_display, &mode) != 0)
{
// In case of error...
Com_Printf("Can't detect default desktop mode: %s\n",

View file

@ -125,7 +125,7 @@ typedef enum {
} ref_restart_t;
// FIXME: bump API_VERSION?
#define API_VERSION 5
#define API_VERSION 6
#define EXPORT
#define IMPORT
@ -153,6 +153,12 @@ typedef struct
// returns true (1) on success
int (EXPORT *InitContext)(void* sdl_window);
// called by GLimp_InitGraphics() *after* creating render
// context. Returns the actual drawable size in the width
// and height variables. This may be differend from the
// window size due to high dpi awareness.
void (EXPORT *GetDrawableSize)(int* width, int* height);
// shuts down rendering (OpenGL) context.
void (EXPORT *ShutdownContext)(void);