Merge kart draws again while the native steering wheel is animated

The animated copy is registered for the vehicle's whole position array, and a
race frame has 228 primitives that bind it. Suppressing draw merging for all of
them turned the kart's display list back into hundreds of separate draw calls,
each recorded once and replayed in the mono pass and both eyes: on a Quest 3 the
app frame went from 15.7 ms to 21.7 ms with the GPU pinned at 97 %, which past
the 72 Hz deadline reads as 56 FPS against 42.

Merging is unsafe only between draws that resolved the array differently, since
the merged whole renders through the binding of the draw it folds into. So
resolve the replacement once per draw, before the merge test, and merge into a
draw that reached the same decision. Draws of an opponent sharing the asset
merge with each other again too, and with no set registered the test is exactly
what it was before the feature.

Measured at the same place on a Quest 3: 60 FPS against 42, the app frame back
to 13.9 ms, 564 draw calls a frame with 11808 primitives merged away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iChris4andClaude Opus 5 committed 2026-09-23 01:04:06 +02:00
1 parent 3d240c890e
commit fd42edf303
4 files changed
+143 -30

No files matched your search

+71
View File
@@ -4,7 +4,9 @@
#include <gtest/gtest.h>
#include <array>
#include <vector>
#include "gx_test_common.hpp"
#include "gx/native_wheel.hpp"
namespace {
@@ -133,4 +135,73 @@ TEST_F(NativeWheelArrayTest, NonFiniteMatrixNeverMatches) {
EXPECT_EQ(aurora::gx::native_wheel_array(array, nullptr, 0, 0, 0), nullptr);
}
// Draw merging around an animated array. A merged draw appends its vertices to the previous draw's range and renders
// through that draw's array binding, so two draws may merge only when they resolved the same replacement. The kart's
// display list is hundreds of same-state primitives, and merging them is worth several ms an eye on a tiler.
class NativeWheelMergeTest : public GXFifoTest {
protected:
void SetUp() override {
GXFifoTest::SetUp();
aurora::gfx::testing::use_draw_command_tracking(true);
aurora::gx::nativeWheelArrays.clear();
aurora::gx::nativeWheelLastDecision = nullptr;
aurora::gx::nativeWheelLastDrawCommand = nullptr;
aurora::gx::NativeWheelArray replacement;
replacement.source = source.data();
replacement.bytes.assign(source.size(), 0);
replacement.bytes[12] = 1; // one animated position
aurora::gx::nativeWheelArrays.push_back(replacement);
auto& state = aurora::gx::g_gxState;
state.lastVtxFmt = GX_VTXFMT0;
state.lastVtxSize = 1;
state.vtxDesc[GX_VA_POS] = GX_INDEX8;
state.arrays[GX_VA_POS].data = source.data();
state.arrays[GX_VA_POS].size = static_cast<u32>(source.size());
state.arrays[GX_VA_POS].stride = 12;
state.stateDirty = true;
}
void TearDown() override {
aurora::gx::nativeWheelArrays.clear();
aurora::gx::nativeWheelLastDecision = nullptr;
aurora::gx::nativeWheelLastDrawCommand = nullptr;
}
void draw() {
std::vector<u8> fifo{static_cast<u8>(GX_TRIANGLES) | static_cast<u8>(GX_VTXFMT0), 0, 3, 0, 1, 2};
decode_fifo(fifo);
}
// The local vehicle's matrix: the replacement's model-view is all zeroes, and so is a default palette slot.
void makeOpponent() { reinterpret_cast<float*>(&aurora::gx::g_gxState.pnMtx[0].pos)[3] = 100.f; }
std::array<uint8_t, 36> source{};
};
TEST_F(NativeWheelMergeTest, PrimitivesSharingTheAnimatedArrayStillMerge) {
draw();
ASSERT_EQ(aurora::gx::nativeWheelLastDecision, &aurora::gx::nativeWheelArrays.front());
draw();
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 1u);
}
TEST_F(NativeWheelMergeTest, PrimitivesThatTakeTheOriginalArrayAlsoStillMerge) {
makeOpponent();
draw();
ASSERT_EQ(aurora::gx::nativeWheelLastDecision, nullptr);
draw();
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 1u);
}
TEST_F(NativeWheelMergeTest, OpponentPrimitiveNeverFoldsIntoAnAnimatedDraw) {
draw();
makeOpponent();
draw();
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u) << "the merged whole would render the opponent animated";
}
TEST_F(NativeWheelMergeTest, AnimatedPrimitiveNeverFoldsIntoAnOpponentDraw) {
makeOpponent();
draw();
reinterpret_cast<float*>(&aurora::gx::g_gxState.pnMtx[0].pos)[3] = 0.f;
draw();
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u) << "the wheel would render on the original vertices";
}
} // namespace