From 02a64fd4ab1a10cd7f92b1d2d062dc4e7b2ff63b Mon Sep 17 00:00:00 2001 From: simplysanatori <149214925+simplysanatori@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:33:27 -0400 Subject: [PATCH] Mock: MOCK_XR_DUMP writes released images as PPM; mockrun KF_MOCKRUN_INTRO=1 shows the first-run guide MOCK_XR_DUMP= (Vulkan) copies the image being released at the MOCK_XR_DUMP_AT seconds (MOCK_XR_DUMP_FRAMES consecutive releases per swapchain) into /t-sc-.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 --- frame/tools/mockrun.sh | 8 +-- frame/tools/xrmock/xrmock.c | 41 +++++++++++ frame/tools/xrmock/xrmock_gfx.h | 3 + frame/tools/xrmock/xrmock_gles.c | 2 +- frame/tools/xrmock/xrmock_vk.c | 117 ++++++++++++++++++++++++++++++- 5 files changed, 165 insertions(+), 6 deletions(-) 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};