mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Add VR object culling functionality
- Introduced a new configuration option for object culling in VR, allowing the game to hide objects outside the camera's view. - Implemented native replacements for the Mario Kart functions responsible for scene culling, ensuring accurate behavior in VR. - Added a new header file `mkw_vr_culling.h` to define the culling logic and structures. - Created `mkw_vr_culling.cpp` to implement the culling logic, including frustum intersection checks and screen info updates. - Updated runtime configuration to include the new object culling option, with appropriate getters and setters. - Enhanced the settings overlay to allow users to toggle object culling in VR. - Added tests in `vr_culling_tests.cpp` to validate the frustum intersection logic and ensure compliance with original behavior. - Updated CMake files to include new source files and tests.
This commit is contained in:
1 parent
3c20f677eb
commit
e2c4eb3c8c
10 files changed
+718
-7
No files matched your search
@@ -418,7 +418,7 @@ add_test(NAME mkw_vr_first_person_tests COMMAND mkw_vr_first_person_tests)
|
||||
# ported from heurazy's mario-kart-wii-VR-port, and tracked hands (grasp, bare-hand
|
||||
# buttons, flick). All header-only.
|
||||
foreach(test_name mkw_steering_wheel_tests mkw_vr_cockpit_tests mkw_vr_bullet_bill_tests mkw_vr_hand_steering_tests mkw_vr_camera_toggle_tests
|
||||
mkw_vr_hand_tracking_tests)
|
||||
mkw_vr_hand_tracking_tests mkw_vr_culling_tests)
|
||||
string(REGEX REPLACE "^mkw_" "" test_source "${test_name}")
|
||||
add_executable(${test_name} "${CMAKE_CURRENT_LIST_DIR}/tests/${test_source}.cpp")
|
||||
target_include_directories(${test_name} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
|
||||
@@ -159,7 +159,9 @@ endforeach()
|
||||
# the same contraction/rounding policy as translated PPC shards.
|
||||
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src/ppc_helpers.cpp"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src/fpu_helpers.cpp")
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src/fpu_helpers.cpp"
|
||||
# Native reimplementations of the game's culling math (VR object culling).
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src/vr/mkw_vr_culling.cpp")
|
||||
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
|
||||
SKIP_UNITY_BUILD_INCLUSION ON
|
||||
SKIP_PRECOMPILE_HEADERS ON
|
||||
|
||||
@@ -76,6 +76,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<float> vrCockpitUnitsPerMeter;
|
||||
std::optional<bool> vrSteeringWheel;
|
||||
std::optional<bool> vrNativeSteeringWheel;
|
||||
std::optional<bool> vrObjectCulling;
|
||||
std::optional<bool> vrHandSteering;
|
||||
std::optional<bool> vrHandTracking;
|
||||
std::optional<float> vrWheelKartDegrees;
|
||||
@@ -235,6 +236,10 @@ inline constexpr float kVrCockpitUnitsPerMeterMax = 400.0f;
|
||||
inline constexpr bool kVrSteeringWheelDefault = true;
|
||||
inline constexpr bool kVrNativeSteeringWheelDefault = true;
|
||||
inline constexpr bool kVrHandSteeringDefault = true;
|
||||
// The game hides karts and objects its own camera cannot see, which a head
|
||||
// turn in VR reveals. object_culling false draws them anyway (see
|
||||
// vr/mkw_vr_culling.h); it only takes effect while VR is enabled.
|
||||
inline constexpr bool kVrObjectCullingDefault = true;
|
||||
// The cockpit hands follow the headset's hand tracking (the controllers' touch
|
||||
// sensors while they are held, the cameras once they are put down, when bare
|
||||
// hands also drive). Opt-in, and only offered on the Quest for now; the
|
||||
@@ -577,6 +582,10 @@ inline void EnsureConfigFile() {
|
||||
"# own model; false draws a separate VR wheel instead.\n"
|
||||
"steering_wheel = true\n"
|
||||
"native_steering_wheel = true\n"
|
||||
"# The game hides karts and objects its own camera cannot see.\n"
|
||||
"# object_culling = false draws them anyway, so a head turn or a\n"
|
||||
"# look over the shoulder shows them; it costs GPU time.\n"
|
||||
"object_culling = true\n"
|
||||
"# Hand steering (by heurazy): squeeze a grip near the wheel or\n"
|
||||
"# handlebar to take hold of it with the tracked controllers, and\n"
|
||||
"# turn it to steer. Releasing both grips gives steering back to the\n"
|
||||
@@ -859,6 +868,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
readRangedFloat("cockpit_units_per_meter", kVrCockpitUnitsPerMeterMin, kVrCockpitUnitsPerMeterMax);
|
||||
config.vrSteeringWheel = FindConfigValue<bool>(document, "vr", "steering_wheel");
|
||||
config.vrNativeSteeringWheel = FindConfigValue<bool>(document, "vr", "native_steering_wheel");
|
||||
config.vrObjectCulling = FindConfigValue<bool>(document, "vr", "object_culling");
|
||||
config.vrHandSteering = FindConfigValue<bool>(document, "vr", "hand_steering");
|
||||
config.vrHandTracking = FindConfigValue<bool>(document, "vr", "hand_tracking");
|
||||
config.vrWheelKartDegrees = readRangedFloat("wheel_kart_degrees", kVrWheelDegreesMin, kVrWheelDegreesMax);
|
||||
@@ -1250,6 +1260,11 @@ inline bool SetVrNativeSteeringWheel(bool value) {
|
||||
return WriteSetting("vr", "native_steering_wheel", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrObjectCulling(bool value) {
|
||||
Mutable().vrObjectCulling = value;
|
||||
return WriteSetting("vr", "object_culling", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrHandSteering(bool value) {
|
||||
Mutable().vrHandSteering = value;
|
||||
return WriteSetting("vr", "hand_steering", value ? "true" : "false");
|
||||
@@ -1727,6 +1742,10 @@ inline bool VrNativeSteeringWheel(bool fallback = kVrNativeSteeringWheelDefault)
|
||||
return Get().vrNativeSteeringWheel.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrObjectCulling(bool fallback = kVrObjectCullingDefault) {
|
||||
return Get().vrObjectCulling.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool VrHandSteering(bool fallback = kVrHandSteeringDefault) {
|
||||
return Get().vrHandSteering.value_or(fallback);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
// Mario Kart's own object culling, and the VR switch that turns it off
|
||||
// (vr.object_culling = false; F10 > Camera, or the headset settings panel's
|
||||
// Camera tab). It applies while VR is enabled, except in the Flat screen race
|
||||
// view.
|
||||
//
|
||||
// The game hides what its chase camera cannot see in two places, both keyed
|
||||
// to the game camera and not to the headset, so in VR a wide head turn or a
|
||||
// first-person look over the shoulder reveals missing karts and characters:
|
||||
//
|
||||
// 1. nw4r::g3d::ScnObjGather::Add tests every scene object's bounding box
|
||||
// against the camera frustum through nw4r::math::FRUSTUM::IntersectAABB_Ex
|
||||
// (0x80086610, its only caller) and drops the ones outside.
|
||||
// 2. ClipInfoMgr::Update tests every ClipInfo (karts, objects, items) against
|
||||
// the per-screen ClipScreenInfo that ClipInfoMgr::UpdateScreenInfo
|
||||
// (0x8078707C) derives from the camera: a draw distance, the area groups
|
||||
// and six side-plane normals. Models outside a plane by more than their
|
||||
// radius are flagged clipped and ModelDirector hides them.
|
||||
//
|
||||
// mkw_vr_culling.cpp replaces both functions natively with faithful
|
||||
// reimplementations (the translator drops the translated body of a natively
|
||||
// registered address, so there is no original left to fall through to). With
|
||||
// culling off, the frustum test reports every box as partially inside and the
|
||||
// screen info carries zero plane normals, which no model can be beyond. The
|
||||
// draw-distance and area-group clipping stay as the game decides them: they do
|
||||
// not depend on where the player looks.
|
||||
//
|
||||
// The pure parts live here so runtime/tests/vr_culling_tests.cpp can check
|
||||
// them without guest memory. Every offset is specific to PAL RMCP01.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
// nw4r::math::AABB: minimum corner, then maximum corner.
|
||||
struct CullingAabb {
|
||||
float min[3];
|
||||
float max[3];
|
||||
};
|
||||
|
||||
// One nw4r::math::PLANE as FRUSTUM stores its six: an outward normal and a
|
||||
// distance, so a point is outside when dot(normal, point) + distance > 0.
|
||||
struct CullingPlane {
|
||||
float normal[3];
|
||||
float distance;
|
||||
};
|
||||
|
||||
// The two parts of nw4r::math::FRUSTUM that IntersectAABB_Ex reads: the
|
||||
// frustum's own bounding box (+0x78) and its six planes (+0x90, 16 bytes each).
|
||||
struct CullingFrustum {
|
||||
CullingAabb box;
|
||||
CullingPlane planes[6];
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kFrustumBoxOffset = 0x78u;
|
||||
inline constexpr uint32_t kFrustumPlanesOffset = 0x90u;
|
||||
inline constexpr uint32_t kFrustumPlaneStride = 0x10u;
|
||||
inline constexpr size_t kFrustumPlaneCount = 6;
|
||||
|
||||
// IntersectAABB_Ex's return value; ScnObjGather::Add keeps the object for
|
||||
// anything but Outside.
|
||||
enum class FrustumAabbResult : int32_t {
|
||||
Outside = 0,
|
||||
Inside = 1,
|
||||
Partial = 2,
|
||||
};
|
||||
|
||||
// The PowerPC compares behind IntersectAABB_Ex's branches (fcmpo). Each is
|
||||
// true only for an ordered result, so a NaN operand makes every one of them
|
||||
// false, and whether that rejects or keeps a box depends on which way the
|
||||
// original branches. The NaN test works on the bits, so it holds even in a
|
||||
// translation unit built with -ffast-math, where a plain float compare may
|
||||
// assume NaN never occurs.
|
||||
inline bool CullingIsNan(float value) noexcept {
|
||||
uint32_t bits;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
return (bits & 0x7FFFFFFFu) > 0x7F800000u;
|
||||
}
|
||||
|
||||
inline bool CullingOrderedGreater(float a, float b) noexcept {
|
||||
return !CullingIsNan(a) && !CullingIsNan(b) && a > b;
|
||||
}
|
||||
|
||||
inline bool CullingOrderedLessOrEqual(float a, float b) noexcept {
|
||||
return !CullingIsNan(a) && !CullingIsNan(b) && a <= b;
|
||||
}
|
||||
|
||||
inline bool CullingOrderedGreaterOrEqual(float a, float b) noexcept {
|
||||
return !CullingIsNan(a) && !CullingIsNan(b) && a >= b;
|
||||
}
|
||||
|
||||
#if defined(__clang__)
|
||||
// The runtime's unity batches build with -ffast-math. mkw_vr_culling.cpp, the
|
||||
// only caller, is built with the translated code's precise options instead
|
||||
// (cmake/PublicProducts.cmake); these pragmas keep the definition's rounding
|
||||
// exact wherever else the header is compiled.
|
||||
#pragma float_control(push)
|
||||
#pragma float_control(precise, on)
|
||||
#endif
|
||||
|
||||
// nw4r::math::FRUSTUM::IntersectAABB_Ex, operation for operation. Every
|
||||
// arithmetic step is a separate single-precision statement, and the one fused
|
||||
// multiply-add (ps_madd, which the translated code runs as one fused
|
||||
// single-precision FMA per lane) is an explicit fmaf, so the result is
|
||||
// bit-identical to the translated original whatever the host's contraction
|
||||
// setting. Each test names the branch the original takes.
|
||||
inline FrustumAabbResult FrustumIntersectAabb(const CullingFrustum& frustum,
|
||||
const CullingAabb& aabb) noexcept {
|
||||
#if defined(__clang__)
|
||||
#pragma clang fp contract(off)
|
||||
#endif
|
||||
const CullingAabb& box = frustum.box;
|
||||
// The frustum's bounding box first: bgt rejects, except for the last
|
||||
// compare, where ble keeps (so a NaN there rejects).
|
||||
for (size_t axis = 0; axis < 3; ++axis) {
|
||||
if (CullingOrderedGreater(aabb.min[axis], box.max[axis])) {
|
||||
return FrustumAabbResult::Outside;
|
||||
}
|
||||
if (axis < 2) {
|
||||
if (CullingOrderedGreater(box.min[axis], aabb.max[axis])) {
|
||||
return FrustumAabbResult::Outside;
|
||||
}
|
||||
} else if (!CullingOrderedLessOrEqual(box.min[axis], aabb.max[axis])) {
|
||||
return FrustumAabbResult::Outside;
|
||||
}
|
||||
}
|
||||
FrustumAabbResult result = FrustumAabbResult::Inside;
|
||||
for (size_t p = 0; p < kFrustumPlaneCount; ++p) {
|
||||
const CullingPlane& plane = frustum.planes[p];
|
||||
// The corner the normal points away from (the box's least value along
|
||||
// the plane) and the corner it points at (its greatest). A NaN normal
|
||||
// component fails the ordered >= and takes the second choice.
|
||||
float least[3];
|
||||
float greatest[3];
|
||||
for (size_t axis = 0; axis < 3; ++axis) {
|
||||
const bool non_negative = CullingOrderedGreaterOrEqual(plane.normal[axis], 0.0f);
|
||||
least[axis] = non_negative ? aabb.min[axis] : aabb.max[axis];
|
||||
greatest[axis] = non_negative ? aabb.max[axis] : aabb.min[axis];
|
||||
}
|
||||
// ps_mul (y, z), ps_madd (x onto y), ps_sum0 (+ z), fadds (+ distance).
|
||||
const auto signed_distance = [&plane](const float corner[3]) noexcept {
|
||||
const float yy = plane.normal[1] * corner[1];
|
||||
const float zz = plane.normal[2] * corner[2];
|
||||
const float xy = std::fmaf(plane.normal[0], corner[0], yy);
|
||||
const float sum = xy + zz;
|
||||
return plane.distance + sum;
|
||||
};
|
||||
// Both branches are ble over "keep going": anything but an ordered
|
||||
// <= 0, NaN included, rejects the box or marks it partial.
|
||||
if (!CullingOrderedLessOrEqual(signed_distance(least), 0.0f)) {
|
||||
return FrustumAabbResult::Outside;
|
||||
}
|
||||
if (!CullingOrderedLessOrEqual(signed_distance(greatest), 0.0f)) {
|
||||
result = FrustumAabbResult::Partial;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#if defined(__clang__)
|
||||
#pragma float_control(pop)
|
||||
#endif
|
||||
|
||||
// ClipScreenInfo, 0x60 bytes per screen, as ClipInfoMgr::UpdateScreenInfo
|
||||
// fills it and ClipInfoMgr::Update reads it.
|
||||
inline constexpr uint32_t kClipScreenInfoBytes = 0x60u;
|
||||
// The camera position (+0x00) and the camera's own distance value (+0x0C).
|
||||
inline constexpr uint32_t kClipScreenCameraOffset = 0x00u;
|
||||
inline constexpr uint32_t kClipScreenDistanceOffset = 0x0Cu;
|
||||
// Six side-plane normals: left and right, their widened copies, top and
|
||||
// bottom, six vectors from +0x10 to +0x57.
|
||||
inline constexpr uint32_t kClipScreenPlanesOffset = 0x10u;
|
||||
inline constexpr uint32_t kClipScreenPlanesBytes = 0x48u;
|
||||
// The squared draw-distance scale (+0x58) and the area 8/9 group bits (+0x5C).
|
||||
inline constexpr uint32_t kClipScreenDrawScaleOffset = 0x58u;
|
||||
inline constexpr uint32_t kClipScreenAreaGroupsOffset = 0x5Cu;
|
||||
|
||||
// Whether the game's culling is in force. Off only while VR is enabled,
|
||||
// vr.object_culling is false and the race view is not Flat screen; the
|
||||
// natives read it on every call.
|
||||
void MkwVRSetObjectCulling(bool enabled) noexcept;
|
||||
bool MkwVRObjectCullingEnabled() noexcept;
|
||||
|
||||
// Applies vr.object_culling for the given VR state, and remembers that state
|
||||
// so the settings overlay can re-apply a changed value or race view.
|
||||
void MkwVRObjectCullingApplyConfiguredSettings(bool vr_enabled) noexcept;
|
||||
void MkwVRObjectCullingApplyConfiguredSettings() noexcept;
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/camera_toggle.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "vr/openxr_diagnostics.h"
|
||||
@@ -167,6 +168,7 @@ int g_vrFirstPersonSeat = RuntimeConfigFile::VrFirstPersonSeat() == "custom" ? 1
|
||||
float g_vrCockpitUnitsPerMeter = RuntimeConfigFile::VrCockpitUnitsPerMeter();
|
||||
bool g_vrSteeringWheel = RuntimeConfigFile::VrSteeringWheel();
|
||||
bool g_vrNativeSteeringWheel = RuntimeConfigFile::VrNativeSteeringWheel();
|
||||
bool g_vrObjectCulling = RuntimeConfigFile::VrObjectCulling();
|
||||
bool g_vrHandSteering = RuntimeConfigFile::VrHandSteering();
|
||||
mkw::vr::WheelTuning g_vrWheelTuning = RuntimeConfigFile::VrWheelTuning();
|
||||
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters();
|
||||
@@ -1410,6 +1412,7 @@ void DrawVrSettings() {
|
||||
mkw::vr::MkwVRPolicySetImmersiveRaces(!g_vrFlatScreen);
|
||||
mkw::vr::OpenXRSetImmersiveWindow(view == RuntimeConfigFile::VrRaceView::ImmersiveWindow);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
mkw::vr::MkwVRObjectCullingApplyConfiguredSettings();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
@@ -1724,6 +1727,19 @@ void DrawVrCameraSettings() {
|
||||
}
|
||||
DrawVrSteeringWheelSettings();
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Object culling");
|
||||
if (ImGui::Checkbox("Hide what the game camera cannot see", &g_vrObjectCulling)) {
|
||||
RuntimeConfigFile::SetVrObjectCulling(g_vrObjectCulling);
|
||||
mkw::vr::MkwVRObjectCullingApplyConfiguredSettings();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"The game's own culling: karts, characters and course objects outside its chase "
|
||||
"camera's view are not drawn, so a head turn or a look over the shoulder finds them "
|
||||
"missing. Off draws them anyway, at some GPU cost. The draw distance is unchanged, "
|
||||
"and the Flat screen race view always keeps the game's culling.");
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Reset first-person defaults")) {
|
||||
g_vrFirstPersonSeat = 0;
|
||||
g_vrCockpitUnitsPerMeter = RuntimeConfigFile::kVrCockpitUnitsPerMeterDefault;
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Native replacements for the two Mario Kart functions that hide objects the
|
||||
// game camera cannot see, with the VR switch that turns that hiding off. See
|
||||
// vr/mkw_vr_culling.h for the design. Both are faithful reimplementations of
|
||||
// the PAL RMCP01 code: with culling on (the default) they compute exactly what
|
||||
// the translated originals did.
|
||||
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
|
||||
#include "hle_stubs.h"
|
||||
#include "isa/ppc_isa_context.h"
|
||||
#include "memory.h"
|
||||
#include "memory_access.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#if defined(__clang__)
|
||||
// The reimplementations below must round like the PowerPC originals (discrete
|
||||
// single-precision operations, explicit fused ones only where the game fuses)
|
||||
// and keep NaN compares unordered. cmake/PublicProducts.cmake builds this file
|
||||
// on its own with the translated code's -fno-fast-math -ffp-contract=off; the
|
||||
// pragmas say the same in the source.
|
||||
#pragma float_control(push)
|
||||
#pragma float_control(precise, on)
|
||||
#endif
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<bool> g_object_culling{true};
|
||||
std::atomic<bool> g_vr_enabled{false};
|
||||
std::atomic<bool> g_logged{false};
|
||||
|
||||
} // namespace
|
||||
|
||||
void MkwVRSetObjectCulling(bool enabled) noexcept {
|
||||
const bool previous = g_object_culling.exchange(enabled, std::memory_order_relaxed);
|
||||
if (previous != enabled || !g_logged.exchange(true, std::memory_order_relaxed)) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "[vr] object culling "
|
||||
<< (enabled ? "on (the game's own)" : "off (objects outside the game camera are drawn)")
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
bool MkwVRObjectCullingEnabled() noexcept {
|
||||
return g_object_culling.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void MkwVRObjectCullingApplyConfiguredSettings(bool vr_enabled) noexcept {
|
||||
g_vr_enabled.store(vr_enabled, std::memory_order_relaxed);
|
||||
// The setting lives under [vr]: a desktop session keeps the game's culling,
|
||||
// and so does the Flat screen race view, which shows the game camera's own
|
||||
// view, where everything culled is off screen anyway.
|
||||
MkwVRSetObjectCulling(!vr_enabled || RuntimeConfigFile::VrObjectCulling() ||
|
||||
RuntimeConfigFile::VrFlatScreen());
|
||||
}
|
||||
|
||||
void MkwVRObjectCullingApplyConfiguredSettings() noexcept {
|
||||
MkwVRObjectCullingApplyConfiguredSettings(g_vr_enabled.load(std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
} // namespace mkw::vr
|
||||
|
||||
namespace {
|
||||
|
||||
using mkw::vr::CullingAabb;
|
||||
using mkw::vr::CullingFrustum;
|
||||
using mkw::vr::FrustumAabbResult;
|
||||
|
||||
void ReadVec3(uint32_t address, float out[3]) noexcept {
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
out[i] = MemoryInline::FlatReadFloat32(address + i * 4u);
|
||||
}
|
||||
}
|
||||
|
||||
// The translated code loads these singles straight from guest memory, so the
|
||||
// unchecked flat reads it uses are the right tool here too: this runs once per
|
||||
// scene object per frame.
|
||||
void ReadFrustum(uint32_t frustum, CullingFrustum& out) noexcept {
|
||||
ReadVec3(frustum + mkw::vr::kFrustumBoxOffset, out.box.min);
|
||||
ReadVec3(frustum + mkw::vr::kFrustumBoxOffset + 12u, out.box.max);
|
||||
for (uint32_t p = 0; p < mkw::vr::kFrustumPlaneCount; ++p) {
|
||||
const uint32_t plane = frustum + mkw::vr::kFrustumPlanesOffset + p * mkw::vr::kFrustumPlaneStride;
|
||||
ReadVec3(plane, out.planes[p].normal);
|
||||
out.planes[p].distance = MemoryInline::FlatReadFloat32(plane + 12u);
|
||||
}
|
||||
}
|
||||
|
||||
// nw4r::math::FRUSTUM::IntersectAABB_Ex (0x80086610). Its one direct caller is
|
||||
// nw4r::g3d::ScnObjGather::Add, with the object's own bounding box.
|
||||
int32_t Nw4rFrustumIntersectAabbEx(uint32_t frustum, uint32_t aabb) {
|
||||
if (!mkw::vr::MkwVRObjectCullingEnabled()) {
|
||||
// Partially inside: gathered and drawn, like a box straddling a plane.
|
||||
return static_cast<int32_t>(FrustumAabbResult::Partial);
|
||||
}
|
||||
CullingFrustum f;
|
||||
ReadFrustum(frustum, f);
|
||||
CullingAabb box;
|
||||
ReadVec3(aabb, box.min);
|
||||
ReadVec3(aabb + 12u, box.max);
|
||||
return static_cast<int32_t>(mkw::vr::FrustumIntersectAabb(f, box));
|
||||
}
|
||||
|
||||
// Guest functions UpdateScreenInfo calls, unchanged translated code.
|
||||
constexpr uint32_t kPSMTXInverse = 0x80199FC8u;
|
||||
constexpr uint32_t kNw4rSinCosFIdx = 0x800851E0u;
|
||||
constexpr uint32_t kNw4rVec3TransformNormal = 0x80085AB0u;
|
||||
constexpr uint32_t kClipInfoMgrNormalizeVector = 0x807872C0u;
|
||||
constexpr uint32_t kClipInfoMgrWidenPlane = 0x807DEBCCu;
|
||||
constexpr uint32_t kClipInfoMgrGetArea8And9GroupIDs = 0x80786FC0u;
|
||||
// The four float constants UpdateScreenInfo reads (its r30 table) and the
|
||||
// reference vector it hands the plane-widening helper (its r31).
|
||||
constexpr uint32_t kClipConstants = 0x808A4808u;
|
||||
constexpr uint32_t kClipReferenceVector = 0x802A4130u;
|
||||
// The original's stack frame, laid out as it uses it.
|
||||
constexpr uint32_t kFrameBytes = 0xA0u;
|
||||
constexpr uint32_t kFrameCos = 0x08u;
|
||||
constexpr uint32_t kFrameSin = 0x0Cu;
|
||||
constexpr uint32_t kFrameVecA = 0x10u;
|
||||
constexpr uint32_t kFrameVecB = 0x1Cu;
|
||||
constexpr uint32_t kFrameVecC = 0x28u;
|
||||
constexpr uint32_t kFrameForward = 0x34u;
|
||||
constexpr uint32_t kFrameInverse = 0x40u;
|
||||
|
||||
float SingleMul(float a, float b) noexcept {
|
||||
#if defined(__clang__)
|
||||
#pragma clang fp contract(off)
|
||||
#endif
|
||||
return a * b;
|
||||
}
|
||||
|
||||
void CopyVec3(uint32_t from, uint32_t to) noexcept {
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
MemoryInline::FlatWriteRam32(to + i * 4u, MemoryInline::FlatRead32(from + i * 4u));
|
||||
}
|
||||
}
|
||||
|
||||
// MTX::PSVECCrossProduct (0x8019ACCC), which the original inlines: paired
|
||||
// single multiply-subtracts, so each component is one fused operation.
|
||||
void CrossProduct(uint32_t a_addr, uint32_t b_addr, uint32_t out_addr) noexcept {
|
||||
#if defined(__clang__)
|
||||
#pragma clang fp contract(off)
|
||||
#endif
|
||||
float a[3];
|
||||
float b[3];
|
||||
ReadVec3(a_addr, a);
|
||||
ReadVec3(b_addr, b);
|
||||
const float x_sub = SingleMul(b[1], a[2]);
|
||||
const float x = std::fmaf(a[1], b[2], -x_sub);
|
||||
const float y_sub = SingleMul(b[0], a[2]);
|
||||
const float y = -std::fmaf(a[0], b[2], -y_sub);
|
||||
const float z_sub = SingleMul(b[1], a[0]);
|
||||
const float z = -std::fmaf(a[1], b[0], -z_sub);
|
||||
MemoryInline::FlatWriteFloat32(out_addr, x);
|
||||
MemoryInline::FlatWriteFloat32(out_addr + 4u, y);
|
||||
MemoryInline::FlatWriteFloat32(out_addr + 8u, z);
|
||||
}
|
||||
|
||||
// ClipInfoMgr::UpdateScreenInfo (0x8078707C): fills one screen's
|
||||
// ClipScreenInfo from its camera. The matrix, trigonometry, normalisation and
|
||||
// plane-widening steps run the game's own translated code; the few
|
||||
// single-precision operations in between mirror the original instruction by
|
||||
// instruction. With culling off the six plane normals are zeroed afterwards,
|
||||
// so ClipInfoMgr::Update finds nothing beyond a plane.
|
||||
void ClipInfoMgrUpdateScreenInfo(uint32_t screen, uint32_t camera) {
|
||||
#if defined(__clang__)
|
||||
#pragma clang fp contract(off)
|
||||
#endif
|
||||
CpuContext* ctx = CurrentCpuContext();
|
||||
// The callees save and restore LR themselves; keeping the entry value
|
||||
// makes this native leave it exactly as the original's epilogue did.
|
||||
const uint32_t caller_lr = ctx->lr;
|
||||
const uint32_t caller_sp = ctx->gpr[1];
|
||||
const uint32_t sp = caller_sp - kFrameBytes;
|
||||
MemoryInline::FlatWriteRam32(sp, caller_sp);
|
||||
ctx->gpr[1] = sp;
|
||||
|
||||
const auto call = [ctx](uint32_t target, uint32_t r3, uint32_t r4, uint32_t r5 = 0, uint32_t r6 = 0) {
|
||||
ctx->gpr[3] = r3;
|
||||
ctx->gpr[4] = r4;
|
||||
ctx->gpr[5] = r5;
|
||||
ctx->gpr[6] = r6;
|
||||
InvokeIndirectCpu(target, ctx);
|
||||
};
|
||||
const auto read = [](uint32_t address) { return MemoryInline::FlatReadFloat32(address); };
|
||||
const auto write = [](uint32_t address, float value) { MemoryInline::FlatWriteFloat32(address, value); };
|
||||
|
||||
// The camera's view matrix, inverted into the frame.
|
||||
const uint32_t view_matrix = MemoryInline::FlatRead32(camera + 0x6Cu) + 4u;
|
||||
const uint32_t inverse = sp + kFrameInverse;
|
||||
call(kPSMTXInverse, view_matrix, inverse);
|
||||
// Camera position: the inverse's translation column.
|
||||
write(screen + 0x00u, read(inverse + 0x0Cu));
|
||||
write(screen + 0x04u, read(inverse + 0x1Cu));
|
||||
write(screen + 0x08u, read(inverse + 0x2Cu));
|
||||
write(screen + 0x0Cu, read(camera + 0x18u));
|
||||
|
||||
const float aspect = static_cast<float>(static_cast<double>(read(camera + 0x08u)) /
|
||||
static_cast<double>(read(camera + 0x0Cu)));
|
||||
const float fov = read(camera + 0x10u);
|
||||
const float k0 = read(kClipConstants + 0x0u);
|
||||
const float k1 = read(kClipConstants + 0x4u);
|
||||
const float k2 = read(kClipConstants + 0x8u);
|
||||
const float k3 = read(kClipConstants + 0xCu);
|
||||
|
||||
// nw4r::math::SinCosFIdx(&sin, &cos, k1 * fov)
|
||||
ctx->fpr[1].d = static_cast<double>(SingleMul(k1, fov));
|
||||
call(kNw4rSinCosFIdx, sp + kFrameSin, sp + kFrameCos);
|
||||
|
||||
// Draw-distance scale: (k3 * min(fov, k2))^2. The original keeps fov only
|
||||
// on an ordered fov <= k2 (fcmpo, ble), so a NaN fov takes k2.
|
||||
const float clamped_fov = mkw::vr::CullingOrderedLessOrEqual(fov, k2) ? fov : k2;
|
||||
const float scaled = SingleMul(k3, clamped_fov);
|
||||
write(screen + mkw::vr::kClipScreenDrawScaleOffset, SingleMul(scaled, scaled));
|
||||
|
||||
const float sin = read(sp + kFrameSin);
|
||||
const float cos = read(sp + kFrameCos);
|
||||
const float sin_aspect = SingleMul(sin, aspect);
|
||||
|
||||
// Left plane: (-cos, k0, sin * aspect) turned into world space, normalised.
|
||||
const uint32_t plane_left = screen + 0x10u;
|
||||
write(plane_left + 0u, -cos);
|
||||
write(plane_left + 4u, k0);
|
||||
write(plane_left + 8u, sin_aspect);
|
||||
call(kNw4rVec3TransformNormal, plane_left, inverse, plane_left);
|
||||
CopyVec3(plane_left, sp + kFrameVecC);
|
||||
call(kClipInfoMgrNormalizeVector, plane_left, sp + kFrameVecC);
|
||||
|
||||
// Right plane: (cos, k0, sin * aspect).
|
||||
const uint32_t plane_right = screen + 0x28u;
|
||||
write(plane_right + 0u, cos);
|
||||
write(plane_right + 4u, k0);
|
||||
write(plane_right + 8u, SingleMul(sin, aspect));
|
||||
call(kNw4rVec3TransformNormal, plane_right, inverse, plane_right);
|
||||
CopyVec3(plane_right, sp + kFrameVecB);
|
||||
call(kClipInfoMgrNormalizeVector, plane_right, sp + kFrameVecB);
|
||||
|
||||
// The view direction from the two side normals, then each side plane
|
||||
// widened around it into the second pair (+0x1C and +0x34).
|
||||
const uint32_t forward = sp + kFrameForward;
|
||||
CrossProduct(plane_left, plane_right, forward);
|
||||
CopyVec3(forward, sp + kFrameVecA);
|
||||
call(kClipInfoMgrNormalizeVector, forward, sp + kFrameVecA);
|
||||
call(kClipInfoMgrWidenPlane, forward, kClipReferenceVector, plane_left, screen + 0x1Cu);
|
||||
call(kClipInfoMgrWidenPlane, forward, kClipReferenceVector, plane_right, screen + 0x34u);
|
||||
|
||||
// Top plane (k0, cos, sin) and bottom plane (k0, -cos, sin), turned into
|
||||
// world space.
|
||||
const uint32_t plane_top = screen + 0x40u;
|
||||
write(plane_top + 0u, k0);
|
||||
write(plane_top + 4u, cos);
|
||||
write(plane_top + 8u, sin);
|
||||
call(kNw4rVec3TransformNormal, plane_top, inverse, plane_top);
|
||||
const uint32_t plane_bottom = screen + 0x4Cu;
|
||||
write(plane_bottom + 0u, k0);
|
||||
write(plane_bottom + 4u, -cos);
|
||||
write(plane_bottom + 8u, sin);
|
||||
call(kNw4rVec3TransformNormal, plane_bottom, inverse, plane_bottom);
|
||||
|
||||
// Area type 8 and 9 group bits for this screen's camera.
|
||||
call(kClipInfoMgrGetArea8And9GroupIDs, screen, 9u);
|
||||
MemoryInline::FlatWriteRam16(screen + mkw::vr::kClipScreenAreaGroupsOffset,
|
||||
static_cast<uint16_t>(ctx->gpr[3] & 0xFFFFu));
|
||||
|
||||
ctx->gpr[1] = caller_sp;
|
||||
ctx->lr = caller_lr;
|
||||
|
||||
if (!mkw::vr::MkwVRObjectCullingEnabled()) {
|
||||
for (uint32_t offset = 0; offset < mkw::vr::kClipScreenPlanesBytes; offset += 4u) {
|
||||
MemoryInline::FlatWriteRam32(screen + mkw::vr::kClipScreenPlanesOffset + offset, 0u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PPC_NATIVE_OVERRIDE(80086610, Nw4rFrustumIntersectAabbEx, int32_t, (uint32_t frustum, uint32_t aabb), (frustum, aabb));
|
||||
PPC_NATIVE_OVERRIDE_VOID(8078707C, ClipInfoMgrUpdateScreenInfo, (uint32_t screen, uint32_t camera), (screen, camera));
|
||||
|
||||
#if defined(__clang__)
|
||||
#pragma float_control(pop)
|
||||
#endif
|
||||
@@ -187,6 +187,12 @@ constexpr MkwVRHookPoint kHookPoints[] = {
|
||||
"Future culling-frustum expansion point for head movement beyond the base camera."},
|
||||
{0x80228180u, "EGG::Frustum::CopyToG3D", MkwVRHookCapability::Culling,
|
||||
"Observe the frustum handed to NW4R without guessing EGG::Frustum fields."},
|
||||
{0x80086610u, "nw4r::math::FRUSTUM::IntersectAABB_Ex", MkwVRHookCapability::Culling,
|
||||
"Replaced natively (mkw_vr_culling.cpp): every box counts as partially inside while "
|
||||
"vr.object_culling is off."},
|
||||
{0x8078707Cu, "ClipInfoMgr::UpdateScreenInfo", MkwVRHookCapability::Culling,
|
||||
"Replaced natively (mkw_vr_culling.cpp): the screen's side-plane normals are zeroed "
|
||||
"while vr.object_culling is off."},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "runtime_config.h"
|
||||
#include "gx_thread.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "vr/mkw_vr_instrumentation.h"
|
||||
@@ -75,6 +76,7 @@ void ConfigurePolicy(bool enabled) noexcept {
|
||||
MkwVRPolicyConfigure(config);
|
||||
MkwVRInstrumentationInitialize();
|
||||
MkwVRFirstPersonApplyConfiguredSettings();
|
||||
MkwVRObjectCullingApplyConfiguredSettings(enabled);
|
||||
}
|
||||
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// The frustum test behind Mario Kart's scene culling (vr/mkw_vr_culling.h):
|
||||
// the native replacement of nw4r::math::FRUSTUM::IntersectAABB_Ex has to
|
||||
// classify boxes exactly as the PowerPC original, since with vr.object_culling
|
||||
// on it runs for every scene object of every frame.
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
using mkw::vr::CullingAabb;
|
||||
using mkw::vr::CullingFrustum;
|
||||
using mkw::vr::CullingPlane;
|
||||
using mkw::vr::FrustumAabbResult;
|
||||
using mkw::vr::FrustumIntersectAabb;
|
||||
|
||||
namespace {
|
||||
|
||||
// nw4r's return codes, which ScnObjGather::Add tests by value.
|
||||
static_assert(static_cast<int32_t>(FrustumAabbResult::Outside) == 0, "");
|
||||
static_assert(static_cast<int32_t>(FrustumAabbResult::Inside) == 1, "");
|
||||
static_assert(static_cast<int32_t>(FrustumAabbResult::Partial) == 2, "");
|
||||
|
||||
// The layout the native reads from guest memory.
|
||||
static_assert(mkw::vr::kFrustumPlanesOffset - mkw::vr::kFrustumBoxOffset == sizeof(CullingAabb), "");
|
||||
static_assert(sizeof(CullingPlane) == mkw::vr::kFrustumPlaneStride, "");
|
||||
static_assert(mkw::vr::kClipScreenPlanesOffset + mkw::vr::kClipScreenPlanesBytes ==
|
||||
mkw::vr::kClipScreenDrawScaleOffset,
|
||||
"six plane normals fill the screen info up to the draw scale");
|
||||
|
||||
// NaN and infinity from their bits: a literal one is undefined behaviour when
|
||||
// the test is built with the runtime's -ffast-math, which it is worth trying.
|
||||
float FloatFromBits(uint32_t bits) {
|
||||
float value;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
CullingAabb Box(float x0, float y0, float z0, float x1, float y1, float z1) {
|
||||
return CullingAabb{{x0, y0, z0}, {x1, y1, z1}};
|
||||
}
|
||||
|
||||
// An axis-aligned "frustum": six outward planes at +-half on each axis, and
|
||||
// the frustum's own bounding box at +-box_half.
|
||||
CullingFrustum AxisFrustum(float half, float box_half) {
|
||||
CullingFrustum f{};
|
||||
f.box = Box(-box_half, -box_half, -box_half, box_half, box_half, box_half);
|
||||
size_t p = 0;
|
||||
for (size_t axis = 0; axis < 3; ++axis) {
|
||||
for (float sign : {1.0f, -1.0f}) {
|
||||
CullingPlane& plane = f.planes[p++];
|
||||
plane.normal[axis] = sign;
|
||||
plane.distance = -half;
|
||||
}
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
int failures = 0;
|
||||
const auto check = [&](bool condition, const char* message) {
|
||||
if (!condition) { std::cerr << "FAILED: " << message << '\n'; ++failures; }
|
||||
};
|
||||
const auto expect = [&](FrustumAabbResult got, FrustumAabbResult want, const char* message) {
|
||||
if (got != want) {
|
||||
std::cerr << "FAILED: " << message << " (got " << static_cast<int>(got) << ", want "
|
||||
<< static_cast<int>(want) << ")\n";
|
||||
++failures;
|
||||
}
|
||||
};
|
||||
|
||||
const CullingFrustum f = AxisFrustum(10.0f, 10.0f);
|
||||
expect(FrustumIntersectAabb(f, Box(-1, -1, -1, 1, 1, 1)), FrustumAabbResult::Inside,
|
||||
"a box within every plane is inside");
|
||||
expect(FrustumIntersectAabb(f, Box(5, -1, -1, 15, 1, 1)), FrustumAabbResult::Partial,
|
||||
"a box across the +x plane is partial");
|
||||
expect(FrustumIntersectAabb(f, Box(-15, -1, -1, -5, 1, 1)), FrustumAabbResult::Partial,
|
||||
"a box across the -x plane is partial (the negative normal picks the other corner)");
|
||||
expect(FrustumIntersectAabb(f, Box(11, -1, -1, 12, 1, 1)), FrustumAabbResult::Outside,
|
||||
"a box past the frustum's bounding box is rejected");
|
||||
expect(FrustumIntersectAabb(f, Box(-1, -1, -12, 1, 1, -11)), FrustumAabbResult::Outside,
|
||||
"the bounding-box rejection covers z as well");
|
||||
expect(FrustumIntersectAabb(f, Box(-10, -10, -10, 10, 10, 10)), FrustumAabbResult::Inside,
|
||||
"touching every plane exactly is still inside (outside needs strictly greater)");
|
||||
|
||||
// Planes tighter than the frustum's bounding box: only the plane test can
|
||||
// reject.
|
||||
const CullingFrustum tight = AxisFrustum(10.0f, 100.0f);
|
||||
expect(FrustumIntersectAabb(tight, Box(20, -1, -1, 30, 1, 1)), FrustumAabbResult::Outside,
|
||||
"a box beyond the +x plane is outside");
|
||||
expect(FrustumIntersectAabb(tight, Box(-1, -30, -1, 1, -20, 1)), FrustumAabbResult::Outside,
|
||||
"a box beyond the -y plane is outside");
|
||||
expect(FrustumIntersectAabb(tight, Box(-50, -50, -50, 50, 50, 50)), FrustumAabbResult::Partial,
|
||||
"a box enclosing the whole frustum is partial");
|
||||
|
||||
// NaN follows each branch of the original. The plane tests keep going only
|
||||
// on an ordered "<= 0", so a NaN distance rejects the box.
|
||||
const float nan = FloatFromBits(0x7FC00000u);
|
||||
expect(FrustumIntersectAabb(f, Box(nan, -1, -1, nan, 1, 1)), FrustumAabbResult::Outside,
|
||||
"a NaN coordinate makes a plane distance NaN, which rejects");
|
||||
CullingFrustum nan_plane = f;
|
||||
nan_plane.planes[0].normal[0] = nan;
|
||||
expect(FrustumIntersectAabb(nan_plane, Box(-1, -1, -1, 1, 1, 1)), FrustumAabbResult::Outside,
|
||||
"a NaN normal gives a NaN distance, which rejects");
|
||||
// The bounding-box compares reject on an ordered ">" (bgt), except the last
|
||||
// one, which keeps on an ordered "<=" (ble). Planes that accept everything
|
||||
// isolate those compares.
|
||||
CullingFrustum open{};
|
||||
open.box = Box(-10, -10, -10, 10, 10, 10);
|
||||
for (CullingPlane& plane : open.planes) plane.distance = -1.0f;
|
||||
expect(FrustumIntersectAabb(open, Box(-1, -1, -1, 1, 1, 1)), FrustumAabbResult::Inside,
|
||||
"zero-normal planes accept a box inside the bounding box");
|
||||
open.box.min[0] = nan;
|
||||
expect(FrustumIntersectAabb(open, Box(-1, -1, -1, 1, 1, 1)), FrustumAabbResult::Inside,
|
||||
"a NaN frustum min.x fails bgt and keeps the box");
|
||||
open.box.min[0] = -10.0f;
|
||||
open.box.min[2] = nan;
|
||||
expect(FrustumIntersectAabb(open, Box(-1, -1, -1, 1, 1, 1)), FrustumAabbResult::Outside,
|
||||
"a NaN frustum min.z fails ble and rejects the box");
|
||||
open.box.min[2] = -10.0f;
|
||||
open.box.max[2] = nan;
|
||||
expect(FrustumIntersectAabb(open, Box(-1, -1, -1, 1, 1, 1)), FrustumAabbResult::Inside,
|
||||
"a NaN frustum max.z fails bgt and keeps the box");
|
||||
check(mkw::vr::CullingIsNan(nan) && !mkw::vr::CullingIsNan(1.0f) &&
|
||||
!mkw::vr::CullingIsNan(FloatFromBits(0x7F800000u)),
|
||||
"the bit-level NaN test");
|
||||
|
||||
// The plane distance is one fused multiply-add for x (ps_madd) after a
|
||||
// rounded product for y: choose values where fusing decides the sign.
|
||||
// nx * vx = 1 + 2^-22 + 2^-46 exactly, which rounds to 1 + 2^-22 on its
|
||||
// own; yy cancels that rounded value, so only a fused step keeps 2^-46.
|
||||
{
|
||||
const float one_plus = 1.0f + std::ldexp(1.0f, -23);
|
||||
CullingFrustum fused = AxisFrustum(1000.0f, 1000.0f);
|
||||
fused.planes[0].normal[0] = one_plus;
|
||||
fused.planes[0].normal[1] = 1.0f;
|
||||
fused.planes[0].normal[2] = 0.0f;
|
||||
fused.planes[0].distance = -std::ldexp(1.0f, -47);
|
||||
const float yy = -(1.0f + std::ldexp(1.0f, -22));
|
||||
expect(FrustumIntersectAabb(fused, Box(one_plus, yy, 0, one_plus, yy, 0)),
|
||||
FrustumAabbResult::Outside, "the x term is fused like ps_madd, so 2^-46 survives");
|
||||
fused.planes[0].distance = -std::ldexp(1.0f, -45);
|
||||
expect(FrustumIntersectAabb(fused, Box(one_plus, yy, 0, one_plus, yy, 0)),
|
||||
FrustumAabbResult::Inside, "and a larger distance still keeps the point inside");
|
||||
}
|
||||
check(mkw::vr::kFrustumPlaneCount == 6, "six frustum planes");
|
||||
|
||||
if (failures == 0) {
|
||||
std::cout << "vr_culling_tests: all passed\n";
|
||||
}
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user