Framegen: wait on a sync fd instead of idling framegen's whole device

Every generated frame ended with vkDeviceWaitIdle on framegen's device. It is
the hardest sync primitive Vulkan has and it ran once per frame, which is a
large part of why frame generation cost more than it returned.

It was there because the two devices share no semaphore. The upstream
semaphore path cannot supply one on Android: framegen's device deliberately
does not enable VK_KHR_external_semaphore_fd -- it shares through
AHardwareBuffer instead -- so vkImportSemaphoreFdKHR resolves to nullptr there
and every semaphore handed across is -1. That is not a flag that can be
flipped.

A fence can be exported where a semaphore cannot. framegen already creates one
per generation pass, so this adds VK_KHR_external_fence_fd, exports a sync fd
after the passes are submitted, and hands it back through a new
armsx3_lsfg_present_fenced. Our side polls that fd instead of idling a device.

Three things this deliberately does not do:

The existing completion fence is not the one exported. SYNC_FD export has copy
transference, which resets the source fence, so exporting it would leave the
slot-reuse wait at the top of present() blocking on a fence with no pending
signal -- a hang traded for a stall. A dedicated fence signalled by a
zero-batch submit is used instead, which signals once all previously submitted
queue work completes.

The extension is probed, not required. framegen's required-extension list
throws on a miss, so a driver without it would fail vkCreateDevice and lose
frame generation altogether -- a regression rather than a degradation.

The device wait stays as the fallback. A null entry point, a failed export, a
poll timeout or a poll error all fall back to it, and the warning for that is
emitted once per session rather than once per frame.

armsx3_lsfg_present keeps its signature and forwards to the new path with a
null fd, so the two bodies cannot drift. ABI goes to 3; the version check is
left exact rather than loosened.
This commit is contained in:
jpolo1224
2026-08-19 23:28:22 -04:00
parent cd829558a8
commit 43dcf6d0ca
4 changed files with 168 additions and 14 deletions
+1
View File
@@ -19,6 +19,7 @@
armsx3_lsfg_initialize;
armsx3_lsfg_create_context_ahb;
armsx3_lsfg_present;
armsx3_lsfg_present_fenced;
armsx3_lsfg_destroy_context;
armsx3_lsfg_wait_idle;
armsx3_lsfg_finalize;
+22 -2
View File
@@ -184,6 +184,22 @@ extern "C" int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, void* co
extern "C" int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count)
{
// Forwarded rather than duplicated. Asking for no fence descriptor is exactly what this
// always did, and keeping one body means the two entry points cannot drift.
return armsx3_lsfg_present_fenced(ctx, in_sem, out_sems, out_count, nullptr);
}
extern "C" int armsx3_lsfg_present_fenced(int32_t ctx, int in_sem, const int* out_sems,
uint32_t out_count, int* out_fence_fd)
{
if (out_fence_fd)
{
// Written before anything that can fail. Every path out of here leaves the caller with a
// value it can act on, so it can never read an uninitialised int and close a descriptor
// belonging to something else -- which on Android is somebody's socket or an open asset.
*out_fence_fd = -1;
}
if (!g_initialized)
{
set_error("not initialized");
@@ -198,13 +214,17 @@ extern "C" int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems,
outs.push_back(out_sems ? out_sems[i] : -1);
}
// framegen writes the descriptor itself and leaves it at -1 when it cannot produce one, so a
// null out_fence_fd degrades to the plain present without a second code path here.
if (g_performance)
{
ARMSX3_LSFG_GUARD(LSFG_3_1P::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
ARMSX3_LSFG_GUARD(LSFG_3_1P::presentContextFenced(ctx, in_sem, outs, out_fence_fd),
ARMSX3_LSFG_ERR_VULKAN)
}
else
{
ARMSX3_LSFG_GUARD(LSFG_3_1::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
ARMSX3_LSFG_GUARD(LSFG_3_1::presentContextFenced(ctx, in_sem, outs, out_fence_fd),
ARMSX3_LSFG_ERR_VULKAN)
}
return ARMSX3_LSFG_OK;
+24 -1
View File
@@ -29,7 +29,7 @@ extern "C" {
// Bump when anything below changes shape. The loader refuses a library whose version it does not
// recognise, so a stale libarmsx3_lsfg.so on a user's device fails loudly at load instead of
// quietly passing mismatched structs.
#define ARMSX3_LSFG_ABI_VERSION 2u
#define ARMSX3_LSFG_ABI_VERSION 3u
// Mark the exported surface explicitly.
//
@@ -103,6 +103,29 @@ ARMSX3_LSFG_API int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, voi
// each out_sems[i] is signalled when output image i is ready. Pass -1 for an unused slot.
ARMSX3_LSFG_API int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count);
// Generate frames for one presented pair, and hand back a fence for the result.
//
// Identical to armsx3_lsfg_present in every respect except that *out_fence_fd receives a sync file
// descriptor that becomes readable once the generation this call submitted has finished. The
// caller owns that fd and must close(2) it.
//
// This is the answer to armsx3_lsfg_wait_idle() below being the only completion signal on offer.
// framegen renders on its OWN VkDevice, so the caller cannot wait on its queues; before this
// entry point existed the only way to know the generated images were ready -- and, more
// importantly, that framegen had finished READING the caller's input images -- was a
// vkDeviceWaitIdle on framegen's device, once per presented frame. A sync fd can be waited on
// with poll(2) instead, which parks a thread rather than draining a GPU.
//
// *out_fence_fd is set to -1 whenever a descriptor is not available: an older library, a driver
// without VK_KHR_external_fence_fd, or work that had already completed by the time it was asked
// for. -1 is not an error and the return code is still ARMSX3_LSFG_OK -- the caller must fall
// back to armsx3_lsfg_wait_idle(), which is always correct.
//
// Added in ABI 3. Resolve it with dlsym rather than assuming it: this is the one entry point a
// caller can do without.
ARMSX3_LSFG_API int armsx3_lsfg_present_fenced(int32_t ctx, int in_sem, const int* out_sems,
uint32_t out_count, int* out_fence_fd);
ARMSX3_LSFG_API int armsx3_lsfg_destroy_context(int32_t ctx);
// Read the user's own Lossless.dll and keep the shaders it contains.