dotnet X11 獲取多屏 edid 信息
我需要獲取準確的顯示器屏幕關聯的設備信息,在屏幕對應的 Edid 信息里面記錄了我需要的物理設備信息。我嘗試通過 /sys/class/drm/ 路徑讀取,但遇到了關聯問題,不知道哪個 Edid 文件應該對應哪個屏幕。我不想去猜測,于是詢問了 Handsome08 了解到了使用 XRRGetOutputProperty 獲取 Edid 數據的方法。具體的方法如下
通過 XRRGetMonitors 方法獲取當前設備的每個顯示器屏幕信息,其方法定義代碼如下
const string libX11Randr = "libXrandr.so.2";
[DllImport(libX11Randr)]
public static extern XRRMonitorInfo* XRRGetMonitors(IntPtr dpy, IntPtr window, bool get_active, out int nmonitors);
返回的 XRRMonitorInfo 結構體定義如下
public unsafe struct XRRMonitorInfo
{
public IntPtr Name;
public int Primary;
public int Automatic;
public int NOutput;
public int X;
public int Y;
public int Width;
public int Height;
public int MWidth;
public int MHeight;
public IntPtr* Outputs;
}
為了方便上層調用,我將其再次封裝,封裝了 MonitorInfo 結構體,代碼如下
public unsafe struct MonitorInfo
{
public IntPtr Name;
public bool IsPrimary;
public int X;
public int Y;
public int Width;
public int Height;
public IntPtr[] Outputs;
public IntPtr Display { get; init; }
public string? GetNameText()
{
var namePtr = XGetAtomName(Display, Name);
var name = Marshal.PtrToStringAnsi(namePtr);
XFree(namePtr);
return name;
}
public override string ToString()
{
var name = GetNameText();
return $"{name}({Name}) IsPrimary={IsPrimary} XY={X},{Y} WH={Width},{Height}";
}
}
于是獲取屏幕信息的代碼就可以這么寫
// Copy from https://github.com/AvaloniaUI/Avalonia \src\Avalonia.X11\Screens\X11Screen.Providers.cs
public class Randr15ScreensImpl
{
public Randr15ScreensImpl(nint display, nint rootWindow)
{
_display = display;
var eventWindow = CreateEventWindow(display, rootWindow);
_window = eventWindow;
XRRSelectInput(display, _window, RandrEventMask.RRScreenChangeNotify);
}
public unsafe MonitorInfo[] GetMonitorInfos()
{
XRRMonitorInfo* monitors = XRRGetMonitors(_display, _window, true, out var count);
var screens = new MonitorInfo[count];
for (var c = 0; c < count; c++)
{
var mon = monitors[c];
var outputs = new nint[mon.NOutput];
for (int i = 0; i < outputs.Length; i++)
{
outputs[i] = mon.Outputs[i];
}
screens[c] = new MonitorInfo()
{
Name = mon.Name,
IsPrimary = mon.Primary != 0,
X = mon.X,
Y = mon.Y,
Width = mon.Width,
Height = mon.Height,
Outputs = outputs,
Display = _display,
};
}
return screens;
}
private readonly IntPtr _display;
private readonly IntPtr _window;
}
以上代碼是從 Avalonia 項目拷貝的。經過了 SeWZC 的考證,傳入 XRRGetMonitors 的窗口應該是 RootWindow 窗口,然而在本文這里和 Avalonia 這里都傳入的是一個 EventWindow 窗口,且傳入 EventWindow 窗口能拿到正確的值,十分有趣,更底層原因我就沒有繼續調查了
以上代碼的 CreateEventWindow 方法的實現如下
public static IntPtr CreateEventWindow(nint display, nint rootWindow)
{
var win = XCreateSimpleWindow(display, rootWindow,
0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);
return win;
}
[DllImport(libX11)]
public static extern IntPtr XCreateSimpleWindow(IntPtr display, IntPtr parent, int x, int y, int width,
int height, int border_width, IntPtr border, IntPtr background);
const string libX11 = "libX11.so.6";
拿到 MonitorInfo 對象之后,可以看到里面有很多個屬性,其中的 Outputs 屬性就是本文的重點
在這里我編寫一個循環將其逐個取出,其中可能有一個就是包含了 EDID 信息,代碼如下
var display = XOpenDisplay(IntPtr.Zero);
var screen = XDefaultScreen(display);
var rootWindow = XDefaultRootWindow(display);
var randr15ScreensImpl = new Randr15ScreensImpl(display, rootWindow);
MonitorInfo[] monitorInfos = randr15ScreensImpl.GetMonitorInfos();
for (var i = 0; i < monitorInfos.Length; i++)
{
MonitorInfo monitorInfo = monitorInfos[i];
Console.WriteLine(monitorInfo);
OutputEdidInfo(monitorInfo);
}
在 OutputEdidInfo 方法里面,咱將進行 EDID 解析邏輯
通過 XRRListOutputProperties 方法讀取 Outputs 里面的每一項,如果某一項中讀取到的屬性包含了 EDID Atom 內容,則證明當前項就是 EDID 信息
其代碼如下
unsafe void OutputEdidInfo(MonitorInfo monitorInfo)
{
var edidAtom = XInternAtom(display, "EDID", only_if_exists: true);
var anyPropertyTypeAtom = XInternAtom(display, "AnyPropertyType", only_if_exists: true);
const nint XA_INTEGER = 19;
for (var i = 0; i < monitorInfo.Outputs.Length; i++)
{
var rrOutput = monitorInfo.Outputs[i];
if (rrOutput == IntPtr.Zero)
{
continue;
}
var properties = XRRListOutputProperties(display, rrOutput, out var propertyCount);
IntPtr prop = 0;
try
{
var hasEDID = false;
for (var pc = 0; pc < propertyCount; pc++)
{
if (properties[pc] == edidAtom)
{
hasEDID = true;
break;
}
}
if (!hasEDID)
{
Console.WriteLine($"Output {rrOutput} does not have EDID property.");
continue;
}
... // 忽略其他代碼
}
finally
{
XLib.XFree(prop);
XLib.XFree(new IntPtr(properties));
}
}
}
如果當前項的 hasEDID 為 true 則通過 XRRGetOutputProperty 讀取屬性的值,代碼如下
// Length of a EDID-Block-Length(128 bytes), XRRGetOutputProperty multiplies offset and length by 4
const int EDIDStructureLength = 32;
XRRGetOutputProperty(display, rrOutput, edidAtom, 0, EDIDStructureLength, false, false,
anyPropertyTypeAtom, out IntPtr actualType, out int actualFormat, out int nItems, out long bytesAfter,
out prop);
// https://gitlab.gnome.org/GNOME/mutter/-/blame/3.29.90/src/backends/x11/meta-output-xrandr.c
if (actualType != XA_INTEGER)
{
continue;
}
if (actualFormat != 8) // Expecting a byte array
{
continue;
}
Span<byte> edid = new Span<byte>((void*) prop, (int) bytesAfter);
此時即可讀取到 edid 二進制信息。此時的 edid 二進制信息還需要進一步的解析才能獲取內容。如何解析 edid 就不在本文范圍內了,大家可以使用自己喜歡的方式進行解析。本文這里只是做了簡單的內容解析,解析出了屏幕的物理尺寸信息。解析代碼封裝在 EdidInfo 結構體里面,如果大家感興趣,可以到本文末尾找到本文所有代碼的下載方法,下載代碼了解 Edid 解析邏輯
解析之后將輸出物理設備信息,代碼如下
Span<byte> edid = new Span<byte>((void*)prop, (int)bytesAfter);
ReadEdidInfoResult edidInfoResult = EdidInfo.ReadEdid(edid);
if (edidInfoResult.IsSuccess)
{
EdidInfo edidInfo = edidInfoResult.EdidInfo;
Console.WriteLine($"EDID Info: ManufacturerName={edidInfo.ManufacturerName} MonitorPhysical={edidInfo.BasicDisplayParameters.MonitorPhysicalWidth.Value}x{edidInfo.BasicDisplayParameters.MonitorPhysicalHeight.Value}cm");
}
else
{
Console.WriteLine($"解析 Edid 失敗 {edidInfoResult.ErrorMessage}");
}
在我的雙屏設備上運行,可以看到大概如下輸出信息。我的 DisplayPort-1 是主屏,放在右邊,是一個 165x93cm 的 75 寸大屏幕。副屏是 DisplayPort-0 放在左邊,是一個 190x107cm 的更大的屏幕
DisplayPort-1(343) IsPrimary=True XY=1920,309 WH=1920,1080
EDID Info: ManufacturerName=IWB MonitorPhysical=165x93cm
DisplayPort-0(344) IsPrimary=False XY=0,0 WH=1920,1080
EDID Info: ManufacturerName=IWB MonitorPhysical=190x107cm
通過以上輸出,對比我的物理設備,發現可以對應上,通過此方法比從 /sys/class/drm/ 路徑下讀取更好,至少不用去猜路徑名。很多設備上,都可以在 /sys/class/drm/ 文件夾內找到和 XRRGetMonitors 返回的顯示器名對應的設備,但這取決于驅動,不一定能對應上。能從 XRRGetOutputProperty 獲取到的 Edid 信息才能完全對應
本文代碼放在 github 和 gitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快
先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 46db729ca3aaa4d73169d07e903c96f0aa2f7fee
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼。如果依然拉取不到代碼,可以發郵件向我要代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 46db729ca3aaa4d73169d07e903c96f0aa2f7fee
獲取代碼之后,進入 X11/FelocerebeWirolerco 文件夾,即可獲取到源代碼
更多 X11 技術博客,請參閱 博客導航
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請訪問 https://blog.lindexi.com/
如圖片看不見,請在瀏覽器開啟不安全http內容兼容

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://www.rzrgm.cn/lindexi)(包含鏈接:http://www.rzrgm.cn/lindexi ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布。如有任何疑問,請與我[聯系](mailto:lindexi_gd@163.com)。

浙公網安備 33010602011771號