Files
dhkatzandClaude Opus 5.5 4670a6a191
CI / build (push) Successful in 27s
Pan between ticks, and move animations at their own rate
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]>
2026-10-28 21:56:09 -07:00

449 lines
17 KiB
C++

#include "engine.hpp"
#include <windows.h>
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <vector>
#include "frame_pacer.hpp"
#include "inner_timing.hpp"
#include "log.hpp"
#include "resources.hpp"
namespace t5de {
using protocol::Op;
using protocol::State;
namespace {
protocol::SetTransform transform_command(const imvu::Transform& t)
{
protocol::SetTransform command{};
std::memcpy(command.m, &t.linear[0][0], sizeof(float) * 9);
command.m[9] = t.translation.x;
command.m[10] = t.translation.y;
command.m[11] = t.translation.z;
return command;
}
} // namespace
Engine::Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder, HWND window)
: inner_(std::move(inner)), recorder_(std::move(recorder)), window_(window), innerTicksAtStats_(innerTicks)
{
QueryPerformanceCounter(&statsStart_);
if (window_ != nullptr) {
frame_pacer::add(window_);
}
}
Engine::~Engine()
{
if (window_ != nullptr) {
frame_pacer::remove(window_);
}
log::info("engine: destroyed after {} frames", frames_);
}
void Engine::setState(State state, std::uint32_t value)
{
recorder_->add(Op::SetState, protocol::SetState{state, value});
}
void Engine::resizeFrameBuffer(imvu::Vec2i size) { const InnerCall timed; inner_->resizeFrameBuffer(size); }
void Engine::enable2dFrameBuffer() { const InnerCall timed; inner_->enable2dFrameBuffer(); }
void Engine::clear(std::uint32_t flags, imvu::Color color)
{
if (innerDraws()) {
{ const InnerCall timed; inner_->clear(flags, color); }
}
recorder_->add(Op::Clear, protocol::Clear{flags, color});
}
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
{
{ const InnerCall timed; inner_->setProjection(viewport, projection); }
if (projection.m[2][3] == 0.0f) {
++stats_.flat;
}
protocol::SetProjection command{viewport.x, viewport.y, {}};
std::memcpy(command.m, &projection.m[0][0], sizeof(command.m));
recorder_->add(Op::SetProjection, command);
}
void Engine::setModelSpace(const imvu::Transform& model)
{
{ const InnerCall timed; inner_->setModelSpace(model); }
recorder_->add(Op::SetModel, transform_command(model));
}
void Engine::setViewSpace(const imvu::Transform& view)
{
{ const InnerCall timed; inner_->setViewSpace(view); }
recorder_->add(Op::SetView, transform_command(view));
}
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
{
{ const InnerCall timed; inner_->setTextures(inner(texture0), inner(texture1)); }
for (imvu::PiRenderTexture* texture : {texture0, texture1}) {
if (texture != nullptr && id(texture) == 0 && foreignLogged_ < 20) {
++foreignLogged_;
log::info("engine: frame {}: a texture the client renderer made ({})", frames_, static_cast<void*>(texture));
}
}
recorder_->add(Op::SetTextures, protocol::SetTextures{id(texture0), id(texture1)});
}
void Engine::setDepthTestingOn(bool on)
{
{ const InnerCall timed; inner_->setDepthTestingOn(on); }
setState(State::DepthTest, on);
}
void Engine::setDepthWritingOn(bool on)
{
{ const InnerCall timed; inner_->setDepthWritingOn(on); }
setState(State::DepthWrite, on);
}
void Engine::setBlendMode(imvu::RenderBlendMode mode)
{
{ const InnerCall timed; inner_->setBlendMode(mode); }
setState(State::BlendMode, static_cast<std::uint32_t>(mode));
}
void Engine::setAlphaTestingOn(bool on)
{
{ const InnerCall timed; inner_->setAlphaTestingOn(on); }
setState(State::AlphaTest, on);
}
void Engine::setAlphaTestValue(float value)
{
{ const InnerCall timed; inner_->setAlphaTestValue(value); }
setState(State::AlphaReference, static_cast<std::uint32_t>(std::lround(value * 255.0f)));
}
void Engine::setBlendingOn(bool on)
{
{ const InnerCall timed; inner_->setBlendingOn(on); }
setState(State::Blending, on);
}
void Engine::setBackFaceCullingOn(bool on)
{
{ const InnerCall timed; inner_->setBackFaceCullingOn(on); }
setState(State::Culling, on);
}
void Engine::setNormalInvertingOn(bool on)
{
{ const InnerCall timed; inner_->setNormalInvertingOn(on); }
setState(State::NormalInverting, on);
}
void Engine::setLightingOn(bool on)
{
{ const InnerCall timed; inner_->setLightingOn(on); }
setState(State::Lighting, on);
}
void Engine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
{
{ const InnerCall timed; inner_->setLightProperties(index, light); }
protocol::SetLight command{index, light.enabled, light.diffuse,
light.type == imvu::LightType::Directional,
{light.vector.x, light.vector.y, light.vector.z}};
// IMVU's Direct3D renderer shines a directional light along -vector, along -x for a zero vector.
if (command.directional && light.vector.x == 0 && light.vector.y == 0 && light.vector.z == 0) {
command.vector[0] = 1;
}
recorder_->add(Op::SetLight, command);
}
void Engine::setFogOn(bool on)
{
{ const InnerCall timed; inner_->setFogOn(on); }
setState(State::Fog, on);
}
void Engine::setFogAttributes(float start, float end, imvu::Color color)
{
{ const InnerCall timed; inner_->setFogAttributes(start, end, color); }
recorder_->add(Op::SetFog, protocol::SetFog{start, end, color});
}
void Engine::setTextureTransformMatrixIdentity(std::uint32_t stage)
{
{ const InnerCall timed; inner_->setTextureTransformMatrixIdentity(stage); }
recorder_->add(Op::SetTextureTransform, protocol::SetTextureTransform{stage, 1, {1, 0, 0, 1, 0, 0}});
}
void Engine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
{
{ const InnerCall timed; inner_->setTextureTransformMatrix(stage, transform); }
protocol::SetTextureTransform command{stage, 0, {}};
std::memcpy(command.m, transform.m, sizeof(command.m));
recorder_->add(Op::SetTextureTransform, command);
}
void Engine::setEnvironmentAmbient(imvu::Color color)
{
{ const InnerCall timed; inner_->setEnvironmentAmbient(color); }
setState(State::Ambient, color);
}
imvu::PiRenderTexture* Engine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
{
imvu::PiRenderTexture* texture = inner_->newRenderTexture(pixmap, flags);
if (texture == nullptr) {
return nullptr;
}
return new Texture(texture, recorder_, recorder_->createTexture(pixmap, flags));
}
imvu::PiInternalIndexBuffer* Engine::newIndexBuffer(std::uint32_t count, const std::uint16_t* indices)
{
imvu::PiInternalIndexBuffer* buffer = inner_->newIndexBuffer(count, indices);
if (buffer == nullptr) {
return nullptr;
}
return new IndexBuffer(buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Index, indices, 2, 2, count));
}
imvu::PiInternalVertexBuffer* Engine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
std::uint32_t elementSize, std::uint32_t elementCount)
{
imvu::PiInternalVertexBuffer* buffer = inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
if (buffer == nullptr) {
return nullptr;
}
return new VertexBuffer(
buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Vertex, data, stride, elementSize, elementCount));
}
imvu::PiInternalVertexBuffer* Engine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
{
imvu::PiInternalVertexBuffer* buffer = inner_->newDynamicVertexBuffer(elementSize, elementCount);
if (buffer == nullptr) {
return nullptr;
}
return new VertexBuffer(
buffer, recorder_,
recorder_->createBuffer(protocol::BufferKind::Vertex, nullptr, 0, elementSize, elementCount));
}
imvu::SharedPtr<imvu::PiPixmapRenderTarget> Engine::newPixmapRenderTarget(imvu::Vec2i size)
{
log::info("engine: frame {}: a {}x{} pixmap render target", frames_, size.x, size.y);
if (!recorder_->active() || size.x <= 0 || size.y <= 0) {
return inner_->newPixmapRenderTarget(size);
}
return imvu::SharedPtr<PixmapTarget>(std::make_unique<PixmapTarget>(recorder_, size));
}
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
{
log::info("engine: frame {}: a {}x{} texture render target", frames_, size.x, size.y);
if (!recorder_->active() || size.x <= 0 || size.y <= 0) {
return inner_->newTextureRenderTarget(size);
}
return imvu::SharedPtr<TextureTarget>(std::make_unique<TextureTarget>(recorder_, size));
}
void Engine::beginRendering(imvu::PiRenderTarget* target)
{
QueryPerformanceCounter(&frameStart_);
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.
ourTarget_ = ours(target) ? target : nullptr;
// Decided per frame: frames into the client renderer's render targets, and frames while the renderer process
// doesn't cover the view, are drawn by the client's renderer.
const bool takenOver = ourTarget_ != nullptr || (target == nullptr && recorder_->covering());
// Frames the client's renderer doesn't draw aren't begun in it either: its endRendering presents, which can wait
// for the display (and would show nothing: the renderer process's window covers it).
innerFrame_ = !takenOver;
if (innerFrame_) {
{ const InnerCall timed; inner_->beginRendering(target); }
}
if (takenOver != takenOver_ && target == nullptr) {
log::info("engine: frame {}: {}", frames_, takenOver ? "drawn by the renderer process" : "drawn by the client's renderer");
}
takenOver_ = takenOver;
if (target != nullptr && foreignLogged_ < 20) {
++foreignLogged_;
log::info("engine: frame {}: drawing into {} render target ({})", frames_,
ourTarget_ != nullptr ? "our" : "the client renderer's", static_cast<void*>(target));
}
protocol::BeginFrame begin{target == nullptr ? 0 : protocol::foreignTarget, 0};
if (const auto* pixmap = dynamic_cast<const PixmapTarget*>(ourTarget_)) {
begin = {pixmap->id(), 1};
} else if (const auto* texture = dynamic_cast<const TextureTarget*>(ourTarget_)) {
begin = {texture->id(), 0};
}
recorder_->add(Op::BeginFrame, begin);
}
void Engine::endRendering()
{
if (innerFrame_) {
{ const InnerCall timed; inner_->endRendering(); }
}
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
recorder_->flush(true);
if (auto* pixmap = dynamic_cast<PixmapTarget*>(ourTarget_)) {
pixmap->setPixels(recorder_->readBack());
} else if (windowFrame_ && window_ != nullptr) {
LARGE_INTEGER end, frequency;
QueryPerformanceCounter(&end);
QueryPerformanceFrequency(&frequency);
const double seconds =
static_cast<double>(end.QuadPart - frameStart_.QuadPart) / static_cast<double>(frequency.QuadPart);
frameSeconds_ += seconds;
frame_pacer::frameDrawn(window_, seconds);
}
ourTarget_ = nullptr;
++frames_;
if (frames_ % 300 == 0) {
LARGE_INTEGER ended, frequency;
QueryPerformanceCounter(&ended);
QueryPerformanceFrequency(&frequency);
const double seconds =
static_cast<double>(ended.QuadPart - statsStart_.QuadPart) / static_cast<double>(frequency.QuadPart);
statsStart_ = ended;
const Recorder::Counters sent = recorder_->takeCounters();
if (statsLogged_ < 6 || frames_ % 3000 == 0) {
++statsLogged_;
log::info("engine: last 300 frames: {:.0f} per second, at most {:.1f} ms apart; each {:.2f} ms beginRendering to "
"endRendering, {:.2f} ms in the client renderer's methods, {:.2f} ms sending, {:.0f} KB sent, {:.1f} "
"buffer updates ({:.0f} KB); drawTriangles {} (and {} with buffers not ours), drawTrianglesFromMemory "
"{} (and {} without indices), point sprites {}, lines {}, 2D projections {}",
300 / seconds, longestGap_ * 1000, frameSeconds_ * 1000 / 300,
static_cast<double>(innerTicks - innerTicksAtStats_) * 1000 / static_cast<double>(frequency.QuadPart) / 300,
sent.sendSeconds * 1000 / 300,
static_cast<double>(sent.bytes) / 1024 / 300, sent.bufferUpdates / 300.0,
static_cast<double>(sent.bufferBytes) / 1024 / 300, stats_.triangles, stats_.foreignTriangles,
stats_.indexed, stats_.unindexed, stats_.sprites, stats_.lines, stats_.flat);
}
stats_ = {};
frameSeconds_ = 0;
longestGap_ = 0;
innerTicksAtStats_ = innerTicks;
}
}
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
imvu::PiInternalVertexBuffer* colors)
{
if (innerDraws()) {
{ const InnerCall timed; inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors)); }
}
++(id(indices) != 0 && id(positions) != 0 ? stats_.triangles : stats_.foreignTriangles);
recorder_->add(Op::DrawTriangles,
protocol::DrawTriangles{id(indices), id(positions), id(normals), id(texCoords), id(colors)});
}
void Engine::drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color)
{
if (innerDraws()) {
{ const InnerCall timed; inner_->drawLine(from, to, color); }
}
++stats_.lines;
}
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
{
if (innerDraws()) {
{ const InnerCall timed; inner_->drawPointSprites(sprites); }
}
++stats_.sprites;
if (sprites.first == nullptr || sprites.last <= sprites.first) {
return;
}
const auto count = static_cast<std::uint32_t>(sprites.last - sprites.first);
if (std::byte* payload = recorder_->add(Op::DrawPointSprites, protocol::DrawPointSprites{count},
std::size_t{count} * sizeof(imvu::PointSprite))) {
std::memcpy(payload, sprites.first, std::size_t{count} * sizeof(imvu::PointSprite));
}
}
void Engine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
const std::uint16_t* indices,
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
const imvu::VertexAttributeArray<imvu::Color>& colors)
{
if (innerDraws()) {
{ const InnerCall timed; inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors); }
}
++(indices != nullptr ? stats_.indexed : stats_.unindexed);
if (positions.data == nullptr || vertexCount == 0) {
return;
}
// Without indices: the vertices in order, as a triangle list.
std::vector<std::uint16_t> sequence;
if (indices == nullptr) {
if (vertexCount > 0xffff) {
return;
}
sequence.resize(vertexCount - vertexCount % 3);
for (std::size_t i = 0; i < sequence.size(); ++i) {
sequence[i] = static_cast<std::uint16_t>(i);
}
indices = sequence.data();
indexCount = static_cast<std::uint32_t>(sequence.size());
}
const bool hasNormals = normals.data != nullptr && normals.count >= vertexCount;
const bool hasTexCoords = texCoords.data != nullptr && texCoords.count >= vertexCount;
const bool hasColors = colors.data != nullptr && colors.count >= vertexCount;
const std::size_t indexBytes = protocol::padded(std::size_t{indexCount} * 2);
const std::size_t size = indexBytes + std::size_t{vertexCount} * (12 + (hasNormals ? 12 : 0) +
(hasTexCoords ? 8 : 0) + (hasColors ? 4 : 0));
std::byte* payload = recorder_->add(Op::DrawArrays,
protocol::DrawArrays{vertexCount, indexCount, hasNormals, hasTexCoords,
hasColors},
size);
if (payload == nullptr) {
return;
}
std::memcpy(payload, indices, std::size_t{indexCount} * 2);
std::byte* next = payload + indexBytes;
pack(next, positions.data, positions.stride, 12, vertexCount);
next += std::size_t{vertexCount} * 12;
if (hasNormals) {
pack(next, normals.data, normals.stride, 12, vertexCount);
next += std::size_t{vertexCount} * 12;
}
if (hasTexCoords) {
pack(next, texCoords.data, texCoords.stride, 8, vertexCount);
next += std::size_t{vertexCount} * 8;
}
if (hasColors) {
pack(next, colors.data, colors.stride, 4, vertexCount);
}
}
} // namespace t5de