Skip to content

feat(dri3): serve DRI3 1.4 syncobjs through DRM - #122

Merged
joske merged 19 commits into
joske:masterfrom
ariel3259:dri3-syncobj-drm-signal
Aug 12, 2026
Merged

feat(dri3): serve DRI3 1.4 syncobjs through DRM#122
joske merged 19 commits into
joske:masterfrom
ariel3259:dri3-syncobj-drm-signal

Conversation

@ariel3259

Copy link
Copy Markdown
Contributor

DRI3 1.4 syncobj: signal through DRM, not Vulkan

Serves DRI3 1.4 syncobjs through the kernel's DRM syncobj interface on every driver, replacing the Vulkan timeline-semaphore path. The NVIDIA blacklist that capped DRI3 at 1.3 is deleted; PresentPixmapSynced is advertised whenever the kernel reports DRM_CAP_SYNCOBJ_TIMELINE.

What changed

  • ImportedSyncobj — a DRM-backed syncobj resource (syncobj_timeline_signal/query, syncobj_eventfd, fd_to_syncobj) plus Device::open_render_node.
  • One-device invariant — every syncobj ioctl and the capability query run on the retained render-node Device (never the KMS node).
  • Split registry — fences keep the Vulkan OwnedSemaphore; syncobjs move to HashMap<u32, (ClientId, Arc<ImportedSyncobj>)>.
  • Conformance — owning client, PresentPixmapSynced Value errors, disconnect purge, duplicate-import and cross-client ownership checks. Error codes follow Xorg (BadIDChoice, BadDrawable, BadValue, BadAccess, BadAlloc).
  • CapabilityDri3Caps::syncobj derives from DRM_CAP_SYNCOBJ_TIMELINE on the render node; supports_dri3_syncobj() blacklist removed.
  • OwnedSemaphore reduced to the XSync fence half; the Vulkan syncobj import and timeline_semaphore(true) feature removed.

Validation (nvidia box, 2026-08-10)

  • Full-Mesa: yserver on the Raphael iGPU (card1, RADV) + mpv Vulkan X11 WSI. DRI3::QueryVersion -> 1.4, 6 ImportSyncobj imports, 192 synced presents with 1 acquire deferred and subsequently signalled, 0 unknown syncobj, 0 DRM eventfd unavailable fallback warnings — the spec's merge gate is satisfied.
  • IGT: full syncobj_* suite passes 212/212 (0 SKIP) on nvidia-drm with CONFIG_SW_SYNC exposed via /sys/kernel/debug/sync/sw_sync.
  • cargo clippy --all-targets -- -D warnings clean; yserver-core 1139 and yserver 780 tests pass (plus 62 DRM #[ignore] passing on real render nodes).

Internal divergence from Xorg (not client-visible)

yserver does not clone Xorg's resource-type machinery for DRI3 syncobjs: they live in a backend HashMap<u32, (ClientId, Arc<ImportedSyncobj>)> with an owner field, instead of first-class X resources. This is internal only — error codes follow Xorg, and the observable behaviour (ownership checks, disconnect purge, protocol validation) is reproduced via the owner field. Justified by the HLD non-goal "being a drop-in clone of Xorg internals". See the spec's "Divergence from Xorg" section.

dri3_1.4/ is not part of this PR

A local reference-documentation directory (DRI3/Present protocols, kernel DRM docs) gathered during research. Excluded via .gitignore (/dri3_1.4/); the derived knowledge lives in the spec and plan in this repo.

ariel3259 and others added 19 commits August 10, 2026 19:29
yserver caps DRI3 at 1.3 on NVIDIA proprietary via a driver blacklist
added in 8c68a28 with an empty commit body, so ImportSyncobj and
PresentPixmapSynced are never offered. status.md:316 records the cost:
the PresentPixmapSynced branch of the merged deferred-Present design
has never run on hardware.

Three measurements on the nvidia box settle the design. nvidia-drm
serves the whole DRM syncobj path (12/12 on both nodes).
vkImportSemaphoreFdKHR on a DRM syncobj fd returns
VK_ERROR_INITIALIZATION_FAILED, so the gate's premise was right --
OPAQUE_FD is a driver-private format and the spec never promised it is
a syncobj, which also means no capability query can predict this. And
libGLX_nvidia resolves import_syncobj plus present_pixmap_synced once
per swapchain against a 1.4 server, so a real client here is waiting
on explicit sync.

Signal through DRM_IOCTL_SYNCOBJ_TIMELINE_SIGNAL on every driver
rather than branching. vkSignalSemaphore is already a host operation
and acquire already waits on a kernel eventfd, so the swap is
CPU-to-CPU; the GPU-side plan the Phase 4.2 design specified was
abandoned in b92b3dd and cannot return while one queue is shared with
every client. Splits OwnedSemaphore so the XSync fence half keeps
Vulkan, which it genuinely needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six tasks: introduce ImportedSyncobj, split the syncobj registry off the
fence registry, move the deferred-acquire polling to the DRM timeline,
strip the syncobj half out of OwnedSemaphore, derive the capability from
DRM_CAP_SYNCOBJ_TIMELINE, then validate on hardware.

The capability flip is deliberately last: it is the step that starts
advertising 1.4, so everything it exposes must already be wired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine blocking findings across both reviewers, all reproduced before
accepting. The three that changed content rather than wording:

The spec claimed the 2026-05-20 gate was added with no recorded reason.
False -- the same commit added a doc comment above the function
(vk/device.rs:78-85) naming ERROR_INITIALIZATION_FAILED exactly. The
Vulkan probe corroborated a documented rationale rather than discovering
one; its value is that the failure still reproduces on 610.57.04.

The spec claimed DRM and Vulkan both reject a stale timeline point, so
the swap was semantically identical. Measured false: the kernel clamps to
max(current, point) and returns success, and the Vulkan side is a VUID
rather than an error return. Out-of-order releases are silent in both.
That invalidated the risk framing and one hardware-validation check,
which predicted an ENOENT that cannot occur. It also cuts in the change's
favour at shutdown, where wakes flush in HashMap order.

Tasks 2 and 3 merge: the acquire path reads the same registry the split
moves, so separate commits left every synced present failing on Mesa at
the boundary while still compiling and passing tests.

All three docs/status.md citations were wrong -- read while checked out
on glx-extension-string-terminator, where that file is longer. Corrected
to 297, 548 and 4063, and the plan now says to re-verify after any branch
switch.

Also: Device::open takes DRM master so no test could open a render node
(adds open_render_node); two existing tests break silently and are now
named; signal_vk goes dead and would fail clippy; ImportSyncobj logs
nothing on success, so the validation grep returned 0 exactly when the
feature worked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Problem section claimed PresentPixmapSynced had never run on
hardware, citing a status.md line that says "structurally untestable on
this box". Dropping that qualifier inverted the risk: status.md:407-424
records the path passing on bee and silence with 2,221 synced requests,
473 deferred and all 473 signalled. This change replaces the release
signal and the readiness poll on the one stack where the path is proven,
so a Mesa run is now a merge gate rather than a nice-to-have.

"Which fd to ask" was left as a preference and the plan then split it:
capability on the render node, ImportedSyncobj on platform.device, the
KMS node. That advertises 1.4 on the strength of one device and operates
another. Now an invariant with the three cases tabulated, including why
the wrong one is the easy one to write by accident.

Four Xorg protocol deviations become reachable once the 1.3 cap lifts,
and they share one root cause: Xorg makes a syncobj a real X resource
(dri3/dri3.c:106) while yserver uses a HashMap with no owning client.
Verified against the local checkout -- LEGAL_NEW_RESOURCE
(dri3_request.c:609), fd<0 -> BadValue (:619), FreeSyncobj through
dixLookupResourceByType with client access (:634), VERIFY_DRI3_SYNCOBJ
plus BadValue (present_request.c:296-302). Two are not cosmetic: one
client can free another's syncobj, and an unknown acquire syncobj leaves
the client blocked with neither reply nor error. The teardown leak moves
in scope because it shares that cause.

Task 1 could not pass its own gate: ImportedSyncobj has no non-test
caller until Task 2, so clippy --all-targets -D warnings fails on the
lib target with dead_code, which the test module does not rescue.

Also found while revising: the plan hardcoded /dev/dri/renderD128 in
three test helpers. kms/render_node.rs:1-8 forbids exactly that -- "on
multi-GPU hosts that selects the wrong device" -- and the box became one
on 2026-08-08, so a run meant to validate Mesa would have exercised
nvidia-drm and reported green.

Hardware: the Ryzen 7700 iGPU is up with RADV on card1/renderD129,
syncobjprobe passes 12/12 there, and cross-device syncobj sharing is
measured working both directions. The display now runs off the board
port, which unblocks the Mesa run and blocks the NVIDIA one instead.
Recorded with it: resolve_drm_device picks the card by connected
connector while pick_physical_device scores by device type, so yserver
launched here with no overrides now scans out on AMD and renders on
NVIDIA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan now, per the round-3 review and the DRM/DRI3 1.4/Present 1.4
documentation:

- serves every syncobj ioctl and the DRM_CAP_SYNCOBJ_TIMELINE query on a
  retained render-node Device (the spec's one-device invariant), never the
  KMS node
- implements the spec's protocol-conformance scope decision as Task 3:
  owning client, BadAlloc/BadValue error paths, PresentPixmapSynced Value
  errors, disconnect purge
- declares deliberate divergence from Xorg (spec section 'Divergence from
  Xorg'): no resource-type machinery, own error codes; only
  PresentPixmapSynced Value errors are protocol-mandated
- adds IGT GPU Tools and the two mandatory env overrides to hardware
  validation (Task 6)
- fixes the acquire path as a fifth registry consumer and the wire tests
  against the permissive install_client fixture

.gitignore: exclude the local dri3_1.4 reference docs (not part of the repo).
PresentPixmapSynced's Value error now carries the offending syncobj xid as
the bad-value argument when a syncobj lookup fails, and 0 for point/order
failures — matching Xorg's VERIFY_DRI3_SYNCOBJ (client->errorValue = id)
and present_request.c's unset errorValue. This is the error ARGUMENT, not
the code, so it is outside the declared divergence.

RecordingBackend's dri3_capabilities was going to hardcode a (1,4)/syncobj
surface globally, which would have broken dri3_hidden_when_caps_unsupported
(DRI3 must stay absent from QueryExtension on the default backend). It is
now a dri3_caps field defaulting to unsupported, with a syncobj_cap_backend()
helper the conformance tests opt into.
A DRI3 syncobj is a kernel object and every operation the server needs on
one -- signal, query, eventfd -- has a DRM ioctl. Importing it into a
VkSemaphore only works where the driver's OPAQUE_FD payload happens to be
a DRM syncobj, which is false on NVIDIA proprietary.

Device::open_render_node comes along because Device::open takes DRM
master, which a render node refuses, so there was no way to reach these
ioctls from a test.

Not wired up yet; the registries still hold OwnedSemaphore.
FenceFromFD and ImportSyncobj registered two different X resource types
into one HashMap of OwnedSemaphore. Only the fence half needs Vulkan
(FDFromFence exports a sync_file from the VkSemaphore); the syncobj half
needs a DRM handle. Split them so the type mismatch is unrepresentable.

PlatformBackend now retains a Device over the render node, and every
syncobj ioctl runs on it — the same fd kind DRI3 hands clients — instead
of the KMS node (which only answers for the display device on split-GPU
boxes).

The acquire path moves in the same commit rather than a later one: it
looks up the same map, so splitting the two would leave every synced
present failing on Mesa at the intermediate commit.

FDFromFence now looks up the registry before the Vulkan gate, so an
unknown xid reports as unknown rather than as a Vulkan failure.
The spec scoped this in as the minimum that makes DRI3 1.4 safe to
advertise: a client can currently free another client's syncobj, and
PresentPixmapSynced with an unknown acquire syncobj gets no reply and no
error — a silent hang indistinguishable from a server crash.

The registry now carries the importing client. ImportSyncobj validates the
xid and the fd (BadAlloc); FreeSyncobj enforces ownership (BadValue); and
PresentPixmapSynced verifies both syncobjs and the point ordering per
presentproto 1.4 (Value errors). Error codes deliberately diverge from
Xorg's BadIDChoice/BadAccess: the protocol is silent there and no client
branches on the distinction (spec 'Divergence from Xorg'). client_disconnected
purges the owning client's syncobjs, closing the teardown leak.
OwnedSemaphore carried a retained DRM handle, an eventfd registration, a
timeline query and a host-signal that existed only for the syncobj half,
now served by ImportedSyncobj. What remains is the XSync fence resource
it always was.

import_drm_syncobj goes with it: vkImportSemaphoreFdKHR on a DRM syncobj
fd is a Mesa-only accident, not a portable interop path.
supports_dri3_syncobj() blacklisted NVIDIA_PROPRIETARY, capping DRI3 at
1.3 and leaving PresentPixmapSynced untestable on the nvidia box. The
blacklist was a correct response to a real failure, recorded in its own
doc comment -- vkImportSemaphoreFdKHR rejects DRM syncobj fds on that
driver. It stops being the right question once nothing imports them.

The capability now comes from DRM_CAP_SYNCOBJ_TIMELINE on the render
node, cached at init because dri3_capabilities sits on the per-request
path. The capability and every syncobj ioctl run on the same retained
render-node Device, per the spec's one-device invariant.
Only the BadAlloc branch logged, so a working explicit-sync client was
indistinguishable from no client at all.
The DRI3 syncobj cap moved off the NVIDIA_PROPRIETARY driver branch and
now derives from DRM_CAP_SYNCOBJ_TIMELINE on the render node, so DRI3
1.4 and ImportSyncobj/PresentPixmapSynced are advertised on the nvidia
box. Hardware validation of the path is still pending.
The PresentPixmapSynced path is no longer 'structurally untestable on this
box': the DRI3 syncobj cap derives from DRM_CAP_SYNCOBJ_TIMELINE on the
render node, and a full-Mesa run on the Raphael iGPU (card1/RADV) passed
the spec's merge gate. yserver on card1 + mpv Vulkan X11 WSI reproduced a
15s testsrc; the log shows DRI3 1.4 advertised, 6 ImportSyncobj imports,
192 synced presents with 1 deferred acquire (subsequently signalled) and 0
fallback warnings.

IGT: syncobj_basic 12/12 on nvidia-drm; eventfd/wait/timeline skip because
the 7.1 kernel compiles CONFIG_SW_SYNC but does not expose /dev/sw_sync.
The full-Mesa validation on the nvidia box (card1/RADV) passed the spec's
merge gate: DRI3 1.4 advertised, 6 ImportSyncobj imports, 192 synced
presents with 1 deferred acquire (subsequently signalled), 0 fallback
warnings. The final-review fix wave (duplicate-xid rejection and the
client-scoped PresentPixmapSynced ownership check) is also in.
With CONFIG_SW_SYNC exposed at /sys/kernel/debug/sync/sw_sync (symlinked
to /dev/sw_sync for IGT 2.3), the syncobj_* suite passes 212/212 (0 SKIP)
on nvidia-drm: syncobj_basic 12, syncobj_eventfd 10, syncobj_wait 69,
syncobj_timeline 121. The earlier eventfd/wait/timeline skips were purely
the missing sw_sync device node. IGT's device filter does not match cards
in this environment, so amdgpu coverage stays with the yserver
imported_syncobj tests (3/3 on renderD129).
@ariel3259
ariel3259 force-pushed the dri3-syncobj-drm-signal branch from 2c1d474 to d16bed4 Compare August 10, 2026 22:33
@joske
joske merged commit 5b730f7 into joske:master Aug 12, 2026
1 check passed
@joske

joske commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Nice, thx. There are some minor fixups needed, but I'll tackle those rather than ask for a few roundtrips.

ariel3259 pushed a commit to ariel3259/yserver that referenced this pull request Aug 12, 2026
Two observability/robustness fixes on the DRI3 1.4 syncobj path landed in
joske#122. Neither changes protocol behaviour.

**Latch the eventfd fallback.** `Dri3Caps::syncobj` derives from
DRM_CAP_SYNCOBJ_TIMELINE, but the acquire path needs DRM_IOCTL_SYNCOBJ_EVENTFD
-- a much later addition. FreeBSD's drm-kmod satisfies the first and not the
second, so we advertise DRI3 1.4, clients use PresentPixmapSynced, and every
acquire then failed the ioctl, logged a warning, and fell back to the 1ms
timeline poll. One warning per frame, on a path where per-frame logging is
known to perturb the timing it reports (it is why the per-Present timeline is
aggregate-only).

The ioctl is now skipped once it has proven unavailable, and the warning
fires once, naming the consequence for the whole session. Only ENOTTY /
ENOSYS / EOPNOTSUPP latch: a transient per-call failure must not disable
kernel notification for the rest of the session.

Observed on FreeBSD (logs on ZFS, not retrievable from Linux); Linux runs
here show zero fallbacks, which is why it did not surface earlier.

**Log successful FreeSyncobj.** Only the error paths logged, so an import
count could not be matched against a free count and "are retired swapchain
syncobjs released?" was unanswerable from a capture. It cost a review
round-trip: 18 imports with no visible frees read as a leak. With the line,
one mpv session answered it -- 12 imports, 12 frees, exact XID pairing, net
zero at exit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ariel3259 pushed a commit to ariel3259/yserver that referenced this pull request Aug 12, 2026
Two defects found reviewing joske#122, both "identity tracked by XID where Xorg
tracks it by reference", neither reachable by a well-behaved client:

1. The release syncobj is re-resolved by XID at completion (backend.rs:1198,
   :19450) rather than pinned at accept. A free+reimport of that XID while the
   Present is parked makes us signal the replacement: the real waiter hangs and
   an unrelated object gets a spurious signal. The acquire path already pins the
   Arc correctly (backend.rs:13188), so release is the outlier.
2. The syncobj XID is never registered in the server's XID namespace, so
   xid_in_use and used_xids_in are blind to it -- CreatePixmap can reuse a live
   syncobj's XID (Xorg: BadIDChoice), and XC-MISC GetXIDRange can hand it out.

They are linked: xcb allocates XIDs monotonically so clients do not recycle their
own, and XC-MISC is the realistic path to reuse -- which defect 2 leaves blind.
Fixing 2 makes 1 unreachable; fixing 1 makes it impossible.

Plan covers the design (carry Arc<dyn SyncobjHandle> in PresentWake, dropping
Copy -- CompletedPresentEvent is already only Clone so the ripple is ~12 sites),
the core-side XID mirror, and a regression test that frees and reimports the
release XID while a Present is parked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ariel3259 pushed a commit to ariel3259/yserver that referenced this pull request Aug 12, 2026
…xid_occupied

Second codex pass. Four new corrections, all verified in source:

1. A NINTH site: process_request.rs:9359 does `let wake = pending.request.wake()`
   before the copy-failure reroute, distinct from the XID signal at :9394. Also
   records that production has exactly two PendingPresentPixmap constructions
   (:9957 ordinary, :10266 synced) and that the destructuring at :9149 must carry
   the wake through.

2. The authoritative occupancy predicate ALREADY EXISTS: ServerState::xid_occupied
   (server.rs:1677), whose doc says outright "extend this (and the
   xid_occupied_covers_every_namespace test) when adding an XID-keyed map".
   joske#122 added an XID-keyed map and did not -- so defect 2 is a missed documented
   obligation, not a design gap. What is missing is a legal_new_resource helper;
   most creation handlers bypass xid_occupied entirely, which is why ImportSyncobj
   can still collide with extension-only maps.

3. Close-down is now ANSWERED rather than deferred. Xorg RETAINS syncobjs:
   dri3_syncobj_type is created without RC_NEVERRETAIN (dri3/dri3.c:106), so they
   follow ordinary resource semantics under RetainPermanent/RetainTemporary. Our
   unconditional purge in Backend::client_disconnected is therefore wrong for
   retained clients, and KillClient cannot currently name a zombie via a syncobj.

4. RecordingBackend needs more than identity-bearing handles: enqueue_present_-
   completion is a no-op default, signal_present_wake does not signal a retained
   handle, and try_present_direct discards the event. Only the four pre-completion
   paths are writable without that machinery; normal completion and direct scanout
   are not. The plan no longer claims otherwise.

Also: dropping Copy affects four shared-reference matches, not two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ariel3259 pushed a commit to ariel3259/yserver that referenced this pull request Aug 12, 2026
… namespace (codex)

Authored by codex; committed on its behalf.

Follows the plan in docs/superpowers/plans/2026-08-12-dri3-syncobj-identity.md,
with one improvement over it: syncobj records live in ResourceTable as a ninth
namespace rather than a ServerState mirror. That removes the two-map drift the
plan worried about, and makes xid_in_use, collect_xids_in, ownership lookup and
close-down retention apply for free.

Two defects from the joske#122 review, both "identity tracked by XID where Xorg tracks
it by reference", neither reachable by a well-behaved client:

1. The release syncobj was re-resolved by XID at completion, so a free+reimport
   of that XID while a Present was parked made us signal the replacement -- the
   real waiter hangs, an unrelated object gets a spurious signal. Pending
   Presents now pin the exact Arc at accept and no deferred path re-resolves.
   PendingPresentRequest::wake() is deleted so the compiler enforces it.

2. Syncobj XIDs were absent from the server's XID namespace, so CreatePixmap
   could reuse a live syncobj's XID (Xorg: BadIDChoice) and XC-MISC could hand
   it out. They now participate in collision checks, XC-MISC allocation,
   ownership lookup and retained-client cleanup.

Close-down semantics now match Xorg, which retains syncobjs (dri3_syncobj_type
carries no RC_NEVERRETAIN, dri3/dri3.c:106): the unconditional purge in
Backend::client_disconnected is gone, disconnect hands core an explicit
freed_dri3_syncobjs list, and KillClient(syncobj XID) / KillClient(AllTemporary)
destroy retained syncobjs.

Adds identity-reuse, reverse-XID-collision, retention, namespace and KillClient
regressions.

Verified here, not just reported: cargo +nightly fmt clean; cargo clippy
--all-targets -- -D warnings clean; 2273 passed / 177 ignored / 0 failed
(+16 over baseline). Hardware: a full mpv --gpu-api=vulkan swapchain lifetime on
silence -- 2683 PresentPixmapSynced accepted, 204 deferred acquires all
signalled, 6 imports and 6 frees pairing to net zero at exit, no protocol errors.

Not covered by any of that: the identity bug itself needs XID reuse mid-flight,
which no real client does, so it rests on the unit tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants