diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 15a46c66..5241558f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // texture/sampler resolution, completeness probe, sync, layout handling, sampler // and view lookups - would recompute the identical descriptor. if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() && - m_samplerResolveMemo[binding].infoValid) { + m_samplerResolveMemo[binding].infoValid && + m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) { outImageInfo = m_samplerResolveMemo[binding].info; return true; } @@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (binding < m_samplerResolveMemo.size()) { if (descriptorMemoUsable) { m_samplerResolveMemo[binding].info = outImageInfo; + m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId(); m_samplerResolveMemo[binding].infoValid = true; } else { // An arrayed binding publishes nothing here, and clears what a previous program diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index a0053d0b..ead1edaa 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -341,8 +341,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // lifetime id, so a freed-and-reallocated sampler or texture at the same heap address // always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a // stale guess can only miss and fall through to the hash, never resolve wrong. Still - // reset each frame alongside the descriptor-set cache. Indexed by binding. + // reset each frame alongside the descriptor-set cache. Indexed by binding, but the + // whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding + // numbers are layout-local and unrelated programs routinely reuse binding 0/1. struct SamplerResolveMemo { + Uint64 infoProgramLifetimeId = 0; Uint64 samplerLifetimeId = 0; Uint64 textureLifetimeId = 0; VkSampler sampler = VK_NULL_HANDLE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 432a81b0..fb6b8aed 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -238,11 +238,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // nothing else across all of gl33. // // The mapping below is derived from - and at full extent exactly reproduces - the pixel - // mapping RemapDefaultFboReadbackToGLOrientation has always used: + // mapping VulkanRenderer::RemapDefaultFramebufferReadback uses: // identity : image(x, H-1-y) -> flip Y // 180 : image(W-1-x, y) -> mirror X (the rotation already flips the rows) - // Quarter turns swap the axes; nothing in this renderer models that (the readback declines to - // remap them and the viewport path only rescales), so they are left exactly as they were. + // Quarter turns swap the axes and are handled by MapDefaultFramebufferReadbackRect rather than + // this same-axis helper. struct DefaultFramebufferRectMapping { Bool flipY = false; Bool mirrorX = false; @@ -2151,47 +2151,6 @@ void main() { return static_cast(value * 255.0f + 0.5f); } - // Re-order the copied BLOCK - not the whole image - from the default framebuffer's stored - // orientation into GL's. The caller has already aimed the copy at the right place with - // MapDefaultFramebufferRectAxis, so what arrives here is exactly the requested - // rectWidth x rectHeight rect, and all that is left is the order of rows (identity) or of - // columns (180) WITHIN it. - // - // This used to iterate the full swapchain extent and index both sides with that stride, - // which is why its caller could only use it on an exact full-extent read - and why every - // partial glReadPixels of the default framebuffer came back in Vulkan row order. Only - // identity/180 share the swapchain extent with the default framebuffer; 90/270 swap - // extents and are still declined. - static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels, - Uint32 rectWidth, - Uint32 rectHeight, - VkSurfaceTransformFlagBitsKHR preTransform, - SizeT texelSize, - Uint8* outPixels) { - if (IsQuarterTurnPreTransform(preTransform)) { - return false; - } - if (rectWidth == 0 || rectHeight == 0 || texelSize == 0) { - return false; - } - const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform); - const SizeT rowBytes = static_cast(rectWidth) * texelSize; - for (Uint32 outY = 0; outY < rectHeight; ++outY) { - const Uint32 srcY = mapping.flipY ? (rectHeight - 1 - outY) : outY; - const Uint8* srcRow = rawPixels + static_cast(srcY) * rowBytes; - Uint8* dstRow = outPixels + static_cast(outY) * rowBytes; - if (!mapping.mirrorX) { - Memcpy(dstRow, srcRow, rowBytes); - continue; - } - for (Uint32 outX = 0; outX < rectWidth; ++outX) { - Memcpy(dstRow + static_cast(outX) * texelSize, - srcRow + static_cast(rectWidth - 1 - outX) * texelSize, texelSize); - } - } - return true; - } - static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) { const SizeT resolvedAlignment = static_cast(std::max(alignment, 1)); return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1); @@ -2738,6 +2697,95 @@ void main() { return formatInfo.texel_block_size; } + Bool VulkanRenderer::MapDefaultFramebufferReadbackRect( + GLint x, GLint y, GLsizei width, GLsizei height, VkExtent2D imageExtent, + VkSurfaceTransformFlagBitsKHR preTransform, VkOffset2D* imageOffset, + VkExtent2D* imageCopyExtent) { + if (width <= 0 || height <= 0 || imageOffset == nullptr || imageCopyExtent == nullptr) { + return false; + } + + const Int imageWidth = static_cast(imageExtent.width); + const Int imageHeight = static_cast(imageExtent.height); + Int mappedX = x; + Int mappedY = y; + Uint32 mappedWidth = static_cast(width); + Uint32 mappedHeight = static_cast(height); + + // InsertPositionFixup first flips GL Y and then applies the surface transform. In pixel + // coordinates that gives these half-open rectangle mappings into the stored image: + // identity: (x, H-y-h), 90: (y, x), 180: (W-x-w, y), 270: (H-y-h, W-x-w). + // Quarter turns also transpose the copied block's extent. + switch (preTransform) { + case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: + mappedX = y; + mappedY = x; + mappedWidth = static_cast(height); + mappedHeight = static_cast(width); + break; + case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: + mappedX = imageWidth - x - width; + mappedY = y; + break; + case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: + mappedX = imageWidth - y - height; + mappedY = imageHeight - x - width; + mappedWidth = static_cast(height); + mappedHeight = static_cast(width); + break; + default: + mappedY = imageHeight - y - height; + break; + } + + if (mappedX < 0 || mappedY < 0 || mappedWidth > imageExtent.width || + mappedHeight > imageExtent.height || + static_cast(mappedX) + mappedWidth > imageExtent.width || + static_cast(mappedY) + mappedHeight > imageExtent.height) { + return false; + } + *imageOffset = {mappedX, mappedY}; + *imageCopyExtent = {mappedWidth, mappedHeight}; + return true; + } + + Bool VulkanRenderer::RemapDefaultFramebufferReadback( + const Uint8* rawPixels, Uint32 logicalWidth, Uint32 logicalHeight, + VkSurfaceTransformFlagBitsKHR preTransform, SizeT texelSize, Uint8* outPixels) { + if (rawPixels == nullptr || outPixels == nullptr || logicalWidth == 0 || logicalHeight == 0 || + texelSize == 0) { + return false; + } + + const Uint32 rawWidth = IsQuarterTurnPreTransform(preTransform) ? logicalHeight : logicalWidth; + for (Uint32 outY = 0; outY < logicalHeight; ++outY) { + for (Uint32 outX = 0; outX < logicalWidth; ++outX) { + Uint32 srcX = outX; + Uint32 srcY = outY; + switch (preTransform) { + case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: + srcX = outY; + srcY = outX; + break; + case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: + srcX = logicalWidth - 1 - outX; + break; + case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: + srcX = logicalHeight - 1 - outY; + srcY = logicalWidth - 1 - outX; + break; + default: + srcY = logicalHeight - 1 - outY; + break; + } + Memcpy(outPixels + (static_cast(outY) * logicalWidth + outX) * texelSize, + rawPixels + (static_cast(srcY) * rawWidth + srcX) * texelSize, + texelSize); + } + } + return true; + } + Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat, GLsizei width, GLsizei height, GLenum destinationFormat, GLenum destinationType, SizeT destinationRowStride, @@ -9138,19 +9186,18 @@ void main() { // The GL rect, aimed at the default framebuffer's stored orientation. Using the GL y // verbatim copied rows [y, y+h) counted from the TOP of the image, i.e. the wrong band for // every read that was not full-height. - Int32 copyOffsetX = x; - Int32 copyOffsetY = y; + VkOffset2D copyOffset{x, y}; + VkExtent2D copyExtent{static_cast(width), static_cast(height)}; if (readIsDefaultFbo) { const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent(); - const DefaultFramebufferRectMapping mapping = - GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform()); - copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast(defaultFboExtent.width), - mapping.mirrorX); - copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast(defaultFboExtent.height), - mapping.flipY); + const Bool mapped = MapDefaultFramebufferReadbackRect( + x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset, + ©Extent); + MOBILEGL_ASSERT(mapped, "ReadPixels: default framebuffer read rectangle is out of bounds"); + if (!mapped) return; } - copyRegion.imageOffset = {copyOffsetX, copyOffsetY, static_cast(srcBinding.depthOffset)}; - copyRegion.imageExtent = {static_cast(width), static_cast(height), 1}; + copyRegion.imageOffset = {copyOffset.x, copyOffset.y, static_cast(srcBinding.depthOffset)}; + copyRegion.imageExtent = {copyExtent.width, copyExtent.height, 1}; vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), 1, ©Region); @@ -9192,16 +9239,14 @@ void main() { // already aimed with the same mapping. The gate is exactly what made every partial // read of the default framebuffer come back in Vulkan row order. Vector remapped(static_cast(width) * static_cast(height) * sourceTexelSize); - if (RemapDefaultFboReadbackToGLOrientation(mapped, static_cast(width), - static_cast(height), preTransform, sourceTexelSize, - remapped.data())) { + if (RemapDefaultFramebufferReadback(mapped, static_cast(width), + static_cast(height), preTransform, sourceTexelSize, + remapped.data())) { PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels, /*applyPackImageParams=*/false, /*applyReadColorClamp=*/true); return; } - // Only a quarter-turn pre-transform reaches this, and nothing in this renderer models - // one. MGLOG_I because the INFO builds are the ones that run conformance. - MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap declined (w=%d h=%d preTransform=%d); falling back " + MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap failed (w=%d h=%d preTransform=%d); falling back " "to raw readback", width, height, static_cast(preTransform)); } @@ -9582,16 +9627,15 @@ void main() { // The swapchain's depth/stencil image is stored display-side-up like its colour twin, so // the GL rect has to be mapped into that space before the copy and the copied rows // re-oriented afterwards - the same two halves the colour ReadPixels path applies. - Int32 copyOffsetX = x; - Int32 copyOffsetY = y; + VkOffset2D copyOffset{x, y}; + VkExtent2D copyExtent{static_cast(width), static_cast(height)}; if (defaultFramebufferOrientation) { const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent(); - const DefaultFramebufferRectMapping mapping = - GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform()); - copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast(defaultFboExtent.width), - mapping.mirrorX); - copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast(defaultFboExtent.height), - mapping.flipY); + const Bool mapped = MapDefaultFramebufferReadbackRect( + x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset, + ©Extent); + MOBILEGL_ASSERT(mapped, "ReadDepthStencilPixels: default framebuffer read rectangle is out of bounds"); + if (!mapped) return; } VkBufferImageCopy regions[2]{}; @@ -9603,8 +9647,8 @@ void main() { region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.layerCount = 1; - region.imageOffset = {copyOffsetX, copyOffsetY, 0}; - region.imageExtent = {static_cast(width), static_cast(height), 1}; + region.imageOffset = {copyOffset.x, copyOffset.y, 0}; + region.imageExtent = {copyExtent.width, copyExtent.height, 1}; } if (wantStencil) { auto& region = regions[regionCount++]; @@ -9613,8 +9657,8 @@ void main() { region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.layerCount = 1; - region.imageOffset = {copyOffsetX, copyOffsetY, 0}; - region.imageExtent = {static_cast(width), static_cast(height), 1}; + region.imageOffset = {copyOffset.x, copyOffset.y, 0}; + region.imageExtent = {copyExtent.width, copyExtent.height, 1}; } vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), regionCount, regions); @@ -9648,23 +9692,21 @@ void main() { Bool remapped = true; if (wantDepth && depthCopyBytes > 0) { remappedDepth.resize(pixelCount * depthCopyBytes); - remapped = RemapDefaultFboReadbackToGLOrientation(depthSrc, static_cast(width), - static_cast(height), preTransform, - depthCopyBytes, remappedDepth.data()); + remapped = RemapDefaultFramebufferReadback(depthSrc, static_cast(width), + static_cast(height), preTransform, + depthCopyBytes, remappedDepth.data()); } if (remapped && wantStencil) { remappedStencil.resize(pixelCount); - remapped = RemapDefaultFboReadbackToGLOrientation(stencilSrc, static_cast(width), - static_cast(height), preTransform, 1, - remappedStencil.data()); + remapped = RemapDefaultFramebufferReadback(stencilSrc, static_cast(width), + static_cast(height), preTransform, 1, + remappedStencil.data()); } if (remapped) { if (!remappedDepth.empty()) depthSrc = remappedDepth.data(); if (!remappedStencil.empty()) stencilSrc = remappedStencil.data(); } else { - // Only a quarter-turn pre-transform reaches this, and nothing in this renderer - // models one. MGLOG_I because the INFO builds are the ones that run conformance. - MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap declined (w=%d h=%d " + MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap failed (w=%d h=%d " "preTransform=%d); falling back to raw readback", width, height, static_cast(preTransform)); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 69d958f8..b8843d6b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -229,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout, VkImageLayout dstRestoreLayout, Bool stencilAspect); static SizeT GetReadbackTexelSize(VkFormat sourceFormat); + // Map a GL bottom-left-origin rectangle into the display-oriented swapchain image. + // Quarter-turn surface transforms swap the copy extent's axes. + static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height, + VkExtent2D imageExtent, + VkSurfaceTransformFlagBitsKHR preTransform, + VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent); + // Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into + // GL row order. The input block has swapped dimensions for 90/270 degree transforms. + static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth, + Uint32 logicalHeight, + VkSurfaceTransformFlagBitsKHR preTransform, + SizeT texelSize, Uint8* outPixels); static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat, GLsizei width, GLsizei height, GLenum destinationFormat, GLenum destinationType, SizeT destinationRowStride, diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 8475f598..f0fccae9 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -936,6 +936,45 @@ TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) { EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u); } +TEST(DirectVulkanSanity, DefaultFramebufferQuarterTurnReadbackMapsRectAndPixels) { + using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer; + using MobileGL::Uint8; + + VkOffset2D offset{}; + VkExtent2D copyExtent{}; + ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect( + 1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR, + &offset, ©Extent)); + EXPECT_EQ(offset.x, 0); + EXPECT_EQ(offset.y, 1); + EXPECT_EQ(copyExtent.width, 1u); + EXPECT_EQ(copyExtent.height, 2u); + + ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect( + 1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR, + &offset, ©Extent)); + EXPECT_EQ(offset.x, 1); + EXPECT_EQ(offset.y, 0); + EXPECT_EQ(copyExtent.width, 1u); + EXPECT_EQ(copyExtent.height, 2u); + + // Logical GL rows, bottom to top, are abc / def. The display-oriented swapchain blocks are + // transposed in opposite directions for 90 and 270 degrees. + const Uint8 raw90[] = {'a', 'd', 'b', 'e', 'c', 'f'}; + const Uint8 raw270[] = {'f', 'c', 'e', 'b', 'd', 'a'}; + const Uint8 expected[] = {'a', 'b', 'c', 'd', 'e', 'f'}; + Uint8 result[sizeof(expected)]{}; + + ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback( + raw90, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR, 1, result)); + EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result))); + + std::fill(std::begin(result), std::end(result), 0); + ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback( + raw270, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR, 1, result)); + EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result))); +} + TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) { using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer; using MobileGL::MG_Util::EncodeFloatToHalfBits; diff --git a/tools/trace_replay/README.md b/tools/trace_replay/README.md index b2b5dabf..eecc33a9 100644 --- a/tools/trace_replay/README.md +++ b/tools/trace_replay/README.md @@ -11,6 +11,8 @@ The bundled fixtures cover: ![Minecraft 1.21.4 startup golden](fixtures/minecraft-1.21.4-startup.0000092195.png) - minecraft-1.21.4-main-menu: captured from Minecraft 1.21.4's main menu. ![Minecraft 1.21.4 main menu golden](fixtures/minecraft-1.21.4-main-menu.0000481787.png) +- minecraft-1.21.11-main-menu: captured from Minecraft 1.21.11's main menu on a Pixel 8 Pro through FCL MobileGL. + ![Minecraft 1.21.11 main menu golden](fixtures/minecraft-1.21.11-main-menu.0000205347.png) - minecraft-1.17-main-menu-854: captured from Minecraft 1.17's 854x480 main menu through FCL MobileGL capture. ![Minecraft 1.17 854x480 main menu golden](fixtures/minecraft-1.17-main-menu-854.0000117757.png) - minecraft-1.21.4-in-world: captured from Minecraft 1.21.4 after entering a singleplayer world. diff --git a/tools/trace_replay/fixtures/minecraft-1.21.11-main-menu.0000205347.png b/tools/trace_replay/fixtures/minecraft-1.21.11-main-menu.0000205347.png new file mode 100644 index 00000000..2a452930 --- /dev/null +++ b/tools/trace_replay/fixtures/minecraft-1.21.11-main-menu.0000205347.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea8db1c1c1414d3b564d65b1559a708277417b06895900bc947644d2f658bb86 +size 340727 diff --git a/tools/trace_replay/fixtures/minecraft-1.21.11-main-menu.tgz b/tools/trace_replay/fixtures/minecraft-1.21.11-main-menu.tgz new file mode 100644 index 00000000..717d7b06 --- /dev/null +++ b/tools/trace_replay/fixtures/minecraft-1.21.11-main-menu.tgz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c2e6a68118f7427c8799b52ad721392594a661a6edccd6f4a01283d31e27dd4 +size 7334997 diff --git a/tools/trace_replay/trace_cases.json b/tools/trace_replay/trace_cases.json index eb2ae58a..ae452409 100644 --- a/tools/trace_replay/trace_cases.json +++ b/tools/trace_replay/trace_cases.json @@ -40,6 +40,13 @@ "target_call": 481787, "timeout_seconds": 180 }, + { + "name": "minecraft-1.21.11-main-menu", + "trace_archive": "minecraft-1.21.11-main-menu.tgz", + "golden": "minecraft-1.21.11-main-menu.0000205347.png", + "target_call": 205347, + "timeout_seconds": 180 + }, { "name": "minecraft-1.17-main-menu-854", "trace_archive": "minecraft-1.17-main-menu-854.tgz",