Skip to content

Latest commit

 

History

History
122 lines (103 loc) · 5.94 KB

File metadata and controls

122 lines (103 loc) · 5.94 KB

English | 简体中文

A user write to /dev/fb0, end to end

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's FB_DAMAGE_CLIPS property.

TL;DR

  • write(2): fb_sys_write() copies into the shadow buffer, then drm_fb_helper_damage_range() turns the byte range into a clip and schedules damage_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 calls fb->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_imageblit draw into the shadow buffer and call drm_fb_helper_damage_area() immediately.
  • Merging old and new damage matters: drm_atomic_helper_damage_merged() runs in the driver's atomic_update().

In plain words

Think of /dev/fb0 as a notepad whose real pages live somewhere DRM controls:

  1. 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.
  2. 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.
  3. The worker copies just the dirty region. drm_fbdev_dma_damage_blit copies the clip rectangle from the shadow buffer into the GEM buffer's kernel mapping.
  4. A commit is built around the damage. The worker calls the framebuffer's dirty hook (drm_atomic_helper_dirtyfb), which creates an atomic state, attaches the clip as the plane's FB_DAMAGE_CLIPS property and commits it.
  5. DRM checks, then commits. The state passes through atomic_check and then the commit machinery, which eventually calls the plane's atomic_update() - the driver's moment to program the hardware. This tutorial driver only logs what it sees.
  6. Damage from old and new states is merged, so a rectangle marked dirty twice is reported once, covering both.

The exact path

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().

Caveats

  • 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.

Related