mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Retain external DMA-BUF ownership after GPU quarantine
This commit is contained in:
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;
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user