Retain external DMA-BUF ownership after GPU quarantine

This commit is contained in:
Leask Wong committed 2026-10-11 14:05:56 -04:00
1 parent 9e6fe314ae
commit 3c3639100f
9 files changed
+384 -19

No files matched your search

@@ -86,6 +86,9 @@ static struct ahb_buf *find_buf(uint64_t id) {
}
static void buf_free(struct ahb_buf *ab) {
/* A display release does not prove the compositor's Vulkan reads finished.
* Keep the AHB reference and its DMA-BUF record even if the client is gone. */
if (vkp_completion_unknown()) return;
if (ab->resource) { wl_list_remove(&ab->resource_destroy.link); ab->resource = NULL; }
if (ab->fence_src) { wl_event_source_remove(ab->fence_src); ab->fence_src = NULL; }
if (ab->fence_fd >= 0) { close(ab->fence_fd); ab->fence_fd = -1; }
@@ -112,6 +115,7 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) {
/* ---- release: SurfaceFlinger is done with the buffer -> the game's next acquire waits, then wl_buffer.release */
static void send_deferred_release(struct ahb_buf *ab) {
if (vkp_completion_unknown()) return;
if (ab->release_pending && ab->resource)
droiddeck_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
ab->release_pending = 0;
+39 -17
View File
@@ -377,6 +377,18 @@ static void perf_note_release(int64_t since_ns) {
/* ------------------------------------------------------------------ buffer release pacing */
/* Completion-unknown is terminal for externally owned DMA-BUF contents too.
* Keeping a VkImage alone cannot stop the client from overwriting its backing
* memory after wl_buffer.release. All paths (including cursors and idle roles)
* use this gate; copied SHM buffers retain their normal release behaviour. */
static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer);
static int buffer_release_blocked(struct wl_resource *buffer) {
return vkp_completion_unknown() && get_dmabuf(buffer) != NULL;
}
static void send_buffer_release(struct wl_resource *buffer) {
if (!buffer_release_blocked(buffer)) wl_buffer_send_release(buffer);
}
static void pending_release_free(struct pending_release *pr) {
if (pr->surface && pr->surface->releases_pending > 0) pr->surface->releases_pending--;
wl_list_remove(&pr->destroy.link);
@@ -384,7 +396,8 @@ static void pending_release_free(struct pending_release *pr) {
free(pr);
}
/* A surface is going away: its queued releases still go out on time, they just stop counting. */
/* A surface is going away: detach its accounting pointer, including queued
* DMA-BUF releases held in the completion-unknown state. */
static void pending_releases_forget_surface(struct surface *s) {
struct pending_release *pr;
wl_list_for_each(pr, &g_pending_releases, link)
@@ -401,7 +414,8 @@ static void arm_release_timer(void) {
int64_t earliest = 0;
if (g_release_timer_fd < 0) return;
wl_list_for_each(pr, &g_pending_releases, link)
if (!earliest || pr->at_ns < earliest) earliest = pr->at_ns;
if (!buffer_release_blocked(pr->buffer) && (!earliest || pr->at_ns < earliest))
earliest = pr->at_ns;
struct itimerspec its = {0};
if (earliest) { its.it_value.tv_sec = earliest / 1000000000LL; its.it_value.tv_nsec = earliest % 1000000000LL; }
timerfd_settime(g_release_timer_fd, TFD_TIMER_ABSTIME, &its, NULL);
@@ -413,8 +427,8 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
int64_t now = now_ns() + 300000; /* anything due within 0.3 ms goes now */
if (read(fd, &expirations, sizeof(expirations)) < 0 && errno != EAGAIN) return 0;
wl_list_for_each_safe(pr, tmp, &g_pending_releases, link) {
if (pr->at_ns <= now) {
wl_buffer_send_release(pr->buffer);
if (pr->at_ns <= now && !buffer_release_blocked(pr->buffer)) {
send_buffer_release(pr->buffer);
perf_note_release(pr->since_ns);
pending_release_free(pr);
}
@@ -435,6 +449,9 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
* delivered (a swapchain rebuild destroys buffers with queued releases) or that a coarser earlier
* limit had spaced out, and the game's first frames after that waited on empty slots. */
static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) {
/* No allocation or immediate-release fallback is allowed for a quarantined
* DMA-BUF. Existing queued releases stay held; their timer is disarmed. */
if (buffer_release_blocked(buffer)) return;
const int limit = g_fps_limit;
/* Without a limit of its own a game is paced to the screen. A swapchain that does not wait for
* frame callbacks (MAILBOX, IMMEDIATE) is held back by nothing else here, so handing a replaced
@@ -443,7 +460,7 @@ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_
* One release per refresh is the back-pressure X11 gives, and the screen shows the same frames
* either way. */
const int64_t interval = limit > 0 ? 1000000000LL / limit : g_refresh_ns;
if (interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
if (interval <= 0 || g_release_timer_fd < 0) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
int64_t now = now_ns();
int64_t at = s->next_release_ns + interval;
int64_t latest = now + interval * (int64_t)(s->releases_pending + 1);
@@ -451,7 +468,7 @@ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_
if (at > latest) at = latest;
s->next_release_ns = at;
struct pending_release *pr = calloc(1, sizeof(*pr));
if (!pr) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
if (!pr) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
pr->buffer = buffer;
pr->surface = s;
pr->at_ns = at;
@@ -586,7 +603,10 @@ static void constraints_focus_entered(struct wl_resource *target, struct wl_clie
/* ------------------------------------------------------------------ dmabuf buffers */
void dmabuf_buffer_unref(struct dmabuf_buffer *b) {
if (!b || --b->refs > 0) return;
/* Surface/client destruction must not close the backing FDs or free the
* imported-buffer record while a GPU read may still be pending. Retain the
* reference without allocating a quarantine node; process exit reclaims it. */
if (!b || vkp_completion_unknown() || --b->refs > 0) return;
vkp_image_destroy(b->img);
for (int i = 0; i < b->n_planes; i++)
if (b->fd[i] >= 0) close(b->fd[i]);
@@ -677,7 +697,8 @@ static void unmap_toplevel(struct surface *s) {
/* Let go of the surface's dmabuf content. paced = 1: the buffer was replaced, give it back on the
* limiter's cadence; paced = 0: the surface is going away, give it back at once (a buffer of a
* destroyed surface is not shown again and must not sit unreleased with the client). */
* destroyed surface is not shown again). Completion-unknown overrides both
* release modes: prior GPU reads may outlive the surface. */
static void drop_dmabuf(struct surface *s, int paced) {
if (s->dmabuf) {
wl_list_remove(&s->dmabuf_destroy.link);
@@ -685,7 +706,7 @@ static void drop_dmabuf(struct surface *s, int paced) {
* ahb_swapchain.c releases it then. */
if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
if (paced) release_buffer(s, s->dmabuf, now_ns());
else wl_buffer_send_release(s->dmabuf);
else send_buffer_release(s->dmabuf);
}
s->dmabuf = NULL;
}
@@ -735,7 +756,7 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
vkp_image_upload_shm(s->shm_img, wl_shm_buffer_get_data(shm), stride);
wl_shm_buffer_end_access(shm);
}
wl_buffer_send_release(buffer);
send_buffer_release(buffer);
s->buf_w = w;
s->buf_h = h;
s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888;
@@ -753,7 +774,7 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou
}
if (!s->shm_img) s->shm_img = vkp_image_create_shm(1, 1);
if (s->shm_img) vkp_image_upload_shm(s->shm_img, bgra, 4);
wl_buffer_send_release(buffer);
send_buffer_release(buffer);
s->buf_w = 1;
s->buf_h = 1;
s->buf_alpha = bgra[3] != 0xff;
@@ -880,8 +901,9 @@ void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b) { return &b->ahb_state
void droiddeck_dmabuf_ref(struct dmabuf_buffer *b) { b->refs++; }
void droiddeck_dmabuf_unref(struct dmabuf_buffer *b) { dmabuf_buffer_unref(b); }
void droiddeck_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns) {
if (buffer_release_blocked(buffer)) return;
if (paced && s) release_buffer(s, buffer, since_ns);
else { wl_buffer_send_release(buffer); perf_note_release(since_ns); }
else { send_buffer_release(buffer); perf_note_release(since_ns); }
}
void droiddeck_surface_describe(const struct surface *s, char *out, size_t size) { describe(s, out, size); }
const struct droiddeck_color *droiddeck_surface_color(const struct surface *s) {
@@ -1046,7 +1068,7 @@ static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) {
struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer);
int64_t px = db ? (int64_t)db->width * db->height
: shm ? (int64_t)wl_shm_buffer_get_width(shm) * wl_shm_buffer_get_height(shm) : 0;
if (px <= 0 || px > CURSOR_MAX_PX) { wl_buffer_send_release(buffer); return; }
if (px <= 0 || px > CURSOR_MAX_PX) { send_buffer_release(buffer); return; }
s->idle_buffer = buffer;
s->idle_buffer_destroy.notify = on_idle_buffer_destroyed;
wl_resource_add_destroy_listener(buffer, &s->idle_buffer_destroy);
@@ -1064,7 +1086,7 @@ static struct wl_resource *surface_take_idle(struct surface *s) {
static void surface_drop_idle(struct surface *s) {
struct wl_resource *buffer = surface_take_idle(s);
if (buffer) wl_buffer_send_release(buffer);
if (buffer) send_buffer_release(buffer);
}
static void surface_commit(struct wl_client *c, struct wl_resource *r) {
@@ -1119,7 +1141,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */
} else {
cursor_publish_buffer(s, buffer);
wl_buffer_send_release(buffer);
send_buffer_release(buffer);
}
} else if (s->role == ROLE_NONE) {
/* Role-less surface: never drawn, but its buffer is kept for a set_cursor to come. */
@@ -1135,7 +1157,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
} else {
drop_dmabuf(s, 1);
s->has_content = 0;
if (buffer) wl_buffer_send_release(buffer);
if (buffer) send_buffer_release(buffer);
}
}
@@ -2322,7 +2344,7 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3
struct wl_resource *buffer = surface_take_idle(s);
if (buffer) {
cursor_publish_buffer(s, buffer);
wl_buffer_send_release(buffer);
send_buffer_release(buffer);
} else if (g_cursor_shown == s) {
cursor_publish_shown();
}
@@ -323,6 +323,7 @@ int vkp_has_window(void) {
}
int vkp_device_lost(void) { return g_dev_state == -2; }
int vkp_completion_unknown(void) { return g_dev_state == -3; }
void vk_present_set_scale_mode(int mode, int alignment) {
if (mode < VKP_MODE_OFF || mode > VKP_MODE_INTEGER) mode = VKP_MODE_FIT;
@@ -91,6 +91,10 @@ int vkp_ready(void);
int vkp_has_window(void);
/* 1 once the Vulkan device was lost: nothing is presented any more (the session must restart). */
int vkp_device_lost(void);
/* Compositor thread only. Submitted reads may still be pending: retain imported
* buffer references and withhold DMA-BUF releases until the app process exits.
* This terminal state is distinct from genuine device loss. */
int vkp_completion_unknown(void);
// Clear to black, blit the draws in order (first = bottom) and present.
// Returns 0 on success, -1 if nothing could be presented (no window yet, etc.).
@@ -28,6 +28,19 @@ without further Vulkan calls. Release fds are still consumed on rejected copies.
The normal frame fence still has its existing infinite timeout; this change is
not a general GPU hang watchdog.
Externally owned DMA-BUF contents need the same protection. `vkp_completion_unknown()`
exposes the terminal state to compositor-thread lifecycle paths. Replacement,
surface/client destruction, cursor/idle-role releases, previously queued paced
releases and SurfaceFlinger callbacks no longer return a DMA-BUF to its client
after quarantine. The compositor retains DMA-BUF references/backing FDs and AHB
references until app process exit. A display fence alone does not establish that
the compositor's Vulkan reads completed. Queued releases detach destroyed surfaces
and remain held without repeatedly arming an expired timer. New terminal releases
require no allocation, so allocation failure cannot fall back to an unsafe release.
Copied SHM releases and healthy/genuine-device-loss cleanup keep their existing
behaviour. This intentionally prevents further client buffer reuse; restarting
the Android app process, not just the guest session, is required.
## Regression coverage
`python3 -m unittest discover -s tools/tests -p test_vk_pending_completion.py -v`
@@ -62,3 +75,14 @@ The change was also exercised in an independently integrated Pixel PowerVR Lab,
but its short game observations do not establish the cause of the earlier GNOC
reboot or sustained device stability. Main's native CI and APK build remain
separate validation.
`python3 -m unittest discover -s tools/tests -p test_vk_dmabuf_quarantine.py -v`
compiles the production compositor/AHB release, reference and listener functions
verbatim, using the real Wayland list layouts/macros with fake IPC/timer/AHB
endpoints. It covers paced replacement, immediate surface cleanup, already queued
releases, a mixed DMA-BUF/SHM queue, client destruction, a display-fence callback,
healthy cleanup and genuine device loss. It checks withheld releases, retained
FDs/references/AHB ownership, removal of surface listeners and disarmed timers.
The same harness can test an old source directory via
`DROIDDECK_DMABUF_TEST_SOURCE`; it does not inject a real driver fault or prove
long-term device stability.
+106
View File
@@ -0,0 +1,106 @@
"""Exercise production DMA-BUF release/ownership paths without a GPU.
Wayland resources and OS timer/Android AHB endpoints are fake; list macros and
the compositor/AHB lifecycle functions come directly from the repository.
"""
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from test_vk_pending_completion import function
ROOT = Path(__file__).resolve().parents[2]
SRC = ROOT / 'app/src/main/cpp/waylandcomp/src'
class VkDmabufQuarantineTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cc = shutil.which(os.environ.get('CC', 'cc'))
if not cc:
raise AssertionError('C compiler required')
# Optional old-head input allows a genuine negative control using the
# identical harness, rather than a model of the previous behaviour.
source_dir = Path(os.environ.get('DROIDDECK_DMABUF_TEST_SOURCE', SRC))
comp = (source_dir / 'compositor.c').read_text()
ahb = (source_dir / 'ahb_swapchain.c').read_text()
vk = (source_dir / 'vk_present.c').read_text()
names = ['get_dmabuf', 'pending_release_free',
'pending_releases_forget_surface', 'on_pending_release_buffer_destroyed',
'arm_release_timer', 'on_release_timer', 'release_buffer',
'dmabuf_buffer_unref', 'drop_dmabuf', 'on_dmabuf_destroyed',
'droiddeck_dmabuf_ahb_slot', 'droiddeck_dmabuf_unref',
'droiddeck_release_buffer']
helpers = []
if re.search(r'\bstatic int buffer_release_blocked\(', comp):
helpers = [function(comp, n) for n in ('buffer_release_blocked', 'send_buffer_release')]
else:
helpers = ['static int __attribute__((unused)) buffer_release_blocked(struct wl_resource *b) { (void)b; return 0; }']
query = function(vk, 'vkp_completion_unknown') if 'int vkp_completion_unknown(' in vk else \
'int vkp_completion_unknown(void) { return g_dev_state == -3; }'
comp_functions = [function(comp, names[0])] + helpers + [function(comp, n) for n in names[1:]]
ahb_functions = [function(ahb, n) for n in ('buf_free', 'maybe_free',
'on_buffer_resource_destroyed', 'send_deferred_release',
'on_fence_readable', 'ahb_swapchain_defer_release',
'ahb_swapchain_surface_gone')]
structs = re.search(r'struct pending_release \{.*?\n\};', comp, re.S).group() + '\n' + \
re.search(r'struct ahb_buf \{.*?\n\};', ahb, re.S).group()
harness = Path(__file__).with_name('vk_dmabuf_quarantine_test.c').read_text()
harness = harness.replace('/* PRODUCTION_STRUCTS */', structs)
harness = harness.replace('/* PRODUCTION_FUNCTIONS */', '\n'.join([query] + comp_functions + ahb_functions))
cls.tmp = tempfile.TemporaryDirectory(prefix='droiddeck-dmabuf-')
cls.addClassCleanup(cls.tmp.cleanup)
probe = Path(cls.tmp.name) / 'probe.c'
probe.write_text(harness)
cls.binary = probe.with_suffix('')
built = subprocess.run([cc, '-std=c11', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-UNDEBUG',
'-I', str(SRC.parent / 'prebuilt/include'),
str(probe), '-o', str(cls.binary)], capture_output=True, text=True)
if built.returncode:
raise AssertionError(built.stdout + built.stderr)
def run_scenario(self, name):
result = subprocess.run([str(self.binary), name], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_replacement_retains_reference_and_release(self):
self.run_scenario('replacement')
def test_surface_destroy_retains_reference_and_release(self):
self.run_scenario('surface-gone')
def test_queued_release_is_held_and_timer_disarmed(self):
self.run_scenario('queued')
def test_mixed_queue_releases_only_copied_shm(self):
self.run_scenario('mixed')
def test_client_destroy_retains_backing_fd(self):
self.run_scenario('client-gone')
def test_ahb_callback_retains_ownership_and_release(self):
self.run_scenario('ahb')
def test_active_client_destroy_retains_reference(self):
self.run_scenario('active-client-gone')
def test_healthy_ahb_callback_and_cleanup(self):
self.run_scenario('ahb-healthy')
def test_device_lost_ahb_callback_and_cleanup(self):
self.run_scenario('ahb-device-lost')
def test_healthy_release_and_reference_cleanup(self):
self.run_scenario('healthy')
def test_genuine_device_loss_keeps_existing_cleanup(self):
self.run_scenario('device-lost')
if __name__ == '__main__':
unittest.main()
+1 -1
View File
@@ -57,7 +57,7 @@ int __android_log_print(int, const char *, const char *, ...);
''')
(tmp / 'wayland-server.h').write_text('#pragma once\nstruct wl_display; struct wl_resource; struct wl_client;\n')
compositor = (SOURCE.parent / 'compositor.c').read_text()
callers = '\n'.join(function(compositor, n) for n in ('take_shm', 'take_pixel'))
callers = '\n'.join(function(compositor, n) for n in ('buffer_release_blocked', 'send_buffer_release', 'take_shm', 'take_pixel'))
harness = (Path(__file__).with_name('vk_terminal_callers_test.c')).read_text()
probe = tmp / 'probe.c'
probe.write_text(harness.replace('/* COMPOSITOR_CALLERS */', callers))
+199
View File
@@ -0,0 +1,199 @@
/* Production lifecycle functions are inserted verbatim by Python. No GPU,
* Android app, external client or real timer is involved in this probe. */
#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <wayland-util.h>
#ifndef __linux__
struct itimerspec { struct timespec it_interval, it_value; };
#endif
struct wl_listener { struct wl_list link; void (*notify)(struct wl_listener *, void *); };
struct wl_resource { int dma; void *user_data; struct wl_list listeners; unsigned releases; };
struct wl_client;
struct wl_event_source { int unused; };
typedef struct AHardwareBuffer { int unused; } AHardwareBuffer;
struct vkp_image { int unused; };
struct dmabuf_buffer {
int refs, n_planes, fd[4];
struct vkp_image *img;
void *ahb_state;
};
struct surface {
struct wl_resource *dmabuf;
struct dmabuf_buffer *dmabuf_buf;
struct wl_listener dmabuf_destroy;
int releases_pending;
int64_t next_release_ns;
};
/* PRODUCTION_STRUCTS */
static int g_dev_state, g_fps_limit, g_release_timer_fd;
static int64_t g_refresh_ns;
static struct wl_list g_pending_releases, g_bufs;
static void *g_display;
static unsigned image_destroys, ahb_releases, source_removes;
static struct itimerspec last_timer;
static const int wl_buffer_interface = 1, dbuf_buffer_impl = 2;
#define TFD_TIMER_ABSTIME 1
/* List operations use the real Wayland layouts/macros. OS/IPC endpoints alone
* are fakes, so no event is sent and no process or hardware is touched. */
void wl_list_init(struct wl_list *l) { l->prev = l; l->next = l; }
void wl_list_insert(struct wl_list *l, struct wl_list *e) {
e->next = l->next; e->prev = l; l->next->prev = e; l->next = e;
}
void wl_list_remove(struct wl_list *e) { e->next->prev = e->prev; e->prev->next = e->next; }
static int wl_resource_instance_of(struct wl_resource *r, const void *i, const void *impl) { return r->dma; }
static void *wl_resource_get_user_data(struct wl_resource *r) { return r->user_data; }
static void wl_resource_add_destroy_listener(struct wl_resource *r, struct wl_listener *l) {
wl_list_insert(r->listeners.prev, &l->link);
}
static void wl_buffer_send_release(struct wl_resource *r) { ++r->releases; }
static void wl_display_flush_clients(void *d) {}
static int timerfd_settime(int fd, int flags, const struct itimerspec *v, void *old) { last_timer = *v; return 0; }
static void perf_note_release(int64_t since) {}
static int64_t now_ns(void) { return 1000000000LL; }
static void vkp_image_destroy(struct vkp_image *img) { ++image_destroys; }
static void AHardwareBuffer_release(AHardwareBuffer *ahb) { ++ahb_releases; }
static int wl_event_source_remove(struct wl_event_source *src) { ++source_removes; return 0; }
static struct dmabuf_buffer *get_dmabuf(struct wl_resource *r);
int ahb_swapchain_defer_release(struct dmabuf_buffer *, struct wl_resource *, struct surface *, int);
/* PRODUCTION_FUNCTIONS */
static void init_resource(struct wl_resource *r, struct dmabuf_buffer *b) {
*r = (struct wl_resource){.dma = b != NULL, .user_data = b};
wl_list_init(&r->listeners);
}
static struct dmabuf_buffer *init_buffer(struct surface *s, struct wl_resource *r) {
struct dmabuf_buffer *b = calloc(1, sizeof(*b)); assert(b);
b->refs = 2; b->n_planes = 1;
int fds[2]; assert(pipe(fds) == 0); b->fd[0] = fds[0]; close(fds[1]);
init_resource(r, b);
assert(get_dmabuf(r) == b);
*s = (struct surface){.dmabuf = r, .dmabuf_buf = b};
s->dmabuf_destroy.notify = on_dmabuf_destroyed;
wl_resource_add_destroy_listener(r, &s->dmabuf_destroy);
return b;
}
static void destroy_resource(struct wl_resource *r) {
while (r->listeners.next != &r->listeners) {
struct wl_listener *l = wl_container_of(r->listeners.next, l, link);
l->notify(l, r);
}
if (r->dma) dmabuf_buffer_unref(r->user_data);
r->user_data = NULL;
}
static void fire_timer(void) {
int fd[2]; uint64_t expirations = 1;
assert(pipe(fd) == 0 && write(fd[1], &expirations, sizeof(expirations)) == sizeof(expirations));
on_release_timer(fd[0], 0, NULL); close(fd[0]); close(fd[1]);
}
static void assert_retained(struct dmabuf_buffer *b, int refs) {
assert(b->refs == refs && fcntl(b->fd[0], F_GETFD) >= 0 && !image_destroys);
}
static void test_drop(int paced) {
struct surface s; struct wl_resource r;
struct dmabuf_buffer *b = init_buffer(&s, &r);
g_dev_state = -3; drop_dmabuf(&s, paced);
assert(!s.dmabuf && !s.dmabuf_buf && !r.releases && !s.releases_pending);
assert(r.listeners.next == &r.listeners); /* no listener into a dead surface */
assert_retained(b, 2);
destroy_resource(&r); assert_retained(b, 2);
close(b->fd[0]); free(b); /* fake GPU only */
}
static void test_queue(int mixed, int client_gone) {
struct surface s; struct wl_resource r, shm;
struct dmabuf_buffer *b = init_buffer(&s, &r);
g_release_timer_fd = 123; g_refresh_ns = 100000;
drop_dmabuf(&s, 1); assert(s.releases_pending == 1 && !r.releases);
assert(last_timer.it_value.tv_sec || last_timer.it_value.tv_nsec);
if (mixed) { init_resource(&shm, NULL); release_buffer(&s, &shm, now_ns()); }
pending_releases_forget_surface(&s);
g_dev_state = -3;
fire_timer();
assert(!r.releases && g_pending_releases.next != &g_pending_releases);
if (mixed) assert(shm.releases == 1);
assert(!last_timer.it_value.tv_sec && !last_timer.it_value.tv_nsec);
assert_retained(b, 1);
if (client_gone) {
destroy_resource(&r);
assert(g_pending_releases.next == &g_pending_releases);
assert_retained(b, 1);
}
close(b->fd[0]); free(b); /* fake GPU only */
}
static void test_active_client_destroy(void) {
struct surface s; struct wl_resource r;
struct dmabuf_buffer *b = init_buffer(&s, &r);
g_dev_state = -3;
destroy_resource(&r);
assert(!s.dmabuf && s.dmabuf_buf == b && !r.releases);
drop_dmabuf(&s, 0); assert(!s.dmabuf_buf);
assert_retained(b, 2);
close(b->fd[0]); free(b); /* fake GPU only */
}
static void test_ahb(int terminal, int lost) {
struct surface s; struct wl_resource r;
struct dmabuf_buffer *b = init_buffer(&s, &r);
struct ahb_buf *ab = calloc(1, sizeof(*ab)); assert(ab);
AHardwareBuffer ahb;
*ab = (struct ahb_buf){.b = b, .ahb = &ahb, .resource = &r, .on_layer = 1, .fence_fd = -1};
b->ahb_state = ab; ++b->refs;
ab->resource_destroy.notify = on_buffer_resource_destroyed;
wl_resource_add_destroy_listener(&r, &ab->resource_destroy);
wl_list_insert(&g_bufs, &ab->link);
drop_dmabuf(&s, 1); assert(ab->release_pending && ab->surface == &s);
int backing_fd = b->fd[0];
g_dev_state = terminal ? -3 : lost ? -2 : 1;
ahb_swapchain_surface_gone(&s); assert(!ab->surface);
/* Actual event-loop callback after the display's fence becomes readable. */
struct wl_event_source source; int fds[2]; assert(pipe(fds) == 0);
ab->on_layer = 0; ab->fence_src = &source; ab->fence_fd = fds[0];
on_fence_readable(fds[0], 0, ab); close(fds[1]);
assert(source_removes == 1 && ab->fence_fd == -1 && !ab->fence_src);
assert(r.releases == (terminal ? 0u : 1u));
assert(ab->release_pending == terminal && !ahb_releases);
destroy_resource(&r);
if (terminal) {
assert(!ab->resource && b->ahb_state == ab && !ahb_releases);
assert_retained(b, 2);
close(b->fd[0]); free(ab); free(b); /* fake GPU only */
} else {
assert(ahb_releases == 1 && image_destroys == 1);
errno = 0; assert(fcntl(backing_fd, F_GETFD) == -1 && errno == EBADF);
}
}
static void test_cleanup(int lost) {
struct surface s; struct wl_resource r;
struct dmabuf_buffer *b = init_buffer(&s, &r);
int fd = b->fd[0]; g_dev_state = lost ? -2 : 1;
assert(!vkp_completion_unknown());
drop_dmabuf(&s, 0);
assert(r.releases == 1 && b->refs == 1 && !image_destroys);
destroy_resource(&r); assert(image_destroys == 1);
errno = 0; assert(fcntl(fd, F_GETFD) == -1 && errno == EBADF);
}
int main(int argc, char **argv) {
assert(argc == 2);
wl_list_init(&g_pending_releases); wl_list_init(&g_bufs);
g_dev_state = 1; g_release_timer_fd = -1;
if (!strcmp(argv[1], "replacement")) test_drop(1);
else if (!strcmp(argv[1], "surface-gone")) test_drop(0);
else if (!strcmp(argv[1], "queued")) test_queue(0, 0);
else if (!strcmp(argv[1], "mixed")) test_queue(1, 0);
else if (!strcmp(argv[1], "client-gone")) test_queue(0, 1);
else if (!strcmp(argv[1], "ahb")) test_ahb(1, 0);
else if (!strcmp(argv[1], "ahb-healthy")) test_ahb(0, 0);
else if (!strcmp(argv[1], "ahb-device-lost")) test_ahb(0, 1);
else if (!strcmp(argv[1], "active-client-gone")) test_active_client_destroy();
else if (!strcmp(argv[1], "healthy")) test_cleanup(0);
else if (!strcmp(argv[1], "device-lost")) test_cleanup(1);
else assert(0 && "unknown scenario");
return 0;
}
+6 -1
View File
@@ -108,6 +108,10 @@ static void wl_shm_buffer_end_access(struct wl_shm_buffer *b) {}
static void wl_buffer_send_release(struct wl_resource *b) { ++releases; }
static struct wl_client *wl_resource_get_client(struct wl_resource *r) { return NULL; }
static struct client_info *client_info_of(struct wl_client *c) { return NULL; }
/* This harness exercises copied SHM/pixel callers; DMA-BUF lifecycle paths are
* compiled separately by test_vk_dmabuf_quarantine.py. */
struct dmabuf_buffer;
static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer) { return NULL; }
/* COMPOSITOR_CALLERS */
static void setup(int extra) {
@@ -174,7 +178,7 @@ static void acquire_failure(int later, VkResult result) {
idle_answer = VK_ERROR_OUT_OF_HOST_MEMORY;
VkImage *retained = g_images;
assert(vkp_render(64, 64, NULL, 0) == -1);
assert(g_dev_state == -3 && !vkp_device_lost());
assert(g_dev_state == -3 && vkp_completion_unknown() && !vkp_device_lost());
assert(acquires == fail_acquire_at && idle_calls == 1);
assert(submits == (unsigned)later && markers == 0 && waits == 0);
assert(destroys == 0 && g_images == retained && g_nimg == 1);
@@ -234,6 +238,7 @@ int main(int argc, char **argv) {
if (!strcmp(name, "acquire-device-lost")) { fail_acquire_at = 2; acquire_answer = VK_ERROR_DEVICE_LOST; }
else present_answer = VK_ERROR_DEVICE_LOST;
assert(vkp_render(64, 64, NULL, 0) == -1 && vkp_device_lost());
assert(!vkp_completion_unknown());
assert(g_dev_state == -2 && lost_notifications == 1 && markers == 0 && waits == 0);
assert(!g_images && !g_swapchain && !g_surface && destroys == 2);
assert_stopped();