Keep the cockpit wheel with the view and let the native wheel recover

Found on a Quest 3 through Virtual Desktop (VirtualDesktopXR, Vulkan):
the race camera's view matched the scene camera exactly and the kart's
own wheel animated (228 draws a frame took the copy), but during the
race's opening pan no draw took it, the 30-frame fallback latched the VR
wheel for that vehicle, and it stayed until the scene changed.

The fallback now heals: copies keep being published, the VR wheel stands
in only while draws are not taking them, and the vehicle's own wheel
returns as soon as they are. Both switches are logged, a few per race.

With first_person_rotation "yaw_pitch" or "full" the wheel copy and the
hands' wheel geometry now ride in the view's own frame (the kart's
orientation around the seat) instead of the level seat, so the wheel no
longer tilts against the view on slopes.

Aurora logs, after about half a second, five seconds and a minute of
copies, how many draws bound a copied array (inside and outside its
window), how many matched, and the closest position matrix against the
expected one, so a mismatch says whether the array or the matrix differs.
This commit is contained in:
iChris4 committed 2026-09-22 03:56:38 +02:00
1 parent 0d07df14c9
commit 131da8a7a9
5 files changed
+145 -40

No files matched your search

+28
View File
@@ -21,6 +21,10 @@ extern "C" void aurora_clear_native_wheel_vertices() {
aurora::gx::fifo::drain();
aurora::gx::nativeWheelLastMatches.store(aurora::gx::nativeWheelMatches);
aurora::gx::nativeWheelMatches = 0;
aurora::gx::nativeWheelPreviousSources.clear();
for (const auto& array : aurora::gx::nativeWheelArrays)
aurora::gx::nativeWheelPreviousSources.push_back(array.source);
aurora::gx::native_wheel_report();
aurora::gx::nativeWheelArrays.clear();
}
extern "C" uint32_t aurora_native_wheel_draw_count() { return aurora::gx::nativeWheelLastMatches.load(); }
@@ -38,6 +42,30 @@ extern "C" void aurora_set_native_wheel_vertices(const void* source, const void*
// Single definition for the `Log` that gx.hpp declares for this directory.
aurora::Module Log("aurora::gx");
namespace aurora::gx {
// Called as a set is cleared: a line at about half a second, five seconds and
// a minute of sets, with the matrices on the first report that saw a draw.
void native_wheel_report() {
auto& diagnostics=nativeWheelDiagnostics;
++diagnostics.sets;
++nativeWheelClears;
if(nativeWheelClears!=30 && nativeWheelClears!=300 && nativeWheelClears!=3600) return;
::Log.info("Native steering wheel: {} sets; draws binding a replaced array {} ({} with a larger range, {} outside a "
"set); matched {}; closest position matrix off by {} ({} matrices)",
diagnostics.sets,diagnostics.boundDraws,diagnostics.oversizeDraws,diagnostics.outsideDraws,
diagnostics.matchedDraws,diagnostics.bestError,diagnostics.bestIndexed?"indexed":"current");
if(diagnostics.boundDraws!=0 && nativeWheelReports++==0) {
const auto& m=diagnostics.bestMatrix;
const auto& e=diagnostics.expected;
::Log.info("Native steering wheel: closest [{} {} {} {} | {} {} {} {} | {} {} {} {}] expected [{} {} {} {} | {} {} "
"{} {} | {} {} {} {}]",
m[0],m[1],m[2],m[3],m[4],m[5],m[6],m[7],m[8],m[9],m[10],m[11],
e[0],e[1],e[2],e[3],e[4],e[5],e[6],e[7],e[8],e[9],e[10],e[11]);
}
diagnostics={};
}
} // namespace aurora::gx
static void GXWriteString(const char* label) {
auto length = strlen(label);
+2
View File
@@ -2422,6 +2422,8 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g
}
auto& array = g_gxState.arrays[i];
const u32 uploadStride = padded_upload_stride(array.stride);
if (i == GX_VA_POS && nativeWheelArrays.empty() && !nativeWheelPreviousSources.empty())
UNLIKELY { native_wheel_note_outside(array.data); }
if (i == GX_VA_POS && !nativeWheelArrays.empty())
UNLIKELY {
if (auto* nativeWheel = native_wheel_array(array, vertices, static_cast<u32>(vtxCount) * vtxStride, vtxStride,
+48 -2
View File
@@ -9,6 +9,7 @@
// thread; the set/clear entry points are posted there by the runtime.
#pragma once
#include "gx.hpp"
#include <algorithm>
#include <vector>
#include <cstring>
#include <cmath>
@@ -26,6 +27,49 @@ struct NativeWheelArray {
inline std::vector<NativeWheelArray> nativeWheelArrays;
inline uint32_t nativeWheelMatches=0;
inline std::atomic<uint32_t> nativeWheelLastMatches{0};
// For the host log: why draws of the replaced arrays did or did not take them.
struct NativeWheelDiagnostics {
uint32_t sets=0; // replacement sets cleared since the last report
uint32_t boundDraws=0; // draws that bound a replacement's source array
uint32_t oversizeDraws=0; // ... whose bound range was larger than the replacement
uint32_t outsideDraws=0; // draws that bound a cleared set's source while no set was active
uint32_t matchedDraws=0;
float bestError=INFINITY; // smallest largest-element difference of a position matrix
bool bestIndexed=false;
std::array<float,12> bestMatrix{};
std::array<float,12> expected{};
};
inline NativeWheelDiagnostics nativeWheelDiagnostics;
inline std::vector<const void*> nativeWheelPreviousSources;
inline uint32_t nativeWheelClears=0;
inline uint32_t nativeWheelReports=0;
inline void native_wheel_note_outside(const void* source) {
for(const void* previous:nativeWheelPreviousSources)
if(previous==source) { ++nativeWheelDiagnostics.outsideDraws;return; }
}
inline void native_wheel_note_bound(const NativeWheelArray& replacement,bool indexedMatrix) {
auto& diagnostics=nativeWheelDiagnostics;
++diagnostics.boundDraws;
for(uint32_t slot=0;slot<MaxPnMtx;++slot) {
if(!indexedMatrix && slot!=g_gxState.currentPnMtx) continue;
const auto* matrix=reinterpret_cast<const float*>(&g_gxState.pnMtx[slot].pos);
float error=0;
for(int i=0;i<12;++i) error=std::max(error,std::abs(matrix[i]-replacement.modelView[i]));
if(error<diagnostics.bestError) {
diagnostics.bestError=error;
diagnostics.bestIndexed=indexedMatrix;
std::memcpy(diagnostics.bestMatrix.data(),matrix,sizeof(float)*12);
diagnostics.expected=replacement.modelView;
}
}
}
// Called as a set is cleared (GXAurora.cpp): one host log line at about half a
// second, five seconds and a minute of sets.
void native_wheel_report();
inline bool native_wheel_source(const void* source) {
for(const auto& replacement:nativeWheelArrays) if(source==replacement.source) return true;
return false;
@@ -35,7 +79,9 @@ inline NativeWheelArray* native_wheel_array(const AttrArray& array,const uint8_t
const bool indexedMatrix=g_gxState.vtxDesc[GX_VA_PNMTXIDX]==GX_DIRECT;
if(!indexedMatrix && g_gxState.currentPnMtx>=MaxPnMtx) return nullptr;
for(auto& replacement:nativeWheelArrays) {
if(array.data!=replacement.source || array.size>replacement.bytes.size()) continue;
if(array.data!=replacement.source) continue;
if(array.size>replacement.bytes.size()) { ++nativeWheelDiagnostics.oversizeDraws;continue; }
native_wheel_note_bound(replacement,indexedMatrix);
// A matching asset alone would also animate an opponent. Multi-joint
// models need a per-position ownership check, not a blanket exclusion.
uint16_t matching=0;
@@ -56,7 +102,7 @@ inline NativeWheelArray* native_wheel_array(const AttrArray& array,const uint8_t
{replacement.bytes.data(),array.size},array.stride,
{vertices,vertexBytes},vertexStride,positionOffset,
g_gxState.vtxDesc[GX_VA_POS]==GX_INDEX8?1:2,matching)) continue;
++nativeWheelMatches;return &replacement;
++nativeWheelMatches;++nativeWheelDiagnostics.matchedDraws;return &replacement;
}
return nullptr;
}