vulkan: a dupe of a hardware frame may signal its ring fence again; hardware-core lane

The regression lane for 53ca1ac (a hardware core's image is let go of
before the core destroys it), and the second bug the lane ran into.

The bug. Under the threaded wrapper the video thread signals a ring
slot's fence after every frame it draws from the slot, and a dupe
draws from the slot last presented again: one frame in two for a
30 fps game, every frame while the menu is up over a hardware core.
The fence was a bare VkFence, signalled by an empty vkQueueSubmit, and
nothing reset it between a frame and its dupe, so every dupe submitted
a fence that was already signalled or still pending. That is invalid
usage (VUID-vkQueueSubmit-fence-00063/-00064); lavapipe aborts on it.

The ring's fence now records whether it is armed. A signal that finds
it armed waits the earlier arming out and resets it first - a wait on
work submitted a frame ago - and a lock orders that against the main
thread's wait-and-reset, so a reset cannot land on a fence the other
thread has just submitted. Direct3D 12's ring fence counts and never
had this.

The lane. The threaded-video harness core gains a Vulkan hardware
mode (HARNESS_CORE_HW_VULKAN=1): one image of its own, set_image and a
hardware frame per retro_run with every third a dupe, destroyed in
context_destroy without a word to the frontend, and an export that
withdraws it with set_image(NULL) and destroys it in mid-session. It
calls Vulkan through the frontend's interface and links nothing.

With that set the harness runs one lane, hw image lifetime:

 - unthreaded: frames in game, then a staged close from the menu;
 - the core again, under the wrapper: hardware frames reach the
   driver; the core withdraws and destroys its image, and no install
   names an image until it presents again; then the staged close, and
   no install names an image after the core's context_destroy.

It also requires that the close presented at least one frame between
the core's teardown and the new drivers, so it cannot pass by no
longer testing the window.

Run in the Vulkan validation job on lavapipe:

   with both fixes                 pass, no validation errors
   53ca1ac taken out               VUID-vkDestroyImageView-imageView-01026
                                   (view in use), then a segfault
   this fence change taken out     VUID-vkQueueSubmit-fence-00063,
                                   then lavapipe aborts

The software-core lanes are unchanged and pass on the same build.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-30 05:15:22 +02:00
1 parent f516093bc4
commit 7a8b2ab26f
5 files changed
+638 -23

No files matched your search

@@ -191,6 +191,14 @@ jobs:
# a window into it take the other upload path (the one
# beetle-psx takes).
HARNESS_CORE_XRGB8888=1 SAN=none VK_DEBUG=1 GFX_INSTRUMENT=1 CYCLES=1 make -C samples/gfx/threaded_video check-vulkan
# And with the harness core as a Vulkan hardware core, which
# runs its own lane alone: the core's image must not be drawn
# from once the core has destroyed it - at a content close,
# unthreaded and under the wrapper, and after a set_image(NULL)
# - and a dupe of a hardware frame must not re-submit a fence
# that is already signalled. Either is a validation error
# here, and on lavapipe a crash.
HARNESS_CORE_HW_VULKAN=1 SAN=none VK_DEBUG=1 GFX_INSTRUMENT=1 CYCLES=1 make -C samples/gfx/threaded_video check-vulkan
make -C samples/gfx/threaded_video clean
vulkan-validation-sample:
+88 -21
View File
@@ -7452,60 +7452,127 @@ static bool vulkan_hw_ring_install(void *data, const void *image,
return true;
}
/* The ring's fence for one slot.
*
* The video thread signals it after every frame it draws from the
* slot, and a dupe draws from the same slot again - one frame in two
* for a 30 fps game, every frame while the menu is up over a hardware
* core - so the fence is signalled again before anyone has waited it.
* A VkFence cannot take that: it is signalled once, and submitting one
* that is signalled, or still pending, is invalid usage
* (VUID-vkQueueSubmit-fence-00063 and -00064). Drivers differ in what
* they then do; lavapipe aborts.
*
* So the fence records whether it is armed, and a signal that finds it
* armed waits the earlier arming out and resets it first. That wait is
* on work submitted a frame ago. The lock orders it against the main
* thread's wait-and-reset: a reset must not land on a fence the other
* thread has just submitted. */
typedef struct
{
VkFence fence;
#ifdef HAVE_THREADS
slock_t *lock;
#endif
bool armed;
} vk_hw_ring_fence_t;
static bool vulkan_hw_ring_fence_new(void *data, void **fence)
{
VkFenceCreateInfo info;
VkFence f = VK_NULL_HANDLE;
vk_hw_ring_fence_t *f;
vk_t *vk = (vk_t*)data;
if (!vk || !vk->context || !fence)
return false;
if (!(f = (vk_hw_ring_fence_t*)calloc(1, sizeof(*f))))
return false;
memset(&info, 0, sizeof(info));
info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
if (vkCreateFence(vk->context->device, &info, NULL, &f) != VK_SUCCESS)
if (vkCreateFence(vk->context->device, &info, NULL, &f->fence) != VK_SUCCESS)
{
free(f);
return false;
*fence = (void*)(uintptr_t)f;
}
#ifdef HAVE_THREADS
if (!(f->lock = slock_new()))
{
vkDestroyFence(vk->context->device, f->fence, NULL);
free(f);
return false;
}
#endif
*fence = f;
return true;
}
static void vulkan_hw_ring_fence_free(void *data, void *fence)
{
vk_t *vk = (vk_t*)data;
if (!vk || !vk->context || !fence)
vk_hw_ring_fence_t *f = (vk_hw_ring_fence_t*)fence;
vk_t *vk = (vk_t*)data;
if (!vk || !vk->context || !f)
return;
vkDestroyFence(vk->context->device, (VkFence)(uintptr_t)fence, NULL);
vkDestroyFence(vk->context->device, f->fence, NULL);
#ifdef HAVE_THREADS
slock_free(f->lock);
#endif
free(f);
}
/* An empty submission that signals the fence: it completes after every
* submission before it on the queue, so after the frame just made. */
static void vulkan_hw_ring_fence_signal(void *data, void *fence)
{
vk_t *vk = (vk_t*)data;
if (!vk || !vk->context || !fence)
vk_hw_ring_fence_t *f = (vk_hw_ring_fence_t*)fence;
vk_t *vk = (vk_t*)data;
if (!vk || !vk->context || !f)
return;
#ifdef HAVE_THREADS
slock_lock(f->lock);
#endif
if (f->armed)
{
vkWaitForFences(vk->context->device, 1, &f->fence, VK_TRUE, UINT64_MAX);
vkResetFences(vk->context->device, 1, &f->fence);
}
f->armed = true;
#ifdef HAVE_THREADS
slock_lock(vk->context->queue_lock);
#endif
vkQueueSubmit(vk->context->queue, 0, NULL, (VkFence)(uintptr_t)fence);
vkQueueSubmit(vk->context->queue, 0, NULL, f->fence);
#ifdef HAVE_THREADS
slock_unlock(vk->context->queue_lock);
slock_unlock(f->lock);
#endif
}
/* Fences need no queue and no lock: waiting and resetting one is safe
* from any thread, and the wrapper is the only user of these. */
/* Waiting needs no queue. Returns true with the fence reset and ready
* to be signalled again, false when timeout_us passed first. */
static bool vulkan_hw_ring_fence_wait(void *data, void *fence, unsigned timeout_us)
{
VkFence f;
vk_t *vk = (vk_t*)data;
if (!vk || !vk->context || !fence)
bool ret = true;
vk_hw_ring_fence_t *f = (vk_hw_ring_fence_t*)fence;
vk_t *vk = (vk_t*)data;
if (!vk || !vk->context || !f)
return true;
f = (VkFence)(uintptr_t)fence;
if (vkWaitForFences(vk->context->device, 1, &f, VK_TRUE,
timeout_us == HW_RING_WAIT_FOREVER
? UINT64_MAX : (uint64_t)timeout_us * 1000) != VK_SUCCESS)
return false;
vkResetFences(vk->context->device, 1, &f);
return true;
#ifdef HAVE_THREADS
slock_lock(f->lock);
#endif
if (f->armed)
{
if (vkWaitForFences(vk->context->device, 1, &f->fence, VK_TRUE,
timeout_us == HW_RING_WAIT_FOREVER
? UINT64_MAX : (uint64_t)timeout_us * 1000) == VK_SUCCESS)
{
vkResetFences(vk->context->device, 1, &f->fence);
f->armed = false;
}
else
ret = false;
}
#ifdef HAVE_THREADS
slock_unlock(f->lock);
#endif
return ret;
}
static retro_time_t vulkan_get_last_present_time(void *data)
+4 -1
View File
@@ -70,6 +70,9 @@ sed -e "s#$objdir/retroarch\.o#$out/retroarch_nomain.o $out/harness_main.o#" \
# the frontend can load; the .so name is only a name, every loader
# takes it.
core_cc=$(awk '{print $1}' "$cc_line")
$core_cc -O1 -g -shared -fPIC -Ilibretro-common/include -o $out/harness_core.so $out/harness_core.c
# gfx/include carries the tree's own Vulkan headers, for the core's
# hardware mode (HARNESS_CORE_HW_VULKAN); it calls Vulkan through the
# frontend's interface and links nothing.
$core_cc -O1 -g -shared -fPIC -Ilibretro-common/include -Igfx/include -o $out/harness_core.so $out/harness_core.c
echo "built $out/threaded_video_test and $out/harness_core.so"
+304 -1
View File
@@ -8,6 +8,10 @@
#include <stdlib.h>
#include <stdint.h>
#include <libretro.h>
/* The hardware mode below reaches Vulkan through the entry points the
* frontend's interface hands over, so the core links nothing. */
#define VK_NO_PROTOTYPES
#include <libretro_vulkan.h>
#define W 320
#define H 240
@@ -22,10 +26,278 @@ static unsigned bpp = 2;
static enum retro_pixel_format pixel_format = RETRO_PIXEL_FORMAT_RGB565;
static unsigned runs;
/* --- hardware mode ------------------------------------------------------
* With HARNESS_CORE_HW_VULKAN set the core is a Vulkan hardware core:
* one image of its own, handed to the frontend with set_image every
* frame, and destroyed when the context goes - without a word to the
* frontend, which is what a core is entitled to do in context_destroy.
* It can also withdraw the image in mid-session the way a core does
* before it rebuilds its renderer: set_image(NULL), then destroy. Either
* way the frontend must not draw from the image again. */
static int hw_mode;
static const struct retro_hw_render_interface_vulkan *hw_vk;
static struct retro_hw_render_callback hw_cb;
static struct retro_vulkan_image hw_image;
static VkImage hw_vkimage;
static VkDeviceMemory hw_memory;
static int hw_have_image;
static int hw_withdrawn;
static PFN_vkCreateImage hw_vkCreateImage;
static PFN_vkDestroyImage hw_vkDestroyImage;
static PFN_vkGetImageMemoryRequirements hw_vkGetImageMemoryRequirements;
static PFN_vkAllocateMemory hw_vkAllocateMemory;
static PFN_vkFreeMemory hw_vkFreeMemory;
static PFN_vkBindImageMemory hw_vkBindImageMemory;
static PFN_vkCreateImageView hw_vkCreateImageView;
static PFN_vkDestroyImageView hw_vkDestroyImageView;
static PFN_vkCreateCommandPool hw_vkCreateCommandPool;
static PFN_vkDestroyCommandPool hw_vkDestroyCommandPool;
static PFN_vkAllocateCommandBuffers hw_vkAllocateCommandBuffers;
static PFN_vkBeginCommandBuffer hw_vkBeginCommandBuffer;
static PFN_vkEndCommandBuffer hw_vkEndCommandBuffer;
static PFN_vkCmdPipelineBarrier hw_vkCmdPipelineBarrier;
static PFN_vkCmdClearColorImage hw_vkCmdClearColorImage;
static PFN_vkQueueSubmit hw_vkQueueSubmit;
static PFN_vkCreateFence hw_vkCreateFence;
static PFN_vkDestroyFence hw_vkDestroyFence;
static PFN_vkWaitForFences hw_vkWaitForFences;
static PFN_vkGetPhysicalDeviceMemoryProperties hw_vkGetPhysicalDeviceMemoryProperties;
#define HW_SYM(name) \
hw_##name = (PFN_##name)hw_vk->get_device_proc_addr(hw_vk->device, #name)
static void hw_barrier(VkCommandBuffer cmd, VkImageLayout from, VkImageLayout to,
VkAccessFlags src_access, VkAccessFlags dst_access,
VkPipelineStageFlags src_stage, VkPipelineStageFlags dst_stage)
{
VkImageMemoryBarrier b;
memset(&b, 0, sizeof(b));
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
b.srcAccessMask = src_access;
b.dstAccessMask = dst_access;
b.oldLayout = from;
b.newLayout = to;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = hw_vkimage;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
b.subresourceRange.levelCount = 1;
b.subresourceRange.layerCount = 1;
hw_vkCmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &b);
}
static void hw_image_destroy(void)
{
if (!hw_have_image)
return;
hw_vkDestroyImageView(hw_vk->device, hw_image.image_view, NULL);
hw_vkDestroyImage(hw_vk->device, hw_vkimage, NULL);
hw_vkFreeMemory(hw_vk->device, hw_memory, NULL);
memset(&hw_image, 0, sizeof(hw_image));
hw_vkimage = VK_NULL_HANDLE;
hw_memory = VK_NULL_HANDLE;
hw_have_image = 0;
}
/* A cleared W x H image in the layout the frontend samples it in. */
static int hw_image_create(void)
{
VkImageCreateInfo ici;
VkMemoryRequirements reqs;
VkPhysicalDeviceMemoryProperties props;
VkMemoryAllocateInfo mai;
VkCommandPoolCreateInfo cpi;
VkCommandBufferAllocateInfo cai;
VkCommandBufferBeginInfo cbi;
VkFenceCreateInfo fci;
VkSubmitInfo si;
VkImageSubresourceRange range;
VkClearColorValue colour;
VkCommandPool pool = VK_NULL_HANDLE;
VkCommandBuffer cmd = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
uint32_t i;
memset(&ici, 0, sizeof(ici));
ici.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
ici.imageType = VK_IMAGE_TYPE_2D;
ici.format = VK_FORMAT_R8G8B8A8_UNORM;
ici.extent.width = W;
ici.extent.height = H;
ici.extent.depth = 1;
ici.mipLevels = 1;
ici.arrayLayers = 1;
ici.samples = VK_SAMPLE_COUNT_1_BIT;
ici.tiling = VK_IMAGE_TILING_OPTIMAL;
ici.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
ici.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (hw_vkCreateImage(hw_vk->device, &ici, NULL, &hw_vkimage) != VK_SUCCESS)
return 0;
hw_vkGetImageMemoryRequirements(hw_vk->device, hw_vkimage, &reqs);
hw_vkGetPhysicalDeviceMemoryProperties(hw_vk->gpu, &props);
for (i = 0; i < props.memoryTypeCount; i++)
if (reqs.memoryTypeBits & (1u << i))
break;
memset(&mai, 0, sizeof(mai));
mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
mai.allocationSize = reqs.size;
mai.memoryTypeIndex = i;
if ( i == props.memoryTypeCount
|| hw_vkAllocateMemory(hw_vk->device, &mai, NULL, &hw_memory) != VK_SUCCESS)
{
hw_vkDestroyImage(hw_vk->device, hw_vkimage, NULL);
return 0;
}
hw_vkBindImageMemory(hw_vk->device, hw_vkimage, hw_memory, 0);
memset(&hw_image, 0, sizeof(hw_image));
hw_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
hw_image.create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
hw_image.create_info.image = hw_vkimage;
hw_image.create_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
hw_image.create_info.format = VK_FORMAT_R8G8B8A8_UNORM;
hw_image.create_info.components.r = VK_COMPONENT_SWIZZLE_R;
hw_image.create_info.components.g = VK_COMPONENT_SWIZZLE_G;
hw_image.create_info.components.b = VK_COMPONENT_SWIZZLE_B;
hw_image.create_info.components.a = VK_COMPONENT_SWIZZLE_A;
hw_image.create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
hw_image.create_info.subresourceRange.levelCount = 1;
hw_image.create_info.subresourceRange.layerCount = 1;
hw_vkCreateImageView(hw_vk->device, &hw_image.create_info, NULL, &hw_image.image_view);
memset(&cpi, 0, sizeof(cpi));
cpi.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
cpi.queueFamilyIndex = hw_vk->queue_index;
hw_vkCreateCommandPool(hw_vk->device, &cpi, NULL, &pool);
memset(&cai, 0, sizeof(cai));
cai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cai.commandPool = pool;
cai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cai.commandBufferCount = 1;
hw_vkAllocateCommandBuffers(hw_vk->device, &cai, &cmd);
memset(&cbi, 0, sizeof(cbi));
cbi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
hw_vkBeginCommandBuffer(cmd, &cbi);
hw_barrier(cmd, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
0, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
memset(&range, 0, sizeof(range));
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
range.levelCount = 1;
range.layerCount = 1;
colour.float32[0] = 0.1f;
colour.float32[1] = 0.6f;
colour.float32[2] = 0.3f;
colour.float32[3] = 1.0f;
hw_vkCmdClearColorImage(cmd, hw_vkimage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
&colour, 1, &range);
hw_barrier(cmd, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
hw_vkEndCommandBuffer(cmd);
memset(&fci, 0, sizeof(fci));
fci.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
hw_vkCreateFence(hw_vk->device, &fci, NULL, &fence);
memset(&si, 0, sizeof(si));
si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
si.commandBufferCount = 1;
si.pCommandBuffers = &cmd;
/* The queue is the frontend's as well. */
hw_vk->lock_queue(hw_vk->handle);
hw_vkQueueSubmit(hw_vk->queue, 1, &si, fence);
hw_vk->unlock_queue(hw_vk->handle);
hw_vkWaitForFences(hw_vk->device, 1, &fence, VK_TRUE, UINT64_MAX);
hw_vkDestroyFence(hw_vk->device, fence, NULL);
hw_vkDestroyCommandPool(hw_vk->device, pool, NULL);
hw_have_image = 1;
return 1;
}
static void hw_context_reset(void)
{
hw_vk = NULL;
if ( !environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, (void*)&hw_vk)
|| !hw_vk
|| hw_vk->interface_type != RETRO_HW_RENDER_INTERFACE_VULKAN)
{
hw_vk = NULL;
return;
}
HW_SYM(vkCreateImage);
HW_SYM(vkDestroyImage);
HW_SYM(vkGetImageMemoryRequirements);
HW_SYM(vkAllocateMemory);
HW_SYM(vkFreeMemory);
HW_SYM(vkBindImageMemory);
HW_SYM(vkCreateImageView);
HW_SYM(vkDestroyImageView);
HW_SYM(vkCreateCommandPool);
HW_SYM(vkDestroyCommandPool);
HW_SYM(vkAllocateCommandBuffers);
HW_SYM(vkBeginCommandBuffer);
HW_SYM(vkEndCommandBuffer);
HW_SYM(vkCmdPipelineBarrier);
HW_SYM(vkCmdClearColorImage);
HW_SYM(vkQueueSubmit);
HW_SYM(vkCreateFence);
HW_SYM(vkDestroyFence);
HW_SYM(vkWaitForFences);
hw_vkGetPhysicalDeviceMemoryProperties = (PFN_vkGetPhysicalDeviceMemoryProperties)
hw_vk->get_instance_proc_addr(hw_vk->instance, "vkGetPhysicalDeviceMemoryProperties");
if (!hw_withdrawn)
hw_image_create();
}
/* The image goes with the context, and the frontend is not told. */
static void hw_context_destroy(void)
{
if (hw_vk)
hw_image_destroy();
hw_vk = NULL;
}
/* RETRO_API, as the exports further down are. 1 when the core is a
* hardware core with its image up. */
RETRO_API unsigned harness_core_hw_active(void)
{
return (hw_mode && hw_vk && hw_have_image) ? 1 : 0;
}
/* Withdraw the image and destroy it; retro_run pushes dupes until
* harness_core_hw_restore(). */
RETRO_API unsigned harness_core_hw_withdraw(void)
{
if (!hw_mode || !hw_vk || !hw_have_image)
return 0;
hw_vk->set_image(hw_vk->handle, NULL, 0, NULL, VK_QUEUE_FAMILY_IGNORED);
hw_vk->wait_sync_index(hw_vk->handle);
hw_image_destroy();
hw_withdrawn = 1;
return 1;
}
RETRO_API unsigned harness_core_hw_restore(void)
{
if (!hw_mode || !hw_vk || hw_have_image)
return 0;
hw_withdrawn = 0;
return hw_image_create() ? 1 : 0;
}
void retro_set_environment(retro_environment_t cb)
{
bool no_content = true;
environ_cb = cb;
{
const char *hw = getenv("HARNESS_CORE_HW_VULKAN");
hw_mode = (hw && *hw) ? 1 : 0;
}
/* RGB565 by default; XRGB8888 with HARNESS_CORE_XRGB8888 set. The
* two take different paths through a driver - Vulkan converts
* RGB565 with a compute shader and samples a linear XRGB8888
@@ -102,6 +374,22 @@ void retro_run(void)
const uint8_t *push = NULL;
runs++;
if (hw_mode && hw_vk)
{
/* The same image every frame, and every third frame a dupe, as
* the software frames below. Nothing but dupes while the image
* is withdrawn. */
if (hw_withdrawn || !hw_have_image || runs % 3 == 0)
video_cb(NULL, W, H, 0);
else
{
hw_vk->set_image(hw_vk->handle, &hw_image, 0, NULL,
VK_QUEUE_FAMILY_IGNORED);
video_cb(RETRO_HW_FRAME_BUFFER_VALID, W, H, 0);
}
return;
}
if (harness_use_fb && h == H)
{
struct retro_framebuffer fb;
@@ -152,7 +440,22 @@ bool retro_serialize(void *data, size_t size) { (void)data; (void)size; return f
bool retro_unserialize(const void *data, size_t size) { (void)data; (void)size; return false; }
void retro_cheat_reset(void) { }
void retro_cheat_set(unsigned index, bool enabled, const char *code) { (void)index; (void)enabled; (void)code; }
bool retro_load_game(const struct retro_game_info *game) { (void)game; return true; }
bool retro_load_game(const struct retro_game_info *game)
{
(void)game;
if (hw_mode)
{
memset(&hw_cb, 0, sizeof(hw_cb));
hw_cb.context_type = RETRO_HW_CONTEXT_VULKAN;
hw_cb.version_major = VK_MAKE_VERSION(1, 0, 18);
hw_cb.context_reset = hw_context_reset;
hw_cb.context_destroy = hw_context_destroy;
/* Refused (another driver): a software core after all. */
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_cb))
hw_mode = 0;
}
return true;
}
bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t num) { (void)type; (void)info; (void)num; return false; }
void retro_unload_game(void) { }
unsigned retro_get_region(void) { return RETRO_REGION_NTSC; }
@@ -45,6 +45,7 @@
#include "../../../menu/menu_setting.h"
#include "../../../verbosity.h"
#include "../../../input/input_driver.h"
#include "../../../tasks/task_content.h"
#ifdef HAVE_X11
#include <X11/Xlib.h>
@@ -56,6 +57,7 @@
#include <features/features_cpu.h>
#include <rthreads/rthreads.h>
#include <file/config_file.h>
#include <queues/task_queue.h>
static unsigned failures = 0;
@@ -4744,6 +4746,230 @@ static void lane_x11_wsi_connection(void)
#endif
}
/* ------------------------------------------------------------------ */
/* Lane: a hardware core's image is not drawn from after it is gone */
/* A Vulkan hardware core hands the frontend an image of its own */
/* and destroys it in context_destroy. A staged content close keeps */
/* the old drivers presenting dupes of the last frame until the new */
/* session's are built, so unless the driver is made to let go of */
/* the image first, every one of those dupes samples a destroyed */
/* VkImageView: reported as a freeze and a fault inside the GPU */
/* driver on closing content with LRPS2 (libretro/ps2 #171). */
/* The same for a core that withdraws its image in mid-session with */
/* set_image(NULL) and then destroys it: under the wrapper the */
/* withdrawal reached only the ring slot the core fills next, and */
/* the dupes that followed re-read the slot last presented. */
/* */
/* Runs with the harness core in its hardware mode, which is a */
/* separate invocation (HARNESS_CORE_HW_VULKAN=1) with this lane */
/* alone: the other lanes are about software frames. Unthreaded the */
/* proof is the run itself - the driver's pointer to the core's */
/* image goes into the unloaded core, and the validation layer sees */
/* the destroyed view. Under the wrapper the ring's installs are */
/* also counted: none may name an image after the core destroyed */
/* it. */
/* ------------------------------------------------------------------ */
static video_poke_interface_t hwlane_poke;
static const video_poke_interface_t *hwlane_inner_poke;
static retro_hw_context_reset_t hwlane_core_destroy;
static retro_atomic_int_t hwlane_gone; /* the core's image is destroyed */
static retro_atomic_int_t hwlane_installs; /* installs that named an image */
static retro_atomic_int_t hwlane_installs_gone; /* ... after it was destroyed */
static bool hwlane_install(void *data, const void *image,
const void *semaphores, unsigned num_semaphores,
unsigned src_queue_family, const void *cmd, unsigned num_cmd)
{
if (image)
{
retro_atomic_fetch_add_int(&hwlane_installs, 1);
if (retro_atomic_load_acquire_int(&hwlane_gone))
retro_atomic_fetch_add_int(&hwlane_installs_gone, 1);
}
return hwlane_inner_poke->hw_ring_install(data, image, semaphores,
num_semaphores, src_queue_family, cmd, num_cmd);
}
/* In front of the core's context_destroy, to know when the image went. */
static void hwlane_context_destroy(void)
{
hwlane_core_destroy();
retro_atomic_store_release_int(&hwlane_gone, 1);
}
static unsigned hwlane_core_call(const char *name)
{
dylib_t lib = runloop_state_get_ptr()->lib_handle;
unsigned (*fn)(void) = lib
? (unsigned (*)(void))dylib_proc(lib, name) : NULL;
return fn ? fn() : 0;
}
/* The frame loop as the frontend's main loop runs it: the staged close
* and load are tasks. */
static void hwlane_pump(unsigned n)
{
unsigned i;
for (i = 0; i < n; i++)
{
runloop_iterate();
task_queue_check();
}
}
/* Counts the wrapper's installs from here on. The wrapper is idle
* while its table is swapped. */
static bool hwlane_probe(void)
{
video_driver_state_t *video_st = video_state_get_ptr();
thread_video_t *thr = (thread_video_t*)video_st->data;
if ( !video_st->thread_wrapper_active
|| !thr || !thr->poke || !thr->poke->hw_ring_install)
return false;
video_thread_wait_idle();
hwlane_inner_poke = thr->poke;
hwlane_poke = *thr->poke;
hwlane_poke.hw_ring_install = hwlane_install;
thr->poke = &hwlane_poke;
return true;
}
/* Closes content from the menu, staged, and runs the frame loop until
* the dummy core's drivers are up. Returns the frames the old drivers
* presented after the core destroyed its image. */
static unsigned hwlane_close(const char *when)
{
struct menu_state *menu_st = menu_state_get_ptr();
video_driver_state_t *video_st = video_state_get_ptr();
unsigned i;
unsigned after = 0;
if (!menu_is_up())
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
hwlane_pump(2);
CHECK(menu_is_up(), "%s: menu did not open", when);
hwlane_core_destroy = video_st->hw_render.context_destroy;
CHECK(hwlane_core_destroy != NULL, "%s: the core has no context_destroy", when);
if (!hwlane_core_destroy)
return 0;
retro_atomic_store_release_int(&hwlane_gone, 0);
retro_atomic_store_release_int(&hwlane_installs_gone, 0);
video_st->hw_render.context_destroy = hwlane_context_destroy;
menu_st->flags |= MENU_ST_FLAG_PENDING_CLOSE_CONTENT;
hwlane_pump(1);
CHECK(runloop_is_content_switching(),
"%s: the close did not start a staged load", when);
for (i = 0; i < 400 && runloop_is_content_switching(); i++)
{
if (retro_atomic_load_acquire_int(&hwlane_gone))
after++;
hwlane_pump(1);
}
CHECK(!runloop_is_content_switching(), "%s: the close did not finish", when);
CHECK(retro_atomic_load_acquire_int(&hwlane_gone),
"%s: the core's context_destroy never ran", when);
CHECK(runloop_state_get_ptr()->current_core_type == CORE_TYPE_DUMMY,
"%s: not on the dummy core after the close", when);
/* The window the lane is about. If the close ever stops presenting
* between the core's teardown and the new drivers, this lane no
* longer tests anything and should say so. */
CHECK(after >= 1,
"%s: no frame presented between the core's teardown and the new drivers",
when);
hwlane_pump(5);
return after;
}
static void lane_hw_image_lifetime(const char *core_path)
{
unsigned had = failures;
unsigned after_plain, after_wrapped;
int before;
unsigned i;
if (!hwlane_core_call("harness_core_hw_active"))
{
CHECK(0, "hw image lifetime: the harness core is not a hardware core"
" (needs the vulkan driver)");
return;
}
/* Unthreaded: frames in game, then the close. */
expect_wrapper(false, "hw unthreaded");
if (menu_is_up())
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
hwlane_pump(20);
after_plain = hwlane_close("hw unthreaded close");
/* The core again, under the wrapper this time. */
CHECK(task_push_load_contentless_core_from_menu(core_path),
"hw image lifetime: the core was not started again");
for (i = 0; i < 400 && runloop_is_content_switching(); i++)
hwlane_pump(1);
CHECK(!runloop_is_content_switching(),
"hw image lifetime: the core's restart did not finish");
hwlane_pump(5);
if (menu_is_up())
command_event(CMD_EVENT_MENU_TOGGLE, NULL);
set_threaded_via_setting(true);
hwlane_pump(20);
expect_wrapper(true, "hw under the wrapper");
CHECK(hwlane_core_call("harness_core_hw_active"),
"hw image lifetime: the core did not come back as a hardware core");
if (!hwlane_probe())
{
CHECK(0, "hw image lifetime: the wrapper has no hardware ring for this driver");
return;
}
retro_atomic_store_release_int(&hwlane_gone, 0);
retro_atomic_store_release_int(&hwlane_installs, 0);
retro_atomic_store_release_int(&hwlane_installs_gone, 0);
hwlane_pump(20);
video_thread_wait_idle();
before = retro_atomic_load_acquire_int(&hwlane_installs);
CHECK(before > 0, "hw image lifetime: no hardware frame reached the driver");
/* The core withdraws its image and destroys it; dupes follow. */
CHECK(hwlane_core_call("harness_core_hw_withdraw"),
"hw image lifetime: the core did not withdraw its image");
retro_atomic_store_release_int(&hwlane_gone, 1);
hwlane_pump(20);
video_thread_wait_idle();
CHECK(retro_atomic_load_acquire_int(&hwlane_installs_gone) == 0,
"hw image lifetime: the core withdrew and destroyed its image, and"
" the ring handed it to the driver %d more time(s)",
retro_atomic_load_acquire_int(&hwlane_installs_gone));
/* And presents again: the ring is not left deaf. */
retro_atomic_store_release_int(&hwlane_gone, 0);
before = retro_atomic_load_acquire_int(&hwlane_installs);
CHECK(hwlane_core_call("harness_core_hw_restore"),
"hw image lifetime: the core did not restore its image");
hwlane_pump(20);
video_thread_wait_idle();
CHECK(retro_atomic_load_acquire_int(&hwlane_installs) > before,
"hw image lifetime: no hardware frame reached the driver after the"
" core presented again");
/* The close, under the wrapper. */
after_wrapped = hwlane_close("hw wrapped close");
CHECK(retro_atomic_load_acquire_int(&hwlane_installs_gone) == 0,
"hw image lifetime: the core destroyed its image at the close, and"
" the ring handed it to the driver %d more time(s)",
retro_atomic_load_acquire_int(&hwlane_installs_gone));
set_threaded_via_setting(false);
hwlane_pump(5);
if (failures == had)
fprintf(stderr, "[pass] hw image lifetime lane (%u + %u frames presented"
" after the core's teardown)\n", after_plain, after_wrapped);
}
int main(int argc, char *argv[])
{
char cfg_path[512];
@@ -4856,6 +5082,13 @@ int main(int argc, char *argv[])
CHECK(menu_is_up(), "menu did not open");
expect_wrapper(false, "boot");
/* The harness core as a hardware core: its own lane, alone. */
if (getenv("HARNESS_CORE_HW_VULKAN"))
{
lane_hw_image_lifetime(core_path);
goto shutdown;
}
lane_line_separation();
lane_vp_params_publish();
lane_toggle_cycle(cycles);
@@ -4914,6 +5147,7 @@ int main(int argc, char *argv[])
fprintf(stderr, "[skip] null-driver instrumented lanes (real driver: %s)\n",
getenv("HARNESS_VIDEO_DRIVER"));
shutdown:
/* Orderly shutdown: the teardown barriers are part of what is
* under test. */
set_threaded_via_setting(true);