-
Notifications
You must be signed in to change notification settings - Fork 18
Description
Introduction
Our current test cases handle frame diffing on certain variables (position/rotation and some of its derivatives), which is typically sufficient for measuring accuracy. However, when we need to address desyncs in these values, the process is often long and convoluted. Currently, the best way to debug external velocity desyncs is to bisect at arbitrary points and binary search down.
With the PyCore fork of Dolphin, we can consistently dereference the pointer chain [0x809c18f8, 0x20, 0x0, 0x24, 0x90, 0x4] and add 0x74 to get the pointer to EV (say &extVel). Then, we set up a memory breakpoint in the range [(uintptr_t)&extVel, (uintptr_t)(&extVel) + sizeof(extVel) - 1] and set up a Python script to react to the breakpoint being hit. We can use the PC to determine exact EV assignment addresses and use the LR to provide additional context if necessary.
With Kinoko, we are far more restricted with what we can do. We're not able to memory breakpoint that range consistently, but we can instead alias the underlying type with a watcher wrapper. As a quick five-minute example that still has limitations:
template <typename T>
class Watcher {
public:
template <typename... Args>
Watcher(Args &&...args) : m_obj(std::forward<Args>(args)...) {}
~Watcher() = default;
operator T &() {
// Track reads here
return m_obj;
}
Watcher &operator=(const T &rhs) {
// Track writes here
m_obj = rhs;
return *this;
}
private:
T m_obj;
};Problems
We are very heavily restricted with this approach, as there are some things we can't account for with the underlying types. Primitive types are very well tracked here, but we start running into problems with vectors and quaternions. As an example:
m_extVel += m_acceleration * dt; // not OK: operator+= cannot be resolved in Watcher, so writes aren't tracked
if (m_killExtVelY) {
m_extVel.y = std::min(0.0f, m_extVel.y); // not OK: direct member access is untrackable this way
}The first problem is solved with the introduction of operator+= to Watcher, but the second problem requires the following adjustment:
m_extVel = EGG::Vector3f(m_extVel.x, std::min(0.0f, m_extVel.y), m_extVel.z);
// alt: m_extVel = EGG::Vector3f(m_extVel).setY(std::min(0.0f, m_extVel.y));NOTE: This problem only happens on logged variables (meaning, Watcher<T> as opposed to T).
Implementation
In addition to m_obj, we may also want to create a watcher heap. This would enable us to create a log history with std::vector for a frame without the need to use std::cout.
Leveraging #390, we may want to introduce the following members:
private:
T m_obj;
+ fixed_string<16> m_name;
+ fixed_string<32> m_context;
+ fixed_string<256> m_workingBuffer;
+ std::vector<fixed_string<256>> m_logs;When we want to track external velocity changes in KartDynamics::calc, we can write something like m_extVel.setLogContext("KartDynamics::calc") to delineate these logs from other calls. We can also leverage a ScopeLock override for the Watcher class to allow for easier block-isolated contexts.
Questions
Q1: Is this an acceptable memory tradeoff? EGG::Vector3f x to Watcher<EGG::Vector3f> x means sizeof(x) becomes sizeof(T) + 0x138.
Q2: Is the lack of direct member access in logged non-primitives an acceptable tradeoff?