CI / build (push) Successful in 32s
The pacer counted only its own frames, so the client's 30 came on top (60 gave about 80), and it added no frames at all when the rate was out of reach. It now asks for a window's next frame once the interval since its last frame began has passed, and the client has had a third of a frame's time idle: all frames within the rate, and as many as the client can draw when it can't reach it. The engine's stats also break a frame's time down: sending to the renderer process, bytes sent, buffer updates. Co-Authored-By: Claude Opus 5.5 <[email protected]>
431 lines
16 KiB
C++
431 lines
16 KiB
C++
#include "engine.hpp"
|
|
|
|
#include <windows.h>
|
|
|
|
#include <bit>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#include "frame_pacer.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)
|
|
{
|
|
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) { inner_->resizeFrameBuffer(size); }
|
|
|
|
void Engine::enable2dFrameBuffer() { inner_->enable2dFrameBuffer(); }
|
|
|
|
void Engine::clear(std::uint32_t flags, imvu::Color color)
|
|
{
|
|
if (innerDraws()) {
|
|
inner_->clear(flags, color);
|
|
}
|
|
recorder_->add(Op::Clear, protocol::Clear{flags, color});
|
|
}
|
|
|
|
void Engine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
|
{
|
|
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)
|
|
{
|
|
inner_->setModelSpace(model);
|
|
recorder_->add(Op::SetModel, transform_command(model));
|
|
}
|
|
|
|
void Engine::setViewSpace(const imvu::Transform& view)
|
|
{
|
|
inner_->setViewSpace(view);
|
|
recorder_->add(Op::SetView, transform_command(view));
|
|
}
|
|
|
|
void Engine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
|
|
{
|
|
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)
|
|
{
|
|
inner_->setDepthTestingOn(on);
|
|
setState(State::DepthTest, on);
|
|
}
|
|
|
|
void Engine::setDepthWritingOn(bool on)
|
|
{
|
|
inner_->setDepthWritingOn(on);
|
|
setState(State::DepthWrite, on);
|
|
}
|
|
|
|
void Engine::setBlendMode(imvu::RenderBlendMode mode)
|
|
{
|
|
inner_->setBlendMode(mode);
|
|
setState(State::BlendMode, static_cast<std::uint32_t>(mode));
|
|
}
|
|
|
|
void Engine::setAlphaTestingOn(bool on)
|
|
{
|
|
inner_->setAlphaTestingOn(on);
|
|
setState(State::AlphaTest, on);
|
|
}
|
|
|
|
void Engine::setAlphaTestValue(float value)
|
|
{
|
|
inner_->setAlphaTestValue(value);
|
|
setState(State::AlphaReference, static_cast<std::uint32_t>(std::lround(value * 255.0f)));
|
|
}
|
|
|
|
void Engine::setBlendingOn(bool on)
|
|
{
|
|
inner_->setBlendingOn(on);
|
|
setState(State::Blending, on);
|
|
}
|
|
|
|
void Engine::setBackFaceCullingOn(bool on)
|
|
{
|
|
inner_->setBackFaceCullingOn(on);
|
|
setState(State::Culling, on);
|
|
}
|
|
|
|
void Engine::setNormalInvertingOn(bool on)
|
|
{
|
|
inner_->setNormalInvertingOn(on);
|
|
setState(State::NormalInverting, on);
|
|
}
|
|
|
|
void Engine::setLightingOn(bool on)
|
|
{
|
|
inner_->setLightingOn(on);
|
|
setState(State::Lighting, on);
|
|
}
|
|
|
|
void Engine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
|
{
|
|
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)
|
|
{
|
|
inner_->setFogOn(on);
|
|
setState(State::Fog, on);
|
|
}
|
|
|
|
void Engine::setFogAttributes(float start, float end, imvu::Color color)
|
|
{
|
|
inner_->setFogAttributes(start, end, color);
|
|
recorder_->add(Op::SetFog, protocol::SetFog{start, end, color});
|
|
}
|
|
|
|
void Engine::setTextureTransformMatrixIdentity(std::uint32_t stage)
|
|
{
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
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;
|
|
if (ourTarget_ == nullptr) {
|
|
inner_->beginRendering(target);
|
|
}
|
|
// 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());
|
|
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 (ourTarget_ == nullptr) {
|
|
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 "
|
|
"of it 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, 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;
|
|
}
|
|
}
|
|
|
|
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
|
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
|
imvu::PiInternalVertexBuffer* colors)
|
|
{
|
|
if (innerDraws()) {
|
|
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()) {
|
|
inner_->drawLine(from, to, color);
|
|
}
|
|
++stats_.lines;
|
|
}
|
|
|
|
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
|
|
{
|
|
if (innerDraws()) {
|
|
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()) {
|
|
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
|