CI / build (push) Successful in 1m31s
The frame pacer repaints the client's 3D windows at the user's frame rate (HKCU\Software\T5DE\Graphics\FrameRate: 30, the default, adds nothing; 60, 120, 240, 360; 0 uncapped), so the client draws frames between its 30-per-second steps. Only when affordable: a window is asked once its last frame is drawn, and only if its frames take under three quarters of the interval; uncapped, the client is left idle a third of a frame between frames. The engine's stats now log frames per second. The extra frames show the same scene for now. Co-Authored-By: Claude Opus 5.5 <[email protected]>
423 lines
15 KiB
C++
423 lines
15 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);
|
|
frame_pacer::frameDrawn(window_, static_cast<double>(end.QuadPart - frameStart_.QuadPart) /
|
|
static_cast<double>(frequency.QuadPart));
|
|
}
|
|
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;
|
|
if (statsLogged_ < 6 || frames_ % 3000 == 0) {
|
|
++statsLogged_;
|
|
log::info("engine: last 300 frames: {:.0f} per second; drawTriangles {} (and {} with buffers not ours), "
|
|
"drawTrianglesFromMemory {} (and {} without indices), point sprites {}, lines {}, 2D projections {}",
|
|
300 / seconds, stats_.triangles, stats_.foreignTriangles, stats_.indexed, stats_.unindexed,
|
|
stats_.sprites, stats_.lines, stats_.flat);
|
|
}
|
|
stats_ = {};
|
|
}
|
|
}
|
|
|
|
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
|