From f013f9d2f7682802bff576c9c047af49b1fb5ea6 Mon Sep 17 00:00:00 2001 From: iChris4 Date: Sat, 26 Sep 2026 01:46:49 +0200 Subject: [PATCH] Implement native wheel node matrix retrieval and enhance topology validation --- OPENXR.md | 24 +++++++----- runtime/include/vr/native_wheel_topology.h | 44 ++++++++++++++++++++++ runtime/src/vr/mkw_vr_first_person.cpp | 24 +++++------- runtime/tests/vr_cockpit_tests.cpp | 41 ++++++++++++++++++++ 4 files changed, 108 insertions(+), 25 deletions(-) diff --git a/OPENXR.md b/OPENXR.md index 39e903a..96cd193 100644 --- a/OPENXR.md +++ b/OPENXR.md @@ -456,11 +456,14 @@ model. Karts bake the wheel into the body, so at the race draw boundary the runt body's MDL0 position arrays and shape connectivity. Hand grips locate the wheel, but its complete rim determines the rotation centre, radius and tilt: grip height/spacing varies by character. Whole rim and spoke components turn together on a copy; a column or chassis component crossing -the selection stays intact. The root bone's authored transform is included when locating and -turning the wheel (the Baby Booster authors its body with rotated axes). The copy goes to the GX -thread; Aurora substitutes it into draws that bind that array with the player's model-view -matrix including the root transform (`aurora_set_native_wheel_vertices`), checking each changed vertex's -matrix slot, so an opponent sharing the asset and other joints of the same draw are untouched. The +the selection stays intact. The authored transform of the bone that draws the body's node 0 is +included when locating and turning the wheel (the Baby Booster authors its body with rotated +axes). That bone is found by its node id, not its place in the bone dictionary: the Flame Flyer and +Cheep Charger list an `nw4r_root` bone first, and taking that one left both on the separate VR +wheel. The copy goes to the GX thread; Aurora substitutes it into draws that bind that array with +the player's model-view matrix including that bone's transform (`aurora_set_native_wheel_vertices`), +checking each changed vertex's matrix slot, so an opponent sharing the asset and other joints of +the same draw are untouched. The guest's own vertices are never written, and the copies are dropped after the frame's draws. Bikes turn their handle part in the game already; its copy is only re-seated on the cockpit frame so the bars stay with your hands while the bike banks. The wheel rides in the same frame as the view: the @@ -470,11 +473,12 @@ which is also what `native_steering_wheel = false` draws. The copy keeps being p vehicle's own wheel returns as soon as draws take it again, and the log notes both switches. Validated on the extracted PAL disc's 216 single-player kart/character and Mii combinations -(all 18 kart types): each selects the complete 21-position rim and 15-position spoke assembly, -with the remaining positions unchanged and connected-piece distances preserved. Regression tests -also cover raised/narrow grips, domed hubs, rotated roots, child joints, chassis triangles crossing -the wheel volume, degenerate strip connectors and malformed MDL0 data. This asset check does not -by itself verify every combination's live draw matching or modded vehicle models. +(all 18 kart types): each resolves its node-0 bone as the runtime does and selects the complete +21-position rim and 15-position spoke assembly, with the remaining positions unchanged and +connected-piece distances preserved. Regression tests also cover raised/narrow grips, domed hubs, +rotated roots, a node-0 bone listed behind another, child joints, chassis triangles crossing the +wheel volume, degenerate strip connectors and malformed MDL0 data. This asset check does not by +itself verify every combination's live draw matching or modded vehicle models. The substitution is decided per draw, and a draw that folds into a neighbour renders through that neighbour's array binding, so only draws that reached the same decision may merge. Deciding this diff --git a/runtime/include/vr/native_wheel_topology.h b/runtime/include/vr/native_wheel_topology.h index ea80fc3..f28cb6f 100644 --- a/runtime/include/vr/native_wheel_topology.h +++ b/runtime/include/vr/native_wheel_topology.h @@ -2,8 +2,10 @@ #pragma once #include +#include #include #include +#include #include #include @@ -171,4 +173,46 @@ inline bool ReadNativeWheelTopology(const uint8_t *mdl, size_t size, uint32_t ar return found; } +// The bone GX draws node `node` through: its 0x70 matrix is that bone's +// model-space transform, parents included. Found by node id, not dictionary +// position: the Flame Flyer and Cheep Charger bodies open with an nw4r_root +// bone on node 2 or 3, and the node-0 bone their wheel belongs to comes third. +inline bool ReadNativeWheelNodeMatrix(const uint8_t *mdl, size_t size, uint32_t node, float out[12]) { + if (!mdl || size < 0x40) + return false; + const auto read32 = [&](size_t at) { + return (uint32_t(mdl[at]) << 24) | (uint32_t(mdl[at + 1]) << 16) | (uint32_t(mdl[at + 2]) << 8) | mdl[at + 3]; + }; + const auto contains = [&](size_t at, size_t length) { return at <= size && length <= size - at; }; + const auto version = read32(8); + if (read32(0) != 0x4d444c30 || version < 8 || version > 11 || read32(4) != size) + return false; + const size_t dictionary = read32(0x14); + if (!dictionary || !contains(dictionary, 8)) + return false; + const auto count = read32(dictionary + 4); + if (!count || count > 4096 || !contains(dictionary + 8, size_t(count + 1) * 16)) + return false; + for (uint32_t entry = 1; entry <= count; ++entry) { + const size_t offset = read32(dictionary + 8 + entry * 16 + 12); + if (offset > size - dictionary) + return false; + const size_t bone = dictionary + offset; + if (!contains(bone, 0xa0)) + return false; + if (read32(bone + 0x10) != node) + continue; + for (unsigned i = 0; i < 12; ++i) { + const uint32_t bits = read32(bone + 0x70 + i * 4); + float value; + std::memcpy(&value, &bits, sizeof value); + if (!std::isfinite(value)) + return false; + out[i] = value; + } + return true; + } + return false; +} + } // namespace mkw::vr diff --git a/runtime/src/vr/mkw_vr_first_person.cpp b/runtime/src/vr/mkw_vr_first_person.cpp index 3f38199..2bf3363 100644 --- a/runtime/src/vr/mkw_vr_first_person.cpp +++ b/runtime/src/vr/mkw_vr_first_person.cpp @@ -740,22 +740,18 @@ bool PublishNativeWheelMesh(uint32_t part, const Mtx34& model_view, const Mtx34& if (version < 8 || version > 11) { return false; } + const uint32_t mdl_size = Memory::Read32(mdl + 4); + const uint8_t* mdl_bytes = mdl_size <= 0x1000000 ? Memory::GetPointer(mdl, mdl_size) : nullptr; Mtx34 body_from_vertices = kIdentityMtx34; Mtx34 wheel_model_view = model_view; if (!whole_part) { - // Body::mtx is the model placement, while the root bone may have - // its own authored rotation (notably the Baby Booster). GX draws - // its positions through placement * root, so both wheel selection - // and Aurora's local-player matrix match must include that root. - const uint32_t bone_dic_offset = Memory::Read32(mdl + 0x14); - if (!bone_dic_offset || bone_dic_offset > 0x100000) return false; - const uint32_t bone_dic = mdl + bone_dic_offset; - if (!Memory::Contains(bone_dic, 40) || !Memory::Read32(bone_dic + 4)) return false; - const uint32_t bone_offset = Memory::Read32(bone_dic + 36); - if (bone_offset > 0x100000) return false; - const uint32_t root_bone = bone_dic + bone_offset; - if (!Memory::Contains(root_bone, 0xa0) || Memory::Read32(root_bone + 0x10) != 0 || - !ReadGuestMtx34(root_bone + 0x70, body_from_vertices)) return false; + // Body::mtx is the model placement, while the bone GX draws the + // body's node 0 through may have its own authored transform (the + // Baby Booster's rotated root). GX draws its positions through + // placement * bone, so both wheel selection and Aurora's + // local-player matrix match must include it. It is found by node + // id: the Flame Flyer and Cheep Charger list an nw4r_root first. + if (!ReadNativeWheelNodeMatrix(mdl_bytes, mdl_size, 0, body_from_vertices.data())) return false; wheel_model_view = ComposeMtx(model_view, body_from_vertices); } const uint32_t dic_offset = Memory::Read32(mdl + 0x18); @@ -814,8 +810,6 @@ bool PublishNativeWheelMesh(uint32_t part, const Mtx34& model_view, const Mtx34& point = detail::TransformPoint(correction, point.x, point.y, point.z); } } else { - const uint32_t mdl_size = Memory::Read32(mdl + 4); - const uint8_t* mdl_bytes = mdl_size <= 0x1000000 ? Memory::GetPointer(mdl, mdl_size) : nullptr; NativeWheelTopology topology(num); if (!ReadNativeWheelTopology(mdl_bytes, mdl_size, Memory::Read32(header + 0x10), topology) || RotateNativeWheelVertices(points, topology, center, radius, angle, &correction, diff --git a/runtime/tests/vr_cockpit_tests.cpp b/runtime/tests/vr_cockpit_tests.cpp index 3df1ed2..1aab0ab 100644 --- a/runtime/tests/vr_cockpit_tests.cpp +++ b/runtime/tests/vr_cockpit_tests.cpp @@ -10,7 +10,9 @@ #include "vr/mkw_vr_first_person.h" #include "vr/native_wheel_mesh.h" +#include #include +#include #include #include @@ -337,6 +339,45 @@ void TestNativeWheelTopology() { Check(!ReadNativeWheelTopology(mdl.data(), mdl.size(), 1, model), "unrelated position array ignored"); put32(shape + 0x2c, UINT32_MAX); Check(!ReadNativeWheelTopology(mdl.data(), mdl.size(), 0, model), "escaping primitive offset rejected"); + // The bone the wheel's positions are drawn through is found by node id: + // the Flame Flyer and Cheep Charger list an nw4r_root bone (node 2) + // before the node-0 bone, which used to leave them on the VR wheel. + std::vector bones(0x1c0, 0); + const auto putBone32 = [&](size_t at, uint32_t value) { + for (unsigned i = 0; i < 4; ++i) + bones[at + i] = uint8_t(value >> ((3 - i) * 8)); + }; + const auto putBoneFloat = [&](size_t at, float value) { + uint32_t bits; + std::memcpy(&bits, &value, sizeof bits); + putBone32(at, bits); + }; + putBone32(0, 0x4d444c30); + putBone32(4, uint32_t(bones.size())); + putBone32(8, 11); + putBone32(0x14, 0x40); + putBone32(0x44, 2); + putBone32(0x40 + 8 + 16 + 12, 0x40); + putBone32(0x40 + 8 + 32 + 12, 0xe0); + putBone32(0x80 + 0x10, 2); + putBone32(0x120 + 0x10, 0); + const float authored[12]{0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0}; + for (unsigned i = 0; i < 12; ++i) { + putBoneFloat(0x80 + 0x70 + i * 4, i % 5 == 0 ? 1.0f : 0.0f); + putBoneFloat(0x120 + 0x70 + i * 4, authored[i]); + } + float bone[12]{}; + Check(ReadNativeWheelNodeMatrix(bones.data(), bones.size(), 0, bone) && + std::equal(std::begin(authored), std::end(authored), bone), + "node-0 bone found behind an nw4r_root bone"); + Check(ReadNativeWheelNodeMatrix(bones.data(), bones.size(), 2, bone) && bone[0] == 1.0f && bone[2] == 0.0f, + "bones are told apart by node id"); + Check(!ReadNativeWheelNodeMatrix(bones.data(), bones.size(), 5, bone), "missing node rejected"); + Check(!ReadNativeWheelNodeMatrix(bones.data(), bones.size() - 1, 0, bone), "MDL0 size mismatch rejected"); + putBoneFloat(0x120 + 0x70, NAN); + Check(!ReadNativeWheelNodeMatrix(bones.data(), bones.size(), 0, bone), "non-finite bone matrix rejected"); + putBone32(0x40 + 8 + 32 + 12, uint32_t(bones.size())); + Check(!ReadNativeWheelNodeMatrix(bones.data(), bones.size(), 0, bone), "escaping bone offset rejected"); } } // namespace