Mock: MOCK_XR_DUMP writes released images as PPM; mockrun KF_MOCKRUN_INTRO=1 shows the first-run guide

MOCK_XR_DUMP=<dir> (Vulkan) copies the image being released at the MOCK_XR_DUMP_AT seconds
(MOCK_XR_DUMP_FRAMES consecutive releases per swapchain) into <dir>/t<s>-sc<id>-<n>.ppm, every
4th pixel, after waiting for the app's queue inside xrReleaseSwapchainImage. The waits cost a few
refreshes at each dump time. KF_MOCKRUN_INTRO=1 leaves welcome/tutorial unseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
simplysanatoriandClaude Opus 5.5 committed 2026-10-13 03:33:27 -04:00
1 parent 57526ee843
commit 02a64fd4ab
5 files changed
+165 -6

No files matched your search

+4 -4
View File
@@ -9,7 +9,8 @@
# ~/.cache/kf-mockrun), KF_XRMOCK_DIR (where the mock is built; default one per checkout under
# ~/.cache/kf-xrmock, rebuilt when its sources change), KF_MOCKRUN_VR (extra lines for the [vr]
# section, e.g. 'frame_pacing = "pipelined"'), KF_MOCKRUN_RENDER_SCALE (render_scale, default 1.0;
# 1.25 is the panel's native 2160 per eye). The game reads its user state from
# 1.25 is the panel's native 2160 per eye), KF_MOCKRUN_INTRO=1 (the first-run welcome and controller
# guide show instead of being seeded done). The game reads its user state from
# $XDG_DATA_HOME/WiiCompiled (Config.toml, NAND, Cache), which this script points at the scratch dir
# and seeds with a Config.toml naming the extracted game (~/Games/KartFrame/data/game or --game) and
# the Frame's VR defaults (render_scale 1.0, VR interpolation auto, plain menu background).
@@ -63,9 +64,8 @@ required = $([ $FLAT = 1 ] && echo false || echo true)
render_scale = ${KF_MOCKRUN_RENDER_SCALE:-1.0}
frame_interpolation_fps = 1
menu_shader_quality = 0
# No first-run welcome or tutorial: the attract-mode demo race starts on its own.
welcome_complete = true
tutorial_completed = 3
$([ "${KF_MOCKRUN_INTRO:-0}" = 1 ] && echo "# KF_MOCKRUN_INTRO=1: the first-run welcome and controller guide show." || printf '%s\n' \
"# No first-run welcome or tutorial: the attract-mode demo race starts on its own." "welcome_complete = true" "tutorial_completed = 3")
${KF_MOCKRUN_VR:-}
TOML
LOG=$OUT/logs/$(date +%Y%m%d-%H%M%S)-$([ $FLAT = 1 ] && echo flat || echo mock).log
+41
View File
@@ -40,6 +40,10 @@
// default none); an INTERACTION_PROFILE_CHANGED
// event follows the first FOCUSED, and suggested
// bindings for it are traced
// MOCK_XR_DUMP=<dir> Vulkan: write released images as PPM files (every 4th pixel)
// <dir>/t<seconds>-sc<id>-<n>.ppm: at the seconds in
// MOCK_XR_DUMP_AT=<s>,<s>... (default every 10 s), the next
// MOCK_XR_DUMP_FRAMES=<n> (1) releases of every swapchain
// The graphics API is chosen by the app's binding (xrmock_gfx.h).
//
// Checks (ERROR lines in the trace, and a summary on stderr when the session
@@ -97,6 +101,7 @@ typedef struct {
int acquired, next;
int waited; // of the images acquired, how many were waited for
int released; // an image was released since the last xrEndFrame
int dumpMark, dumped; // MOCK_XR_DUMP: the dump time this chain is on, images written for it
} Chain;
static Chain gChains[MAXSC];
static int gErrors = 0; // ERROR lines in the trace
@@ -348,6 +353,41 @@ static XrResult XRAPI_CALL mWaitImage(XrSwapchain sc, const XrSwapchainImageWait
c->waited++;
return XR_SUCCESS;
}
// MOCK_XR_DUMP (see the top): the image being released, written at the dump times.
static void maybeDump(Chain* c, XrSwapchain sc) {
static const char* dir = NULL;
static double at[64];
static int atCount = -1, frames = 1;
if (atCount < 0) {
dir = getenv("MOCK_XR_DUMP");
atCount = 0;
const char* list = getenv("MOCK_XR_DUMP_AT");
for (const char* q = list; q && *q && atCount < 64;) {
at[atCount++] = atof(q);
q = strchr(q, ',');
if (q) q++;
}
if (!atCount)
for (; atCount < 64; atCount++) at[atCount] = 10.0 * (atCount + 1);
if (getenv("MOCK_XR_DUMP_FRAMES")) frames = atoi(getenv("MOCK_XR_DUMP_FRAMES"));
}
if (!dir || !gGfx || !gGfx->dump_image || !gT0) return;
double t = (double)(nowNs() - gT0) / 1e9;
int mark = 0;
while (mark < atCount && at[mark] <= t) mark++;
if (mark == 0) return;
if (c->dumpMark != mark) {
c->dumpMark = mark;
c->dumped = 0;
}
if (c->dumped >= frames || t - at[mark - 1] > 5.0) return;
int idx = ((c->next - c->acquired) % c->count + c->count) % c->count;
char path[512];
snprintf(path, sizeof(path), "%s/t%03d-sc%d-%02d.ppm", dir, (int)at[mark - 1], chainId(sc), c->dumped);
if (gGfx->dump_image(c->images, idx, c->w, c->h, c->format, 4, path)) trace("dump sc%d %s", chainId(sc), path);
c->dumped++;
}
static XrResult XRAPI_CALL mRelease(XrSwapchain sc, const XrSwapchainImageReleaseInfo* ri) {
(void)ri;
Chain* c = chainOf(sc);
@@ -389,6 +429,7 @@ static XrResult XRAPI_CALL mRelease(XrSwapchain sc, const XrSwapchainImageReleas
gErrors++;
}
}
maybeDump(c, sc);
c->acquired--;
c->waited--;
c->released = 1;
+3
View File
@@ -43,6 +43,9 @@ typedef struct MockGfx {
int (*poll_snapshots)(int wait);
// How many releases the exact check covered.
int (*snapshots_checked)(void);
// MOCK_XR_DUMP: writes image `index` (waits for the app's queue first) as a PPM, every
// `step`-th pixel. Null: not available.
int (*dump_image)(void* images, int index, int width, int height, int64_t format, int step, const char* path);
} MockGfx;
extern const MockGfx kMockGfxGles;
+1 -1
View File
@@ -74,4 +74,4 @@ static XrResult fillImages(void* images, int count, XrSwapchainImageBaseHeader*
return XR_SUCCESS;
}
const MockGfx kMockGfxGles = {"gles", ownsBinding, describeBinding, getProc, formats, formatOf, createImages, fillImages, NULL, NULL, NULL, NULL};
const MockGfx kMockGfxGles = {"gles", ownsBinding, describeBinding, getProc, formats, formatOf, createImages, fillImages, NULL, NULL, NULL, NULL, NULL};
+116 -1
View File
@@ -132,6 +132,8 @@ static void* createImages(int64_t format, uint64_t usage, int width, int height,
if (usage & XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT) u |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
if (usage & XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) u |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
if (usage & XR_SWAPCHAIN_USAGE_TRANSFER_SRC_BIT) u |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
// MOCK_XR_DUMP copies released images out.
if (!depth && getenv("MOCK_XR_DUMP")) u |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
if (usage & XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT) u |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
if (usage & XR_SWAPCHAIN_USAGE_SAMPLED_BIT) u |= VK_IMAGE_USAGE_SAMPLED_BIT;
// The compositor samples every image.
@@ -332,5 +334,118 @@ static void releaseSnapshot(void* images, int index) {
static int snapshotsChecked(void) { return gChecked; }
// MOCK_XR_DUMP: inside xrReleaseSwapchainImage (the app holds its queue): wait for the queue,
// copy the image (COLOR_ATTACHMENT_OPTIMAL, as the app must leave it) into a host buffer and
// write every `step`-th pixel as a binary PPM. 8-bit RGBA / BGRA formats only.
static int dumpImage(void* images, int index, int width, int height, int64_t format, int step, const char* path) {
if (!gQueue || !gDevice) return 0;
int bgra = format == VK_FORMAT_B8G8R8A8_SRGB || format == VK_FORMAT_B8G8R8A8_UNORM;
if (!bgra && format != VK_FORMAT_R8G8B8A8_SRGB && format != VK_FORMAT_R8G8B8A8_UNORM) return 0;
VkImage image = ((VkImage*)images)[index];
VkDeviceSize size = (VkDeviceSize)width * height * 4;
VkBufferCreateInfo bci;
memset(&bci, 0, sizeof(bci));
bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bci.size = size;
bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
VkBuffer buf;
if (DEV(vkCreateBuffer)(gDevice, &bci, NULL, &buf) != VK_SUCCESS) return 0;
VkMemoryRequirements req;
DEV(vkGetBufferMemoryRequirements)(gDevice, buf, &req);
VkPhysicalDeviceMemoryProperties mp;
INST(vkGetPhysicalDeviceMemoryProperties)(gPhys, &mp);
uint32_t type = UINT32_MAX;
const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
for (uint32_t t = 0; t < mp.memoryTypeCount; t++)
if ((req.memoryTypeBits & (1u << t)) && (mp.memoryTypes[t].propertyFlags & want) == want) {
type = t;
break;
}
VkDeviceMemory mem = VK_NULL_HANDLE;
VkMemoryAllocateInfo mai;
memset(&mai, 0, sizeof(mai));
mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
mai.allocationSize = req.size;
mai.memoryTypeIndex = type;
if (type == UINT32_MAX || DEV(vkAllocateMemory)(gDevice, &mai, NULL, &mem) != VK_SUCCESS) {
DEV(vkDestroyBuffer)(gDevice, buf, NULL);
return 0;
}
DEV(vkBindBufferMemory)(gDevice, buf, mem, 0);
VkCommandPoolCreateInfo pci;
memset(&pci, 0, sizeof(pci));
pci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
pci.queueFamilyIndex = gQueueFamily;
VkCommandPool pool;
DEV(vkCreateCommandPool)(gDevice, &pci, NULL, &pool);
VkCommandBufferAllocateInfo ai;
memset(&ai, 0, sizeof(ai));
ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
ai.commandPool = pool;
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
ai.commandBufferCount = 1;
VkCommandBuffer cb;
DEV(vkAllocateCommandBuffers)(gDevice, &ai, &cb);
VkCommandBufferBeginInfo bi;
memset(&bi, 0, sizeof(bi));
bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
DEV(vkBeginCommandBuffer)(cb, &bi);
VkImageMemoryBarrier b;
memset(&b, 0, sizeof(b));
b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
b.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
b.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = image;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
b.subresourceRange.levelCount = 1;
b.subresourceRange.layerCount = 1;
DEV(vkCmdPipelineBarrier)(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, &b);
VkBufferImageCopy region;
memset(&region, 0, sizeof(region));
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.layerCount = 1;
region.imageExtent.width = (uint32_t)width;
region.imageExtent.height = (uint32_t)height;
region.imageExtent.depth = 1;
DEV(vkCmdCopyImageToBuffer)(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &region);
b.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
b.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
b.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
DEV(vkCmdPipelineBarrier)(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, NULL, 0, NULL, 1, &b);
DEV(vkEndCommandBuffer)(cb);
VkSubmitInfo si;
memset(&si, 0, sizeof(si));
si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
si.commandBufferCount = 1;
si.pCommandBuffers = &cb;
int ok = DEV(vkQueueSubmit)(gQueue, 1, &si, VK_NULL_HANDLE) == VK_SUCCESS && DEV(vkQueueWaitIdle)(gQueue) == VK_SUCCESS;
void* map = NULL;
if (ok && DEV(vkMapMemory)(gDevice, mem, 0, VK_WHOLE_SIZE, 0, &map) == VK_SUCCESS) {
FILE* f = fopen(path, "wb");
if (f) {
int ow = (width + step - 1) / step, oh = (height + step - 1) / step;
fprintf(f, "P6\n%d %d\n255\n", ow, oh);
const unsigned char* px = (const unsigned char*)map;
for (int y = 0; y < height; y += step)
for (int x = 0; x < width; x += step) {
const unsigned char* q = px + ((size_t)y * width + x) * 4;
unsigned char rgb[3] = {bgra ? q[2] : q[0], q[1], bgra ? q[0] : q[2]};
fwrite(rgb, 1, 3, f);
}
fclose(f);
} else ok = 0;
DEV(vkUnmapMemory)(gDevice, mem);
}
DEV(vkDestroyCommandPool)(gDevice, pool, NULL);
DEV(vkDestroyBuffer)(gDevice, buf, NULL);
DEV(vkFreeMemory)(gDevice, mem, NULL);
return ok;
}
const MockGfx kMockGfxVk = {"vk", ownsBinding, describeBinding, getProc, formats, formatOf, createImages, fillImages, releaseWaitMs,
releaseSnapshot, pollSnapshots, snapshotsChecked};
releaseSnapshot, pollSnapshots, snapshotsChecked, dumpImage};