Userspace never writes into the GEM buffer directly. It writes into a
vzalloc'ed shadow copy; a workqueue accumulates the dirty rectangle and, on flush, blits that region into the GEM buffer and triggers one atomic commit carrying the damage clip as the plane'sFB_DAMAGE_CLIPSproperty.
write(2):fb_sys_write()copies into the shadow buffer, thendrm_fb_helper_damage_range()turns the byte range into a clip and schedulesdamage_work.- Worker:
drm_fb_helper_damage_work()→drm_fb_helper_fb_dirty()→drm_fbdev_dma_helper_fb_dirty()blits the clip into the GEM buffer, then callsfb->funcs->dirty = drm_atomic_helper_dirtyfb(). mmap()(used by the tests): page faults go through deferred I/O (fb_deferred_io_fault()), which after a delay reaches the same damage work.- fbcon uses a third, equivalent route:
fb_fillrect/fb_copyarea/fb_imageblitdraw into the shadow buffer and calldrm_fb_helper_damage_area()immediately. - Merging old and new damage matters:
drm_atomic_helper_damage_merged()runs in the driver'satomic_update().
Think of /dev/fb0 as a notepad whose real pages live somewhere DRM controls:
- The write lands in a scratch copy. Your bytes are copied into a private shadow buffer in system memory. This is fast and takes no DRM locks, so fbcon can paint at any time without blocking the display pipeline. The real pixel memory (the GEM buffer) is not touched yet.
- The kernel remembers the dirty rectangle. The framebuffer core turns
the byte range you wrote into a rectangle of pixels, merges it into the
helper's damage clip, then schedules a background worker. Writes are
batched: your
write()returns immediately; the actual DRM work happens later. - The worker copies just the dirty region.
drm_fbdev_dma_damage_blitcopies the clip rectangle from the shadow buffer into the GEM buffer's kernel mapping. - A commit is built around the damage. The worker calls the framebuffer's
dirtyhook (drm_atomic_helper_dirtyfb), which creates an atomic state, attaches the clip as the plane'sFB_DAMAGE_CLIPSproperty and commits it. - DRM checks, then commits. The state passes through
atomic_checkand then the commit machinery, which eventually calls the plane'satomic_update()- the driver's moment to program the hardware. This tutorial driver only logs what it sees. - Damage from old and new states is merged, so a rectangle marked dirty twice is reported once, covering both.
write(2) (exercised when a program uses write(); the tests use mmap()):
write(2) /dev/fb0
→ fbmem.c fb_write()
→ info->fbops->fb_write = drm_fbdev_dma_shadowed_defio_write
[generated by FB_GEN_DEFAULT_DEFERRED_DMAMEM_OPS, include/linux/fb.h]
├─ fb_sys_write(): copy_from_user into the shadow buffer
│ (info->screen_buffer, a vzalloc'ed system-memory copy)
└─ drm_fb_helper_damage_range(info, offset, ret)
└─ drm_fb_helper_memory_range_to_clip(): byte range → clip {x1,y1,x2,y2}
└─ drm_fb_helper_damage():
├─ merge clip into helper->damage_clip (spinlock)
└─ schedule_work(&helper->damage_work)
[system workqueue]
drm_fb_helper_damage_work()
└─ drm_fb_helper_fb_dirty()
└─ helper->funcs->fb_dirty = drm_fbdev_dma_helper_fb_dirty()
├─ drm_fbdev_dma_damage_blit() [drm_fbdev_dma.c]
│ └─ drm_fbdev_dma_damage_blit_real()
│ copy the clip rect from the shadow buffer into the
│ GEM buffer's kernel mapping (buffer->map, per scanline
│ using fb->pitches[0])
└─ helper->fb->funcs->dirty(fb, NULL, 0, 0, clip, 1)
= drm_atomic_helper_dirtyfb() [drm_damage_helper.c]
├─ drm_atomic_state_alloc()
├─ for each plane whose plane->state->fb == fb:
│ drm_atomic_get_plane_state()
│ drm_property_replace_blob(&plane_state->fb_damage_clips,
│ damage_blob)
└─ drm_atomic_commit(state) → atomic-commit.md
mmap() (used by the tests) enters through deferred I/O instead:
mmap /dev/fb0 → fb_deferred_io_mmap() (fb_mmap in the shadowed fbops)
→ page fault → fb_deferred_io_fault() [fb_defio.c]
→ fb_deferred_io_track_page(): mark page dirty,
schedule_delayed_work(&info->deferred_work, delay)
→ fb_deferred_io_work()
→ info->fbdefio->deferred_io = drm_fb_helper_deferred_io()
→ drm_fb_helper_memory_range_to_clip()
→ drm_fb_helper_damage() → same damage_work as above
fbcon rendering takes a third, equivalent route: its fb_fillrect /
fb_copyarea / fb_imageblit map to drm_fbdev_dma_shadowed_defio_*, which
draw into the shadow buffer and immediately call
drm_fb_helper_damage_area().
Everything from drm_fb_helper_damage() onward is identical in all three
routes; the mmap() path just skips write(2) and starts at
fb_deferred_io_fault().
- The symbol and file names in the trees above are specific to the WSL2 kernel documented in the root README. They were not re-verified against another kernel version.
- Deferred I/O waits before flushing
mmap'ed pages, so a written pixel is not guaranteed to be committed synchronously.
- fbdev-emulation.md - how the shadow buffer is installed
- atomic-commit.md - what
drm_atomic_commit()does with the clip - gem-dma-framebuffers.md - the GEM buffer being blitted into