Files
t5de-renderer/adapter/engine.cpp
T
dhkatzandClaude Opus 5.5 065c7e9300
CI / build (push) Successful in 1m15s
Never make the client wait for drawing, or hold up its steps
The renderer process now takes each view's chunks on a thread of its own as
they come, and draws the newest frame on its main thread: the client no longer
waits for the previous frame to be drawn (on a VM, 22 of a frame's 25 ms).

The adapter records the client's steps (SceneViewer::step, by replacing its
entry in the exported IFrameListener vtable, after checking it), and the frame
pacer doesn't ask for a frame that wouldn't end before the next step is due: a
late step made the client catch up two at once, which showed as hitches. While
the renderer process covers the view, the client's renderer doesn't begin or
present frames, which could wait for the display.

The engine's stats now include the time in the client renderer's methods.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:30:46 -07:00

438 lines
17 KiB
C++

#include "engine.hpp"
#include <windows.h>
#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;
// 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; 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, 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;
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