Frames may be rendered but not displayed on screen; successfully importing a dma-buf does not guarantee it can be directly scanned out. DRM must separately interpret rendering, buffer sharing, and display submission. Devices without monitors may still provide rendering capabilities.
Lookup Goal
Reading Location
User/kernel responsibilities and KMS
Sections 1–2
Submission completion and object handles
Sections 3–4
Sharing synchronization and black screen troubleshooting
Sections 5–6
1. What User Space and Kernel Space Do
User-space drivers (UMD, such as OpenGL/Vulkan implementations in Mesa) handle graphics APIs, shader compilation, and command organization. Kernel drivers (KMD, such as amdgpu, i915, xe, nouveau) provide device access, buffer management, address spaces, scheduling, and synchronization mechanisms; drivers supporting display also implement KMS.
This is a separation of responsibilities, not "the kernel checks commands at all." Validation, isolation, and submission interfaces vary by hardware and driver; UMD must also match the UAPI and hardware capabilities supported by the kernel. Upgrading user space does not automatically add missing kernel features.
Device Node
Common Use
Permission Meaning
/dev/dri/cardN
Primary node, including display control interfaces
Modeset operations are subject to DRM master, lease, and other rules
/dev/dri/renderDN
Rendering and related resource operations
Does not provide modesetting; still subject to file permissions and security policies
card0 is an enumeration name and does not guarantee being the "primary GPU" currently used by the desktop. Render nodes do not require obtaining DRM master, but this does not mean any user can naturally access them. Refer to the DRM User-space API for specific interfaces.
2. KMS: Describing Display Resources and Connections
Plane: Provides an image layer, including framebuffer, source crop, and destination position. Types include primary, overlay, and cursor; cursors should not be uniformly classified as overlays.
CRTC: Manages scan timing and associated planes, outputting images according to modes. This historical name does not imply modern monitors use cathode ray tubes.
Encoder: Abstracts output encoding and routing resources.
Connector: Represents connections to display terminals, capable of reporting connection status, modes, EDID, and other information.
Connections are constrained by hardware resources: planes may not connect to arbitrary CRTCs, and display bandwidth and scaling capabilities are limited. A CRTC can be cloned to multiple outputs if hardware supports it; do not simply assume a one-to-one correspondence between CRTCs and monitors.
A framebuffer is an image layout object, not equivalent to memory allocation itself. It describes dimensions, pixel format, pitch, offset, modifier, and associations with underlying buffers. Even if two buffers have the same byte count, they may not be interchangeable due to different formats or modifiers. See KMS documentation for objects and attributes.
Calculate pitch and offset for a linear image
Consider a 3×2 linear XRGB8888 image, where each pixel occupies 4 bytes; assume a row pitch of 16 bytes, and the image starts at offset=8 of the underlying object. Each row's actual pixels occupy 12 bytes, but the next row advances by 16 bytes. The starting position of pixel (x, y) is offset + y × pitch + x × 4, so the last pixel (2, 1) is located at bytes 32–35, rather than packing the six pixels tightly from 8 to 31.
Interval (inclusive left, exclusive right)
Content
[0, 8)
Object space before this image
[8, 20)
Three pixels of row 0
[20, 24)
Inter-row padding
[24, 36)
Three pixels of row 1
defpixel_span(width,height,pitch,offset,object_size,x,y):ifmin(width,height)<=0oroffset<0orpitch<width*4:raiseValueError("Invalid linear layout")end=offset+(height-1)*pitch+width*4ifend>object_sizeornot(0<=x<widthand0<=y<height):raiseValueError("Pixel or image out of bounds")start=offset+y*pitch+x*4returnstart, start+4assertpixel_span(3,2,16,8,36,2,1)==(32,36)assertpixel_span(3,2,16,8,36,0,1)==(24,28)forargsin[(3,2,16,8,35,2,1),(3,2,8,8,36,0,0),(3,2,16,8,36,3,0),(3,2,16,8,36,0,2)]:try:pixel_span(*args)exceptValueError:passelse:raiseAssertionError("Invalid layout or coordinates should be rejected")
The 36 bytes here are just the mathematical lower bound covering the last valid pixel; real allocations are also affected by driver alignment, page size, and interface requirements. Use the pitch and size returned by the interface. Python integers do not overflow; when porting the formula to C, you must also check for multiplication-addition overflow.
This addressing scheme only applies to the assumed linear, single-format-plane layout. Storage with tile or compressed modifiers cannot be addressed this way; multi-plane pixel formats like NV12 require describing the pitch and offset of each plane separately. The "plane" in pixel formats is also not the same concept as the KMS display overlay Plane: a single KMS Plane can scan out an image composed of multiple format planes.
3. Atomic Modesetting: Validate a Set of States First
User space submits a collection of object attributes via atomic ioctls, such as placing a specific framebuffer on a plane and setting the CRTC mode and connector routing. The kernel constructs states for relevant objects, checking resources, formats, bandwidth, and driver constraints; drm_atomic_state is an internal kernel structure, not a generic C structure allocated directly by user space and passed in.
TEST_ONLY can validate without changing the display state, but passing a test does not reserve resources for future submissions. Actual submissions still require checking current conditions.
The value of atomic is to avoid user space having to modify objects one by one and manually handle unusable intermediate configurations. It does not promise that all hardware registers change in a single CPU instruction, nor does it guarantee that any mode change operation is completely black-screen-free. When involving multiple CRTCs, it cannot be deduced that all monitors physically refresh simultaneously.
Why must combinations be validated, not just each object individually?
Suppose two display pipelines share a teaching bandwidth budget of 10, with each currently consuming 4. When testing A increasing to 6 alone, the total is 6+4=10; testing B increasing to 6 alone also passes. However, the combination of both increasing to 6 requires 12 and must be rejected. Locally valid configurations that pass individually cannot be pieced together into a valid global configuration.
Similarly, after testing A's upgrade passes, if B submits its upgrade first, when A is actually submitted later, it must face the new state 4+6. The significance of TEST_ONLY is to check candidate states, not to reserve that remaining resource. Actual drivers also check conditions such as clocks, routing, scalers, and links; this single number only helps understand shared constraints and cannot be used to calculate specific GPU available display bandwidth.
Returning success vs. screen already displaying
Blocking and non-blocking commits have different wait behaviors. After accepting a non-blocking commit, hardware work can continue; completion must be determined via events or output fences. Input fences can indicate rendering dependencies for new buffers.
Standard page flips often pair with vblank to avoid tearing, but async updates, full modesets, and different driver paths have their own constraints and cannot be uniformly written as "verified, tear-free atomic application on vblank." Reuse of old framebuffers should follow corresponding completion and release protocols and cannot rely solely on the ioctl having returned.
New frame starting to display does not mean the new frame can immediately overwrite
Suppose CRTC A is currently scanning buffer X and preparing to page flip to Y. Y's input fence completion indicates the renderer has finished writing Y; A's page flip completion indicates this display switch has completed, and the old scan usage of X on A can end. At this point, Y is the current scan object, not the object just released. If CRTC B is still scanning X, X cannot be returned to the producer for overwriting just because A has finished flipping.
Below, we record "usage" as (CRTC, buffer) relationships, simulating only one scan layer per CRTC and at most one pending flip per CRTC at any time. Completion notifications are handled according to their respective CRTCs, without assuming two screens complete simultaneously.
classScanout:def__init__(self):self.current={"A":"X","B":"X"}self.pending={}self.ready={"X"}defrender_done(self,buffer):self.ready.add(buffer)defsubmit(self,crtc,new):assertcrtcinself.currentandcrtcnotinself.pendingself.pending[crtc]=new# Accepting request does not mean switch is complete
defcomplete_flip(self,crtc):new=self.pending[crtc]ifnewnotinself.ready:raiseRuntimeError("Input rendering dependency not yet complete")self.current[crtc]=newdelself.pending[crtc]defreusable(self,buffer):returnbuffernotinself.current.values()andbuffernotinself.pending.values()s=Scanout()s.submit("A","Y")assertnots.reusable("X")andnots.reusable("Y")try:s.complete_flip("A")exceptRuntimeError:passelse:raiseAssertionError("Cannot bypass input dependencies")s.render_done("Y")s.complete_flip("A")assertnots.reusable("X")# B is still using X
assertnots.reusable("Y")# A is scanning Y
s.render_done("Z")s.submit("B","Z")assertnots.reusable("X")# ioctl acceptance is still insufficient to release
s.complete_flip("B")asserts.reusable("X")assertnots.reusable("Y")andnots.reusable("Z")
Reusability in the model only indicates no remaining scan or pending submission usage; real systems must also merge release conditions for other consumers such as GPUs and video encoders. OUT_FENCE_PTR belongs to the CRTC and cannot be interpreted as a global "unused" proof for any dma-buf; writeback output fences indicate write completion for the writeback target and should be interpreted according to the corresponding attribute protocol.
Render into A or B, queue a commit, and advance vblank. The screen keeps scanning the current frame until the pending frame meets its fence and presentation conditions.
Preparing the visual
Rendering, accepted commit, and presentation happen separately
Buffer existence does not mean rendering is complete, and successful atomic submission does not mean presentation. The old buffer can be overwritten only after its scanout reference is released.
{"id":"drm-commit-and-scanout","title":"Rendering, accepted commit, and presentation happen separately","summary":"Buffer existence does not mean rendering is complete, and successful atomic submission does not mean presentation. The old buffer can be overwritten only after its scanout reference is released.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><p class=\"caption\" data-i18n=\"choose\"></p><div class=\"two\"><button class=\"buffer\" data-buffer=\"0\"><header>A</header><div class=\"pixels\" id=\"pixels-0\"></div><strong id=\"frame-0\"></strong><span id=\"state-0\"></span><code id=\"fence-0\"></code></button><button class=\"buffer\" data-buffer=\"1\"><header>B</header><div class=\"pixels\" id=\"pixels-1\"></div><strong id=\"frame-1\"></strong><span id=\"state-1\"></span><code id=\"fence-1\"></code></button></div><div class=\"actions\"><button id=\"render\" data-i18n=\"render\"></button><button id=\"finish\" data-i18n=\"finish\"></button></div><div class=\"actions\"><button id=\"test\" data-i18n=\"test\"></button><button id=\"commit\" data-i18n=\"commit\"></button></div><div class=\"actions\"><button id=\"vblank\" data-i18n=\"vblank\"></button></div><div class=\"monitor\"><header data-i18n=\"screen\"></header><div class=\"pixels\" id=\"screen-pixels\"></div><strong id=\"screen-frame\"></strong></div><p class=\"caption\" data-i18n=\"testResult\"></p><div id=\"test-result\" class=\"readout\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.buffer{display:block;min-width:0;padding:12px;font-size:12px;text-align:left}.buffer[aria-current=true]{box-shadow:inset 0 0 0 1px var(--accent)}.buffer header{font:18px var(--viz-font-ui)}.pixels{display:grid;grid-template-columns:repeat(8,1fr);aspect-ratio:2/1;background:var(--surface);margin:10px 0}.pixels i{background:var(--accent)}.pixels i.light{background:var(--second)}.buffer strong,.buffer span,.buffer code{display:block;overflow-wrap:anywhere}.buffer span{min-height:4.8em;margin:8px 0}.monitor{border:2px solid var(--rule);border-bottom-width:6px;border-radius:9px;padding:15px;margin:18px 0;font-size:13px}.monitor .pixels{max-width:260px;margin:14px auto}.monitor strong{display:block;text-align:center;font:18px var(--viz-font-ui)}.readout{min-height:5.3em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let selected=1,front=0,pending=null,frames=[0,0],rendering=[false,false],targets=[0,0],serial=0,tested=null;function paint(id,n){$('#'+id).replaceChildren(...Array.from({length:32},(_,i)=>{const e=document.createElement('i');e.classList.toggle('light',(i%8+Math.floor(i/8)+n)%5<2);return e;}));}function draw(){for(let i=0;i<2;i++){document.querySelector('[data-buffer=\"'+i+'\"]').setAttribute('aria-current',String(i===selected));paint('pixels-'+i,frames[i]);set('frame-'+i,'frame '+frames[i]);set('state-'+i,t(i===front?'front':i===pending?'queued':rendering[i]?'working':'free'));set('fence-'+i,'fence '+Number(!rendering[i]));}paint('screen-pixels',frames[front]);set('screen-frame','AB'[front]+' · frame '+frames[front]);set('test-result',tested===null?t('none'):'AB'[tested]+' · '+t('valid'));$('#render').disabled=selected===front||selected===pending||rendering[selected];$('#finish').disabled=!rendering[selected];$('#commit').disabled=pending!==null||selected===front;$('#vblank').disabled=pending===null;set('explanation',t('note'));}document.querySelectorAll('[data-buffer]').forEach(e=>e.onclick=()=>{selected=+e.dataset.buffer;draw();});$('#render').onclick=()=>{targets[selected]=++serial;rendering[selected]=true;draw();};$('#finish').onclick=()=>{frames[selected]=targets[selected];rendering[selected]=false;draw();};$('#test').onclick=()=>{tested=selected;draw();};$('#commit').onclick=()=>{pending=selected;draw();};$('#vblank').onclick=()=>{if(pending!==null&&!rendering[pending]){front=pending;pending=null;}draw();};$('#reset').onclick=()=>{selected=1;front=0;pending=null;frames=[0,0];rendering=[false,false];targets=[0,0];serial=0;tested=null;draw();};draw();","audio":false,"strings":{"choose":"Select buffer to render or submit","commit":"Queue nonblocking flip","finish":"Finish selected buffer’s render","free":"Available for rendering","front":"Scanning out","heading":"Rendering, accepted commit, and presentation happen separately","intro":"Select buffer A/B, start and finish rendering, then queue a flip. TEST_ONLY does not change the screen. A queued commit waits for its input fence, then presents at model vblank.","none":"Not tested yet","note":"Start rendering B, queue it, then reach vblank: its fence is unfinished, so A stays visible. Finish B, then a later vblank presents it and releases A.","queued":"Queued, awaiting presentation","render":"Start rendering new frame","reset":"Reset","scope":"One CRTC/plane and two buffers with a fixed supported format, ordinary synchronized flips. A nonblocking commit can await a render fence. Presentation combines vblank and this example’s completion notification, omitting full driver phases, async flips, modesets, and multi-display synchronization. Scanout/pending buffers cannot be overwritten.","scopeLabel":"Model scope and assumptions","screen":"Currently presented frame","test":"TEST_ONLY","testResult":"Last TEST_ONLY result","valid":"Layout test passed; no presentation","vblank":"Reach one vblank","working":"GPU rendering"}}
4. GEM: Objects, Handles, and Actual Storage
GEM provides graphics buffer objects and related lifecycle frameworks; allocation methods are determined by the driver and specific interfaces, and there is no universally callable drm_gem_object_create(dev, size) to represent all GPU allocations.
Object storage can come from system memory or VRAM; mechanisms like TTM can manage placement and migration, but not all GEM drivers must use TTM. CPU mapping, GPU mapping, and display scan capabilities are established separately and are not naturally all available after creating an object.
Cross-process sharing: Pass references, do not copy integer handles
GEM handles usually belong to a specific DRM file context. Sending an integer handle to another process that independently opens a DRM device will not automatically point to the same object. Cross-process sharing is often done by exporting a dma-buf fd and passing it via SCM_RIGHTS of a Unix socket; the receiver obtains its own reference and imports it according to the interface. See DRM Memory Management.
5. dma-buf: Sharing Underlying Storage Also Requires Coordinating Access
Example: An application renders a frame, the compositor receives its dma-buf, and decides whether to composite it as a texture into an output buffer or, if conditions are met, directly hand it to the display plane for scanning.
Both paths can share the application's underlying storage, but the latter also requires all constraints on image format, modifier, scaling, position, output color processing, etc., to be met. Successful import does not guarantee direct scanout; cross-device may also require migration or copying, so dma-buf is not an unconditional zero-copy promise.
Handoff Direction
Must Wait For What
Producer hands new frame to consumer
GPU rendering complete before consumer can read
Consumer returns old frame to producer
Display or other consumption complete before producer can overwrite
References guarantee the object still exists, but do not prove there are no read/write races.
Implicit synchronization can track dependencies via reservation objects and fences; explicit synchronization passes sync objects like fences and syncobjs via API/protocol. Which mechanism to choose depends on the entire path. Cache synchronization when CPU accesses dma-buf cannot replace waiting for GPU work completion: cache visibility and task completion are different conditions. See dma-buf documentation.
6. fbdev and Troubleshooting Entry Points
fbdev provides traditional framebuffer interfaces with limited functionality and consistent resource modeling capabilities, but historically there were interfaces for acceleration and waiting for vertical sync, so it is absolutely incorrect to say "no GPU acceleration, no vsync." Modern DRM drivers can provide fbdev compatibility layers based on configuration, and not every system necessarily exposes /dev/fb0.
When troubleshooting, locate issues by boundaries: Did the application successfully submit rendering? Did the fence complete? Did the compositor receive the buffer? Did the KMS configuration pass? Is the connector connected? Do not attribute every black screen to "VRAM not allocated."
connected only indicates connection detection status and does not prove that the output is scanning a valid image; the absence of these directories does not prove the system has no GPU at all, as the device may only provide render capabilities.