В первой части были рассмотрены системные настройки масштабирования, предоставляемые встроенными утилитами, и набор возможностей отличается кардинально. Почему? Потому что в Linux нет единого API для работы с масштабированием, каждое окружение конфигурирует по своему и каждый UI-toolkit определяет их по своему, в итоге шанс того, что что-то где-то будет отображаться криво очень велик.

В этой статье мы рассмотрим то, как определить коэффициенты масштабирования для X11 приложения самостоятельно.

Вариант 1

Одна из наиболее известных и часто используемых настроек это Xft.dpi из XResources. По умолчанию там обычно 96, означающее 100% масштаб. Многие окружения записывают туда значение 96, умноженное на коэффициент масштабирования выставленный в настройках. Но полностью доверять этому значению нельзя, в некоторых случаях (например XFCE, MATE) утилита конфигурации не меняет это значение. В случае Gnome, если fractional scaling включен, то независимо от коэффициента там будет значение по умолчанию 96, а если выключен то 96*ScaleFactor главного дисплея, поэтому если использовать только это значение, то на главном дисплее приложение отобразится корректно, а на тех, где настроен другой коэффициент - нет. А в некоторых случаях (например LXQt или WSL) Xft.dpi вообще отсутствует.

Вариант 2

GTK API: в libgtk есть функция

[DllImport("libgdk-3.so.0")]
public static extern int gdk_monitor_get_scale_factor(GdkMonitor* monitor);

возвращающая коэффициент масштабирования. Как она работает внутри погружаться не будем, важно что на вход принимает дескриптор монитора, т. е. она может возвращать разные значения для разных мониторов:

gtk_init_check(0, IntPtr.Zero);

var display = gdk_display_get_default();
var screen = gdk_display_get_default_screen(display);
var monitorsCount = gdk_display_get_n_monitors(display);

for (var i = 0; i < monitorsCount; i++)
{
  var monitor = gdk_display_get_monitor(display, i);
  var scale = LibGdk.gdk_monitor_get_scale_factor(monitor);
}

Но есть проблема - результат это целочисленное значение, а в системных настройках может быть указано что-нибудь вроде 150%, что является дробным коэффициентом 1.5. gdk_monitor_get_scale_factor в этом случае может возвращать как округленные вниз (например на KDE), так и округленные вверх (например Gnome) значения в зависимости от окружения.

И это еще не все подводные камни. Например окружение Gnome по умолчанию не подразумевает дробное масштабирование (fractional scaling), и если оно выключено то gdk_monitor_get_scale_factor вернет то что нам нужно, но если включено - она вернет значения округленные вверх, т.е. 2 для 200% и 3 для 225%. Но дело в том, что если окружение под Wayland, то ожидается что все X11 приложения в этом случае рисуются в 100% без какого либо масштабирования, и система потом просто растягивает картинку в scale factor раз, естественно жутко ее размыливая, потому если мы применим коэффициент сами, то вместо размыленных 225% получим 675%. В этом случае Xft.dpi сработает корректнее.

К счастью определить включен ли режим fractional scaling в Gnome довольно легко через тот же GTK, запросив GSettings. Искомая информация в разделе org.gnome.mutter, в массиве experimental-features и называется scale-monitor-framebuffer

var settings = g_settings_new("org.gnome.mutter");
var stringArrayPtr = g_settings_get_strv(settings, "experimental-features");
while (true)
{
    var stringPtr = Marshal.ReadIntPtr(stringArrayPtr);
    if (stringPtr == IntPtr.Zero)
        break;
    stringArrayPtr += IntPtr.Size;
    var value = Marshal.PtrToStringAnsi(stringPtr);
    if (value == "scale-monitor-framebuffer")
        return true;
}

Допустим мы получили список коэффициентов, как понять какой относится к какому монитору? Для этого есть функция

[DllImport("libgdk-3.so.0")]
public static extern IntPtr gdk_x11_screen_get_monitor_output(GdkScreen* screen, int monitorNum);

По возвращенному хендлу можно сопоставить информацию, полученную от библиотеки GTK с, например, структурами от libxrandr (полученные через XRRGetMonitors и XRRGetOutputInfo). Но следует отметить, что если окружение под Wayland, то gdk_x11_screen_get_monitor_output упадет с seg fault, потому вызывать ее в этом случае нельзя, и определить какая Xrandr структура какому GTK объекту соотвествует можно только через сравнение их координат.

Вариант 3

Переменные окружения. Некоторые окружения записывают искомые нами значения в переменные GDK_SCALE, QT_SCALE_FACTOR, QT_SCREEN_SCALE_FACTORS. Первые две содержат одно значение (причем к примеру LXQt может записать дробное значение в GDK_SCALE, тогда как там подразумевается только целочисленное, потому что GTK не поддерживает дробные значения), а последняя - несколько значений (для нескольких дисплеев), которые могут быть перечислены как просто через точку с запятой, так и с именами дисплеев (например eDP-1=1.75;DP-1=1.75;DP-2=1.75;DP-3=1.75;). Надо ли говорить что никаких гарантий что эти переменные будут установлены нет, и поэтому пользоваться этим способом можно только в крайнем случае, если все остальные варианты не сработали.

Вариант 4

libwayland-client. Подходит соответственно только для окружений работающих под Wayland. X11 приложения в них работают через прослойку XWayland.

Работа с API Wayland очень неблагодарное занятие, X11 конечно тоже не подарок, но тут совсем мрак. Чтобы получить какую то информацию, надо подписаться на callback, вызвать еще одну функцию которая запустит обработку запросов, и затем среди десятков сработавших коллбеков ловить тот, который вам нужен. Чтобы получить в нем объект, в котором снова надо подписаться на коллбек и запускать обработку запросов, чтобы среди десятков сработавших коллбеков получить интересующие значения... Подробный разбор этой дичи тянет на отдельную статью, потому вкратце скажу только что нас интересует структура

struct wl_output_listener 
{
void function(void* data, wl_output* wl_output, int x, int y, int physical_width, int physical_height, int subpixel, const(char)* make, const(char)* model, int transform) geometry;
void function(void* data, wl_output* wl_output, uint flags, int width, int height, int refresh) mode;
void function(void* data, wl_output* wl_output) done;
void function(void* data, wl_output* wl_output, int factor) scale;
}

с помощью которой можно получить всю интересующую нас информацию для каждого монитора в отдельности, включая коэффициенты масштабирования, но точно так же как и в GTK они опять целочисленные.

Но в случае окружения KDE все интереснее - у них есть свое расширение для протокола Wayland, которое позворяет получить больше информации. Нас интересует структура kde_output_device_v2_listener... Но если поискать ее в гугле, то кроме этой статьи ничего не найдется, потому что следующий круг wayland-ада это генерировать код по xml файлам протокола. Xml-файлы для KDE протоколов можно взять здесь, а генерируется код с помощью вызова утилиты wayland-scanner client-header < protocol.xml > protocol.h. Сгенерированные файлы будут содержать все необходимое, включая неплохие комментарии и вышеупомянутую структуру:

struct kde_output_device_v2_listener 
{
	void (*geometry)(void *data,
			 struct kde_output_device_v2 *kde_output_device_v2,
			 int32_t x,
			 int32_t y,
			 int32_t physical_width,
			 int32_t physical_height,
			 int32_t subpixel,
			 const char *make,
			 const char *model,
			 int32_t transform);

	void (*current_mode)(void *data,
			     struct kde_output_device_v2 *kde_output_device_v2,
			     struct kde_output_device_mode_v2 *mode);

	void (*mode)(void *data,
		     struct kde_output_device_v2 *kde_output_device_v2,
		     struct kde_output_device_mode_v2 *mode);

	void (*done)(void *data,
		     struct kde_output_device_v2 *kde_output_device_v2);

	void (*scale)(void *data,
		      struct kde_output_device_v2 *kde_output_device_v2,
		      wl_fixed_t factor);

	void (*edid)(void *data,
		     struct kde_output_device_v2 *kde_output_device_v2,
		     const char *raw);

	void (*enabled)(void *data,
			struct kde_output_device_v2 *kde_output_device_v2,
			int32_t enabled);

	void (*uuid)(void *data,
		     struct kde_output_device_v2 *kde_output_device_v2,
		     const char *uuid);

	void (*serial_number)(void *data,
			      struct kde_output_device_v2 *kde_output_device_v2,
			      const char *serialNumber);

	void (*eisa_id)(void *data,
			struct kde_output_device_v2 *kde_output_device_v2,
			const char *eisaId);

	void (*capabilities)(void *data,
			     struct kde_output_device_v2 *kde_output_device_v2,
			     uint32_t flags);

	void (*overscan)(void *data,
			 struct kde_output_device_v2 *kde_output_device_v2,
			 uint32_t overscan);

	void (*vrr_policy)(void *data,
			   struct kde_output_device_v2 *kde_output_device_v2,
			   uint32_t vrr_policy);

	void (*rgb_range)(void *data,
			  struct kde_output_device_v2 *kde_output_device_v2,
			  uint32_t rgb_range);

	void (*name)(void *data,
		     struct kde_output_device_v2 *kde_output_device_v2,
		     const char *name);

	void (*high_dynamic_range)(void *data,
				   struct kde_output_device_v2 *kde_output_device_v2,
				   uint32_t hdr_enabled);

	void (*sdr_brightness)(void *data,
			       struct kde_output_device_v2 *kde_output_device_v2,
			       uint32_t sdr_brightness);

	void (*wide_color_gamut)(void *data,
				 struct kde_output_device_v2 *kde_output_device_v2,
				 uint32_t wcg_enabled);
};

Сделав еще один кусок плохо читаемого кода с подписками-коллбеками, мы получим искомую информацию, и главное на что стоит обратить внимание - scale здесь имеет тип wl_fixed_t - число с фиксированной точкой, это дробный коэффициент.

Но зачем все это нужно если выше я говорил что под Wayland ожидается что приложения сами себя не масштабируют? Дело в том что в KDE появилась опция, разрешающая X11 приложениям самим управлять своим масштабированием, и система в этом случае не будет растягивать отрисованные в 100% масштабе окна до требуемых значений, размыливая их. А если она выключена, то поведение будет такое же как в Gnome. Как определить что приложение не может само себя масштабировать? Размер области полученный от Xrandr, умноженный на коэффициент масштабирования полученного от libwayland в этом случае приблизительно равен физическому разрешению (тут могут быть погрешности на пару пикселей из за различных преобразований). Возможно еще откуда-то можно прочесть настройки KDE и ничего не высчитывать, но я не искал как.

Вариант 5

Чтение файлов настроек самостоятельно. Например Gnome хранит их в файле ~/.config/monitors.xml, а Сinnamon в файле ~/.config/cinnamon-monitors.xml, там конечно хранятся правильные значения, точно такие как пользователь установил в настройках, но полагаться на фиксированные пути к файлам это плохая идея. Где и как хранят остальные даже не изучал.

Как работает per-monitor дробное масштабирование там, где кажется, что оно есть

Рассмотрим некоторые примеры:

  1. Сinnamon + Enable fractional scaling:

    На самом деле никак - приложения отрисовываются в 2х и потом просто осуществляется downscaling. Допустим у нас есть монитор с физическим разрешением 3840x2400 и мы ставим в настройках 1.25. Получается что в "логических пикселях" разрешение 3072х1920, но если мы отрисуем 3072х1920 и растянем до 3840x2400 будет мыло, потому размер области в требуемых логических пикселях увеличивается вдвое по каждой стороне и получается 6144x3840, а затем результат уменьшается до физических 3840x2400. Какой коэффициент масштабирования должно использовать приложение в этом случае? Правильный ответ - 2, причем для любых дробных настроек, потому что дробный коэффициент уже применен к области, куда выводится изображение. Поэтому даже если для двух мониторов указать разные настройки, приложения будут выглядеть корректно на обоих.

    Примерно также это сделано в MacOS, там тоже все рисуется в 2х и затем растягивается или сжимается к физическому разрешению - поставьте для 4K монитора в настройках логическое разрешение 1152х648 - все будет отрисовываться в 2304х1296 и затем растягиваться то 3840х2160, в итоге вы будете наблюдать ощутимо размытое изображение, вывод на монитор пиксель-в-пиксель вы никак не получите если только физическое разрешение не равно логическому или ровно вдвое больше него (и такая система пользуется почетом у дизайнеров...). В отличие от Windows, где при любых значениях, даже 350% (логическое разрешение примерно 1097х617), идеально четкая картинка.

  2. KDE + Wayland + Allow X11 apps scale themselves:

    Размер областей отрисовки устанавливается следующий - размер области соответствующая дисплею с наибольшим коэффициентом равняется физическому разрешению. Размеры всех остальных областей равны физическому разрешению умноженному на наибольший коэффициент и поделенное на их собственный. Таким образом для 3840x2400@1.25+1920x1080@1.75 требуемые логические размеры это 3072х1920+1097х617, поскольку второй дисплей имеет больший коэфициент, его область остается равной физическому разрешению 1920x1080 и приложение само должно себя отмасштабировать в 175%. Область для первого будет 5376x3360 (ширина и высота в 1.4 раз больше физического разрешения - 1.75/1.25), и при выводе будет сжата до физического разрешения 3840x2400. Почему так? Да потому что при такой математике приложению не надо "думать" на каком дисплее оно в текущий момент показывается - оно просто всегда масштабирует себя на в 175% и выглядит корректно на обоих, несмотря на их разные настройки. Поэтому в этом случае нас интересует только наибольший коэффициент, без привязки к конкретным мониторам.

  3. В подавляющем большинстве остальных случаев окружения не имеют раздельных настроек для разных мониторов, и приложение может использовать один и тот же коэффициент.

Результат

Из вышеизложенного становится очевидно что нельзя так просто взять и корректно отобразить приложение под Linux, и различным UI-тулкитам приходится очень непросто в попытках подстроиться под этот зоопарк.

Поэтому более менее рабочий способ определения масштабирования может получиться только путем комбинации нескольких подходов, в зависимости от текущих условий. И алгоритм покрывающий достаточно большое количество случаев получается такой (псевдокод):

if (Wayland) 
{
  if (Gnome)
  {
    if (GSettings.HasFractionalScaling)
    {
      return 1.0;
    }
    else
    {
      return WaylandScaleFactors[]; // Несколько значений для разных дисплеев
    }
  }
  else if (KDE)
  {
    if (X11ApplyScalingThemselves)
    {     
      return Max(KDEwaylandScaleFactors[]);
    }
    else
    {
      return 1.0;
    }
  }
  else
  {
    return WaylandScaleFactors[]; // Несколько значений для разных дисплеев
  }
}
else // X11
{
  if (Xft.dpi > 96)
  {
    return Xft.dpi / 96.0;
  }
  else if (CanGetGtkInfo)
  {
    return gdk_monitor_get_scale_factor()[]; // Несколько значений для разных дисплеев
  }
  // что то пошло не так или мы не хотим зависеть от GTK
  // переменные GDK_SCALE, QT_SCALE_FACTOR, QT_SCREEN_SCALE_FACTORS
  else if (HasEnvVariables)
  {
    return ScalingFromEnvVariables[]; // одно или несколько значений
  }
  else
  {
    return 1.0; // fallback значение, если попали сюда то это какое то редкое и странное окружение.
  }
}

Код к статье с примером реализации вышеописанных действий можно найти здесь. Для запуска потребуется .NET.

Комментарии (2)


  1. Johan_Palych
    29.05.2023 16:41

    Есть годный wiki HiDPI:
    https://wiki.archlinux.org/title/HiDPI
    Затащить в linux такое возможно, но лучше пусть тестируют на Microsoft CBL-Mariner Linux
    Install the .NET SDK or the .NET Runtime on Debian
    LinuxDisplayScale.sln GetDisplayScaling.csproj


    1. Einherjar Автор
      29.05.2023 16:41

      Да, в вики ArchLinux вообще много годных статей про различные особенности линукса, причем хорошо что они охватывают очень большой спектр различных конфигураций.