FrameBridge: late-drawing check compares every pixel of rarely acquired panels (a sparse sample missed AC Nexus' text-only notice), read back asynchronously so frames never wait on it

This commit is contained in:
spoopyghosty0 committed 2026-10-23 19:14:22 -04:00
1 parent 7fd9e20e21
commit fdaba13f51
4 files changed
+133 -61

No files matched your search

+2 -2
View File
@@ -14,11 +14,11 @@ e32f9b3222bfe2e58c193db809ac2cf54086df36fd2ec6651aa2e58308f6b39d ./arm64-v8a/li
4a13a33e112c9e4e3c5ff4337ed6e988a9e5b3602f48d654df148ef5fafbf426 ./arm64-v8a/libfpglmv.so
36400eae4ba3807bcfcd54463daa217347032b07218792faa448d8cbbd086a0e ./arm64-v8a/libframe_xrshim.so
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
0e3f187f6421d0d3a7baf62ff34ff522ed3c3f1a486987eb6b8634d2b2d2541f ./arm64-v8a/libopenxr_loader_generic.so
972d3f08a9c6ad4a7620cd727ddbe754e4dad7be3210ae67d7ab84b8c20ede0d ./arm64-v8a/libopenxr_loader_generic.so
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi.so
a4ed80ae45ca90758239f152ea5a2e8c127d491e1e697ea5680db6fd7f72db07 ./arm64-v8a/libvrapi_foveation.so
dc775816ff44ac8dbaea933916f5b6aa87b65662cbeeaa07583229fa7646064b ./armeabi-v7a/libopenxr_loader_generic.so
4a90896b4a2934b447eb892ec89dd72580912e1cb55d4d53910737c47366e2ca ./armeabi-v7a/libopenxr_loader_generic.so
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
55aae57d292e9536701f2f8a9dbaf1edcfdd766c5748d07a34da2f5ef197f212 ./hevc/libstagefrighthw.so
Binary file not shown.
Binary file not shown.
+131 -59
View File
@@ -62,9 +62,17 @@ static struct {
VkDeviceMemory scratch_memory;
VkFormat scratch_format;
uint32_t scratch_w, scratch_h;
// image samples for late-drawing checks (flip_vk.c sample_print): 16x16 texels, host-visible
VkBuffer print_buf;
VkDeviceMemory print_mem;
// late-drawing checks (sample_start/sample_poll): copies read back a frame or more later, never waited for
struct {
VkCommandBuffer cmd;
VkFence fence;
VkBuffer buf;
VkDeviceMemory mem;
VkDeviceSize capacity, size;
int busy, kind; // kind 1: the image right after its flip, 2: a later check
XrSwapchain handle;
uint32_t index, acquires;
} sample[4];
} vk;
typedef struct {
@@ -84,6 +92,7 @@ typedef struct {
uint32_t flipped_count[MAX_IMAGES];
// late drawing: a sample of the image right after its flip, the last sample, and frames left to watch it
uint32_t print[MAX_IMAGES], seen[MAX_IMAGES], watch[MAX_IMAGES];
uint32_t acquires; // since creation: panels the app redraws get new images, static ones are watched
uint32_t alpha_inverted[MAX_IMAGES]; // alpha_blend_vk.c: array layers whose alpha was inverted since the acquire
int dumped; // flip_dump saved this swapchain's quad (its dump number)
int shown_dumped, frames_since_dump;
@@ -121,8 +130,7 @@ static void flip_on_create_session(const XrSessionCreateInfo *info) {
vk.scratch = VK_NULL_HANDLE; // belonged to the previous session's device
vk.scratch_memory = VK_NULL_HANDLE;
vk.scratch_w = vk.scratch_h = 0;
vk.print_buf = VK_NULL_HANDLE;
vk.print_mem = VK_NULL_HANDLE;
memset(vk.sample, 0, sizeof(vk.sample)); // the previous session's device
LOG("flip: Vulkan session (queue family %u index %u)", vk.family, vk.index);
}
}
@@ -153,6 +161,7 @@ static void flip_on_acquire(XrSwapchain handle, uint32_t index) {
t->last_acquired = index;
if (index < MAX_IMAGES) t->flipped_count[index] = 0; // new content: upside down again
if (index < MAX_IMAGES) t->watch[index] = 0;
++t->acquires;
if (index < MAX_IMAGES) t->alpha_inverted[index] = 0;
if(t->acquired_count<MAX_IMAGES)t->acquired[t->acquired_count++]=index;
if (t->held_count < MAX_IMAGES) t->held[t->held_count++] = index;
@@ -315,66 +324,139 @@ static int scratch_fit(VkFormat format, uint32_t w, uint32_t h) {
// flip_dump=N (diagnostics): the quad's rectangle as the app drew it ("raw") and as submitted after the flip
// ("upright"), saved as files/fb_flip_<n>_<raw|upright>.ppm (8-bit RGBA formats only).
#define PRINT_GRID 16
#define PRINT_FRAMES 180 // how long a flipped image is watched for late drawing (~2 s)
#define PRINT_PIXELS (4u << 20) // larger rectangles: evenly spaced whole rows up to this many pixels
#define PRINT_MAX_ROWS 1024
#define PRINT_FRAMES 900 // how long a flipped image is watched for late drawing (~10 s: every frame for the first
// ~1.5 s, then every 8th)
#define PRINT_STATIC_ACQUIRES 6 // only panels acquired this rarely are watched (redrawn panels get new images)
// A fingerprint of the rectangle: 16x16 texels on a grid, hashed (0 = no sample). Unity sometimes finishes a panel
// after xrEndFrame, so its drawing landed after the flip, upside down, and the image was never acquired again (AC
// Nexus' "calibrate arms" screen).
static int sample_print(VkImage app, uint32_t layer, XrRect2Di rect, int64_t format, uint32_t *out) {
// A fingerprint of the rectangle: every pixel (very large ones: evenly spaced whole rows), hashed (0 = no sample).
// Unity sometimes finishes a panel after xrEndFrame, so its drawing landed after the flip, upside down, and the image
// was never acquired again (AC Nexus' "calibrate arms" screen; its "work of fiction" notice is a few lines of text on
// an empty panel, which a sparse sample missed).
// Starts a copy of the rectangle (every pixel; very large ones: evenly spaced whole rows) into a free slot; the result
// is read by sample_poll on a later frame. Waiting for it here stalled every frame behind the game's GPU work.
static int sample_start(tracked_swapchain *t, uint32_t index, uint32_t layer, XrRect2Di rect, int kind) {
int64_t format = t->info.format;
if (format != VK_FORMAT_R8G8B8A8_SRGB && format != VK_FORMAT_R8G8B8A8_UNORM && format != VK_FORMAT_B8G8R8A8_SRGB &&
format != VK_FORMAT_B8G8R8A8_UNORM) return 0;
VkDeviceSize size = PRINT_GRID * PRINT_GRID * 4;
if (!vk.print_buf) {
int slot = -1;
for (int i = 0; i < 4 && slot < 0; ++i)
if (!vk.sample[i].busy) slot = i;
if (slot < 0) return 0;
__typeof__(vk.sample[0]) *sm = &vk.sample[slot];
uint32_t width = (uint32_t)rect.extent.width, height = (uint32_t)rect.extent.height;
uint32_t rows = (uint64_t)width * height <= PRINT_PIXELS ? height : PRINT_PIXELS / width;
if (rows > height) rows = height;
if (rows < height && rows > PRINT_MAX_ROWS) rows = PRINT_MAX_ROWS;
if (!rows) return 0;
VkDeviceSize size = (VkDeviceSize)rows * width * 4;
if (!sm->cmd) {
VkCommandBufferAllocateInfo alloc = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, NULL, vk.pool,
VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1};
VkFenceCreateInfo fence = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, NULL, 0};
if (vk.AllocateCommandBuffers(vk.device, &alloc, &sm->cmd) != VK_SUCCESS) { sm->cmd = VK_NULL_HANDLE; return 0; }
if (vk.CreateFence(vk.device, &fence, NULL, &sm->fence) != VK_SUCCESS) { sm->fence = VK_NULL_HANDLE; return 0; }
}
if (!sm->fence) return 0;
if (sm->buf && sm->capacity < size) {
vk.DestroyBuffer(vk.device, sm->buf, NULL);
vk.FreeMemory(vk.device, sm->mem, NULL);
sm->buf = VK_NULL_HANDLE;
sm->mem = VK_NULL_HANDLE;
}
if (!sm->buf) {
VkBufferCreateInfo bi = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, NULL, 0, size, VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VK_SHARING_MODE_EXCLUSIVE, 0, NULL};
if (vk.CreateBuffer(vk.device, &bi, NULL, &vk.print_buf) != VK_SUCCESS) { vk.print_buf = VK_NULL_HANDLE; return 0; }
if (vk.CreateBuffer(vk.device, &bi, NULL, &sm->buf) != VK_SUCCESS) { sm->buf = VK_NULL_HANDLE; return 0; }
VkMemoryRequirements req;
vk.GetBufferMemoryRequirements(vk.device, vk.print_buf, &req);
vk.GetBufferMemoryRequirements(vk.device, sm->buf, &req);
uint32_t type = UINT32_MAX;
for (uint32_t i = 0; i < vk.memory.memoryTypeCount && type == UINT32_MAX; ++i)
if ((req.memoryTypeBits & (1u << i)) && (vk.memory.memoryTypes[i].propertyFlags &
(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) ==
(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) type = i;
VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, req.size, type};
if (type == UINT32_MAX || vk.AllocateMemory(vk.device, &ai, NULL, &vk.print_mem) != VK_SUCCESS ||
vk.BindBufferMemory(vk.device, vk.print_buf, vk.print_mem, 0) != VK_SUCCESS) {
if (vk.print_mem) vk.FreeMemory(vk.device, vk.print_mem, NULL);
vk.DestroyBuffer(vk.device, vk.print_buf, NULL);
vk.print_buf = VK_NULL_HANDLE;
vk.print_mem = VK_NULL_HANDLE;
if (type == UINT32_MAX || vk.AllocateMemory(vk.device, &ai, NULL, &sm->mem) != VK_SUCCESS ||
vk.BindBufferMemory(vk.device, sm->buf, sm->mem, 0) != VK_SUCCESS) {
if (sm->mem) vk.FreeMemory(vk.device, sm->mem, NULL);
vk.DestroyBuffer(vk.device, sm->buf, NULL);
sm->buf = VK_NULL_HANDLE;
sm->mem = VK_NULL_HANDLE;
return 0;
}
sm->capacity = size;
}
if (!vk_cmd_idle()) return 0;
VkBufferImageCopy regions[PRINT_GRID * PRINT_GRID];
for (int y = 0; y < PRINT_GRID; ++y)
for (int x = 0; x < PRINT_GRID; ++x) {
int i = y * PRINT_GRID + x;
regions[i] = (VkBufferImageCopy){(VkDeviceSize)i * 4, 0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1},
{rect.offset.x + (int32_t)((2 * x + 1) * (int64_t)rect.extent.width / (2 * PRINT_GRID)),
rect.offset.y + (int32_t)((2 * y + 1) * (int64_t)rect.extent.height / (2 * PRINT_GRID)), 0}, {1, 1, 1}};
}
static VkBufferImageCopy regions[PRINT_MAX_ROWS];
uint32_t count = rows == height ? 1 : rows;
if (rows == height) // the whole rectangle
regions[0] = (VkBufferImageCopy){0, 0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1},
{rect.offset.x, rect.offset.y, 0}, {width, height, 1}};
else
for (uint32_t y = 0; y < rows; ++y)
regions[y] = (VkBufferImageCopy){(VkDeviceSize)y * width * 4, 0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1},
{rect.offset.x, rect.offset.y + (int32_t)((2 * y + 1) * (int64_t)height / (2 * rows)), 0}, {width, 1, 1}};
VkImage app = t->images[index];
VkCommandBufferBeginInfo begin = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL,
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, NULL};
vk.ResetCommandBuffer(vk.cmd, 0);
vk.BeginCommandBuffer(vk.cmd, &begin);
VkCommandBuffer saved = vk.cmd; // barrier() records into vk.cmd
vk.cmd = sm->cmd;
vk.ResetCommandBuffer(sm->cmd, 0);
vk.BeginCommandBuffer(sm->cmd, &begin);
barrier(app, layer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
vk.CmdCopyImageToBuffer(vk.cmd, app, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vk.print_buf, PRINT_GRID * PRINT_GRID, regions);
vk.CmdCopyImageToBuffer(sm->cmd, app, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sm->buf, count, regions);
barrier(app, layer, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
vk.EndCommandBuffer(vk.cmd);
if (vk_cmd_submit(100000000ull) != 1) return 0; // still running: no sample this frame
void *mapped = NULL;
if (vk.MapMemory(vk.device, vk.print_mem, 0, size, 0, &mapped) != VK_SUCCESS) return 0;
uint32_t hash = 2166136261u;
for (VkDeviceSize i = 0; i < size; ++i) hash = (hash ^ ((const unsigned char *)mapped)[i]) * 16777619u;
vk.UnmapMemory(vk.device, vk.print_mem);
*out = hash ? hash : 1;
vk.cmd = saved;
vk.EndCommandBuffer(sm->cmd);
VkSubmitInfo submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO, NULL, 0, NULL, NULL, 1, &sm->cmd, 0, NULL};
if (vk.QueueSubmit(vk.queue, 1, &submit, sm->fence) != VK_SUCCESS) return 0;
sm->busy = 1;
sm->kind = kind;
sm->size = size;
sm->handle = t->handle;
sm->index = index;
sm->acquires = t->acquires;
return 1;
}
// Reads finished copies: a "kind 1" sample is the image right after its flip; a later one that differs from it and
// matches the previous check (drawing settled) is the app's late drawing, upside down: flipped again on its next quad.
static void sample_poll(void) {
for (int i = 0; i < 4; ++i) {
__typeof__(vk.sample[0]) *sm = &vk.sample[i];
if (!sm->busy || vk.WaitForFences(vk.device, 1, &sm->fence, VK_TRUE, 0) != VK_SUCCESS) continue;
vk.ResetFences(vk.device, 1, &sm->fence);
sm->busy = 0;
void *mapped = NULL;
if (vk.MapMemory(vk.device, sm->mem, 0, sm->size, 0, &mapped) != VK_SUCCESS) continue;
uint64_t h = 1469598103934665603ull; // 8 bytes at a time: a panel is up to 4 MB
const uint64_t *words = mapped;
for (VkDeviceSize k = 0; k < sm->size / 8; ++k) h = (h ^ words[k]) * 1099511628211ull;
uint32_t hash = (uint32_t)(h ^ (h >> 32));
vk.UnmapMemory(vk.device, sm->mem);
tracked_swapchain *t = find_tracked(sm->handle, 0);
uint32_t x = sm->index;
if (!t || x >= MAX_IMAGES || t->acquires != sm->acquires || !t->flipped_count[x]) continue; // new content since
if (sm->kind == 1) {
t->print[x] = t->seen[x] = hash;
t->watch[x] = PRINT_FRAMES;
} else if (t->watch[x] && hash != t->print[x]) {
if (hash == t->seen[x]) {
static int late;
if (late++ < 10)
LOG("flip: quad swapchain=%p (%ux%u) image %u was drawn after its flip, flipping it again",
(void *)(uintptr_t)t->handle, t->info.width, t->info.height, x);
t->flipped_count[x] = 0;
t->watch[x] = 0;
} else {
t->seen[x] = hash;
}
}
}
}
static int dump_rect(VkImage app, uint32_t layer, XrRect2Di rect, int64_t format, int n, const char *what) {
int written = 0;
if (!*snap_dir || (format != VK_FORMAT_R8G8B8A8_SRGB && format != VK_FORMAT_R8G8B8A8_UNORM &&
@@ -474,20 +556,13 @@ static const XrCompositionLayerQuad *flip_quad(XrSession session, const XrCompos
else if (flip_dumps < flip_dump && dump_rect(t->images[index], layer, rect, t->info.format, flip_dumps + 1, "late"))
++flip_dumps;
}
sample_poll();
if (t->flipped_count[index] && t->watch[index] && t->flipped[index][0].layer == layer &&
!memcmp(&t->flipped[index][0].rect, &rect, sizeof rect)) { // the image's first flipped quad: drawn into late?
--t->watch[index];
uint32_t now = 0;
if (sample_print(t->images[index], layer, rect, t->info.format, &now) && now != t->print[index]) {
if (now == t->seen[index]) { // changed since the flip and settled: the app's late drawing, upside down
static int late;
if (late++ < 10)
LOG("flip: quad swapchain=%p (%ux%u) image %u was drawn after its flip, flipping it again",
(void *)(uintptr_t)q->subImage.swapchain, t->info.width, t->info.height, index);
t->flipped_count[index] = 0;
}
t->seen[index] = now;
}
!memcmp(&t->flipped[index][0].rect, &rect, sizeof rect) && // the image's first flipped quad: drawn into late?
(--t->watch[index] > PRINT_FRAMES - 108 || t->watch[index] % 8 == 0)) {
int pending = 0;
for (int i = 0; i < 4; ++i) pending |= vk.sample[i].busy && vk.sample[i].handle == t->handle;
if (!pending) sample_start(t, index, layer, rect, 2);
}
for (uint32_t i = 0; i < t->flipped_count[index]; ++i)
if (t->flipped[index][i].layer == layer && !memcmp(&t->flipped[index][i].rect, &rect, sizeof rect)) goto done;
@@ -553,11 +628,8 @@ static const XrCompositionLayerQuad *flip_quad(XrSession session, const XrCompos
if (dump) dump_rect(app, layer, rect, t->info.format, dump, "upright");
t->flipped[index][t->flipped_count[index]].rect = rect;
t->flipped[index][t->flipped_count[index]++].layer = layer;
if (t->flipped_count[index] == 1 && submitted == 1 &&
sample_print(app, layer, rect, t->info.format, &t->print[index])) {
t->seen[index] = t->print[index];
t->watch[index] = PRINT_FRAMES;
}
if (t->flipped_count[index] == 1 && t->acquires <= PRINT_STATIC_ACQUIRES)
sample_start(t, index, layer, rect, 1); // right behind the flip on the queue; watched once it's read
if (layer_debug && t->traced++ < 12) // layer_debug: its first flips, with the app's acquire/release state
LOG("flip: swapchain=%p %ux%u image %u (acquired %u, released %d, held %u) rect=%d,%d layer %u", (void *)(uintptr_t)q->subImage.swapchain,
t->info.width, t->info.height, index, t->last_acquired, t->released_any ? (int)t->last_released : -1,