diff --git a/frame/tools/mockrun.sh b/frame/tools/mockrun.sh
index 261c2d4..a27ec46 100755
--- a/frame/tools/mockrun.sh
+++ b/frame/tools/mockrun.sh
@@ -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
diff --git a/frame/tools/xrmock/xrmock.c b/frame/tools/xrmock/xrmock.c
index 5ac63e3..a3be8e8 100644
--- a/frame/tools/xrmock/xrmock.c
+++ b/frame/tools/xrmock/xrmock.c
@@ -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=
Vulkan: write released images as PPM files (every 4th pixel)
+// /t-sc-.ppm: at the seconds in
+// MOCK_XR_DUMP_AT=,... (default every 10 s), the next
+// MOCK_XR_DUMP_FRAMES= (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;
diff --git a/frame/tools/xrmock/xrmock_gfx.h b/frame/tools/xrmock/xrmock_gfx.h
index ee74e33..2af27ef 100644
--- a/frame/tools/xrmock/xrmock_gfx.h
+++ b/frame/tools/xrmock/xrmock_gfx.h
@@ -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;
diff --git a/frame/tools/xrmock/xrmock_gles.c b/frame/tools/xrmock/xrmock_gles.c
index d4246e3..45e821b 100644
--- a/frame/tools/xrmock/xrmock_gles.c
+++ b/frame/tools/xrmock/xrmock_gles.c
@@ -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};
diff --git a/frame/tools/xrmock/xrmock_vk.c b/frame/tools/xrmock/xrmock_vk.c
index 6334998..d0e0275 100644
--- a/frame/tools/xrmock/xrmock_vk.c
+++ b/frame/tools/xrmock/xrmock_vk.c
@@ -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(®ion, 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, ®ion);
+ 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};