vulkan: run the real context setup under the validation layer

The other vulkan samples in this directory each pin down one known
defect.  This one asserts a property instead -- "gfx/common/
vulkan_common.c provokes no complaint from VK_LAYER_KHRONOS_validation"
-- so it catches regressions nobody has thought of yet.  Wrong queue
family indices, extensions used without being enabled, child objects
outliving their parent, semaphores destroyed while still pending: all
of that is invisible on a forgiving driver and all of it is a
validation error here.

No debug messenger of its own.  vulkan_common.c already has one: built
with -DVULKAN_DEBUG it enables the validation layer and
VK_EXT_debug_utils and installs vulkan_debug_cb(), which formats every
message through RARCH_LOG.  The test supplies that RARCH_LOG and counts
validation lines.  So the driver's own debug path is the
instrumentation, and if it ever stops reporting, this test stops
passing.

Headless, via VULKAN_WSI_NONE -- no window, surface or swapchain.  On
CI that means lavapipe:

    apt-get install mesa-vulkan-drivers vulkan-validationlayers
    export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json

With no Vulkan device at all it exits 77, the automake convention for
skipped, so it is safe to run unconditionally.

Three checks: one init/destroy round trip; three of them in a row,
since a leak visible only at vkDestroyInstance shows up on the first
pass while one that needs state carried over from a previous context --
a cached device, a stale queue index -- only shows up on a later one,
and RetroArch does exactly this on every driver reinit; and that every
memory type the device advertises is selectable through
vulkan_find_memory_type(), which is where a wrong answer turns into a
validation error at the first vkBindImageMemory rather than at the
point of the mistake.

The harness has teeth, verified by breaking the driver on purpose.
Removing the vkDestroyDebugUtilsMessengerEXT() call from
vulkan_context_destroy() makes the run fail with

  ERROR Validation: [ VUID-vkDestroyInstance-instance-00629 ]
  ... VkDebugUtilsMessengerEXT 0x10000000001[] has not been destroyed
  errors=1

rather than passing quietly.

Scope, since the request was for vulkan.c: gfx/drivers/vulkan.c itself
is out of reach here.  It has 169 undefined frontend symbols, so
linking it would mean stubbing most of the frontend, and its texture
and descriptor helpers are static besides.  What this covers is
vulkan_common.c -- instance creation, physical device and queue family
selection, logical device creation, the extension and layer
negotiation around all of it, and the teardown.  That is where the
validation-visible mistakes in this backend have historically been.

Passes plain and under SANITIZER=address,undefined.

One observation left in a comment rather than acted on: under
VULKAN_WSI_NONE, vk.context.gpu_properties reads back zeroed even
though vulkan_context_init_device() fills it two lines before it fills
memory_properties, which does come through. Worth a look separately.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-07-28 23:36:09 +02:00
1 parent 669b0e8c85
commit f1270e8788
3 files changed
+402

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
TARGET := vulkan_validation_test
# Path back to the repo root from this sample dir. Like the
# display_servers samples, this one links the real code rather than
# mirroring it -- the point is to run RetroArch's own Vulkan setup
# under the validation layer, so a copy would prove nothing.
REPO_ROOT := ../../..
LIBRETRO_COMM_DIR := $(REPO_ROOT)/libretro-common
# VULKAN_DEBUG is the flag RetroArch already uses to enable
# VK_LAYER_KHRONOS_validation and VK_EXT_debug_utils and to install
# vulkan_debug_cb(). That callback reports through RARCH_LOG, which
# stubs_retroarch.c supplies and scores, so the test needs no debug
# messenger of its own.
SOURCES := vulkan_validation_test.c \
stubs_retroarch.c \
$(REPO_ROOT)/gfx/common/vulkan_common.c \
$(LIBRETRO_COMM_DIR)/vulkan/vulkan_symbol_wrapper.c \
$(LIBRETRO_COMM_DIR)/rthreads/rthreads.c \
$(LIBRETRO_COMM_DIR)/lists/string_list.c \
$(LIBRETRO_COMM_DIR)/dynamic/dylib.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c
CFLAGS += -Wall -std=gnu99 -g -O0 \
-DHAVE_VULKAN -DVULKAN_DEBUG -DHAVE_THREADS -DHAVE_DYLIB \
-I$(REPO_ROOT) \
-I$(LIBRETRO_COMM_DIR)/include
LDFLAGS += -lvulkan -lpthread -ldl
OBJS := $(SOURCES:.c=.o)
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET)
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
# Needs a Vulkan device and the validation layer. lavapipe is enough:
# apt-get install mesa-vulkan-drivers vulkan-validationlayers
# With no device at all the test exits 77 (skipped) rather than failing.
VK_DRIVER_FILES ?= /usr/share/vulkan/icd.d/lvp_icd.json
run: $(TARGET)
VK_DRIVER_FILES=$(VK_DRIVER_FILES) ./$(TARGET)
clean:
rm -f $(TARGET) $(OBJS)
.PHONY: all run clean
@@ -0,0 +1,73 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (stubs_retroarch.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* The frontend symbols gfx/common/vulkan_common.c reaches for, plus
* the logging hook the test scores itself on.
*
* vulkan_common.c is built here with -DVULKAN_DEBUG, which is the flag
* RetroArch already uses to enable VK_LAYER_KHRONOS_validation and
* VK_EXT_debug_utils and to install vulkan_debug_cb(). That callback
* formats every message through RARCH_LOG, so counting validation
* lines here needs no messenger of our own -- the driver's existing
* debug path is the instrumentation. */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
int g_vk_validation_errors;
int g_vk_validation_warnings;
static void vk_log(const char *fmt, va_list ap)
{
char line[4096];
vsnprintf(line, sizeof(line), fmt, ap);
if (strstr(line, "Validation"))
{
if (strstr(line, "ERROR")) g_vk_validation_errors++;
if (strstr(line, "WARNING")) g_vk_validation_warnings++;
fputs(line, stderr);
}
}
void RARCH_LOG(const char *fmt, ...){va_list a;va_start(a,fmt);vk_log(fmt,a);va_end(a);}
void RARCH_ERR(const char *fmt, ...){va_list a;va_start(a,fmt);vk_log(fmt,a);va_end(a);}
void RARCH_WARN(const char *fmt, ...){va_list a;va_start(a,fmt);vk_log(fmt,a);va_end(a);}
void RARCH_DBG(const char *fmt, ...){(void)fmt;}
/* --- The rest of what vulkan_common.c reaches for --- */
#include <boolean.h>
#include <stddef.h>
#include <stdint.h>
void *XGetXCBConnection(void *dpy) { (void)dpy; return NULL; }
static char s_settings[1 << 18];
void *config_get_ptr(void) { return s_settings; }
void *video_state_get_ptr(void) { static char st[1 << 16]; return st; }
const char *msg_hash_to_str(int e) { (void)e; return "stub"; }
void video_driver_set_gpu_api_devices(int api, void *list) { (void)api; (void)list; }
void video_driver_set_gpu_api_version_string(const char *s) { (void)s; }
void video_driver_cache_context_ack_set(void) { }
uint32_t video_driver_get_disp_flags(void) { return 0; }
void video_driver_set_disp_flags(uint32_t f) { (void)f; }
@@ -0,0 +1,270 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (vulkan_validation_test.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* Runs RetroArch's real Vulkan context setup and teardown under
* VK_LAYER_KHRONOS_validation and fails on any validation
* error or warning.
*
* The other vulkan samples in this directory each pin down one
* known defect. This one is the opposite shape: it asserts a
* property -- "gfx/common/vulkan_common.c provokes no
* complaint from the validation layer" -- so it catches
* regressions nobody has thought of yet. Wrong queue family
* indices, extensions used without being enabled, objects
* outliving their parent, semaphores destroyed while pending:
* all of it is invisible to a normal run on a forgiving driver
* and all of it is a validation error here.
*
* HOW IT IS WIRED
*
* No debug messenger of our own. vulkan_common.c already has
* one: built with -DVULKAN_DEBUG it enables the validation layer
* and VK_EXT_debug_utils and installs vulkan_debug_cb(), which
* formats every message through RARCH_LOG. The test supplies
* that RARCH_LOG (see stubs_retroarch.c) and counts the lines.
* So the instrumentation being exercised is the driver's own,
* which is worth something on its own account -- if the debug
* path stops reporting, this test stops passing.
*
* The context is brought up with VULKAN_WSI_NONE, so no window,
* surface or swapchain is needed and it runs headless. On CI
* that means lavapipe:
*
* apt-get install mesa-vulkan-drivers vulkan-validationlayers
* export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json
*
* With no Vulkan device at all the test exits 77 (the automake
* convention for "skipped") rather than failing, so it is safe
* to run unconditionally.
*
* WHAT IT IS NOT
*
* gfx/drivers/vulkan.c proper is out of reach: it has 169
* undefined frontend symbols, so standing it up would mean
* stubbing most of the frontend, and its texture and descriptor
* helpers are static besides. What is covered here is
* vulkan_common.c -- instance creation, physical device
* selection, queue family selection, logical device creation,
* the extension and layer negotiation around all of it, and the
* teardown. That is where the validation-visible mistakes in
* the Vulkan backend have historically lived.
*
* THE HARNESS HAS TEETH
*
* Verified by deliberately breaking the driver: removing the
* vkDestroyDebugUtilsMessengerEXT() call from
* vulkan_context_destroy() makes the run fail with
*
* ERROR Validation: [ VUID-vkDestroyInstance-instance-00629 ]
* ... VkDebugUtilsMessengerEXT 0x10000000001[] has not been
* destroyed ...
* errors=1
*
* so a leaked child object really does come back as a failure
* rather than passing quietly.
*
* Build and run:
*
* make
* VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json \
* ./vulkan_validation_test
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../../../gfx/common/vulkan_common.h"
/* Scored by the RARCH_LOG hook in stubs_retroarch.c. */
extern int g_vk_validation_errors;
extern int g_vk_validation_warnings;
#define SKIP_EXIT_CODE 77
static void reset_counts(void)
{
g_vk_validation_errors = 0;
g_vk_validation_warnings = 0;
}
static int report(const char *what)
{
if (g_vk_validation_errors || g_vk_validation_warnings)
{
fprintf(stderr,
"FAIL: %s produced %d validation error(s)"
" and %d warning(s)\n",
what, g_vk_validation_errors, g_vk_validation_warnings);
return 1;
}
printf("[pass] %s is validation-clean\n", what);
return 0;
}
/* Bring the context up and take it down again. Everything
* vulkan_common.c does before a surface exists happens here. */
static int test_context_init_destroy(int *skipped)
{
gfx_ctx_vulkan_data_t vk;
*skipped = 0;
reset_counts();
memset(&vk, 0, sizeof(vk));
if (!vulkan_context_init(&vk, VULKAN_WSI_NONE))
{
fputs("SKIP: no usable Vulkan device"
" (set VK_DRIVER_FILES for a software ICD)\n", stderr);
*skipped = 1;
return 0;
}
/* Not printing vk.context.gpu_properties here on purpose: under
* VULKAN_WSI_NONE it reads back zeroed even though
* vulkan_context_init_device() fills it unconditionally two lines
* before it fills memory_properties, which does come through. That
* is worth a look on its own and is not this test's business. */
vulkan_context_destroy(&vk, false);
return report("context init/destroy");
}
/* Repeat it. A leak that validation only notices at
* vkDestroyInstance shows up the first time round; one that
* needs state left over from a previous context -- a cached
* device, a stale queue index -- only shows up on the second.
* RetroArch does this for real on every driver reinit. */
static int test_context_cycle(void)
{
int i;
for (i = 0; i < 3; i++)
{
gfx_ctx_vulkan_data_t vk;
reset_counts();
memset(&vk, 0, sizeof(vk));
if (!vulkan_context_init(&vk, VULKAN_WSI_NONE))
{
fprintf(stderr, "FAIL: context init failed on cycle %d\n", i);
return 1;
}
vulkan_context_destroy(&vk, false);
if (g_vk_validation_errors || g_vk_validation_warnings)
{
fprintf(stderr,
"FAIL: cycle %d produced %d validation error(s)"
" and %d warning(s)\n",
i, g_vk_validation_errors, g_vk_validation_warnings);
return 1;
}
}
printf("[pass] three init/destroy cycles are validation-clean\n");
return 0;
}
/* vulkan_find_memory_type() picks the heap for every allocation
* the backend makes; a wrong answer is a validation error at the
* first vkBindImageMemory rather than here, so check the
* contract directly. The fallback form must never return the
* "nothing matched" sentinel when the primary form would have
* succeeded. */
static int test_memory_type_selection(void)
{
gfx_ctx_vulkan_data_t vk;
uint32_t i;
int fail = 0;
reset_counts();
memset(&vk, 0, sizeof(vk));
if (!vulkan_context_init(&vk, VULKAN_WSI_NONE))
{
fputs("FAIL: context init failed\n", stderr);
return 1;
}
for (i = 0; i < vk.context.memory_properties.memoryTypeCount; i++)
{
const VkMemoryPropertyFlags want =
vk.context.memory_properties.memoryTypes[i].propertyFlags;
uint32_t got;
if (!want)
continue;
got = vulkan_find_memory_type(&vk.context.memory_properties,
1u << i, want);
if (got >= vk.context.memory_properties.memoryTypeCount)
{
fprintf(stderr,
"FAIL: memory type %u advertises 0x%x but"
" vulkan_find_memory_type() could not match it\n",
i, (unsigned)want);
fail = 1;
break;
}
if (!(vk.context.memory_properties.memoryTypes[got].propertyFlags
& want))
{
fprintf(stderr,
"FAIL: asked for 0x%x, got type %u with 0x%x\n",
(unsigned)want, got,
(unsigned)vk.context.memory_properties
.memoryTypes[got].propertyFlags);
fail = 1;
break;
}
}
vulkan_context_destroy(&vk, false);
if (fail)
return 1;
printf("[pass] every advertised memory type is selectable\n");
return 0;
}
int main(void)
{
int skipped = 0;
if (test_context_init_destroy(&skipped))
return 1;
if (skipped)
return SKIP_EXIT_CODE;
if (test_context_cycle())
return 1;
if (test_memory_type_selection())
return 1;
puts("ALL OK");
return 0;
}