mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Framegen: restore the sync-fd handoff
This is the change that stopped turning frame generation off leaving the frame rate tanked -- confirmed by which build that was reported on, and by the SUBOPTIMAL acquire fix not reproducing it on its own. It replaces the per-frame vkDeviceWaitIdle on framegen's device with a poll on a sync fd exported from a fence framegen already creates. Reasons it is safe to bring back while the other three stay reverted: it only runs when frame generation is producing frames, every failure path falls back to the device wait, and it changes no layout, no ownership and no barrier -- which is where the rendering damage came from. Still out, and staying out: the cross-device ownership barrier, the generated-frame acquire semaphore with its teardown, and the deferred-present guard in frame_context_cleanup.
This commit is contained in:
Vendored
+1
@@ -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;
|
||||
|
||||
Vendored
+22
-2
@@ -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;
|
||||
|
||||
Vendored
+24
-1
@@ -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.
|
||||
|
||||
@@ -465,6 +465,27 @@ namespace gl::es
|
||||
glNamedFramebufferTextureEXT(framebuffer, attachment, texture, level);
|
||||
}
|
||||
|
||||
// Layered attach, needed since upstream started binding 3D/array/cube levels through DSA.
|
||||
// EXT_direct_state_access has no NamedFramebufferTextureLayer, so this binds the framebuffer
|
||||
// and uses the non-DSA entry point -- correct, and no worse than what the caller did before
|
||||
// DSA_CALL2 existed. The previous binding is restored so the caller sees no side effect.
|
||||
void glNamedFramebufferTextureLayer(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer)
|
||||
{
|
||||
GLint previous = 0;
|
||||
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous);
|
||||
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
|
||||
glFramebufferTextureLayer(GL_DRAW_FRAMEBUFFER, attachment, texture, level, layer);
|
||||
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(previous));
|
||||
}
|
||||
|
||||
// DSA_CALL2 resolves either spelling, so both have to exist here.
|
||||
void glNamedFramebufferTextureLayerEXT(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer)
|
||||
{
|
||||
glNamedFramebufferTextureLayer(framebuffer, attachment, texture, level, layer);
|
||||
}
|
||||
|
||||
GLenum glCheckNamedFramebufferStatus(GLuint framebuffer, GLenum target)
|
||||
{
|
||||
return glCheckNamedFramebufferStatusEXT(framebuffer, target);
|
||||
|
||||
@@ -376,6 +376,8 @@ namespace gl::es
|
||||
void glTextureParameteriv(GLuint texture, GLenum pname, const GLint* params);
|
||||
|
||||
void glNamedFramebufferTexture(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level);
|
||||
void glNamedFramebufferTextureLayer(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer);
|
||||
void glNamedFramebufferTextureLayerEXT(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer);
|
||||
GLenum glCheckNamedFramebufferStatus(GLuint framebuffer, GLenum target);
|
||||
void glNamedFramebufferDrawBuffers(GLuint framebuffer, GLsizei n, const GLenum* bufs);
|
||||
void glNamedFramebufferReadBuffer(GLuint framebuffer, GLenum src);
|
||||
|
||||
+121
-11
@@ -14,6 +14,12 @@
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <android/hardware_buffer.h>
|
||||
// The completion signal framegen hands back is a sync file descriptor, not a VkFence we could
|
||||
// wait on -- it renders on its own device. poll(2) waits on it, close(2) releases it, and errno
|
||||
// is read to tell a signal-interrupted poll apart from a real failure.
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#endif
|
||||
|
||||
LOG_CHANNEL(framegen_log, "FRAMEGEN");
|
||||
@@ -31,6 +37,9 @@ namespace vk::frame_gen
|
||||
int (*initialize)(uint64_t, int, float, uint64_t, int, armsx3_lsfg_shader_loader, void*) = nullptr;
|
||||
int32_t (*create_context_ahb)(void*, void*, void* const*, uint32_t, uint32_t, uint32_t, int32_t) = nullptr;
|
||||
int (*present)(int32_t, int, const int*, uint32_t) = nullptr;
|
||||
// ABI 3 and later only, and the one entry point that is allowed to stay null: without
|
||||
// it the present path keeps the device-wide wait it has always used.
|
||||
int (*present_fenced)(int32_t, int, const int*, uint32_t, int*) = nullptr;
|
||||
int (*destroy_context)(int32_t) = nullptr;
|
||||
void (*wait_idle)() = nullptr;
|
||||
void (*finalize)() = nullptr;
|
||||
@@ -255,6 +264,23 @@ namespace vk::frame_gen
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolved on its own, with dlsym rather than resolve(), because a null result here is
|
||||
// not a failure: this entry point arrived in ABI 3 and the library is perfectly usable
|
||||
// without it -- the present path simply keeps its vkDeviceWaitIdle. resolve() would log
|
||||
// an error and, worse, fold into the `all` chain above and refuse the whole library.
|
||||
//
|
||||
// The version gate above already turns away anything that is not exactly this build's
|
||||
// ABI, so in practice this is only null for a library that reports 3 and then does not
|
||||
// export the symbol. Cheap enough to check rather than assume.
|
||||
g_api.present_fenced = reinterpret_cast<decltype(g_api.present_fenced)>(
|
||||
dlsym(g_api.handle, "armsx3_lsfg_present_fenced"));
|
||||
|
||||
if (!g_api.present_fenced)
|
||||
{
|
||||
framegen_log.notice("Frame generation library has no fenced present; every generated "
|
||||
"frame will cost a device wait");
|
||||
}
|
||||
|
||||
g_api.ok = true;
|
||||
framegen_log.success("Frame generation library loaded (ABI %u)", ARMSX3_LSFG_ABI_VERSION);
|
||||
}
|
||||
@@ -1014,6 +1040,74 @@ namespace vk::frame_gen
|
||||
|
||||
g_context_outputs = 0;
|
||||
}
|
||||
|
||||
// How long to wait on framegen's completion fence before giving up on it.
|
||||
//
|
||||
// Generation is a handful of compute dispatches over framebuffer-sized images; anything
|
||||
// past this is not slow, it is wrong -- a lost device, or a descriptor that will never
|
||||
// signal. The bound is what keeps a failure here a stall rather than a hang, and a frozen
|
||||
// emulator is a far worse outcome than a frame of judder.
|
||||
constexpr int k_fence_timeout_ms = 250;
|
||||
|
||||
// Wait until the generation started by the present above has finished.
|
||||
//
|
||||
// Our device wrote the input images framegen reads, and there is no semaphore shared
|
||||
// between the two devices, so something has to stand in for one. A sync fd is that
|
||||
// something: poll(2) parks this thread until the fence framegen submitted signals, and
|
||||
// framegen's queues keep running -- where the device wait below drains them entirely.
|
||||
//
|
||||
// Every path that does not end in a signalled descriptor falls back to that device wait,
|
||||
// because it is always correct. The cost of being wrong here is not a slow frame, it is
|
||||
// reading an image that is still being written.
|
||||
void wait_for_generation(int fence_fd)
|
||||
{
|
||||
if (fence_fd < 0)
|
||||
{
|
||||
// No descriptor to wait on: a library with no fenced present, a driver without
|
||||
// VK_KHR_external_fence_fd, or work that had already finished by the time framegen
|
||||
// asked for the fd -- which the specification allows to come back as -1. All three
|
||||
// want exactly what this line used to do unconditionally.
|
||||
g_api.wait_idle();
|
||||
return;
|
||||
}
|
||||
|
||||
pollfd waiter{};
|
||||
waiter.fd = fence_fd;
|
||||
waiter.events = POLLIN;
|
||||
|
||||
int rc = 0;
|
||||
|
||||
do
|
||||
{
|
||||
rc = ::poll(&waiter, 1, k_fence_timeout_ms);
|
||||
}
|
||||
while (rc < 0 && errno == EINTR);
|
||||
|
||||
// A sync fd becomes readable when the fence it carries signals. Anything else -- a
|
||||
// timeout (0), an error (< 0), or POLLERR/POLLNVAL on a descriptor that went away --
|
||||
// leaves us not knowing whether framegen has finished reading the inputs the next
|
||||
// capture is about to overwrite, so take the wait that is always right.
|
||||
if (rc <= 0 || !(waiter.revents & POLLIN))
|
||||
{
|
||||
// Once, and only once. This runs on the present path: a line per frame would not
|
||||
// be a diagnostic, it would be a second performance problem stacked on the first.
|
||||
static bool s_warned = false;
|
||||
|
||||
if (!s_warned)
|
||||
{
|
||||
s_warned = true;
|
||||
framegen_log.warning("Frame generation fence did not signal within %dms (poll %d, revents 0x%x); "
|
||||
"falling back to a device wait. Reported once per session.",
|
||||
k_fence_timeout_ms, rc, static_cast<u32>(waiter.revents));
|
||||
}
|
||||
|
||||
g_api.wait_idle();
|
||||
}
|
||||
|
||||
// Ours to close either way -- framegen handed over ownership with the descriptor, and
|
||||
// leaking one per frame would run the process out of file descriptors in minutes.
|
||||
::close(fence_fd);
|
||||
}
|
||||
}
|
||||
|
||||
u32 generated_frame_count()
|
||||
@@ -1178,25 +1272,41 @@ namespace vk::frame_gen
|
||||
}
|
||||
|
||||
// Our device wrote the inputs; framegen's device is about to read them, and there is no
|
||||
// semaphore shared between the two. A device-level wait is the only barrier available.
|
||||
// semaphore shared between the two. Something has to stand in for one.
|
||||
//
|
||||
// It cannot be pipelined away, and that is a property of the library rather than of this
|
||||
// code: presentContext() submits on framegen's own device and the only completion signal it
|
||||
// exposes is armsx3_lsfg_wait_idle(), a vkDeviceWaitIdle. Upstream's semaphore path takes
|
||||
// sync FDs and imports them as OPAQUE_FD (framegen/src/core/semaphore.cpp), which Turnip and
|
||||
// Mesa do not support on Android -- which is why every semaphore handed over below is -1.
|
||||
// So what this costs is bounded by moving the wait, not by removing it: it now runs before
|
||||
// this frame's command buffer is submitted, on inputs that are a frame old, rather than
|
||||
// after a full frame-completion wait.
|
||||
// Upstream's semaphore path takes sync FDs and imports them as OPAQUE_FD
|
||||
// (framegen/src/core/semaphore.cpp), which framegen's Android device deliberately does not
|
||||
// enable -- vkImportSemaphoreFdKHR resolves to null inside it -- which is why every
|
||||
// semaphore handed over below is -1. That left armsx3_lsfg_wait_idle(), a vkDeviceWaitIdle
|
||||
// on framegen's whole device, as the only completion signal the library exposed, and it was
|
||||
// paid once per presented frame.
|
||||
//
|
||||
// The fenced present replaces it with the fence framegen already submits alongside the
|
||||
// generation work, exported as a sync fd: the same barrier, waited on with poll(2), with
|
||||
// framegen's queues left running instead of drained. The plain present stays as the
|
||||
// fallback for a library that does not export the new entry point, and
|
||||
// wait_for_generation() falls back to the same device wait whenever no descriptor arrives
|
||||
// or the poll does not complete -- neither may ever become a hang.
|
||||
//
|
||||
// The placement is unchanged and still matters: this runs before this frame's command
|
||||
// buffer is submitted, on inputs that are a frame old, rather than after a full
|
||||
// frame-completion wait.
|
||||
int out_sems[3] = {-1, -1, -1};
|
||||
int fence_fd = -1;
|
||||
|
||||
if (g_api.present(g_context, -1, out_sems, g_context_outputs) != ARMSX3_LSFG_OK)
|
||||
const int rc = g_api.present_fenced
|
||||
? g_api.present_fenced(g_context, -1, out_sems, g_context_outputs, &fence_fd)
|
||||
: g_api.present(g_context, -1, out_sems, g_context_outputs);
|
||||
|
||||
if (rc != ARMSX3_LSFG_OK)
|
||||
{
|
||||
// fence_fd is still -1 on every failing path out of the shim, so there is nothing
|
||||
// stranded here to close.
|
||||
disable(g_api.last_error());
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_api.wait_idle();
|
||||
wait_for_generation(fence_fd);
|
||||
|
||||
// Report the real vs generated rate once a second.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user