故事的背景我在一个双屏设备上,我想要获取每个显示器屏幕对应的 EDID 信息。我在一台麒麟系统的设备上,通过 RandR 的方式获取 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 信息才能完全对应

本文代码放在 githubgitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 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/post/dotnet-X11-%E8%8E%B7%E5%8F%96%E5%A4%9A%E5%B1%8F-edid-%E4%BF%A1%E6%81%AF.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利