mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Test] (Magma): OversizedRecordingSplitScenario - a 1024-draw frame, one pixel and one uniform value per draw, lands whole on both backends; the DirectVulkan.SplitRecording lane pins the draw budget to 64 and asserts the split fired
This commit is contained in:
@@ -128,6 +128,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
Scenarios/OversizedRecordingSplitScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -376,6 +377,12 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/primgen-query-reroute.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
# The command-buffer draw budget pinned far below one frame's draws, with the log path for the
|
||||
# split's latched MGLOG_I - the same arming reasoning as the two entries above.
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_SPLIT_RECORDING_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=64"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/oversized-recording-split.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
# The point-size demotion pinned on, per backend, with a per-lane log file for the arming
|
||||
# assertion - the same MOBILEGL_LOG_FILE_PATH reasoning as the UnlocatedIoBlocks lane above.
|
||||
# Two lanes because the demotion runs in the SHARED phase-B chain and each backend then
|
||||
@@ -588,6 +595,22 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_PRIMGEN_REROUTE_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# OversizedRecordingSplitScenario with the command-buffer draw budget PINNED to 64. The ambient
|
||||
# registrations run its 1024-draw frame under the default budget, which never splits it; this
|
||||
# entry splits it 15 times, so the pixels prove a split loses nothing, and the arming assertion
|
||||
# (the renderer's latched MGLOG_I) proves the split happened at all - on a renderer that records
|
||||
# the whole frame into one command buffer, it is the assertion that goes red. DirectVulkan only:
|
||||
# the budget steers nothing on DirectGLES, whose driver flushes on its own.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.SplitRecording."
|
||||
TEST_FILTER "OversizedRecordingSplitScenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_SPLIT_RECORDING_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
|
||||
# registrations above cover the narrow storage - on every CI driver the widening's POST
|
||||
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/OversizedRecordingSplitScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A FRAME WITH MORE DRAWS THAN ONE COMMAND BUFFER MAY HOLD IS SPLIT, AND LOSES NOTHING.
|
||||
//
|
||||
// DirectVulkan records a frame into one command buffer, and used to submit it only at Present.
|
||||
// A driver keeps a command buffer's GPU memory until the buffer is freed, and the Adreno 830
|
||||
// driver maps that memory into the process in 16 KiB chunks. Minecraft rd12's loading frame
|
||||
// records 981,654 draws (3.1M commands) into one buffer. It grew to ~40k such mappings and
|
||||
// aborted inside the driver's own calloc once vm.max_map_count (65530) was spent. The renderer
|
||||
// now submits the open buffer after MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER draws and goes
|
||||
// on recording into a fresh one (VulkanRenderer::SplitOversizedRecording).
|
||||
//
|
||||
// Each draw here writes exactly one pixel of a 32x32 target, in its own colour, selected by a
|
||||
// per-draw uniform, so every draw carries its own uniform state, like Minecraft's chunk draws.
|
||||
// After 1024 draws every pixel must hold its own draw's colour. Each of these split bugs leaves
|
||||
// a pixel wrong: a re-begun render pass that does not LOAD, a draw recorded into the retired
|
||||
// buffer, or state that is not re-emitted on the fresh one.
|
||||
//
|
||||
// The ambient registrations run the same draws unsplit, because the default budget is far above
|
||||
// 1024; that is the control. The DirectVulkan.SplitRecording. entry pins the budget to 64, so
|
||||
// the frame is split 15 times. There the case also asserts the renderer's latched MGLOG_I, the
|
||||
// one assertion that is red on a renderer that never splits.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kSide = 32;
|
||||
constexpr int kDraws = kSide * kSide;
|
||||
|
||||
// aPos spans the unit square; the uniform moves it onto cell (i % 32, i / 32) of a
|
||||
// 32x32 grid, which is exactly one pixel of a 32x32 target.
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform int uIndex;
|
||||
void main() {
|
||||
vec2 cell = vec2(float(uIndex % 32), float(uIndex / 32));
|
||||
gl_Position = vec4((cell + aPos) / 16.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// Multiples of 8/255 survive an RGBA8 round trip exactly, so the readback bytes are
|
||||
// (8x, 8y, 255, 255) for the pixel at (x, y) and nothing else can produce them.
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
uniform int uIndex;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
o_color = vec4(float(uIndex % 32) * 8.0 / 255.0, float(uIndex / 32) * 8.0 / 255.0, 1.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// The budget the process was launched with, or 0 when it is unset or not a number.
|
||||
unsigned long PinnedDrawBudget() {
|
||||
const char* value = std::getenv("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER");
|
||||
if (value == nullptr || *value == '\0') return 0;
|
||||
char* end = nullptr;
|
||||
const unsigned long parsed = std::strtoul(value, &end, 10);
|
||||
return (end != nullptr && *end == '\0') ? parsed : 0;
|
||||
}
|
||||
|
||||
// The library log, for the arming assertion. Same machinery and reasoning as
|
||||
// UnlocatedIoBlockScenario: MOBILEGL_LOG_FILE_PATH is read at log-init, every process in
|
||||
// the lane appends to the file, and only bytes appended after the snapshot count.
|
||||
std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path) : std::filesystem::path();
|
||||
}
|
||||
|
||||
std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
::testing::AssertionResult EveryPixelHoldsItsOwnDraw(const Image& image) {
|
||||
int wrong = 0;
|
||||
int firstX = -1, firstY = -1;
|
||||
Rgba8 firstSeen{};
|
||||
for (int y = 0; y < kSide; ++y) {
|
||||
for (int x = 0; x < kSide; ++x) {
|
||||
const Rgba8 expected{static_cast<std::uint8_t>(8 * x), static_cast<std::uint8_t>(8 * y), 255, 255};
|
||||
const Rgba8 seen = image.At(x, y);
|
||||
if (seen != expected) {
|
||||
if (wrong == 0) {
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
firstSeen = seen;
|
||||
}
|
||||
++wrong;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrong == 0) {
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
return ::testing::AssertionFailure()
|
||||
<< wrong << " of " << kDraws << " pixels do not hold their own draw's colour. First: (" << firstX
|
||||
<< ", " << firstY << ") = " << firstSeen << ", drawn by draw " << (firstY * kSide + firstX)
|
||||
<< " (a pixel no draw reached still holds the clear colour 0,0,0,0)";
|
||||
}
|
||||
|
||||
class OversizedRecordingSplitScenario : public ScenarioTest {
|
||||
protected:
|
||||
// kDraws single-pixel quads into a fresh target, one uniform value per draw, with no
|
||||
// readback, flush or rebind in between, so the renderer sees one uninterrupted
|
||||
// recording. Then the whole target is read back.
|
||||
Image DrawEveryPixelWithItsOwnDraw(std::string* error) {
|
||||
const unsigned int program = CompileProgram(kVS, kFS, error);
|
||||
if (program == 0) return Image();
|
||||
ColorFbo target = MakeColorFbo(kSide, kSide);
|
||||
if (target.fbo == 0) {
|
||||
*error = "MakeColorFbo failed";
|
||||
glDeleteProgram(program);
|
||||
return Image();
|
||||
}
|
||||
|
||||
static const float kUnitQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0, vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kUnitQuad), kUnitQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
BindFbo(target);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glUseProgram(program);
|
||||
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
|
||||
for (int i = 0; i < kDraws; ++i) {
|
||||
glUniform1i(indexLocation, i);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
Image image = ReadPixels(kSide, kSide);
|
||||
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(target);
|
||||
glDeleteProgram(program);
|
||||
return image;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(OversizedRecordingSplitScenario, EveryDrawOfTheFrameLandsAcrossCommandBufferSplits) {
|
||||
if (!Ready()) return;
|
||||
|
||||
// Taken before the draws, so the line the arming check looks for can only have been
|
||||
// written by them. The renderer latches it at its first split.
|
||||
const std::uintmax_t logBefore = LibraryLogSize();
|
||||
|
||||
std::string error;
|
||||
const Image image = DrawEveryPixelWithItsOwnDraw(&error);
|
||||
ASSERT_FALSE(image.Empty()) << error;
|
||||
EXPECT_TRUE(EveryPixelHoldsItsOwnDraw(image));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Arming: only where the budget is pinned below this frame's draw count, on the backend
|
||||
// that has the budget, and with a log to read. Everywhere else this case is the unsplit
|
||||
// control and stops here.
|
||||
const unsigned long budget = PinnedDrawBudget();
|
||||
const bool splitExpected = budget > 0 && budget < static_cast<unsigned long>(kDraws) &&
|
||||
Gl().BackendName() == std::string("DirectVulkan") && !LibraryLogPath().empty();
|
||||
RecordProperty("arming_checked", splitExpected ? "yes" : "no");
|
||||
if (!splitExpected) {
|
||||
return;
|
||||
}
|
||||
const std::string appended = LibraryLogSince(logBefore);
|
||||
EXPECT_NE(appended.find("draws into one command buffer; submitting it"), std::string::npos)
|
||||
<< "MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=" << budget << " and one frame recorded " << kDraws
|
||||
<< " draws, but DirectVulkan never reported splitting the recording. A renderer that does not "
|
||||
"split grows a single command buffer without bound (rd12 on Adreno: ~40k driver mappings, "
|
||||
"then SIGABRT). See VulkanRenderer::SplitOversizedRecording. Log appended by this test:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user