mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Stop quarantined GPU callers before retries and mapped writes
This commit is contained in:
1 parent
68dc1c4549
commit
9e6fe314ae
5 files changed
+404
-8
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user