diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index 24c037c5d..dfd3e9f5d 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -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 */ } diff --git a/docs/development/gpu-pending-completion.md b/docs/development/gpu-pending-completion.md index 16304d038..3beb4d1dd 100644 --- a/docs/development/gpu-pending-completion.md +++ b/docs/development/gpu-pending-completion.md @@ -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, diff --git a/tools/tests/test_vk_pending_completion.py b/tools/tests/test_vk_pending_completion.py index 5563f5cfa..5f69c820d 100644 --- a/tools/tests/test_vk_pending_completion.py +++ b/tools/tests/test_vk_pending_completion.py @@ -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 */ diff --git a/tools/tests/test_vk_terminal_callers.py b/tools/tests/test_vk_terminal_callers.py new file mode 100644 index 000000000..caacb47a0 --- /dev/null +++ b/tools/tests/test_vk_terminal_callers.py @@ -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 +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() diff --git a/tools/tests/vk_terminal_callers_test.c b/tools/tests/vk_terminal_callers_test.c new file mode 100644 index 000000000..e395df07d --- /dev/null +++ b/tools/tests/vk_terminal_callers_test.c @@ -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 + +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; +}