Close Menu

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    SwifDoo PDF Pro drops to $35—and it’s a lifetime license

    This $60 Bitcoin miner can be run on your desk

    Internet Archive unveils tool to save the web from dead links

    Facebook X (Twitter) Instagram
    • Artificial Intelligence
    • Business Technology
    • Cryptocurrency
    • Gadgets
    • Gaming
    • Health
    • Software and Apps
    • Technology
    Facebook X (Twitter) Instagram Pinterest Vimeo
    Tech AI Verse
    • Home
    • Artificial Intelligence

      Read the extended transcript: President Donald Trump interviewed by ‘NBC Nightly News’ anchor Tom Llamas

      February 6, 2026

      Stocks and bitcoin sink as investors dump software company shares

      February 4, 2026

      AI, crypto and Trump super PACs stash millions to spend on the midterms

      February 2, 2026

      To avoid accusations of AI cheating, college students are turning to AI

      January 29, 2026

      ChatGPT can embrace authoritarian ideas after just one prompt, researchers say

      January 24, 2026
    • Business

      New VoidLink malware framework targets Linux cloud servers

      January 14, 2026

      Nvidia Rubin’s rack-scale encryption signals a turning point for enterprise AI security

      January 13, 2026

      How KPMG is redefining the future of SAP consulting on a global scale

      January 10, 2026

      Top 10 cloud computing stories of 2025

      December 22, 2025

      Saudia Arabia’s STC commits to five-year network upgrade programme with Ericsson

      December 18, 2025
    • Crypto

      Tether Freezes $500 Million in Assets Linked to Turkish Gambling Ring

      February 7, 2026

      Crypto.com CEO Pivots to AI Agents, Launch Planned For Super Bowl

      February 7, 2026

      Will Solana’s Price Recovery Be Challenging? Here’s What On-Chain Signals Suggest

      February 7, 2026

      China Widens Crypto Ban to Choke Off Stablecoins and Asset Tokenization

      February 7, 2026

      CFTC Expands Crypto Collateral Pilot to Include National Trust Bank Stablecoins

      February 7, 2026
    • Technology

      SwifDoo PDF Pro drops to $35—and it’s a lifetime license

      February 8, 2026

      This $60 Bitcoin miner can be run on your desk

      February 8, 2026

      Internet Archive unveils tool to save the web from dead links

      February 8, 2026

      I’m obsessed with this 3D-printed tray for Costco hot dogs

      February 8, 2026

      This refurbed Acer with an RTX 5060 may be the best deal in gaming PCs right now

      February 8, 2026
    • Others
      • Gadgets
      • Gaming
      • Health
      • Software and Apps
    Check BMI
    Tech AI Verse
    You are at:Home»Technology»Linux mode setting, from the comfort of OCaml
    Technology

    Linux mode setting, from the comfort of OCaml

    TechAiVerseBy TechAiVerseNovember 16, 2025No Comments19 Mins Read2 Views
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr Email Reddit
    Share
    Facebook Twitter LinkedIn Pinterest WhatsApp Email

    Linux mode setting, from the comfort of OCaml

    Linux provides the KMS (Kernel Mode Setting) API to let applications query and configure display settings.
    It’s used by Wayland compositors and other programs that need to configure the hardware directly.
    I found the C API a little verbose and hard to follow so I made libdrm-ocaml,
    which lets us run commands interactively in a REPL.

    We’ll start by discovering what hardware is available and how it’s currently configured,
    then configure a monitor to display a simple bitmap, and then finally render a 3D animation.
    The post should be a useful introduction to KMS even if you don’t know OCaml.

    ( this post also appeared on Hacker News )

    Table of Contents

    • Running it yourself
    • Querying the current state
      • Finding devices
      • Listing resources
      • Connectors
      • Modes
      • Properties
      • Encoders
      • CRT Controllers
      • Framebuffers
      • CRTC planes
      • Expanded resources diagram
    • Making changes
      • Non-atomic mode setting
      • Dumb buffers
      • Atomic mode setting
    • 3D rendering
    • Linux VTs
    • Debugging
    • Conclusions

    Running it yourself

    If you want to follow along, you’ll need to install libdrm-ocaml and an interactive REPL like utop.
    With Nix, you can set everything up like this:

    git clone https://github.com/talex5/libdrm-ocaml
    cd libdrm-ocaml
    nix develop
    dune utop
    

    You should see a utop # prompt, where you can enter OCaml expressions.
    Use ;; to tell the REPL you’ve finished typing and it’s time to evaluate, e.g.

    1
    2
    
    utop # 1+1;;
    - : int = 2
    

    Alternatively, you can install things using opam (OCaml’s package manager):

    opam install libdrm utop
    utop
    

    Then, at the utop prompt enter #require "libdrm";; (including the leading #).

    Querying the current state

    Before changing anything, we’ll start by discovering what hardware is available.

    I’ll introduce the API as we go along, but you can check the API reference docs
    if you want more information.

    Finding devices

    To list available graphics devices:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    utop # Drm.Device.list ();;
    - : Drm.Device.Info.t list =
    [{primary_node = Some "/dev/dri/card0";
      render_node = Some "/dev/dri/renderD128";
      info = PCI {bus = {domain = 0; bus = 1; dev = 0; func = 0};
                  dev = {vendor_id = 0x1002;
                         device_id = 0x67ff;
                         subvendor_id = 0x1458;
                         subdevice_id = 0x230b;
                         revision_id = 0xff}}}]
    

    libdrm scans the /dev/dri/ directory looking for devices.
    It uses stat to find the device major and minor numbers and uses the virtual /sys filesystem to get information about each one.
    This is a PCI device, and the information corresponds to the values from lspci, e.g.

    $ lspci -nns 0:1:0.0
    01:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI]
      Baffin [Radeon RX 550 640SP / RX 560/560X] [1002:67ff] (rev ff)
    

    Each graphics device can have a primary and a render node.
    The primary node gives full access to the device, including configuring monitors,
    while the render node just allows applications to render scenes to memory.
    In the last post I was using the render to node to create a 3D image,
    and then sending it to the Wayland compositor for display.
    This time we’ll be doing the display ourselves, so we need to open the primary node:

    1
    2
    
    utop # let dev = Unix.openfile "/dev/dri/card0" [O_CLOEXEC; O_RDWR] 0;;
    val dev : Unix.file_descr = <abstr>
    

    To check the driver version:

    1
    2
    3
    
    utop # Drm.Device.Version.get dev;;
    - : Drm.Device.Version.t =
    {version = 3.61.0; name = "amdgpu"; date = "0"; desc = "AMD GPU"}
    

    If you’re familiar with the C API, this corresponds to the drmGetVersion function,
    and Drm.Device.list corresponds to drmGetDevices2;
    I reorganised things a bit to make better use of OCaml’s modules.

    Listing resources

    Let’s see what resources we’ve got to play with:

    1
    2
    3
    4
    5
    6
    7
    8
    
    utop # let resources = Drm.Kms.Resources.get dev;;
    val resources : K.Resources.t =
      {fbs = [];
       crtcs = [57; 60; 63; 66; 69];
       connectors = [71; 78; 84];
       encoders = [70; 76; 83; 86; 87; 88; 89; 90];
       min_width,max_width = 0,16384;
       min_height,max_height = 0,16384}
    

    Note: The Kernel Mode Setting functions are in the Drm.Kms module.
    The C API calls these functions drmMode*, but I found that confusing as
    e.g. drmModeGetResources sounds like you’re asking for the resources of a mode.

    A CRTC is a CRT Controller, and typically controls a single monitor
    (known as a Cathode Ray Tube for historical reasons).
    Framebuffers provide image data to a CRTC (we create framebuffers as needed).
    Connectors correspond to physical connectors (e.g. where you plug in a monitor cable).
    An Encoder encodes data from the CRTC for a particular connector.

    Resources diagram (simplified)

    Connectors

    To save a bit of typing, I’ll create an alias for the Drm.Kms module:

    1
    
    utop # module K = Drm.Kms;;
    

    You could also open Drm.Kms to avoid needing any prefix, but I’ll keep using K for clarity.

    To get details for the first connector (the head of the list):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    utop # K.Connector.get dev (List.hd resources.connectors);;
    - : K.Connector.t =
    {connector_id = 71; (* DP-1 *)
     connector_type = DisplayPort;
     connector_type_id = 1;
     connection = Connected;
     mm_width,mm_height = 700,390;
     subpixel = Unknown;
     modes = [3840x2160 60.00Hz;
              3840x2160 30.00Hz;
              3840x2160 29.97Hz;
              2560x1440 59.95Hz;
              ...];
     props = [1:77; 2:0; 5:0; 6:0; 4:0; 34:0; 35:0; 36:0; 37:0; 72:8; 73:0; 
              7:0; 74:0; 75:15];
     encoder_id = Some 70;
     encoders = [70]}
    

    This is DisplayPort connector 1 (usually called DP-1) and it’s currently Connected.
    The connector also says which modes are available on the connected monitor.

    I was lucky in that the first connector was the one I’m using,
    but really we should get all the connectors and filter them to find the connected ones.
    List.map can be used to run get on each of them:

    1
    2
    3
    4
    5
    
    utop # let connectors = List.map (K.Connector.get dev) resources.connectors;;
    val connectors : K.Connector.t list =
      [{connector_id = 71; (* DP-1 *) ...};
       {connector_id = 78; (* HDMI-A-1 *) ...};
       {connector_id = 84; (* DVI-D-1 *) ...}]
    

    Then to filter:

    1
    2
    3
    4
    5
    6
    
    utop # let is_connected (c : K.Connector.t) = (c.connection = Connected);;
    val is_connected : K.Connector.t -> bool = <fun>
    
    utop # let connected = List.filter is_connected connectors;;
    val connected : K.Connector.t list =
      [{connector_id = 71; (* DP-1 *) ...}]
    

    We’ll investigate c, the first connected one:

    1
    2
    3
    
    utop # let c = List.hd connected;;
    val c : K.Connector.t =
      {connector_id = 71; (* DP-1 *) ...}
    

    A note on IDs

    In the libdrm C API, IDs are just integers.
    To avoid mix-ups, I made them distinct types in the OCaml API.
    For example, if you try to use an encoder ID as a connector ID:

    1
    2
    3
    4
    5
    6
    
    utop # K.Connector.get dev (List.hd resources.encoders);;
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    Error: This expression has type Drm.Kms.Encoder.id = [ `Encoder ] Drm.Id.t
           but an expression was expected of type
             K.Connector.id = [ `Connector ] Drm.Id.t
           These two variant types have no intersection
    

    Normally this is what you want, but for interactive use it’s annoying that you can’t just pass a plain integer.
    e.g.

    1
    2
    3
    4
    
    utop # K.Connector.get dev 71;;
                               ^^
    Error: The constant 71 has type int but an expression was expected of type
             K.Connector.id = [ `Connector ] Drm.Id.t
    

    You can get any kind of ID with Drm.Id.of_int (e.g. K.Connector.get dev (Drm.Id.of_int 71)),
    but that’s still a bit verbose, so you might prefer to (re)define a prefix operator for it, e.g.

    1
    2
    3
    
    utop # let ( ! ) = Drm.Id.of_int;;
    utop # K.Connector.get dev !71;;
    - : K.Connector.t = {connector_id = 71; ...}
    

    (note: ! is the only single-character prefix operator available in OCaml)

    Modes

    Modes are shown in abbreviated form in the connector output.
    To see the full list:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    utop # c.modes;;
    - : K.Mode_info.t list =
    [3840x2160 60.00Hz; 3840x2160 30.00Hz; 3840x2160 29.97Hz; 2560x1440 59.95Hz;
     1920x1200 60.00Hz; 1920x1080 60.00Hz; 1920x1080 59.94Hz; 1600x1200 60.00Hz;
     1680x1050 59.95Hz; 1600x900 60.00Hz; 1280x1024 75.02Hz; 1280x1024 60.02Hz;
     1440x900 59.89Hz; 1280x800 59.81Hz; 1152x864 75.00Hz; 1280x720 60.00Hz;
     1280x720 59.94Hz; 1024x768 75.03Hz; 1024x768 70.07Hz; 1024x768 60.00Hz;
     832x624 74.55Hz; 800x600 75.00Hz; 800x600 72.19Hz; 800x600 60.32Hz;
     800x600 56.25Hz; 640x480 75.00Hz; 640x480 72.81Hz; 640x480 66.67Hz;
     640x480 60.00Hz; 640x480 59.94Hz; 720x400 70.08Hz]
    

    Note: I annotated various pretty-printer functions with [@@ocaml.toplevel_printer],
    which causes utop to use them by default to display values of the corresponding type.
    For example, showing a list of modes uses this short summary form.
    Displaying an individual mode shows all the information.
    Here’s the first mode:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    # List.hd c.modes;;
    - : K.Mode_info.t =
    {name = "3840x2160";
     typ = preferred+driver;
     flags = phsync+nvsync;
     stereo_mode = None;
     aspect_ratio = None;
     clock = 533250;
     hdisplay,vdisplay = 3840,2160;
     hsync_start = 3888;
     hsync_end = 3920;
     htotal = 4000;
     hskew = 0;
     vsync_start = 2163;
     vsync_end = 2168;
     vtotal = 2222;
     vscan = 0;
     vrefresh = 60}
    

    Properties

    Some resources can also have extra properties.
    Use get_properties to fetch them:

    1
    2
    3
    4
    5
    6
    
    utop # K.Connector.get_properties dev c.connector_id;;
    - : [ `Connector ] K.Properties.t =
    {EDID = 92; DPMS = On; TILE = None; link-status = Good; non-desktop = 0;
     HDR_OUTPUT_METADATA = None; scaling mode = None; underscan = off;
     underscan hborder = 0; underscan vborder = 0; max bpc = 8;
     Colorspace = Default; vrr_capable = 0; subconnector = Native}
    

    Linux only returns a subset of the properties until you enable the atomic feature.
    Let’s turn that on now:

    1
    2
    
    utop # Drm.Client_cap.(set atomic) dev true;;
    - : (unit, Unix.error) result = Ok ()
    

    (Module.(expr) is a short-hand that brings all of Module‘s symbols into scope for expr,
    so we don’t have to repeat the module name for both set and atomic)

    And getting the properties again, we now have an extra CRTC_ID,
    telling us which controller this connector is currently using:

    1
    2
    3
    4
    5
    6
    
    utop # let c_props = K.Connector.get_properties dev c.connector_id;;
    val c_props : [ `Connector ] K.Properties.t =
    {EDID = 92; DPMS = On; TILE = None; link-status = Good; non-desktop = 0;
     HDR_OUTPUT_METADATA = None; CRTC_ID = 57; scaling mode = None;
     underscan = off; underscan hborder = 0; underscan vborder = 0; max bpc = 8;
     Colorspace = Default; vrr_capable = 0; subconnector = Native}
    

    Encoders

    The Linux documentation says:

    Those are really just internal artifacts of the helper libraries used to
    implement KMS drivers. Besides that they make it unnecessarily more
    complicated for userspace to figure out which connections between a CRTC and
    a connector are possible, and what kind of cloning is supported, they serve
    no purpose in the userspace API. Unfortunately encoders have been exposed to
    userspace, hence can’t remove them at this point. Furthermore the exposed
    restrictions are often wrongly set by drivers, and in many cases not powerful
    enough to express the real restrictions.

    OK. Well, let’s take a look anyway:

    1
    2
    3
    4
    5
    6
    7
    
    utop # let e = K.Encoder.get dev (Option.get c.encoder_id);;
    val e : K.Encoder.t =
      {encoder_id = 70;
       encoder_type = TMDS;
       crtc_id = Some 57;
       possible_crtcs = 0x1f;
       possible_clones = 0x1}
    

    Note: We need Option.get here because a connector might not have an encoder set yet.
    Where the C API uses 0 to indicate no resource,
    the OCaml API uses None to force us to think about that case.

    As the documentation says, the encoder is mainly useful to get the CRTC ID:

    1
    2
    
    utop # let crtc_id = Option.get e.crtc_id;;
    val crtc_id : Drm.Kms.Crtc.id = 57
    

    We could instead have got that directly from the connector using its properties:

    1
    2
    
    utop # K.Properties.Values.get_value_exn c_props K.Connector.crtc_id;;
    - : [ `Crtc ] Drm.Id.t option = Some 57
    

    CRT Controllers

    1
    2
    3
    4
    5
    6
    7
    
    utop # let crtc = K.Crtc.get dev crtc_id;;
    val crtc : K.Crtc.t =
      {crtc_id = 57;
       fb_id = Some 93;
       x,y = 0,0;
       width,height = 3840,2160;
       mode = Some 3840x2160 60.00Hz}
    

    An active CRTC has a mode set (presumably from the connector’s list of supported modes),
    and a framebuffer with the image to be displayed.

    If I keep calling Crtc.get, I see that it is sometimes showing framebuffer 93 and sometimes 94.
    My Wayland compositor (Sway) updates one framebuffer while the other is being shown, then switches which one is displayed.

    Framebuffers

    My CRTC is currently displaying the contents of framebuffer 93:

    1
    2
    
    utop # let fb_id = Option.get crtc.fb_id;;
    val fb_id : Drm.Kms.Fb.id = 93
    
    1
    2
    3
    4
    5
    6
    7
    
    utop # let fb = K.Fb.get dev fb_id;;
    val fb : K.Fb.t =
      {fb_id = 93;
       width,height = 3840,2160;
       pixel_format, modifier = XR24, None;
       interlaced = false;
       planes = [{handle = None; pitch = 15360; offset = 0}]}
    

    A framebuffer has up to 4 framebuffer planes (not to be confused with CRTC planes; see later),
    each of which references a buffer object (also known as a BO and referenced with a GEM handle).

    This framebuffer is using the XR24 format, where there is a single BO with 32 bits for each pixel
    (8 for red, 8 green, 8 blue and 8 unused).
    Some formats use e.g. a separate buffer for each component
    (or a different part of the same buffer, using offset).

    Modern graphics cards also support format modifiers, but my card is too old so I just get None.
    Linux’s fourcc.h header file describes the various formats and modifiers.
    Modifiers seem to be mainly used to specify the tiling.

    I don’t have permission to see the buffer object, so it appears as (handle = None).
    The pitch is the number of bytes from one row to the next (also known as the stride).
    Here, the 15360 is simply the width (3840) multiplied by the 4 bytes per pixel.

    CRTC planes

    In fact, Crtc.get is an old API that only covers the basic case of a single framebuffer.
    In reality, a CRTC can combine multiple CRTC planes, which for some reason aren’t returned with the other resources
    and must be requested separately:

    1
    2
    
    utop # let plane_ids = K.Plane.list dev;;
    val plane_ids : K.Plane.id list = [40; 43; 46; 49; 52; 55; 58; 61; 64; 67]
    

    (note: you need to enable “atomic” mode before requesting planes; we already did that above)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    utop # let planes = List.map (K.Plane.get dev) plane_ids;;
    val planes : K.Plane.t list =
      [{formats = [XR24; AR24; RA24; XR30; XB30; AR30; AB30; XR48; XB48; 
                   AR48; AB48; XB24; AB24; RG16; XR4H; AR4H; XB4H; AB4H];
        plane_id = 40;
        crtc_id = None;
        fb_id = None;
        crtc_x,crtc_y = 0,0;
        x,y = 0,0;
        possible_crtcs = 0x10};
       ...
      ]
    

    A lot of these planes aren’t being used (don’t have a CRTC),
    which we can check for with a helper function:

    1
    2
    
    utop # let has_crtc (x : K.Plane.t) = (x.crtc_id <> None);;
    val has_crtc : K.Plane.t -> bool = <fun>
    

    Looks like Sway is using two planes at the moment:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    utop # let active_planes = List.filter has_crtc planes;;
    val active_planes : K.Plane.t list =
      [{formats = [XR24; AR24; RA24; XR30; XB30; AR30; AB30; XR48; XB48; 
                   AR48; AB48; XB24; AB24; RG16; XR4H; AR4H; XB4H; AB4H];
        plane_id = 52;
        crtc_id = Some 57;
        fb_id = Some 94;
        crtc_x,crtc_y = 0,0;
        x,y = 0,0;
        possible_crtcs = 0x1};
       {formats = [AR24];
        plane_id = 55;
        crtc_id = Some 57;
        fb_id = Some 98;
        crtc_x,crtc_y = 0,0;
        x,y = 0,0;
        possible_crtcs = 0x1}]
    

    More information is available as properties:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    utop # let active_plane_ids = List.map K.Plane.id active_planes;;
    val active_plane_ids : K.Plane.id list = [52; 55]
    
    utop # List.map (K.Plane.get_properties dev) active_plane_ids;;
    - : [ `Plane ] K.Properties.t list =
    [{CRTC_H = 2160; CRTC_ID = 57; CRTC_W = 3840; CRTC_X = 0; CRTC_Y = 0;
      FB_ID = 93; IN_FENCE_FD = -1; SRC_H = 141557760; SRC_W = 251658240;
      SRC_X = 0; SRC_Y = 0; rotation = [rotate-0]; type = Primary; zpos = 0};
     {CRTC_H = 128; CRTC_ID = 57; CRTC_W = 128; CRTC_X = 3105; CRTC_Y = 1518;
      FB_ID = 98; IN_FENCE_FD = -1; SRC_H = 8388608; SRC_W = 8388608; SRC_X = 0;
      SRC_Y = 0; type = Cursor; zpos = 255}]
    
    • Plane 52 is a Primary plane and is using framebuffer 93 (as we saw before).
    • Plane 55 is a Cursor plane, using framebuffer 98 (and the AR24 format, with alpha/transparency).

    A plane chooses which part of the frame buffer to show (SRC_X, SRC_Y, SRC_W and SRC_H)
    and where it should appear on the screen (CRTC_X, CRTC_Y, CRTC_W and CRTC_H).
    The source values are in 16.16 format (i.e. shifted left 16 bits).

    Oddly, Plane.get returned crtc_x,crtc_y = 0,0 for both planes, but
    the properties show the correct cursor location (CRTC_X = 3105; CRTC_Y = 1518;).

    Having the cursor on a separate plane avoids having to modify the main screen image
    whenever the mouse pointer moves, which is good for low latency
    (especially if the GPU is busy rendering something else at the time),
    power consumption (the GPU can stay powered down),
    and allows showing an application’s buffer full screen without the compositor
    needing to modify the application’s buffer.

    You might also have some Overlay planes,
    which can be useful for displaying video.
    My graphics card seems to be too old for that.

    Expanded resources diagram

    Here’s an expanded diagram showing some more possibilities:

    Expanded resources diagram

    • Some framebuffer formats take the input data from multiple buffers.
    • A framebuffer can be shared by multiple CRTCs (perhaps with each plane showing a different part of it).
    • A CRTC can have multiple planes (e.g. primary and cursor).
    • A single CRTC can show the same image on multiple monitors.

    Making changes

    If I try turning off the CRTC (by setting the mode to None) from my desktop environment it fails:

    1
    2
    
    utop # K.Crtc.set dev crtc_id ~pos:(0,0) ~connectors:[] None;;
    Exception: Unix.Unix_error(Unix.EACCES, "drmModeSetCrtc", "")
    

    The reason is that I’m currently running a graphical desktop and Sway owns the device
    (so my dev is not the DRM “master”):

    1
    2
    
    utop # Drm.Device.is_master dev;;
    - : bool = false
    

    That can be fixed by switching to a different VT (e.g. with Ctrl-Alt-F2) and running it there.
    However, this will result in a second problem: I won’t be able to see what I’m doing!

    If you have a second computer then you can SSH in and test things out from there, but
    for simplicity we’ll leave the utop REPL at this point and write some programs instead.

    For example, query.ml shows the information we discovered above:

    dune exec -- ./examples/query.exe
    
    1
    2
    3
    4
    
    devices:                              
      [{primary_node = Some "/dev/dri/card0";
        render_node = Some "/dev/dri/renderD128";
    ...
    

    Non-atomic mode setting

    Linux provides two ways to configure modes: the old non-atomic API and the newer atomic one.

    examples/nonatomic.ml contains a simple example of the older (but simpler) API.
    It starts by finding a device (the first one with a primary node supporting KMS), then
    finds all connected connectors (as we did above), and calls show_test_page on each one:

    1
    2
    3
    4
    5
    6
    
    let () =
      Utils.with_device @@ fun t ->
      let connected = List.filter Utils.is_connected t.connectors in
      Utils.restoring_afterwards t @@ fun () ->
      List.iter (show_test_page t) connected;
      Unix.sleep 2
    

    restoring_afterwards stores the current configuration, runs the callback,
    and then puts things back to normal when that finishes (or you press Ctrl-C).

    The program waits for 2 seconds after showing the test page before exiting.

    show_test_page finds the CRTC (as we did above),
    takes the first supported mode, creates a test framebuffer of that size,
    and configures the CRTC to display it:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    let show_test_page (t : Resources.t) (c : K.Connector.t) =
      match c.encoder_id with
      | None -> println "%a has no encoder (skipping)" K.Connector.pp_name c
      | Some encoder_id ->
        match K.Encoder.get t.dev encoder_id with
        | { crtc_id = None; _ } ->
          println "%a's encoder has no CRTC (skipping)" K.Connector.pp_name c
        | { crtc_id = Some crtc_id; _ } ->
          println "Showing test page on %a" K.Connector.pp_name c;
          let mode = List.hd c.modes in
          let size = (mode.hdisplay, mode.vdisplay) in
          let fb = Test_image.create t.dev size in
          K.Crtc.set t.dev crtc_id (Some mode) ~fb ~pos:(0,0)
            ~connectors:[c.connector_id]
    

    If the connector doesn’t have a CRTC, we could find a suitable one and use that,
    but for simplicity the example just skips such connectors.

    To run the example (switch away from any graphical desktop first or it won’t work):

    dune exec -- ./examples/nonatomic.exe
    

    Dumb buffers

    Typically the pixel data to be displayed comes from some complex rendering pipeline,
    but Linux also provides dumb buffers for simple cases such as testing.
    The Test_image.create function used above creates a dumb buffer with a test pattern:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    let create_dumb dev size =
      let dumb_buffer = Drm.Buffer.Dumb.create dev ~bpp:32 size in
      let arr = Drm.Buffer.Dumb.map dev dumb_buffer Int32 in
      for row = 0 to snd size - 1 do
        for col = 0 to fst size - 1 do
          let c =
            (row land 0xff) lor
            ((col land 0xff) lsl 8) lor
            (((row lsr 8) lor (col lsr 8)) lsl 18)
          in
          arr.{row, col} <- Int32.of_int c
        done;
      done;
      dumb_buffer
    

    Dumb.create allocates memory for the image data.
    Dumb.map makes it appear in host-memory as an OCaml bigarray.
    The loop sets each 32-bit int in the image to some colour c.

    Then we wrap this data up as an XR24-format framebuffer with a single plane:

    1
    2
    3
    4
    
    let create dev size =
      let buffer = create_dumb dev size in
      let planes = [K.Fb.Plane.v buffer.handle ~pitch:buffer.pitch] in
      K.Fb.add dev ~size ~planes ~pixel_format:Drm.Fourcc.xr24
    

    Atomic mode setting

    examples/atomic.ml demonstrates the newer atomic API:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    let () =
      Utils.with_device @@ fun t ->
      Drm.Client_cap.(set_exn atomic) t.dev true;
      let connected = List.filter Utils.is_connected t.connectors in
      println "Found %d connected connectors" (List.length connected);
      let free_planes = ref (K.Plane.list t.dev) in
      let rq = K.Atomic_req.create () in
      List.iter (show_test_page ~free_planes t rq) connected;
      println "Checking that commit will work...";
      match K.Atomic_req.commit ~test_only:true t.dev rq with
      | exception Unix.Unix_error (code, _, _) ->
        println "Mode-setting would fail with error: %s" (Unix.error_message code)
      | () ->
        println "Pre-commit test passed.";
        Utils.restoring_afterwards t @@ fun () ->
        K.Atomic_req.commit t.dev rq;
        Unix.sleep 2
    

    The steps are:

    1. Use set_exn atomic to enable atomic mode.
    2. Create an atomic request (rq).
    3. Use show_test_page to populate it with the desired property changes.
    4. (optional) Check that it will work (~test_only:true).
    5. Commit the changes (Atomic_req.commit).

    The advantage here is that either all changes are successfully applied at once or nothing changes.
    This avoids various problems with flickering or trying to roll back partial changes.

    show_test_page needs a couple of modifications.
    First, we have to find a plane (rather than using the old Crtc.set which assumes a single plane),
    and then we set the plane’s FB_ID property to the new framebuffer in the request:

    1
    
    K.Atomic_req.add_property rq plane K.Plane.fb_id (Some fb)
    

    For the example, I actually set more properties and defined an operator to make the code a bit neater:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    let ( .%{}<- ) obj prop value =
      K.Atomic_req.add_property rq obj prop value
    in
    plane.%{ K.Plane.fb_id } <- Some fb;
    (* Source region on frame-buffer: *)
    plane.%{ K.Plane.src_x } <- Drm.Ufixed.of_int 0;
    plane.%{ K.Plane.src_y } <- Drm.Ufixed.of_int 0;
    plane.%{ K.Plane.src_w } <- Drm.Ufixed.of_int (fst size);
    plane.%{ K.Plane.src_h } <- Drm.Ufixed.of_int (snd size);
    (* Destination region on CRTC: *)
    plane.%{ K.Plane.crtc_x } <- 0;
    plane.%{ K.Plane.crtc_y } <- 0;
    plane.%{ K.Plane.crtc_w } <- fst size;
    plane.%{ K.Plane.crtc_h } <- snd size;
    

    In libdrm-ocaml, properties are typed, so you can’t forget to convert the source values to fixed point format.

    3D rendering

    The examples above use a dumb-buffer, but it’s fairly simple to replace that with a Vulkan buffer.
    The code in the last post exported the image memory from Vulkan as a dmabuf FD and sent it to the Wayland compositor.
    Now, instead of sending it we just need to import it into our device (with Drm.Dmabuf.to_handle)
    and use that handle instead of the dumb-buffer one.

    I added a simple surface abstraction to the test code, wrapping the Window module’s API
    so that the rendering code doesn’t need to care whether it’s rendering to a Wayland window or directly to the screen.
    Then I made a Vt module implementing the new Surface.t type for rendering directly to a Linux VT.

    To get the animation working, I used K.Crtc.page_flip to update the framebuffer (I could also have used the atomic API).
    The kernel waits until the encoder has finishing sending the current frame before switching to the new one,
    which avoids tearing.
    We also need to ask the kernel to tell us when this happens, which is done by setting the optional ~event argument to some number.
    You can read events from the device file and parse them with Drm.Event.parse.

    If you want to try it, this should produce an animated room:

    git clone https://github.com/talex5/vulkan-test -b kms-3d
    cd vulkan-test
    nix develop
    make download-example
    dune exec -- ./src/main.exe 10000 viking_room.obj viking_room.png
    

    If run with $WAYLAND_DISPLAY set, it will open a Wayland window (as before),
    but if run from a text console then it should render the animation directly using KMS.

    Linux VTs

    When the user switches to another virtual terminal (e.g. with Ctrl-Alt-F3),
    we should call Drm.Device.drop_master to give up being the master,
    allowing the application running on the new terminal to take over.

    We should also switch the VT to KD_GRAPHICS mode while using it,
    to stop the kernel trying to manage it.

    I didn’t implement either of these features, but see How VT-switching works for details.

    Debugging

    If you get an unhelpful error code from the kernel (e.g. EINVAL), enabling debug messages is often helpful.
    Writing 4 to /sys/module/drm/parameters/debug enables KMS debug messages, which can be seen in the dmesg output.
    Write 0 to the file afterwards to turn the messages off again.
    modinfo -p drm lists the various options.

    Conclusions

    I hope you found being able to explore the libdrm API interactively from the OCaml top-level
    made it easier to learn about how Linux manages displays.
    As when doing Vulkan in OCaml,
    a lot of the noise from C is removed and I think that the essentials of what is going on are easier to see.

    I used ocaml-ctypes for the C bindings, and this was my first time using it in “stubs” mode
    (where it pre-generates C bindings from OCaml definitions).
    This has the advantage that the C type checker checks that the definitions are correct,
    and it worked well.
    Dune’s Stub Generation feature generates the build rules for this semi-automatically.

    Deciding what OCaml types to use for the C types was quite difficult.
    For example, C has many different integer types (int, long, uint32_t, etc),
    but using lots of types is more painful in OCaml where e.g. + only works on int.
    I used OCaml’s int type when possible, and other types only when the value might not fit
    (e.g. an image size on a 32-bit platform might not fit into an OCaml int, which is one bit shorter).

    The C API is somewhat inconsistent about types.
    e.g. drmModePageFlipTarget takes a uint32_t target_vblank argument for the sequence number,
    while page_flip_handler confirms the event by giving it as unsigned int sequence.
    Meanwhile, the sequence_handler event gives it as uint64_t sequence.
    I’m not sure what happens if the sequence number gets too large to fit in a 32-bit integer.

    Anyway, I think I understand mode setting a lot better now,
    and I’m getting faster at debugging graphics problems on Linux
    (e.g. when element-desktop failed to start recently after I updated it).

    Thanks to the OCaml Software Foundation for sponsoring this work.

    Share. Facebook Twitter Pinterest LinkedIn Reddit WhatsApp Telegram Email
    Previous Article62 chapter open-source Zig book
    Next Article I have recordings proving Coinbase knew about breach 4 months before disclosure
    TechAiVerse
    • Website

    Jonathan is a tech enthusiast and the mind behind Tech AI Verse. With a passion for artificial intelligence, consumer tech, and emerging innovations, he deliver clear, insightful content to keep readers informed. From cutting-edge gadgets to AI advancements and cryptocurrency trends, Jonathan breaks down complex topics to make technology accessible to all.

    Related Posts

    SwifDoo PDF Pro drops to $35—and it’s a lifetime license

    February 8, 2026

    This $60 Bitcoin miner can be run on your desk

    February 8, 2026

    Internet Archive unveils tool to save the web from dead links

    February 8, 2026
    Leave A Reply Cancel Reply

    Top Posts

    Ping, You’ve Got Whale: AI detection system alerts ships of whales in their path

    April 22, 2025658 Views

    Lumo vs. Duck AI: Which AI is Better for Your Privacy?

    July 31, 2025245 Views

    6.7 Cummins Lifter Failure: What Years Are Affected (And Possible Fixes)

    April 14, 2025148 Views

    6 Best MagSafe Phone Grips (2025), Tested and Reviewed

    April 6, 2025111 Views
    Don't Miss
    Technology February 8, 2026

    SwifDoo PDF Pro drops to $35—and it’s a lifetime license

    SwifDoo PDF Pro drops to $35—and it’s a lifetime license Image: StackCommerce TL;DR: SwifDoo PDF Pro gives…

    This $60 Bitcoin miner can be run on your desk

    Internet Archive unveils tool to save the web from dead links

    I’m obsessed with this 3D-printed tray for Costco hot dogs

    Stay In Touch
    • Facebook
    • Twitter
    • Pinterest
    • Instagram
    • YouTube
    • Vimeo

    Subscribe to Updates

    Get the latest creative news from SmartMag about art & design.

    About Us
    About Us

    Welcome to Tech AI Verse, your go-to destination for everything technology! We bring you the latest news, trends, and insights from the ever-evolving world of tech. Our coverage spans across global technology industry updates, artificial intelligence advancements, machine learning ethics, and automation innovations. Stay connected with us as we explore the limitless possibilities of technology!

    Facebook X (Twitter) Pinterest YouTube WhatsApp
    Our Picks

    SwifDoo PDF Pro drops to $35—and it’s a lifetime license

    February 8, 20263 Views

    This $60 Bitcoin miner can be run on your desk

    February 8, 20263 Views

    Internet Archive unveils tool to save the web from dead links

    February 8, 20264 Views
    Most Popular

    7 Best Kids Bikes (2025): Mountain, Balance, Pedal, Coaster

    March 13, 20250 Views

    VTOMAN FlashSpeed 1500: Plenty Of Power For All Your Gear

    March 13, 20250 Views

    This new Roomba finally solves the big problem I have with robot vacuums

    March 13, 20250 Views
    © 2026 TechAiVerse. Designed by Divya Tech.
    • Home
    • About Us
    • Contact Us
    • Privacy Policy
    • Terms & Conditions

    Type above and press Enter to search. Press Esc to cancel.