Add native steering-wheel vertex substitution to Aurora

The VR first-person camera will animate the local vehicle's steering wheel
by handing Aurora a rotated copy of one of the vehicle's position arrays.
A draw picks up the copy only when it binds that exact array with the local
vehicle's position matrix (per-position ownership for indexed-matrix
draws), so opponents sharing the asset keep the original vertices. The copy
takes the same padded upload path as the original and never touches the
shared array cache.

Ported from heurazy's mario-kart-wii-VR-port (GPL-3.0-or-later).
This commit is contained in:
iChris4 committed 2026-09-22 03:54:33 +02:00
1 parent 709fa89ed9
commit f3d7277ff3
7 files changed
+293 -2

No files matched your search

+1
View File
@@ -48,6 +48,7 @@ if (AURORA_ENABLE_GX)
stereo_replay_test.cpp
stereo_interpolation_test.cpp
stereo_mirror_test.cpp
native_wheel_test.cpp
texture_bind_group_cache_key_test.cpp
../lib/gfx/efb_ram_encoder.cpp
# GX API implementations (encoders)
+136
View File
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Native steering wheel: indexed-matrix ownership and array matching. The
// ownership cases come from heurazy's mario-kart-wii-VR-port test set.
#include <gtest/gtest.h>
#include <array>
#include "gx/native_wheel.hpp"
namespace {
// Three positions, 12 bytes each. Position 1 is on the wheel; 0 and 2 belong to
// the body/wing.
struct OwnershipFixture {
std::array<uint8_t, 36> original{};
std::array<uint8_t, 36> replacement{};
// PN matrix byte, texture matrix byte, big-endian position index.
std::array<uint8_t, 12> vertices{0, 30, 0, 0, 0, 30, 0, 1, 3, 30, 0, 2};
OwnershipFixture() { replacement[12] = 7; }
bool matches(uint16_t mask) const {
return aurora::NativeWheelDrawMatches(original, replacement, 12, vertices, 4, 2, 2, mask);
}
};
TEST(NativeWheelMatch, MixedBodyAndWingDrawAcceptsWheelPositionsOnLocalBody) {
OwnershipFixture f;
EXPECT_TRUE(f.matches(1));
}
TEST(NativeWheelMatch, UnrelatedWingJointCannotAnimateWheel) {
OwnershipFixture f;
EXPECT_FALSE(f.matches(2));
}
TEST(NativeWheelMatch, OpponentWithoutLocalMatrixIsRejected) {
OwnershipFixture f;
EXPECT_FALSE(f.matches(0));
}
TEST(NativeWheelMatch, WheelPositionUnderAnotherMatrixIsRejected) {
OwnershipFixture f;
f.vertices[4] = 6;
EXPECT_FALSE(f.matches(1));
EXPECT_TRUE(f.matches(4)) << "local body can occupy a different palette slot";
}
TEST(NativeWheelMatch, MalformedMatrixSelectorRejected) {
OwnershipFixture f;
f.vertices[4] = 1;
EXPECT_FALSE(f.matches(1));
}
TEST(NativeWheelMatch, OutOfRangePositionIndexRejected) {
OwnershipFixture f;
f.vertices[11] = 3;
EXPECT_FALSE(f.matches(1));
}
TEST(NativeWheelMatch, SelectionTouchingAnotherJointCannotDeformIt) {
OwnershipFixture f;
f.replacement[24] = 1;
EXPECT_FALSE(f.matches(1));
}
TEST(NativeWheelMatch, EightBitIndicesAndLayoutValidation) {
OwnershipFixture f;
std::array<uint8_t, 4> indexed8{0, 1, 3, 2};
EXPECT_TRUE(aurora::NativeWheelDrawMatches(f.original, f.replacement, 12, indexed8, 2, 1, 1, 1));
EXPECT_FALSE(aurora::NativeWheelDrawMatches(f.original, f.replacement, 12, indexed8, 2, 2, 1, 1))
<< "invalid vertex layout rejected";
EXPECT_FALSE(aurora::NativeWheelDrawMatches(f.original, f.original, 12, indexed8, 2, 1, 1, 1))
<< "unchanged positions are not counted as animation";
}
// Draw-time matching against the GX position-matrix palette.
class NativeWheelArrayTest : public ::testing::Test {
protected:
void SetUp() override {
aurora::gx::g_gxState = {};
aurora::gx::nativeWheelArrays.clear();
aurora::gx::nativeWheelMatches = 0;
aurora::gx::NativeWheelArray replacement;
replacement.source = source.data();
replacement.bytes.assign(36, 0);
replacement.bytes[12] = 1;
aurora::gx::nativeWheelArrays.push_back(replacement);
array.data = source.data();
array.size = 36;
array.stride = 12;
}
void TearDown() override {
aurora::gx::g_gxState = {};
aurora::gx::nativeWheelArrays.clear();
}
static float* pos(uint32_t slot) { return reinterpret_cast<float*>(&aurora::gx::g_gxState.pnMtx[slot].pos); }
std::array<uint8_t, 36> source{};
aurora::gx::AttrArray array{};
};
TEST_F(NativeWheelArrayTest, MatchesLocalVehicleMatrixOnly) {
EXPECT_NE(aurora::gx::native_wheel_array(array, nullptr, 0, 0, 0), nullptr);
EXPECT_EQ(aurora::gx::nativeWheelMatches, 1u);
// Same asset drawn by an opponent: a translated matrix.
pos(0)[3] = 100;
EXPECT_EQ(aurora::gx::native_wheel_array(array, nullptr, 0, 0, 0), nullptr);
EXPECT_EQ(aurora::gx::nativeWheelMatches, 1u);
}
TEST_F(NativeWheelArrayTest, UnboundSourceIsNotAWheelArray) {
std::array<uint8_t, 36> other{};
aurora::gx::AttrArray unrelated = array;
unrelated.data = other.data();
EXPECT_EQ(aurora::gx::native_wheel_array(unrelated, nullptr, 0, 0, 0), nullptr);
EXPECT_TRUE(aurora::gx::native_wheel_source(source.data()));
EXPECT_FALSE(aurora::gx::native_wheel_source(other.data()));
}
TEST_F(NativeWheelArrayTest, MultiJointBodyChecksPerPositionOwnership) {
// Multi-joint body with an independently animated wing, as in Mario's
// mb/mc kart models. Only the wheel position uses the matching body slot.
aurora::gx::g_gxState.vtxDesc[GX_VA_PNMTXIDX] = GX_DIRECT;
aurora::gx::g_gxState.vtxDesc[GX_VA_POS] = GX_INDEX16;
for (uint32_t slot = 0; slot < aurora::gx::MaxPnMtx; ++slot) pos(slot)[3] = 100;
pos(2)[3] = 0;
uint8_t vertices[]{6, 0, 1, 3, 0, 2};
EXPECT_NE(aurora::gx::native_wheel_array(array, vertices, sizeof(vertices), 3, 1), nullptr);
vertices[0] = 0; // opponent wheel while an unrelated palette slot still matches
EXPECT_EQ(aurora::gx::native_wheel_array(array, vertices, sizeof(vertices), 3, 1), nullptr);
}
TEST_F(NativeWheelArrayTest, NonFiniteMatrixNeverMatches) {
pos(0)[0] = __builtin_nanf("");
EXPECT_EQ(aurora::gx::native_wheel_array(array, nullptr, 0, 0, 0), nullptr);
}
} // namespace