Files
NX2navi/runtime/gfx_window_probe.c
2026-11-07 05:04:38 -05:00

219 lines
11 KiB
C

// --probe-window: presents 300 frames of a cycling clear colour through vk_present_image, reads the swapchain image back
// and compares every pixel, measures the vsync rate, flips the focus state and drives the cursor release with synthetic
// window events (the window back end's wsys_probe_*, gfx_wsys.h) and resizes the window. Run with NX2NAVI_VK_VALIDATION=1
// to count validation errors.
#include <stdio.h>
#include <string.h>
#include "gfx.h"
#include "gfx_wsys.h"
#include "input.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
#define SRC_W 640
#define SRC_H 360
#define FRAMES 300
typedef struct {
VkImage img;
VmaAllocation alloc;
VkCommandPool pool;
VkCommandBuffer cb;
VkFence fence;
} Source;
static int fail(const char *what) {
printf("probe-window: FAILED (%s)\n", what);
return 1;
}
static void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags2 s0,
VkAccessFlags2 a0, VkPipelineStageFlags2 s1, VkAccessFlags2 a1) {
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = s0, .srcAccessMask = a0,
.dstStageMask = s1, .dstAccessMask = a1, .oldLayout = from, .newLayout = to,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &b};
vkCmdPipelineBarrier2(cb, &d);
}
static int source_create(Source *s) {
VkCtx *c = vk_ctx();
VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
.format = VK_FORMAT_R8G8B8A8_UNORM, .extent = {SRC_W, SRC_H, 1}, .mipLevels = 1, .arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ii, &ai, &s->img, &s->alloc, NULL) != VK_SUCCESS) return 0;
VkCommandPoolCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = c->gfx_family};
if (vkCreateCommandPool(c->device, &pi, NULL, &s->pool) != VK_SUCCESS) return 0;
VkCommandBufferAllocateInfo ba = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = s->pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1};
if (vkAllocateCommandBuffers(c->device, &ba, &s->cb) != VK_SUCCESS) return 0;
VkFenceCreateInfo fi = {.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
return vkCreateFence(c->device, &fi, NULL, &s->fence) == VK_SUCCESS;
}
static void source_destroy(Source *s) {
VkCtx *c = vk_ctx();
if (s->fence) vkDestroyFence(c->device, s->fence, NULL);
if (s->pool) vkDestroyCommandPool(c->device, s->pool, NULL);
if (s->img) vmaDestroyImage(c->vma, s->img, s->alloc);
}
// Clears the source to rgba and leaves it in TRANSFER_SRC_OPTIMAL; the GPU work is waited for here, which is stricter
// than the vk_present_image contract needs.
static int source_clear(Source *s, int first, const float rgba[4]) {
VkCtx *c = vk_ctx();
vkResetCommandBuffer(s->cb, 0);
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(s->cb, &bi);
barrier(s->cb, s->img, first ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, first ? 0 : VK_ACCESS_2_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
VkClearColorValue cv;
memcpy(cv.float32, rgba, sizeof cv.float32);
VkImageSubresourceRange rng = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(s->cb, s->img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &cv, 1, &rng);
barrier(s->cb, s->img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT);
vkEndCommandBuffer(s->cb);
VkCommandBufferSubmitInfo cbi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = s->cb};
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .commandBufferInfoCount = 1, .pCommandBufferInfos = &cbi};
vk_queue_lock();
VkResult r = vkQueueSubmit2(c->gfx_queue, 1, &si, s->fence);
vk_queue_unlock();
if (r != VK_SUCCESS) return 0;
r = vkWaitForFences(c->device, 1, &s->fence, VK_TRUE, 5000000000ull);
vkResetFences(c->device, 1, &s->fence);
return r == VK_SUCCESS;
}
static uint8_t to_byte(float f) { return (uint8_t)(f * 255.0f + 0.5f); }
// Presents one colour and checks every pixel of the readback. Returns 0 when it matches.
static int present_and_check(Source *s, int first, int i, int *w_out, int *h_out) {
float rgba[4] = {(float)((i * 7) & 255) / 255.0f, (float)((i * 13) & 255) / 255.0f, (float)((i * 29) & 255) / 255.0f, 1.0f};
if (!source_clear(s, first, rgba)) return 1;
vk_present_image(s->img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_FORMAT_R8G8B8A8_UNORM, SRC_W, SRC_H, NULL);
const uint8_t *px;
int w, h;
VkFormat fmt;
if (!vk_present_captured(&px, &w, &h, &fmt)) { printf("probe-window: frame %d was not presented\n", i); return 2; }
uint8_t rgb[3] = {to_byte(rgba[0]), to_byte(rgba[1]), to_byte(rgba[2])};
uint8_t want[4];
if (fmt == VK_FORMAT_B8G8R8A8_UNORM) { want[0] = rgb[2]; want[1] = rgb[1]; want[2] = rgb[0]; }
else if (fmt == VK_FORMAT_R8G8B8A8_UNORM) { want[0] = rgb[0]; want[1] = rgb[1]; want[2] = rgb[2]; }
else { printf("probe-window: swapchain format %d is not UNORM RGBA/BGRA\n", (int)fmt); return 3; }
want[3] = 255;
uint32_t wv, black = 0xff000000u; // opaque black in both byte orders
memcpy(&wv, want, 4);
int r[4]; // the picture's rectangle; bars of black around it when the window's shape differs
gfx_fit_rect(SRC_W, SRC_H, w, h, r);
const uint32_t *p = (const uint32_t *)px;
for (long k = 0, n = (long)w * h; k < n; k++) {
int x = (int)(k % w), y = (int)(k / w);
uint32_t e = x >= r[0] && x < r[0] + r[2] && y >= r[1] && y < r[1] + r[3] ? wv : black;
if (p[k] != e) {
printf("probe-window: frame %d pixel %ld (%d, %d) is %08x, expected %08x\n", i, k, x, y, p[k], e);
return 4;
}
}
*w_out = w;
*h_out = h;
return 0;
}
int probe_window(void) {
vk_hdr_off(); // the checks read 8-bit swapchain pixels
if (!menu_font_check()) return fail("the menu's font check takes a damaged font or refuses a good one");
gfx_window_start();
VkCtx *c = vk_ctx();
if (!c) return fail("no Vulkan device");
vk_present_capture(1);
Source s = {0};
if (!source_create(&s)) return fail("source image");
int w = 0, h = 0;
for (int i = 0; i < FRAMES; i++)
if (present_and_check(&s, i == 0, i, &w, &h)) return fail("presented pixels differ");
printf("probe-window: %d presents, readback %dx%d exact\n", FRAMES, w, h);
// Focus: synthetic focus loss and gain, delivered synchronously on the window thread.
unsigned m0 = gfx_window_focus_messages();
wsys_probe_focus(0);
int off = gfx_window_focused();
wsys_probe_focus(1);
int on = gfx_window_focused();
unsigned m1 = gfx_window_focus_messages();
if (off != 0 || on != 1 || m1 - m0 != 2) {
printf("probe-window: focus off=%d on=%d messages=%u\n", off, on, m1 - m0);
return fail("focus");
}
printf("probe-window: focus off/on pushed 2 messages (15)\n");
// Cursor release: Esc releases, a client click recaptures, a title bar activation releases (where the window system
// reports one). The probe window may not be the foreground window, so this checks the release flag, not the capture.
wsys_probe_escape();
int esc = input_cursor_released();
wsys_probe_click(0);
int click = input_cursor_released();
wsys_probe_click(1);
int client = input_cursor_released();
int caption = wsys_probe_click(2) ? input_cursor_released() : 1;
wsys_probe_focus(0);
wsys_probe_focus(1);
int back = input_cursor_released();
if (menu_is_open()) menu_toggle(); // Esc opened the menu: the presents below compare the bare game image
if (!esc || click || client || !caption || back) {
printf("probe-window: released esc=%d click=%d client=%d caption=%d reactivated=%d\n", esc, click, client, caption, back);
return fail("cursor release");
}
printf("probe-window: cursor released by Esc and title bar activation, recaptured by a click\n");
// Resize: the client area changes, the swapchain follows, presenting still reads back exact.
gfx_window_resize(1280, 720);
int cw = 0, ch = 0;
wsys_client_size(&cw, &ch);
for (int i = 0; i < 10; i++)
if (present_and_check(&s, 0, FRAMES + i, &w, &h)) return fail("presents after resize");
if (w != cw || h != ch) {
printf("probe-window: readback %dx%d, client area %dx%d\n", w, h, cw, ch);
return fail("swapchain size after resize");
}
printf("probe-window: resized to %dx%d, presents still exact\n", w, h);
vk_present_capture(0);
// Vsync: count ticks between two tick edges about 5 s apart.
static volatile int32_t ticks;
gfx_vsync_bind_counter(&ticks);
long a = ticks;
while (ticks == a) plat_sleep_ms(1);
uint64_t t0 = plat_now_ns();
long n0 = ticks;
plat_sleep_ms(5000);
long b = ticks;
while (ticks == b) plat_sleep_ms(1);
uint64_t t1 = plat_now_ns();
long n1 = ticks;
double rate = (double)(n1 - n0) * 1e9 / (double)(t1 - t0);
printf("probe-window: vsync %ld ticks in %.3f s = %.3f Hz\n", n1 - n0, (double)(t1 - t0) / 1e9, rate);
if (rate < 60.0 * 0.995 || rate > 60.0 * 1.005) return fail("vsync rate");
source_destroy(&s);
vk_shutdown();
long errors = vk_validation_errors_total();
if (errors) {
printf("probe-window: FAILED (%ld validation errors)\n", errors);
return 1;
}
printf("probe-window: passed (%d frames, %ld validation errors, vsync %.1f Hz)\n", FRAMES, errors, rate);
return 0;
}