Stop quarantined GPU callers before retries and mapped writes

This commit is contained in:
Leask Wong committed 2026-10-11 01:18:18 -04:00
1 parent 68dc1c4549
commit 9e6fe314ae
5 files changed
+404 -8

No files matched your search

+18 -7
View File
@@ -126,6 +126,8 @@ static void quarantine_pending_work(VkResult result, const char *where) {
/* vkWaitForFences on the compositor's own work, timed for the perf line. */
static VkResult timed_wait(VkFence fence, uint64_t timeout) {
if (g_dev_state != 1)
return g_dev_state == -2 ? VK_ERROR_DEVICE_LOST : VK_ERROR_UNKNOWN;
int64_t t0 = perf_now();
VkResult r = g_vk.WaitForFences(g_dev, 1, &fence, VK_TRUE, timeout);
perf_add(&g_perf.wait_ns, &g_perf.wait_max_ns, &g_perf.waits, perf_now() - t0);
@@ -1109,10 +1111,10 @@ struct vkp_image *vkp_image_create_shm(int w, int h) {
return img;
}
/* Renders are synchronous (we wait for the frame's fence), so writing between frames
* never races the GPU. */
/* Between successfully completed frames the GPU is idle. After an unconfirmed
* wait, retain the mapped bytes too: the previous submit may still read them. */
void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride) {
if (!img || !img->map || !data) return;
if (g_dev_state != 1 || !img || !img->map || !data) return;
size_t rowbytes = (size_t)img->w * 4;
if ((size_t)stride < rowbytes) rowbytes = (size_t)stride;
for (int y = 0; y < img->h; y++)
@@ -1415,6 +1417,7 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int
* so they never rebuild; they fail and the frame ends early. 0 on success. */
static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) {
for (int attempt = 0; ; attempt++) {
if (g_dev_state != 1) return -1;
if (!g_swapchain && (!first || swap_init() != 0)) return -1;
/* A bounded wait: a surface that went away without telling us must not park the
* compositor thread forever (clients are paced from this thread). */
@@ -1426,7 +1429,7 @@ static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) {
if (ar == VK_ERROR_OUT_OF_DATE_KHR || ar == VK_ERROR_SURFACE_LOST_KHR) {
droiddeck_log("gpu", "screen surface %s on acquire: rebuilding the swapchain", vk_result_name(ar));
destroy_swapchain();
if (first && attempt == 0) continue;
if (first && attempt == 0 && g_dev_state == 1) continue;
return -1;
}
droiddeck_log("error", "present: acquire %d failed (%s %d)", k, vk_result_name(ar), (int)ar);
@@ -1554,6 +1557,7 @@ static void send_hdr_metadata(const struct droiddeck_color *c) {
static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, int n,
const struct vkp_hdr_frame *hf, int *how) {
if (how) *how = 0;
if (g_dev_state < 0) return -1;
vkp_apply_window_request();
check_surface_changed(); /* a rotation or a fold the rebind did not cover: rebuild at the new size */
if (g_dev_state == -2) return -1;
@@ -1787,6 +1791,10 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
if (k > 0) {
VkResult ark;
if (acquire_image(k, 0, &img, &ark) != 0) {
/* Teardown may have retained this frame's pending work, or
* acquire may have reported real device loss. Neither permits
* a completion-marker submit or another fence wait. */
if (g_dev_state != 1) return -1;
droiddeck_log("framegen", "present %d/%d: no swapchain image; the frame ends early", k + 1, presents);
pr = VK_ERROR_OUT_OF_DATE_KHR;
break;
@@ -1819,7 +1827,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
pr = g_vk.QueuePresentKHR(g_queue, &pi);
perf_add(&g_perf.present_ns, &g_perf.present_max_ns, &g_perf.presents, perf_now() - t_pres);
if (k < ngen) presented_gen++;
if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR || pr == VK_ERROR_DEVICE_LOST) break;
if (pr == VK_ERROR_DEVICE_LOST) { device_lost("present"); return -1; }
if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR) break;
}
if (!fence_submitted) {
/* Ended early: an empty submit carrying the fence is ordered after everything queued
@@ -2244,8 +2253,9 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) {
if (!g_pass.active) { if (wait_fd >= 0) close(wait_fd); return -1; }
g_pass.active = 0;
if (g_dev_state != 1) { if (wait_fd >= 0) close(wait_fd); return -1; }
VkCommandBuffer cmd = g_cmds[0];
if (!dst || !dst->blit_dst || g_dev_state == -2) {
if (!dst || !dst->blit_dst) {
if (wait_fd >= 0) close(wait_fd);
g_vk.EndCommandBuffer(cmd);
return -1;
@@ -2300,5 +2310,6 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) {
void vkp_pass_abort(void) {
if (!g_pass.active) return;
g_pass.active = 0;
g_vk.EndCommandBuffer(g_cmds[0]); /* never submitted; the buffer is reset before its next use */
if (g_dev_state == 1)
g_vk.EndCommandBuffer(g_cmds[0]); /* never submitted; reset before its next use */
}
@@ -17,6 +17,14 @@ state, so the diagnostic asks for an app restart.
Initialization checks the black-image submit and fence result. An unsuccessful
empty completion-marker submit no longer causes a wait on an unsignalled fence.
Surface-rebuild and effect-map paths stop when teardown cannot establish idle.
Acquire retries also stop: retaining the old swapchain is not permission to
acquire from it again. A later frame-generation acquire that ends in a terminal
state returns before the completion marker, fence wait or any further recording.
Real device loss on present is handled before attempting that marker, too.
Mapped SHM bytes are retained unchanged, including same-size `wl_shm` and
single-pixel commits; keeping only the allocation would still race a GPU read.
Off-screen pass copy/abort and the internal fence wait reject terminal states
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.
@@ -29,6 +37,25 @@ errors, genuine device loss, resource retention and suppressed repeated teardown
No real GPU fault is injected. The harness matches main's Vulkan-image resources;
it does not assume the experimental PowerVR branch's additional AHB fields.
`python3 -m unittest discover -s tools/tests -p test_vk_terminal_callers.py -v`
additionally compiles the **entire production `vk_present.c`**, with fake Vulkan
dispatch callbacks and stub external modules. The compositor's `take_shm` and
`take_pixel` callers are inserted verbatim, not rewritten. It needs the Android
NDK's Vulkan-Headers (`ANDROID_HOME`/`ANDROID_SDK_ROOT`/`NDK`), a host Vulkan
development package, or `DROIDDECK_VULKAN_INCLUDE`; absent headers are explicitly
reported as a skip. No Android runtime or GPU is required.
The 12 scenarios include first/later OUT_OF_DATE and SURFACE_LOST followed by an
idle failure, a finite blit timeout followed by same-size SHM/single-pixel
uploads, normal presentation/frame generation, a healthy later acquire timeout
whose earlier work still needs draining, real acquire/present device loss, and
terminal off-screen pass entry. Every work-issuing fake callback asserts the
device is live. After quarantine, the tests verify retained handles and unchanged
mapped bytes, and probe the public render/blit/readback/import/pass entry points
for suppressed acquire/submit/wait/reset and destruction. Consumed fds are checked.
Against PR #560's `68dc1c45` the same harness compiles, but nine scenarios fail
(the three healthy controls pass); the corrected production unit passes all 12.
The baseline production fence/destructor compiles but fails the timeout-state
oracle; the corrected functions pass both cases with `-Wall -Wextra -Werror`.
The change was also exercised in an independently integrated Pixel PowerVR Lab,
+2 -1
View File
@@ -40,6 +40,7 @@ typedef uintptr_t VkFence;
#define VK_TIMEOUT 2
#define VK_ERROR_DEVICE_LOST -4
#define VK_ERROR_OUT_OF_HOST_MEMORY -1
#define VK_ERROR_UNKNOWN -13
static int g_dev_state, g_base_black;
static uintptr_t g_dev = 1;
static struct { int64_t wait_ns, wait_max_ns; unsigned waits; } g_perf;
@@ -83,7 +84,7 @@ int main(void) {
assert(destroyed==1 && unmapped==1 && freed_memory==1 && forgotten==1);
unsigned first_errors=errors;
assert(first_errors>0);
assert(timed_wait(1, 1000)==VK_TIMEOUT);
assert(timed_wait(1, 1000)==VK_ERROR_UNKNOWN);
assert(errors==first_errors); /* one terminal diagnostic, no repeated recovery */
free(pending); /* fake GPU exists only in this test */
+115
View File
@@ -0,0 +1,115 @@
"""Fault-inject complete production render/blit/upload callers, without a GPU.
Uses Vulkan-Headers from the Android NDK (as installed by APK CI), or the host
Vulkan development package. Only Android platform declarations and the external
effect/frame-generation modules are mocked; vk_present.c is compiled unmodified.
"""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from test_vk_pending_completion import function
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / 'app/src/main/cpp/waylandcomp/src/vk_present.c'
def vulkan_include():
candidates = [Path(p) for p in [os.environ.get('DROIDDECK_VULKAN_INCLUDE', ''),
'/usr/include', '/usr/local/include'] if p]
for name in ('ANDROID_HOME', 'ANDROID_SDK_ROOT', 'ANDROID_NDK_HOME', 'NDK'):
if os.environ.get(name):
root = Path(os.environ[name])
candidates += sorted(root.glob('ndk/*/toolchains/llvm/prebuilt/*/sysroot/usr/include'), reverse=True)
candidates += list(root.glob('toolchains/llvm/prebuilt/*/sysroot/usr/include'))
return next((p for p in candidates if (p / 'vulkan/vulkan.h').is_file()), None)
class VkTerminalCallersTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
headers = vulkan_include()
if headers is None:
raise unittest.SkipTest('NDK or host Vulkan-Headers required for full production-C caller tests')
cc = shutil.which(os.environ.get('CC', 'cc'))
if not cc:
raise AssertionError('C compiler required')
cls.tmp = tempfile.TemporaryDirectory(prefix='droiddeck-callers-')
cls.addClassCleanup(cls.tmp.cleanup)
tmp = Path(cls.tmp.name)
(tmp / 'android').mkdir()
(tmp / 'android/native_window.h').write_text('''
#pragma once
#include <stdint.h>
typedef struct ANativeWindow ANativeWindow;
int32_t ANativeWindow_getWidth(ANativeWindow *);
int32_t ANativeWindow_getHeight(ANativeWindow *);
void ANativeWindow_release(ANativeWindow *);
''')
(tmp / 'android/hardware_buffer.h').write_text('#pragma once\ntypedef struct AHardwareBuffer AHardwareBuffer;\n')
(tmp / 'android/log.h').write_text('''
#pragma once
#define ANDROID_LOG_INFO 4
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'))
harness = (Path(__file__).with_name('vk_terminal_callers_test.c')).read_text()
probe = tmp / 'probe.c'
probe.write_text(harness.replace('/* COMPOSITOR_CALLERS */', callers))
cls.binary = tmp / 'probe'
built = subprocess.run([cc, '-std=c11', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-Wno-missing-field-initializers', '-UNDEBUG',
'-I', str(tmp), '-idirafter', str(headers), '-I', str(SOURCE.parent),
str(probe), '-pthread', '-o', str(cls.binary)],
capture_output=True, text=True)
if built.returncode:
raise AssertionError(built.stdout + built.stderr)
def run_scenario(self, name):
ran = subprocess.run([str(self.binary), name], capture_output=True, text=True, timeout=10)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
def test_first_acquire_idle_failure(self):
self.run_scenario('first-out-of-date')
def test_first_surface_lost_idle_failure(self):
self.run_scenario('first-surface-lost')
def test_later_framegen_acquire_idle_failure(self):
self.run_scenario('later-out-of-date')
def test_later_framegen_surface_lost_idle_failure(self):
self.run_scenario('later-surface-lost')
def test_finite_blit_timeout_same_size_shm_upload(self):
self.run_scenario('shm-timeout')
def test_finite_blit_timeout_same_size_pixel_upload(self):
self.run_scenario('pixel-timeout')
def test_normal_present(self):
self.run_scenario('normal')
def test_normal_framegen_present(self):
self.run_scenario('framegen')
def test_healthy_later_acquire_timeout_drains_work(self):
self.run_scenario('later-timeout')
def test_real_device_lost_on_acquire(self):
self.run_scenario('acquire-device-lost')
def test_real_device_lost_on_present(self):
self.run_scenario('present-device-lost')
def test_pass_copy_and_abort_terminal_entry(self):
self.run_scenario('pass-terminal')
if __name__ == '__main__':
unittest.main()
+242
View File
@@ -0,0 +1,242 @@
/* Compiled with the complete, unmodified production translation unit. The
* compositor's take_shm/take_pixel functions are inserted verbatim by Python. */
#include "vk_present.c"
#include <assert.h>
struct vk_api g_vk;
static unsigned operations, acquires, submits, markers, waits, idle_calls, resets;
static unsigned destroys, releases, lost_notifications, presented_notifications;
static VkResult idle_answer, fence_answer, acquire_answer, present_answer;
static unsigned fail_acquire_at;
static int generated;
/* Every work-issuing callback is an oracle: no GPU operation after a terminal
* result. Object destruction is counted separately for genuine device loss. */
static void live(void) { assert(g_dev_state == 1); ++operations; }
static VkResult acquire(VkDevice d, VkSwapchainKHR s, uint64_t t, VkSemaphore a, VkFence f, uint32_t *i) {
live(); ++acquires; *i = 0;
return acquires == fail_acquire_at ? acquire_answer : VK_SUCCESS;
}
static VkResult idle(VkDevice d) { live(); ++idle_calls; return idle_answer; }
static VkResult wait_fences(VkDevice d, uint32_t n, const VkFence *f, VkBool32 all, uint64_t t) {
live(); ++waits; return fence_answer;
}
static VkResult submit(VkQueue q, uint32_t n, const VkSubmitInfo *s, VkFence f) {
live(); ++submits; if (!s->commandBufferCount) ++markers; return VK_SUCCESS;
}
static VkResult present(VkQueue q, const VkPresentInfoKHR *p) { live(); return present_answer; }
static VkResult reset_cmd(VkCommandBuffer c, VkCommandBufferResetFlags f) { live(); ++resets; return VK_SUCCESS; }
static VkResult reset_fence(VkDevice d, uint32_t n, const VkFence *f) { live(); ++resets; return VK_SUCCESS; }
static VkResult begin_cmd(VkCommandBuffer c, const VkCommandBufferBeginInfo *b) { live(); return VK_SUCCESS; }
static VkResult end_cmd(VkCommandBuffer c) { live(); return VK_SUCCESS; }
static void barrier(VkCommandBuffer c, VkPipelineStageFlags s, VkPipelineStageFlags d, VkDependencyFlags f,
uint32_t nm, const VkMemoryBarrier *m, uint32_t nb, const VkBufferMemoryBarrier *b,
uint32_t ni, const VkImageMemoryBarrier *i) { live(); }
static void clear(VkCommandBuffer c, VkImage i, VkImageLayout l, const VkClearColorValue *v,
uint32_t n, const VkImageSubresourceRange *r) { live(); }
static void blit(VkCommandBuffer c, VkImage s, VkImageLayout sl, VkImage d, VkImageLayout dl,
uint32_t n, const VkImageBlit *b, VkFilter f) { live(); }
static void destroy_swap(VkDevice d, VkSwapchainKHR s, const VkAllocationCallbacks *a) { ++destroys; }
static void destroy_surface(VkInstance i, VkSurfaceKHR s, const VkAllocationCallbacks *a) { ++destroys; }
static void destroy_sem(VkDevice d, VkSemaphore s, const VkAllocationCallbacks *a) { live(); ++destroys; }
static VkResult create_sem(VkDevice d, const VkSemaphoreCreateInfo *c, const VkAllocationCallbacks *a, VkSemaphore *s) {
live(); *s = (VkSemaphore)(uintptr_t)1; return VK_SUCCESS;
}
static void destroy_image(VkDevice d, VkImage i, const VkAllocationCallbacks *a) { ++destroys; }
static void free_memory(VkDevice d, VkDeviceMemory m, const VkAllocationCallbacks *a) { ++destroys; }
static void unmap(VkDevice d, VkDeviceMemory m) { ++destroys; }
/* External modules are not the subject of this fault injection. Their declared
* signatures remain checked by the production headers. */
int __android_log_print(int p, const char *t, const char *f, ...) { return 0; }
void droiddeck_log(const char *t, const char *f, ...) {}
void droiddeck_on_first_frame(void) {}
int32_t ANativeWindow_getWidth(ANativeWindow *w) { return 64; }
int32_t ANativeWindow_getHeight(ANativeWindow *w) { return 64; }
void ANativeWindow_release(ANativeWindow *w) {}
void sc_layer_window_gone(void) {}
int vk_loader_open(const char *a, const char *b, const char *c) { assert(0); return -1; }
void vk_loader_load_instance(VkInstance i) { assert(0); }
void vk_loader_load_device(VkDevice d) { assert(0); }
PFN_vkGetInstanceProcAddr vk_loader_gipa(void) { return NULL; }
void vkp_effects_bind_device(VkDevice d, VkPhysicalDevice p, const VkPhysicalDeviceMemoryProperties *m) {}
int vkp_effects_active(void) { return 0; }
VkFilter vkp_effects_blit_filter(void) { return VK_FILTER_NEAREST; }
void vkp_effects_set_formats(VkFormat a, VkFormat b) {}
VkImage vkp_effects_run(VkCommandBuffer c, VkImage i, int w, int h, int ow, int oh, int *rw, int *rh) {
*rw = w; *rh = h; return i;
}
const void *vkp_framegen_device_features(VkInstance i, PFN_vkGetInstanceProcAddr p, VkPhysicalDevice d) { return NULL; }
void vkp_framegen_device_ready(VkDevice d, VkQueue q, uint32_t f, int e) {}
void vkp_framegen_device_lost(void) { ++lost_notifications; }
int vkp_framegen_active(void) { return generated != 0; }
int vkp_framegen_extra_images(void) { return generated; }
int vkp_framegen_format_ok(VkFormat f) { return 0; }
int vkp_framegen_run(VkCommandBuffer c, VkImage s, VkImageView v, int w, int h,
VkFormat f, VkImage images[VKP_FG_MAX_GENERATIONS]) {
for (int i = 0; i < generated; ++i) images[i] = s;
return generated;
}
void vkp_framegen_presented(int n) { ++presented_notifications; }
void hdrc_bind_device(VkDevice d, const VkPhysicalDeviceMemoryProperties *m) {}
void hdrc_forget_image(VkImage i) {}
int hdrc_encode(VkCommandBuffer c, VkImage a, VkImage b, VkFormat f, int w, int h, const struct hdrc_params *p) { return -1; }
void blendp_bind_device(VkDevice d) {}
void blendp_forget_image(VkImage i) {}
void blendp_begin_frame(void) {}
int blendp_draw(VkCommandBuffer c, VkImage s, VkFormat sf, int sw, int sh, int sg,
VkImage d, VkFormat df, int dw, int dh, const VkImageBlit *b) { return -1; }
int droiddeck_color_requested(void) { return 0; }
float droiddeck_color_sdr_white(void) { return 203.0f; }
struct surface {
struct vkp_image *shm_img;
struct wl_resource *resource;
int buf_w, buf_h, buf_alpha, has_content;
};
struct client_info { int asked_feedback, shm_frames; };
struct wl_shm_buffer { int w, h, stride; const void *data; };
#define WL_SHM_FORMAT_ARGB8888 0
static unsigned g_stat_shm;
static int wl_shm_buffer_get_width(struct wl_shm_buffer *b) { return b->w; }
static int wl_shm_buffer_get_height(struct wl_shm_buffer *b) { return b->h; }
static int wl_shm_buffer_get_stride(struct wl_shm_buffer *b) { return b->stride; }
static int wl_shm_buffer_get_format(struct wl_shm_buffer *b) { return WL_SHM_FORMAT_ARGB8888; }
static const void *wl_shm_buffer_get_data(struct wl_shm_buffer *b) { return b->data; }
static void wl_shm_buffer_begin_access(struct wl_shm_buffer *b) {}
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; }
/* COMPOSITOR_CALLERS */
static void setup(int extra) {
g_vk = (struct vk_api){.AcquireNextImageKHR = acquire, .DeviceWaitIdle = idle,
.WaitForFences = wait_fences, .QueueSubmit = submit, .QueuePresentKHR = present,
.ResetCommandBuffer = reset_cmd, .ResetFences = reset_fence, .BeginCommandBuffer = begin_cmd,
.EndCommandBuffer = end_cmd, .CmdPipelineBarrier = barrier, .CmdClearColorImage = clear,
.CmdBlitImage = blit, .DestroySwapchainKHR = destroy_swap, .DestroySurfaceKHR = destroy_surface,
.DestroySemaphore = destroy_sem, .CreateSemaphore = create_sem,
.DestroyImage = destroy_image, .FreeMemory = free_memory, .UnmapMemory = unmap};
idle_answer = fence_answer = acquire_answer = present_answer = VK_SUCCESS;
generated = g_swap_extra = extra;
g_dev_state = 1;
g_dev = (VkDevice)(uintptr_t)1; g_inst = (VkInstance)(uintptr_t)1;
g_window = (ANativeWindow *)(uintptr_t)1; g_queue = (VkQueue)(uintptr_t)1;
g_swapchain = (VkSwapchainKHR)(uintptr_t)2; g_surface = (VkSurfaceKHR)(uintptr_t)3;
g_images = calloc(1, sizeof(*g_images)); assert(g_images);
*g_images = (VkImage)(uintptr_t)4; g_nimg = 1;
g_extent = g_caps_extent = (VkExtent2D){64, 64};
g_surface_checked_ns = INT64_MAX; /* no simulated geometry change */
g_scene.img = (VkImage)(uintptr_t)5; g_scene.mem = (VkDeviceMemory)(uintptr_t)6;
g_scene.w = g_scene.h = 64;
for (int k = 0; k < MAX_PRESENTS; ++k) {
g_cmds[k] = (VkCommandBuffer)(uintptr_t)(10 + k);
g_acqs[k] = (VkSemaphore)(uintptr_t)(20 + k);
g_rnds[k] = (VkSemaphore)(uintptr_t)(30 + k);
}
g_fence = (VkFence)(uintptr_t)40;
}
/* Probe the public GPU entry points after a terminal result. This includes an
* active off-screen pass and consumed release fds, not just the render guard. */
static void assert_stopped(void) {
unsigned before = operations, before_destroys = destroys;
struct vkp_image src = {.image = (VkImage)(uintptr_t)7, .w = 1, .h = 1};
struct vkp_image dst = {.image = (VkImage)(uintptr_t)8, .w = 1, .h = 1, .blit_dst = 1};
uint32_t pixel = 0; uint64_t mod = 0;
assert(vkp_render(64, 64, NULL, 0) == -1);
assert(vkp_render_plain(64, 64) == -1);
assert(vkp_render_hdr(64, 64, NULL, 0, NULL, NULL) == -1);
assert(vkp_base_black(64, 64) == -1);
assert(vkp_update_map(64, 64) == -1);
assert(vkp_ready() == -1);
assert(vkp_image_create_shm(1, 1) == NULL);
assert(vkp_image_import_dmabuf(-1, 0, VKP_MOD_LINEAR, 1, 1, 4, 0, 0) == NULL);
assert(vkp_dmabuf_modifiers(0, &mod, 1) == 0);
assert(vkp_image_readback(&src, &pixel, 1) == -1);
assert(vkp_pass_begin(64, 64, NULL, 0, NULL, NULL) == -1);
int fd[2]; assert(pipe(fd) == 0);
assert(vkp_blit_image(&src, &dst, fd[0]) == -1);
errno = 0; assert(close(fd[0]) == -1 && errno == EBADF); close(fd[1]);
g_pass.active = 1;
assert(pipe(fd) == 0);
assert(vkp_pass_copy_to(&dst, fd[0]) == -1 && !g_pass.active);
errno = 0; assert(close(fd[0]) == -1 && errno == EBADF); close(fd[1]);
g_pass.active = 1; vkp_pass_abort(); assert(!g_pass.active);
assert(timed_wait(g_fence, UINT64_MAX) != VK_SUCCESS);
assert(operations == before && destroys == before_destroys);
}
static void acquire_failure(int later, VkResult result) {
setup(later ? 1 : 0);
fail_acquire_at = later ? 2 : 1; acquire_answer = 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(acquires == fail_acquire_at && idle_calls == 1);
assert(submits == (unsigned)later && markers == 0 && waits == 0);
assert(destroys == 0 && g_images == retained && g_nimg == 1);
assert(g_swapchain && g_surface && g_fence && g_scene.img && g_scene.mem);
assert_stopped();
free(g_images); /* test-owned fake resources only */
}
static void upload_timeout(int single_pixel) {
setup(0);
uint8_t bytes[48], saved[48], data[16];
memset(bytes, 0xa5, sizeof(bytes)); memset(data, 0x3c, sizeof(data));
struct vkp_image *img = calloc(1, sizeof(*img)); assert(img);
*img = (struct vkp_image){.image = (VkImage)(uintptr_t)7, .mem = (VkDeviceMemory)(uintptr_t)8,
.map = bytes, .offset = 3, .row_pitch = 16,
.w = single_pixel ? 1 : 2, .h = single_pixel ? 1 : 2};
struct surface s = {.shm_img = img};
struct wl_shm_buffer b = {.w = img->w, .h = img->h, .stride = img->w * 4, .data = data};
if (single_pixel) take_pixel(&s, data, NULL); else take_shm(&s, &b, NULL);
assert(bytes[3] == 0x3c && bytes[0] == 0xa5 && releases == 1);
memcpy(saved, bytes, sizeof(saved));
struct vkp_image dst = {.image = (VkImage)(uintptr_t)9, .blit_dst = 1, .w = img->w, .h = img->h};
fence_answer = VK_TIMEOUT;
assert(vkp_blit_image(img, &dst, -1) == -1);
assert(g_dev_state == -3 && submits == 1 && waits == 1);
memset(data, 0x7e, sizeof(data));
if (single_pixel) take_pixel(&s, data, NULL); else take_shm(&s, &b, NULL);
assert(s.shm_img == img && memcmp(saved, bytes, sizeof(saved)) == 0 && releases == 2);
vkp_image_destroy(img);
assert(destroys == 0 && img->map == bytes && img->image && img->mem);
assert_stopped();
free(img); free(g_images); /* fake resources only, not production cleanup */
}
int main(int argc, char **argv) {
assert(argc == 2);
const char *name = argv[1];
if (!strcmp(name, "first-out-of-date")) acquire_failure(0, VK_ERROR_OUT_OF_DATE_KHR);
else if (!strcmp(name, "first-surface-lost")) acquire_failure(0, VK_ERROR_SURFACE_LOST_KHR);
else if (!strcmp(name, "later-out-of-date")) acquire_failure(1, VK_ERROR_OUT_OF_DATE_KHR);
else if (!strcmp(name, "later-surface-lost")) acquire_failure(1, VK_ERROR_SURFACE_LOST_KHR);
else if (!strcmp(name, "shm-timeout")) upload_timeout(0);
else if (!strcmp(name, "pixel-timeout")) upload_timeout(1);
else if (!strcmp(name, "pass-terminal")) { setup(0); g_dev_state = -3; assert_stopped(); free(g_images); }
else if (!strcmp(name, "normal") || !strcmp(name, "framegen")) {
int extra = !strcmp(name, "framegen"); setup(extra);
assert(vkp_render(64, 64, NULL, 0) == 0 && g_dev_state == 1);
assert(acquires == (unsigned)(1 + extra) && submits == (unsigned)(1 + extra));
assert(waits == 1 && markers == 0 && idle_calls == 0 && destroys == 0);
assert(presented_notifications == (unsigned)extra); free(g_images);
} else if (!strcmp(name, "later-timeout")) {
setup(1); fail_acquire_at = 2; acquire_answer = VK_TIMEOUT;
assert(vkp_render(64, 64, NULL, 0) == -1 && g_dev_state == 1);
assert(acquires == 2 && submits == 2 && markers == 1 && waits == 1); free(g_images);
} else if (!strcmp(name, "acquire-device-lost") || !strcmp(name, "present-device-lost")) {
setup(1);
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(g_dev_state == -2 && lost_notifications == 1 && markers == 0 && waits == 0);
assert(!g_images && !g_swapchain && !g_surface && destroys == 2);
assert_stopped();
} else assert(0 && "unknown scenario");
return 0;
}