Pan between ticks, and move animations at their own rate
CI / build (push) Successful in 27s

The point the camera looks at is its position plus an offset (setCameraFocus,
at each tick), so between ticks it now moves with the eased position: panning
moves the target the position eases to, and was still at 30 per second.

Between ticks, an animation is moved on at the rate it moved at the last step
(per tick: the client steps several ticks at once when it falls behind), not
only when it moved exactly a frame: animations played at other speeds were
posed at the tick, between smooth ones.

The engine's stats also log the longest time between frames.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-28 21:56:09 -07:00
1 parent d1437a3923
commit 4670a6a191
3 files changed
+48 -19

No files matched your search

+16 -5
View File
@@ -2,6 +2,7 @@
#include <windows.h> #include <windows.h>
#include <algorithm>
#include <bit> #include <bit>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
@@ -263,6 +264,15 @@ void Engine::beginRendering(imvu::PiRenderTarget* target)
{ {
QueryPerformanceCounter(&frameStart_); QueryPerformanceCounter(&frameStart_);
windowFrame_ = target == nullptr; windowFrame_ = target == nullptr;
if (windowFrame_) {
if (lastWindowFrame_ != 0) {
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
longestGap_ = std::max(longestGap_, static_cast<double>(frameStart_.QuadPart - lastWindowFrame_) /
static_cast<double>(frequency.QuadPart));
}
lastWindowFrame_ = frameStart_.QuadPart;
}
// Frames into our render targets are drawn by the renderer process only; the client's renderer doesn't know them. // Frames into our render targets are drawn by the renderer process only; the client's renderer doesn't know them.
ourTarget_ = ours(target) ? target : nullptr; ourTarget_ = ours(target) ? target : nullptr;
// Decided per frame: frames into the client renderer's render targets, and frames while the renderer process // Decided per frame: frames into the client renderer's render targets, and frames while the renderer process
@@ -324,11 +334,11 @@ void Engine::endRendering()
const Recorder::Counters sent = recorder_->takeCounters(); const Recorder::Counters sent = recorder_->takeCounters();
if (statsLogged_ < 6 || frames_ % 3000 == 0) { if (statsLogged_ < 6 || frames_ % 3000 == 0) {
++statsLogged_; ++statsLogged_;
log::info("engine: last 300 frames: {:.0f} per second; each {:.2f} ms beginRendering to endRendering, {:.2f} ms " log::info("engine: last 300 frames: {:.0f} per second, at most {:.1f} ms apart; each {:.2f} ms beginRendering to "
"in the client renderer's methods, {:.2f} ms sending, {:.0f} KB sent, {:.1f} buffer updates ({:.0f} " "endRendering, {:.2f} ms in the client renderer's methods, {:.2f} ms sending, {:.0f} KB sent, {:.1f} "
"KB); drawTriangles {} (and {} with buffers not ours), drawTrianglesFromMemory {} (and {} without " "buffer updates ({:.0f} KB); drawTriangles {} (and {} with buffers not ours), drawTrianglesFromMemory "
"indices), point sprites {}, lines {}, 2D projections {}", "{} (and {} without indices), point sprites {}, lines {}, 2D projections {}",
300 / seconds, frameSeconds_ * 1000 / 300, 300 / seconds, longestGap_ * 1000, frameSeconds_ * 1000 / 300,
static_cast<double>(innerTicks - innerTicksAtStats_) * 1000 / static_cast<double>(frequency.QuadPart) / 300, static_cast<double>(innerTicks - innerTicksAtStats_) * 1000 / static_cast<double>(frequency.QuadPart) / 300,
sent.sendSeconds * 1000 / 300, sent.sendSeconds * 1000 / 300,
static_cast<double>(sent.bytes) / 1024 / 300, sent.bufferUpdates / 300.0, static_cast<double>(sent.bytes) / 1024 / 300, sent.bufferUpdates / 300.0,
@@ -337,6 +347,7 @@ void Engine::endRendering()
} }
stats_ = {}; stats_ = {};
frameSeconds_ = 0; frameSeconds_ = 0;
longestGap_ = 0;
innerTicksAtStats_ = innerTicks; innerTicksAtStats_ = innerTicks;
} }
} }
+2
View File
@@ -91,6 +91,8 @@ private:
Stats stats_; Stats stats_;
LARGE_INTEGER statsStart_{}; // when the stats' 300 frames started LARGE_INTEGER statsStart_{}; // when the stats' 300 frames started
LARGE_INTEGER frameStart_{}; // when the current frame began LARGE_INTEGER frameStart_{}; // when the current frame began
std::int64_t lastWindowFrame_ = 0; // when the last window frame began
double longestGap_ = 0; // the stats' longest time between window frames' beginnings, in seconds
double frameSeconds_ = 0; // the stats' window frames' time, beginRendering to endRendering double frameSeconds_ = 0; // the stats' window frames' time, beginRendering to endRendering
bool windowFrame_ = false; // the current frame is drawn into the window bool windowFrame_ = false; // the current frame is drawn into the window
bool innerFrame_ = false; // the current frame is begun in the client's renderer bool innerFrame_ = false; // the current frame is begun in the client's renderer
+30 -14
View File
@@ -26,6 +26,8 @@ GetTransform originalGetCameraTransform = nullptr;
float cameraEasing = 0; // the share of the way to its target the camera moves at each tick float cameraEasing = 0; // the share of the way to its target the camera moves at each tick
std::atomic<std::int64_t> stepTime = 0; std::atomic<std::int64_t> stepTime = 0;
std::atomic<std::uint64_t> steps = 0; std::atomic<std::uint64_t> steps = 0;
// How many ticks the last step covered: the client steps a scene several ticks at once when it falls behind.
std::atomic<int> stepTicks = 1;
std::atomic<bool> stepHooked = false; std::atomic<bool> stepHooked = false;
std::atomic<bool> poseHooked = false; std::atomic<bool> poseHooked = false;
std::atomic<bool> cameraHooked = false; std::atomic<bool> cameraHooked = false;
@@ -50,10 +52,16 @@ void __fastcall step(void* self, void* /*edx*/)
{ {
LARGE_INTEGER now; LARGE_INTEGER now;
QueryPerformanceCounter(&now); QueryPerformanceCounter(&now);
if (now.QuadPart - stepTime.load() > frequency().QuadPart / 200) { const std::int64_t last = stepTime.load();
if (now.QuadPart - last > frequency().QuadPart / 200) {
++steps; ++steps;
if (last != 0 && millisecondsPerFrame != nullptr && *millisecondsPerFrame != 0) {
const double ticks = static_cast<double>(now.QuadPart - last) * 1000.0 /
static_cast<double>(frequency().QuadPart) / *millisecondsPerFrame;
stepTicks = std::clamp(static_cast<int>(ticks + 0.5), 1, 5);
} }
stepTime = now.QuadPart; stepTime = now.QuadPart;
}
originalStep(self); originalStep(self);
} }
@@ -76,8 +84,8 @@ T& field(void* object, std::size_t offset)
return *reinterpret_cast<T*>(static_cast<std::byte*>(object) + offset); return *reinterpret_cast<T*>(static_cast<std::byte*>(object) + offset);
} }
// Each effect's time at the last two ticks it was posed at, to tell an animation playing on (one frame a tick) from // Each effect's time at the last two ticks it was posed at, to tell an animation playing on (at any rate: the time it
// one held, looping back or just started (posed as it is). // moved per tick at the last step) from one held, looping back or just started (posed as it is).
struct EffectHistory { struct EffectHistory {
std::uint64_t step = 0; std::uint64_t step = 0;
float time = 0; float time = 0;
@@ -99,9 +107,9 @@ float tick_fraction()
} }
// Our ModelInstance::updateCalSkeleton, called where ModelInstance::render poses the model before drawing it: between // Our ModelInstance::updateCalSkeleton, called where ModelInstance::render poses the model before drawing it: between
// ticks, poses it at the time between them. Each playing animation that moved on a frame at the last tick is set // ticks, poses it at the time between them. Each playing animation that moved on at the last step is set ahead by the
// ahead by the fraction of a frame since, the skeleton posed (by IMVU's), and the times put back, with the skeleton // fraction of a tick since, at the rate it moved (up to 4 frames a tick), the skeleton posed (by IMVU's), and the
// marked to be posed again, so everything else sees the tick's pose. // times put back, with the skeleton marked to be posed again, so everything else sees the tick's pose.
void __fastcall updateCalSkeleton(void* model, void* /*edx*/) void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
{ {
const float fraction = tick_fraction(); const float fraction = tick_fraction();
@@ -136,13 +144,13 @@ void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
history.time = time; history.time = time;
history.step = tick; history.step = tick;
} }
const float moved = history.time - history.previous; const float perTick = (history.time - history.previous) / static_cast<float>(stepTicks.load());
if (history.previous < 0 || moved < frame * 0.5f || moved > frame * 1.5f) { if (history.previous < 0 || perTick <= frame * 0.01f || perTick > frame * 4) {
continue; // held, looped back, or just started: as it is continue; // held, looped back, or just started: as it is
} }
float& posedTime = field<float>(animation, animationTime); float& posedTime = field<float>(animation, animationTime);
saved[count++] = {&posedTime, posedTime}; saved[count++] = {&posedTime, posedTime};
posedTime = time + fraction * frame; posedTime = time + fraction * perTick;
} }
if (count == 0) { if (count == 0) {
originalUpdateCalSkeleton(model); originalUpdateCalSkeleton(model);
@@ -162,13 +170,16 @@ void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
} }
// The camera (SceneWindow.dll's CameraController::step, at each tick): each of these values moves a share k of the way // The camera (SceneWindow.dll's CameraController::step, at each tick): each of these values moves a share k of the way
// to its target (k is 0.4): the camera's position (+0x118, 3 floats, to +0xf0), and its orbit angles and zoom, in // to its target (k is 0.4): the camera's position (+0x118, 3 floats, to +0xf0; panning moves the target), and its
// degrees (+0x140, 3 floats, to +0x134; the difference taken between -180 and 180). // orbit angles and zoom, in degrees (+0x140, 3 floats, to +0x134; the difference taken between -180 and 180).
// A fraction f of a tick on, the same easing has moved it 1 - (1 - k)^f of the way: exactly, nothing guessed. // A fraction f of a tick on, the same easing has moved it 1 - (1 - k)^f of the way: exactly, nothing guessed. The point
// the camera looks at (+0x160, 3 floats) is the position plus an offset (setCameraFocus, at each tick), so it moves with
// the position.
constexpr std::size_t cameraPosition = 0x118; constexpr std::size_t cameraPosition = 0x118;
constexpr std::size_t cameraPositionTarget = 0xf0; constexpr std::size_t cameraPositionTarget = 0xf0;
constexpr std::size_t cameraAngles = 0x140; constexpr std::size_t cameraAngles = 0x140;
constexpr std::size_t cameraAnglesTarget = 0x134; constexpr std::size_t cameraAnglesTarget = 0x134;
constexpr std::size_t cameraFocus = 0x160;
// Nonzero: the camera shows a fixed view ("take"), not eased. // Nonzero: the camera shows a fixed view ("take"), not eased.
constexpr std::size_t cameraTake = 0xec; constexpr std::size_t cameraTake = 0xec;
@@ -191,11 +202,15 @@ void* __fastcall getCameraTransform(void* camera, void* /*edx*/, void* result)
return originalGetCameraTransform(camera, result); return originalGetCameraTransform(camera, result);
} }
const float moved = 1.0f - std::pow(1.0f - cameraEasing, fraction); const float moved = 1.0f - std::pow(1.0f - cameraEasing, fraction);
float saved[6]; float saved[9];
for (int i = 0; i < 3; ++i) { for (int i = 0; i < 3; ++i) {
float& position = field<float>(camera, cameraPosition + i * 4); float& position = field<float>(camera, cameraPosition + i * 4);
float& focus = field<float>(camera, cameraFocus + i * 4);
saved[i] = position; saved[i] = position;
position += (field<float>(camera, cameraPositionTarget + i * 4) - position) * moved; saved[6 + i] = focus;
const float step = (field<float>(camera, cameraPositionTarget + i * 4) - position) * moved;
position += step;
focus += step;
float& angle = field<float>(camera, cameraAngles + i * 4); float& angle = field<float>(camera, cameraAngles + i * 4);
saved[3 + i] = angle; saved[3 + i] = angle;
angle = degrees180(angle + degrees180(field<float>(camera, cameraAnglesTarget + i * 4) - angle) * moved); angle = degrees180(angle + degrees180(field<float>(camera, cameraAnglesTarget + i * 4) - angle) * moved);
@@ -204,6 +219,7 @@ void* __fastcall getCameraTransform(void* camera, void* /*edx*/, void* result)
for (int i = 0; i < 3; ++i) { for (int i = 0; i < 3; ++i) {
field<float>(camera, cameraPosition + i * 4) = saved[i]; field<float>(camera, cameraPosition + i * 4) = saved[i];
field<float>(camera, cameraAngles + i * 4) = saved[3 + i]; field<float>(camera, cameraAngles + i * 4) = saved[3 + i];
field<float>(camera, cameraFocus + i * 4) = saved[6 + i];
} }
return transform; return transform;
} }