Before you start#
Read Wayland objects and wire messages and graphics APIs and presentation. This chapter uses Wayland 1.23.0 and wayland-protocols 1.36 as separate historical source baselines; installed clients and compositors may implement different versions and extensions. No display connection is needed for the exercise.
Learning goals#
Explain when a surface state becomes visible to the compositor, construct the initial xdg-shell configure handshake, distinguish three kinds of completion, and integrate a display socket into an event loop without stranding other readers. Track both a buffer object's protocol lifetime and the lifetime of its backing storage.
Source walkthrough#
- ClientCompositor
Commit initial empty surface
- CompositorClient
Return. Configure with serial
- ClientCompositor
Acknowledge serial; attach and commit
- CompositorBuffer storage
Read or retain attached storage
- CompositorClient
Return. Frame callback and release can arrive separately
Time runs downward. Dashed arrows show returns.
Read the text version
- Client → Compositor: Commit initial empty surface
- Compositor → Client (return): Configure with serial
- Client → Compositor: Acknowledge serial; attach and commit
- Compositor → Buffer storage: Read or retain attached storage
- Compositor → Client (return): Frame callback and release can arrive separately
A client connects to a compositor and obtains proxy objects representing server resources. A wl_surface represents a rectangular area whose contents and relationship to other surfaces the compositor manages; it is not itself a window manager, a pixel allocation, or a guarantee of on-screen visibility. Requests are ordered on the connection, but the client's function returning normally often means only that a request was marshalled, not that the compositor processed it. The protocol descriptions in Wayland 1.23.0's core XML define the actual request and event contracts.
State set by requests such as attach and damage is pending until wl_surface.commit. Commit atomically applies that pending surface state as one transaction from the protocol's viewpoint. This does not mean physical scanout is atomic with the client CPU's stores, or that the frame was ever displayed. A commit can have no new buffer: a client can change other state, or submit the empty initial commit needed by xdg-shell. Pending damage describes areas changed for the compositor's bookkeeping. It is not a permission to overwrite storage that the compositor is still reading. damage_buffer uses buffer coordinates whereas older damage uses surface coordinates; scale and transform affect their relationship. A compositor may repaint more than the requested region.
In wayland.xml, wl_surface.commit, Wayland 1.23.0 core protocol, MIT-style permission notice:
<request name="commit">
<description summary="commit pending surface state">
Surface state (input, opaque, and damage regions, attached buffers,
etc.) is double-buffered. Protocol requests modify the pending state,
as opposed to the active state in use by the compositor.
A commit request atomically creates a content update from the pending
state, even if the pending state has not been touched. The content
update is placed in a queue until it becomes active. After commit, the
new pending state is as documented for each related request.
When the content update is applied, the wl_buffer is applied before all
other state. This means that all coordinates in double-buffered state
are relative to the newly attached wl_buffers, except for
wl_surface.attach itself. If there is no newly attached wl_buffer, the
coordinates are relative to the previous content update.
All requests that need a commit to become effective are documented
to affect double-buffered state.
Other interfaces may add further double-buffered surface state.
</description>
</request>
The description supports reading commit as one content update applied in order, with the buffer applied before other state. It does not say that the display shows all changes at once, so the atomicity described here is a protocol statement only.
A surface needs a role to give it shell semantics. An xdg toplevel is created through xdg_wm_base.get_xdg_surface and xdg_surface.get_toplevel, subject to the protocol's role rules. The client makes an initial commit without a buffer, handles the compositor's configuration events, acknowledges the xdg_surface.configure serial, and only then attaches content and commits. The role-specific events describe suggested size and state; the concluding xdg-surface configure groups them. A serial is a token to acknowledge, not a timestamp or a monotonically comparable application frame number. The xdg-shell 1.36 XML defines this ordering and protocol errors.
The following short excerpt is from stable/xdg-shell/xdg-shell.xml, xdg_surface.configure, wayland-protocols 1.36; the XML has the permissive project copyright and license notice. Omitted surrounding description and the argument declaration are explicit:
Clients should arrange their surface for the new states, and then send
an ack_configure request with the serial sent in this configure event at
some point before committing the new surface.
The first sentence ties the acknowledged serial to an actual received configuration. The second requires acknowledgement before the corresponding content commit, not before receiving the event. If several configurations arrive before the client responds, the protocol allows it to acknowledge only the last. Acknowledging it consumes that serial and all earlier configure serials for this surface; the client still has to obey the acknowledgement rules. Attaching a buffer before the initial configure is a protocol error, not a harmless request to display a default window. A fatal protocol error normally invalidates the connection; the application should report it and destroy its own client-side objects rather than continue sending requests as though the connection were healthy.
- ClientCompositor
get_xdg_surface, get_toplevel
- ClientCompositor
wl_surface.commit with no buffer
- CompositorClient
Return. xdg_surface.configure serial 41
- ClientCompositor
ack_configure 41
- ClientCompositor
attach buffer A, damage, frame request, commit
- CompositorClient
Return. wl_buffer.release A (may precede the callback)
- CompositorClient
Return. wl_callback.done
Time runs downward. Dashed arrows show returns.
Read the text version
- Client → Compositor: get_xdg_surface, get_toplevel
- Client → Compositor: wl_surface.commit with no buffer
- Compositor → Client (return): xdg_surface.configure serial 41
- Client → Compositor: ack_configure 41
- Client → Compositor: attach buffer A, damage, frame request, commit
- Compositor → Client (return): wl_buffer.release A (may precede the callback)
- Compositor → Client (return): wl_callback.done
Storage, descriptors and completion
A shared-memory buffer is described by a wl_shm_pool, backed by a file descriptor shared with the compositor, plus an offset, width, height, stride and format for each wl_buffer. The client creates and sizes suitable shared storage, maps it for CPU writes, creates a pool, and creates buffers within its bounds. The compositor receives its own descriptor through the Unix-socket descriptor channel. The sender can close its local descriptor after successful transfer according to its own resource management; closing that descriptor does not unmap an existing mapping or erase another process's received reference. However, closing it prematurely before marshalling/transmission is unsafe, and it does not make reusing the underlying memory safe. Pool resizing has its own protocol rules; truncating storage underneath a mapping can lead to faults.
A buffer object names storage and metadata; a surface attachment refers to it. The same storage may be accessible through several mappings, so an application must account for aliasing. After committing an attachment, it must not modify storage while the compositor may still use it. wl_buffer.release tells the client that the compositor has finished using that buffer for the relevant committed use and the buffer may be reused under the protocol's constraints. Destroying the proxy is not a substitute for respecting backing-store lifetime. If the same wl_buffer is committed to more than one surface, core Wayland leaves delivery of wl_buffer.release undefined; do not rely on that event to track reuse in this case. Use separate buffer objects and track all outstanding uses, or use wp_linux_buffer_release where available. Destroying a wl_buffer before release is permitted only if its backing storage is not reused or mutated; mutating it before release makes the surface contents undefined.
Linux DMA-BUF is another sharing mechanism. The linux-dmabuf 1.36 XML describes plane descriptors, offsets, strides, format and modifiers. Planes can describe portions of one or more shared allocations; format and modifier describe the layout, not necessarily contiguous CPU pixels. A compositor checks the parameters and whether it can import that combination; errors or asynchronous creation failure are real outcomes. This mechanism does not guarantee direct scanout, a CPU mapping, explicit synchronization, or zero copies. Import, rendering, and scanout choices depend on driver, devices, advertised feedback and synchronization extensions. A received descriptor is a reference to a shared kernel object, not a physical address supplied to the client.
Three events answer different questions. A frame callback is a pacing hint that the client may prepare another update; it is not proof that a previous buffer is free. A buffer release is an ownership/reuse signal, not a presentation timestamp. Presentation feedback, when the optional presentation-time protocol is available, can report presentation or discard; it is separate from both and depends on the extension and compositor. Treating these as interchangeable causes flicker, corruption or needless stalls. Frame callbacks may be delayed for an occluded surface, so an application must not assume a fixed refresh rate or use them as its only general-purpose event source.
In wayland.xml, wl_buffer.release, Wayland 1.23.0 core protocol, MIT-style permission notice:
<event name="release">
<description summary="compositor releases buffer">
Sent when this wl_buffer is no longer used by the compositor.
The client is now free to reuse or destroy this buffer and its
backing storage.
If a client receives a release event before the frame callback
requested in the same wl_surface.commit that attaches this
wl_buffer to a surface, then the client is immediately free to
reuse the buffer and its backing storage, and does not need a
second buffer for the next surface content update. Typically
this is possible, when the compositor maintains a copy of the
wl_surface contents, e.g. as a GL texture. This is an important
optimization for GL(ES) compositors with wl_shm clients.
</description>
</event>
The description allows a release to arrive before the frame callback from the same commit, a different legal order from the worked sequence below. Neither event is a fixed ordering rule, and the release is the reuse signal for the storage.
A worked two-buffer sequence
The following sequence is a protocol model, not a captured execution. Begin with two independently backed buffers A and B, both free. The client creates its role and sends an empty commit. It receives a configuration with serial 41 and acknowledges 41. It draws into A, attaches A, requests a frame callback, damages its changed rectangle, and commits. A is now busy. The compositor schedules work and sends the frame callback while still retaining A. At this point A is still busy: the callback gives permission to start producing another frame, not to overwrite A. The client draws into free B and commits B; B becomes busy too. If the compositor then releases A, A becomes reusable; B remains busy until its own release. With no free storage, the client waits or drops/coalesces work rather than racing the compositor.
- Free
client may draw into the storage; compositor holds no use
- Attached and committed
buffer is busy; the compositor may still read it
- Frame callback
permission to prepare the next frame; the buffer is still busy
- wl_buffer.release
compositor no longer uses it; reuse follows the protocol
- Reuse
rewriting storage is safe only after the release that covers this use
Suppose a new configure serial 42 arrives while B is busy. The application records the new dimensions, allocates appropriately sized storage if needed, acknowledges 42 before the next corresponding surface commit, and retains the old storage until all uses end. A resize does not cancel the compositor's existing reference. If allocation fails, it can report failure and clean up without claiming that a differently sized buffer meets a new size recommendation. Configuration values and application policy determine what content is suitable; this example does not claim every size suggestion is mandatory.
Dispatch and the read-lock protocol
A wl_proxy identifies an object and its event queue. Listener callbacks run when the application dispatches the queue, not automatically whenever bytes become readable. A single connection can have multiple queues; events are routed to the relevant queue while the underlying socket is shared. An application must ensure that listener data and proxies remain alive through callbacks and that destroying an object in a callback does not leave another application thread using stale pointers. The library protects internal display state, but it cannot protect an application's own buffer bookkeeping or make arbitrary callbacks thread-safe. Assign a clear thread or lock ownership rule to each application object.
From Wayland 1.23.0 src/wayland-client.c, wl_display_prepare_read_queue, MIT-style file license, with preceding declarations and following return omitted:
if (!wl_list_empty(&queue->event_list)) {
errno = EAGAIN;
ret = -1;
} else {
display->reader_count++;
ret = 0;
}
The code executes while the display mutex is held. A nonempty queue returns EAGAIN so the application dispatches pending events before sleeping. Otherwise the reader count records an intention to read. Every successful prepare must be paired with wl_display_read_events or wl_display_cancel_read, including timeout, interruption and unrelated-fd readiness paths. If multiple threads prepared, the library coordinates them; leaving one thread prepared and waiting indefinitely can prevent the others from completing their read. This is a synchronization protocol, not an exclusive lock held across the application's entire event loop.
A correct loop dispatches pending events until prepare succeeds, flushes outgoing requests, and polls the display fd together with other sources. If the fd is readable it calls read-events, then dispatches pending callbacks. If poll times out, is interrupted, or returns only another source, it cancels the prepared read before handling that source or trying again. A flush can return EAGAIN; the loop must wait for writability as well as readability and retry, while still balancing its prepare. Fatal socket or display errors require cancellation if prepared and then orderly application teardown. Merely polling followed by a blocking general dispatch can miss the relationship between existing queued events and socket readiness.
Client-side proxy destruction, a protocol destructor request and the server's later delete_id message have related but distinct meanings. An ID must not be reused until libwayland's object-map rules permit it; an application should use the library rather than manually recycle identifiers. On disconnect, callbacks will not provide the releases the application hoped for; stop rendering and destroy local resources after ensuring application threads no longer use them. Server resource cleanup follows connection loss but does not synchronize arbitrary client threads.
Object cleanup and asynchronous configuration
The lifetime of a role also constrains teardown. A client should destroy role-specific objects in the order required by the relevant protocol, then destroy the underlying surface when no longer needed. This ordering prevents the server from observing a role object whose underlying surface has already disappeared. The local proxy and the server resource live in different processes; a local pointer disappearing is not proof that the server has already processed the destructor request. A connection failure can prevent the normal acknowledgement and release sequence, so the client must provide a separate error path that stops rendering and frees its own allocations only after its own workers have stopped using them.
Consider receiving serial 41, then serial 42 before the next commit. The application may choose to respond to 42, discarding the older configuration as allowed by the protocol. It must still apply the intended state and acknowledge the chosen serial before its corresponding commit. It should not derive dimensions from the serial number: size and state come from the role-specific configure events that preceded the final serial. If a renderer needs several milliseconds to allocate a larger buffer, another configure can arrive during allocation. The application can cancel stale work or decide which received configuration it will answer, but it must preserve the correct connection between the acknowledged token and the committed state.
Shared memory introduces a second dimension of lifetime during resize. Suppose A is a region of a pool still read by the compositor while B is a region of a larger replacement pool. Creating B does not release A. The application can begin rendering B only after checking its own worker access, while keeping A's mapping or other backing resource available for its outstanding use. Pool and buffer protocol objects may be destroyed under their documented rules, but doing so does not retroactively turn an in-flight pixel write into synchronized behavior. This distinction is especially important when an allocator recycles the same bytes under a new buffer ID: the ID has changed, but the memory conflict remains.
Practice#
This ordinary Python model sends no display requests and reads no session data. It demonstrates the specific callback-before-release counterexample; it is not a compositor test. Run it in any shell with Python 3:
python3 - <<'PY'
busy = {'A': False, 'B': False}
busy['A'] = True # attach and commit
print('frame callback; A busy:', busy['A'])
assert busy['A'] # callback is not release
busy['B'] = True # second frame
busy['A'] = False # release A
print('reusable:', [k for k, v in busy.items() if not v])
PY
Expected output reports True and then only A. Change the assertion to require A to be free at the callback: it fails, demonstrating why frame pacing cannot prove reuse. As a source-only alternative, use the supplied sequence above and list the pending serial, each buffer's busy flag, and the exact event that changes it after every transition. Deliver a table with the two busy flags and identify the first legal reuse of A. No running compositor, GPU or screen observation is implied.
Knowledge check#
- Why is the first xdg toplevel commit empty, and which serial is acknowledged?
- Can a frame callback make A writable while the compositor retains A?
- What must happen after a successful prepare-read if poll times out?
- Why does closing a local shared-memory fd not prove the memory is reusable?
- How do DMA-BUF import failure and presentation failure differ?
Answers#
- It requests initial configuration without attaching content; the client acknowledges a received
xdg_surface.configureserial before its corresponding content commit. The serial connects its response to compositor state. - No. The callback paces production; only the appropriate buffer release and all other outstanding aliases/uses make the storage reusable.
- Cancel the prepared read. Otherwise reader coordination can stall; afterward the application may process its timeout and prepare again.
- Mappings and transferred references have separate lifetimes. The compositor can still hold and read the shared storage, so reuse follows the release contract.
- Import can fail before a usable compositor buffer is created. A successfully imported buffer can still be discarded, composited, or fail presentation for other reasons; import is not presentation proof.
Checkpoint#
Produce the two-buffer state table and an event-loop pseudocode listing every successful prepare's read-or-cancel path. Pass when the table leaves A busy at the frame callback, pairs serial 41 correctly, and handles timeout and disconnect without claiming display. If any transition is ambiguous, reread the two quoted source contracts and repeat the model; this checkpoint is practice, not certification.
Sources#
The pinned core protocol XML, client implementation, xdg-shell XML, and linux-dmabuf XML define these version-scoped examples. Optional protocol availability and actual runtime behavior remain compositor-dependent.