Compare commits
2
Commits
5c35c8b63a
...
a04dba18c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a04dba18c1 | ||
|
|
42fc55cb91 |
No files matched your search
@@ -1,11 +1,14 @@
|
|||||||
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
|
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
|
||||||
|
|
||||||
add_library(t5de_render_engine SHARED
|
add_library(t5de_render_engine SHARED
|
||||||
|
engine.cpp
|
||||||
log.cpp
|
log.cpp
|
||||||
passthrough_engine.cpp
|
pixmap.cpp
|
||||||
plugin.cpp
|
plugin.cpp
|
||||||
plugin.def
|
plugin.def
|
||||||
renderer_process.cpp)
|
recorder.cpp
|
||||||
|
renderer_process.cpp
|
||||||
|
resources.cpp)
|
||||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
|
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
#include "engine.hpp"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <bit>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#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)
|
||||||
|
: inner_(std::move(inner)), recorder_(std::move(recorder))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine::~Engine() { 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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
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));
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return inner_->newPixmapRenderTarget(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
imvu::SharedPtr<imvu::PiTextureRenderTarget> Engine::newTextureRenderTarget(imvu::Vec2i size)
|
||||||
|
{
|
||||||
|
return inner_->newTextureRenderTarget(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::beginRendering(imvu::PiRenderTarget* target)
|
||||||
|
{
|
||||||
|
inner_->beginRendering(target);
|
||||||
|
recorder_->add(Op::BeginFrame, protocol::BeginFrame{target != nullptr});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::endRendering()
|
||||||
|
{
|
||||||
|
inner_->endRendering();
|
||||||
|
LARGE_INTEGER now;
|
||||||
|
QueryPerformanceCounter(&now);
|
||||||
|
recorder_->add(Op::EndFrame, protocol::EndFrame{static_cast<std::uint64_t>(now.QuadPart)});
|
||||||
|
recorder_->flush(true);
|
||||||
|
++frames_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||||
|
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||||
|
imvu::PiInternalVertexBuffer* colors)
|
||||||
|
{
|
||||||
|
inner_->drawTriangles(inner(indices), inner(positions), inner(normals), inner(texCoords), inner(colors));
|
||||||
|
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) { inner_->drawLine(from, to, color); }
|
||||||
|
|
||||||
|
void Engine::drawPointSprites(const imvu::PointSpriteVector& sprites)
|
||||||
|
{
|
||||||
|
inner_->drawPointSprites(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)
|
||||||
|
{
|
||||||
|
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
||||||
|
if (indices == nullptr || positions.data == nullptr || vertexCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// A renderer that forwards every call to another one (IMVU's Direct3D engine) and logs what a frame is made of: proof
|
// The adapter's engine: what the client draws with. Everything goes to the client's own Direct3D renderer, which
|
||||||
// that our declaration of the interface matches IMVU's, and a way to see how the client draws.
|
// still draws the whole 3D view, and is recorded for the renderer process, which draws its part on top (the right
|
||||||
|
// half for now, to compare). If the renderer process fails, the client's renderer carries on alone.
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "renderer_process.hpp"
|
#include "recorder.hpp"
|
||||||
#include "t5de/imvu/render_engine.hpp"
|
#include "t5de/imvu/render_engine.hpp"
|
||||||
|
|
||||||
namespace t5de {
|
namespace t5de {
|
||||||
|
|
||||||
class PassthroughEngine final : public imvu::PiRenderEngine {
|
class Engine final : public imvu::PiRenderEngine {
|
||||||
public:
|
public:
|
||||||
// renderer may be null.
|
// recorder may be inactive (no renderer process).
|
||||||
PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::unique_ptr<RendererProcess> renderer);
|
Engine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::shared_ptr<Recorder> recorder);
|
||||||
~PassthroughEngine() override;
|
~Engine() override;
|
||||||
|
|
||||||
void resizeFrameBuffer(imvu::Vec2i size) override;
|
void resizeFrameBuffer(imvu::Vec2i size) override;
|
||||||
void enable2dFrameBuffer() override;
|
void enable2dFrameBuffer() override;
|
||||||
@@ -53,7 +53,7 @@ public:
|
|||||||
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||||
imvu::PiInternalVertexBuffer* colors) override;
|
imvu::PiInternalVertexBuffer* colors) override;
|
||||||
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color) override;
|
void drawLine(imvu::Vec3f from, imvu::Vec3f to, imvu::Color color) override;
|
||||||
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
|
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
|
||||||
void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount, const std::uint16_t* indices,
|
void 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>& positions,
|
||||||
@@ -61,15 +61,11 @@ public:
|
|||||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||||
const imvu::VertexAttributeArray<imvu::Color>& colors) override;
|
const imvu::VertexAttributeArray<imvu::Color>& colors) override;
|
||||||
|
|
||||||
// The interface's methods, by vtable slot (slot 0, the destructor, isn't counted).
|
|
||||||
static constexpr std::size_t slots = 35;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void count(std::size_t slot);
|
void setState(protocol::State state, std::uint32_t value);
|
||||||
|
|
||||||
imvu::SharedPtr<imvu::PiRenderEngine> inner_;
|
imvu::SharedPtr<imvu::PiRenderEngine> inner_;
|
||||||
std::unique_ptr<RendererProcess> renderer_;
|
std::shared_ptr<Recorder> recorder_;
|
||||||
std::array<std::uint32_t, slots> calls_{};
|
|
||||||
std::uint64_t frames_ = 0;
|
std::uint64_t frames_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
#include "passthrough_engine.hpp"
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "log.hpp"
|
|
||||||
|
|
||||||
namespace t5de {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr std::array<const char*, PassthroughEngine::slots> names = {
|
|
||||||
"~PiRenderEngine",
|
|
||||||
"resizeFrameBuffer",
|
|
||||||
"enable2dFrameBuffer",
|
|
||||||
"clear",
|
|
||||||
"setProjection",
|
|
||||||
"setModelSpace",
|
|
||||||
"setViewSpace",
|
|
||||||
"setTextures",
|
|
||||||
"setDepthTestingOn",
|
|
||||||
"setDepthWritingOn",
|
|
||||||
"setBlendMode",
|
|
||||||
"setAlphaTestingOn",
|
|
||||||
"setAlphaTestValue",
|
|
||||||
"setBlendingOn",
|
|
||||||
"setBackFaceCullingOn",
|
|
||||||
"setNormalInvertingOn",
|
|
||||||
"setLightingOn",
|
|
||||||
"setLightProperties",
|
|
||||||
"setFogOn",
|
|
||||||
"setFogAttributes",
|
|
||||||
"setTextureTransformMatrixIdentity",
|
|
||||||
"setTextureTransformMatrix",
|
|
||||||
"setEnvironmentAmbient",
|
|
||||||
"newRenderTexture",
|
|
||||||
"newIndexBuffer",
|
|
||||||
"newStaticVertexBuffer",
|
|
||||||
"newDynamicVertexBuffer",
|
|
||||||
"newPixmapRenderTarget",
|
|
||||||
"newTextureRenderTarget",
|
|
||||||
"beginRendering",
|
|
||||||
"endRendering",
|
|
||||||
"drawTriangles",
|
|
||||||
"drawLine",
|
|
||||||
"drawPointSprites",
|
|
||||||
"drawTrianglesFromMemory",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Frames whose calls are logged: the first few, then one a minute or so at 30 FPS.
|
|
||||||
bool logged(std::uint64_t frame) { return frame < 5 || frame % 1800 == 0; }
|
|
||||||
|
|
||||||
// Whether to log a call's arguments: the first few calls of each method, in the whole process. Enough to check that
|
|
||||||
// our layouts of IMVU's types read sensible values.
|
|
||||||
bool sampled(std::size_t slot)
|
|
||||||
{
|
|
||||||
static std::array<std::atomic<int>, PassthroughEngine::slots> calls{};
|
|
||||||
return calls[slot]++ < 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string text(const imvu::Vec3f& v) { return std::format("({:.3f}, {:.3f}, {:.3f})", v.x, v.y, v.z); }
|
|
||||||
|
|
||||||
std::string text(const imvu::Transform& t)
|
|
||||||
{
|
|
||||||
const auto& m = t.linear;
|
|
||||||
return std::format("[{:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f}] + {}", m[0][0], m[0][1],
|
|
||||||
m[0][2], m[1][0], m[1][1], m[1][2], m[2][0], m[2][1], m[2][2], text(t.translation));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A pixmap's size, from pixmap.dll's exported Pixmap::width and Pixmap::height.
|
|
||||||
imvu::Vec2i pixmap_size(const imvu::Pixmap& pixmap)
|
|
||||||
{
|
|
||||||
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
|
|
||||||
struct Getters {
|
|
||||||
Getter width;
|
|
||||||
Getter height;
|
|
||||||
};
|
|
||||||
static const Getters getters = [] {
|
|
||||||
HMODULE module = GetModuleHandleW(L"pixmap.dll");
|
|
||||||
if (module == nullptr) {
|
|
||||||
return Getters{};
|
|
||||||
}
|
|
||||||
return Getters{reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ")),
|
|
||||||
reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"))};
|
|
||||||
}();
|
|
||||||
if (getters.width == nullptr || getters.height == nullptr) {
|
|
||||||
return {-1, -1};
|
|
||||||
}
|
|
||||||
return {static_cast<int>(getters.width(&pixmap)), static_cast<int>(getters.height(&pixmap))};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
PassthroughEngine::PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner,
|
|
||||||
std::unique_ptr<RendererProcess> renderer)
|
|
||||||
: inner_(std::move(inner)), renderer_(std::move(renderer))
|
|
||||||
{
|
|
||||||
log::info("passthrough: wrapping the engine at {}", static_cast<void*>(inner_.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
PassthroughEngine::~PassthroughEngine() { log::info("passthrough: destroyed after {} frames", frames_); }
|
|
||||||
|
|
||||||
void PassthroughEngine::count(std::size_t slot) { ++calls_[slot]; }
|
|
||||||
|
|
||||||
void PassthroughEngine::resizeFrameBuffer(imvu::Vec2i size)
|
|
||||||
{
|
|
||||||
log::info("resizeFrameBuffer({}, {})", size.x, size.y);
|
|
||||||
count(1);
|
|
||||||
inner_->resizeFrameBuffer(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::enable2dFrameBuffer()
|
|
||||||
{
|
|
||||||
count(2);
|
|
||||||
inner_->enable2dFrameBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::clear(std::uint32_t flags, imvu::Color color)
|
|
||||||
{
|
|
||||||
if (sampled(3)) {
|
|
||||||
log::info("clear(flags {}, color {:08x})", flags, color);
|
|
||||||
}
|
|
||||||
count(3);
|
|
||||||
inner_->clear(flags, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection)
|
|
||||||
{
|
|
||||||
if (sampled(4)) {
|
|
||||||
const auto& m = projection.m;
|
|
||||||
log::info("setProjection({}x{}, [{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}; "
|
|
||||||
"{:.3f} {:.3f} {:.3f} {:.3f}; {:.3f} {:.3f} {:.3f} {:.3f}])",
|
|
||||||
viewport.x, viewport.y, m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2],
|
|
||||||
m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3]);
|
|
||||||
}
|
|
||||||
count(4);
|
|
||||||
inner_->setProjection(viewport, projection);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setModelSpace(const imvu::Transform& model)
|
|
||||||
{
|
|
||||||
if (sampled(5)) {
|
|
||||||
log::info("setModelSpace({})", text(model));
|
|
||||||
}
|
|
||||||
count(5);
|
|
||||||
inner_->setModelSpace(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setViewSpace(const imvu::Transform& view)
|
|
||||||
{
|
|
||||||
if (sampled(6)) {
|
|
||||||
log::info("setViewSpace({})", text(view));
|
|
||||||
}
|
|
||||||
count(6);
|
|
||||||
inner_->setViewSpace(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1)
|
|
||||||
{
|
|
||||||
count(7);
|
|
||||||
inner_->setTextures(texture0, texture1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setDepthTestingOn(bool on)
|
|
||||||
{
|
|
||||||
count(8);
|
|
||||||
inner_->setDepthTestingOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setDepthWritingOn(bool on)
|
|
||||||
{
|
|
||||||
count(9);
|
|
||||||
inner_->setDepthWritingOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setBlendMode(imvu::RenderBlendMode mode)
|
|
||||||
{
|
|
||||||
if (sampled(10)) {
|
|
||||||
log::info("setBlendMode({})", static_cast<int>(mode));
|
|
||||||
}
|
|
||||||
count(10);
|
|
||||||
inner_->setBlendMode(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setAlphaTestingOn(bool on)
|
|
||||||
{
|
|
||||||
count(11);
|
|
||||||
inner_->setAlphaTestingOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setAlphaTestValue(float value)
|
|
||||||
{
|
|
||||||
if (sampled(12)) {
|
|
||||||
log::info("setAlphaTestValue({:.3f})", value);
|
|
||||||
}
|
|
||||||
count(12);
|
|
||||||
inner_->setAlphaTestValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setBlendingOn(bool on)
|
|
||||||
{
|
|
||||||
count(13);
|
|
||||||
inner_->setBlendingOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setBackFaceCullingOn(bool on)
|
|
||||||
{
|
|
||||||
count(14);
|
|
||||||
inner_->setBackFaceCullingOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setNormalInvertingOn(bool on)
|
|
||||||
{
|
|
||||||
count(15);
|
|
||||||
inner_->setNormalInvertingOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setLightingOn(bool on)
|
|
||||||
{
|
|
||||||
count(16);
|
|
||||||
inner_->setLightingOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setLightProperties(std::uint32_t index, const imvu::LightProperties& light)
|
|
||||||
{
|
|
||||||
if (sampled(17)) {
|
|
||||||
log::info("setLightProperties({}, enabled {}, diffuse {:08x}, vector {}, type {})", index,
|
|
||||||
light.enabled, light.diffuse, text(light.vector), static_cast<int>(light.type));
|
|
||||||
}
|
|
||||||
count(17);
|
|
||||||
inner_->setLightProperties(index, light);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setFogOn(bool on)
|
|
||||||
{
|
|
||||||
count(18);
|
|
||||||
inner_->setFogOn(on);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setFogAttributes(float start, float end, imvu::Color color)
|
|
||||||
{
|
|
||||||
if (sampled(19)) {
|
|
||||||
log::info("setFogAttributes(start {:.3f}, end {:.3f}, color {:08x})", start, end, color);
|
|
||||||
}
|
|
||||||
count(19);
|
|
||||||
inner_->setFogAttributes(start, end, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setTextureTransformMatrixIdentity(std::uint32_t stage)
|
|
||||||
{
|
|
||||||
count(20);
|
|
||||||
inner_->setTextureTransformMatrixIdentity(stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform)
|
|
||||||
{
|
|
||||||
if (sampled(21)) {
|
|
||||||
const auto& m = transform.m;
|
|
||||||
log::info("setTextureTransformMatrix({}, [{:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f}])", stage, m[0],
|
|
||||||
m[1], m[2], m[3], m[4], m[5]);
|
|
||||||
}
|
|
||||||
count(21);
|
|
||||||
inner_->setTextureTransformMatrix(stage, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::setEnvironmentAmbient(imvu::Color color)
|
|
||||||
{
|
|
||||||
if (sampled(22)) {
|
|
||||||
log::info("setEnvironmentAmbient({:08x})", color);
|
|
||||||
}
|
|
||||||
count(22);
|
|
||||||
inner_->setEnvironmentAmbient(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
|
|
||||||
{
|
|
||||||
if (sampled(23)) {
|
|
||||||
const auto size = pixmap_size(pixmap);
|
|
||||||
log::info("newRenderTexture({}x{} pixmap, flags {:x})", size.x, size.y, flags);
|
|
||||||
}
|
|
||||||
count(23);
|
|
||||||
return inner_->newRenderTexture(pixmap, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
imvu::PiInternalIndexBuffer* PassthroughEngine::newIndexBuffer(std::uint32_t indexCount, const std::uint16_t* indices)
|
|
||||||
{
|
|
||||||
if (sampled(24)) {
|
|
||||||
log::info("newIndexBuffer({} indices)", indexCount);
|
|
||||||
}
|
|
||||||
count(24);
|
|
||||||
return inner_->newIndexBuffer(indexCount, indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
|
||||||
std::uint32_t elementSize, std::uint32_t elementCount)
|
|
||||||
{
|
|
||||||
if (sampled(25)) {
|
|
||||||
log::info("newStaticVertexBuffer(stride {}, element size {}, {} elements)", stride, elementSize,
|
|
||||||
elementCount);
|
|
||||||
}
|
|
||||||
count(25);
|
|
||||||
return inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount)
|
|
||||||
{
|
|
||||||
if (sampled(26)) {
|
|
||||||
log::info("newDynamicVertexBuffer(element size {}, {} elements)", elementSize, elementCount);
|
|
||||||
}
|
|
||||||
count(26);
|
|
||||||
return inner_->newDynamicVertexBuffer(elementSize, elementCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> PassthroughEngine::newPixmapRenderTarget(imvu::Vec2i size)
|
|
||||||
{
|
|
||||||
log::info("newPixmapRenderTarget({}, {})", size.x, size.y);
|
|
||||||
count(27);
|
|
||||||
return inner_->newPixmapRenderTarget(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> PassthroughEngine::newTextureRenderTarget(imvu::Vec2i size)
|
|
||||||
{
|
|
||||||
log::info("newTextureRenderTarget({}, {})", size.x, size.y);
|
|
||||||
count(28);
|
|
||||||
return inner_->newTextureRenderTarget(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::beginRendering(imvu::PiRenderTarget* target)
|
|
||||||
{
|
|
||||||
count(29);
|
|
||||||
inner_->beginRendering(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::endRendering()
|
|
||||||
{
|
|
||||||
count(30);
|
|
||||||
inner_->endRendering();
|
|
||||||
if (renderer_) {
|
|
||||||
renderer_->frameDrawn();
|
|
||||||
}
|
|
||||||
if (logged(frames_)) {
|
|
||||||
std::string line = std::format("frame {}:", frames_);
|
|
||||||
for (std::size_t slot = 1; slot < slots; ++slot) {
|
|
||||||
if (calls_[slot] != 0) {
|
|
||||||
line += std::format(" {} x{},", names[slot], calls_[slot]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line.pop_back();
|
|
||||||
log::write(line);
|
|
||||||
}
|
|
||||||
calls_ = {};
|
|
||||||
++frames_;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
|
||||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
|
||||||
imvu::PiInternalVertexBuffer* colors)
|
|
||||||
{
|
|
||||||
count(31);
|
|
||||||
inner_->drawTriangles(indices, positions, normals, texCoords, colors);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color)
|
|
||||||
{
|
|
||||||
if (sampled(32)) {
|
|
||||||
log::info("drawLine({}, {}, {:08x})", text(from), text(to), color);
|
|
||||||
}
|
|
||||||
count(32);
|
|
||||||
inner_->drawLine(from, to, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::drawPointSprites(const imvu::PointSpriteVector& sprites)
|
|
||||||
{
|
|
||||||
count(33);
|
|
||||||
inner_->drawPointSprites(sprites);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PassthroughEngine::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 (sampled(34)) {
|
|
||||||
log::info("drawTrianglesFromMemory({} vertices, {} indices; positions {} stride {}, colors {} stride {})",
|
|
||||||
vertexCount, indexCount, positions.count, positions.stride, colors.count, colors.stride);
|
|
||||||
}
|
|
||||||
count(34);
|
|
||||||
inner_->drawTrianglesFromMemory(vertexCount, indexCount, indices, positions, normals, texCoords, colors);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace t5de
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#include "pixmap.hpp"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using Getter = unsigned(__thiscall*)(const imvu::Pixmap*);
|
||||||
|
using PixelsGetter = std::uint32_t*(__thiscall*)(const imvu::Pixmap*);
|
||||||
|
|
||||||
|
struct Methods {
|
||||||
|
Getter width = nullptr;
|
||||||
|
Getter height = nullptr;
|
||||||
|
Getter rowBytes = nullptr;
|
||||||
|
PixelsGetter pixels = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Methods& methods()
|
||||||
|
{
|
||||||
|
static const Methods methods = [] {
|
||||||
|
Methods found;
|
||||||
|
if (HMODULE module = GetModuleHandleW(L"pixmap.dll")) {
|
||||||
|
found.width = reinterpret_cast<Getter>(GetProcAddress(module, "?width@Pixmap@@QBEIXZ"));
|
||||||
|
found.height = reinterpret_cast<Getter>(GetProcAddress(module, "?height@Pixmap@@QBEIXZ"));
|
||||||
|
found.rowBytes = reinterpret_cast<Getter>(GetProcAddress(module, "?rowBytes@Pixmap@@QBEIXZ"));
|
||||||
|
found.pixels = reinterpret_cast<PixelsGetter>(GetProcAddress(module, "?pixels@Pixmap@@QBEPAIXZ"));
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}();
|
||||||
|
return methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap)
|
||||||
|
{
|
||||||
|
const Methods& m = methods();
|
||||||
|
if (m.width == nullptr || m.height == nullptr || m.rowBytes == nullptr || m.pixels == nullptr) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return PixmapPixels{m.width(&pixmap), m.height(&pixmap), m.rowBytes(&pixmap), m.pixels(&pixmap)};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
// Reading IMVU's images (Pixmap) through pixmap.dll's exported methods, rather than depending on their layout.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "t5de/imvu/types.hpp"
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
struct PixmapPixels {
|
||||||
|
std::uint32_t width;
|
||||||
|
std::uint32_t height;
|
||||||
|
std::uint32_t rowBytes;
|
||||||
|
const std::uint32_t* pixels; // 0xAARRGGBB, rows rowBytes apart
|
||||||
|
};
|
||||||
|
|
||||||
|
// Null if pixmap.dll's methods can't be found.
|
||||||
|
std::optional<PixmapPixels> pixmap_pixels(const imvu::Pixmap& pixmap);
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
+18
-5
@@ -1,12 +1,15 @@
|
|||||||
// The renderer plugin's entry points, which SceneWindow.dll's createDLLRenderer looks up by name (exported
|
// The renderer plugin's entry points, which SceneWindow.dll's createDLLRenderer looks up by name (exported
|
||||||
// undecorated by plugin.def). For now the renderer is the pass-through: IMVU's Direct3D engine, wrapped.
|
// undecorated by plugin.def). The engine is the adapter's (engine.hpp): IMVU's Direct3D engine draws, and the
|
||||||
|
// renderer process draws over part of it.
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "passthrough_engine.hpp"
|
#include "engine.hpp"
|
||||||
|
#include "recorder.hpp"
|
||||||
#include "renderer_process.hpp"
|
#include "renderer_process.hpp"
|
||||||
#include "t5de/imvu/render_engine.hpp"
|
#include "t5de/imvu/render_engine.hpp"
|
||||||
|
|
||||||
@@ -30,6 +33,15 @@ imvu::SharedPtr<imvu::PiRenderEngine> create_imvu_engine(imvu::SharedPtr<imvu::I
|
|||||||
return create(std::move(window), imvu::RendererType::Direct3D);
|
return create(std::move(window), imvu::RendererType::Direct3D);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the renderer process draws: T5DE_RENDERER_VIEW=off, split (the default: the right half) or full.
|
||||||
|
protocol::View view()
|
||||||
|
{
|
||||||
|
wchar_t value[16] = {};
|
||||||
|
GetEnvironmentVariableW(L"T5DE_RENDERER_VIEW", value, 16);
|
||||||
|
const std::wstring_view setting = value;
|
||||||
|
return setting == L"off" ? protocol::View::Off : setting == L"full" ? protocol::View::Full : protocol::View::Split;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
extern "C" int __cdecl isRenderEngineSupported() { return 1; }
|
extern "C" int __cdecl isRenderEngineSupported() { return 1; }
|
||||||
@@ -45,9 +57,10 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
|
|||||||
if (!inner) {
|
if (!inner) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
auto renderer = hwnd != nullptr ? RendererProcess::start(hwnd) : nullptr;
|
const protocol::View shown = view();
|
||||||
return imvu::SharedPtr<imvu::PiRenderEngine>(
|
auto renderer = hwnd != nullptr && shown != protocol::View::Off ? RendererProcess::start(hwnd, shown) : nullptr;
|
||||||
std::make_unique<PassthroughEngine>(std::move(inner), std::move(renderer)));
|
return imvu::SharedPtr<imvu::PiRenderEngine>(std::make_unique<Engine>(
|
||||||
|
std::move(inner), std::make_shared<Recorder>(std::move(renderer))));
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
|
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
|
||||||
log::info("createRenderEngine failed: {}", e.what());
|
log::info("createRenderEngine failed: {}", e.what());
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#include "recorder.hpp"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "pixmap.hpp"
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Between frames, recorded commands (mostly textures and buffers being loaded) are sent once they reach this size.
|
||||||
|
constexpr std::size_t flushSize = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
|
||||||
|
std::uint32_t count)
|
||||||
|
{
|
||||||
|
const auto* from = static_cast<const std::byte*>(source);
|
||||||
|
if (stride == 0 || stride == elementSize) {
|
||||||
|
std::memcpy(destination, from, std::size_t{elementSize} * count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (std::uint32_t i = 0; i < count; ++i) {
|
||||||
|
std::memcpy(destination + std::size_t{i} * elementSize, from + std::size_t{i} * stride, elementSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Recorder::Recorder(std::unique_ptr<RendererProcess> process) : process_(std::move(process)) {}
|
||||||
|
|
||||||
|
void Recorder::flush(bool endsFrame)
|
||||||
|
{
|
||||||
|
if (!active()) {
|
||||||
|
writer_.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process_->send(writer_.bytes(), endsFrame);
|
||||||
|
writer_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Recorder::flushIfLarge()
|
||||||
|
{
|
||||||
|
if (writer_.size() >= flushSize) {
|
||||||
|
flush(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Recorder::addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||||
|
{
|
||||||
|
const auto pixels = pixmap_pixels(pixmap);
|
||||||
|
if (!pixels || pixels->pixels == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t rowSize = std::size_t{pixels->width} * 4;
|
||||||
|
std::byte* payload = nullptr;
|
||||||
|
if (op == protocol::Op::CreateTexture) {
|
||||||
|
payload = add(op, protocol::CreateTexture{id, pixels->width, pixels->height, flags}, rowSize * pixels->height);
|
||||||
|
} else {
|
||||||
|
payload = add(op, protocol::UpdateTexture{id, pixels->width, pixels->height}, rowSize * pixels->height);
|
||||||
|
}
|
||||||
|
if (payload == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto* rows = reinterpret_cast<const std::byte*>(pixels->pixels);
|
||||||
|
for (std::uint32_t y = 0; y < pixels->height; ++y) {
|
||||||
|
std::memcpy(payload + y * rowSize, rows + std::size_t{y} * pixels->rowBytes, rowSize);
|
||||||
|
}
|
||||||
|
flushIfLarge();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t Recorder::createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags)
|
||||||
|
{
|
||||||
|
const std::uint32_t id = nextId_++;
|
||||||
|
addPixels(protocol::Op::CreateTexture, id, pixmap, flags);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Recorder::updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap)
|
||||||
|
{
|
||||||
|
addPixels(protocol::Op::UpdateTexture, id, pixmap, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Recorder::destroyTexture(std::uint32_t id) { add(protocol::Op::DestroyTexture, protocol::DestroyTexture{id}); }
|
||||||
|
|
||||||
|
std::uint32_t Recorder::createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
|
||||||
|
std::uint32_t elementSize, std::uint32_t count)
|
||||||
|
{
|
||||||
|
const std::uint32_t id = nextId_++;
|
||||||
|
const std::size_t size = data != nullptr ? std::size_t{elementSize} * count : 0;
|
||||||
|
if (std::byte* payload = add(protocol::Op::CreateBuffer, protocol::CreateBuffer{id, kind, elementSize, count}, size);
|
||||||
|
payload != nullptr && size != 0) {
|
||||||
|
pack(payload, data, stride, elementSize, count);
|
||||||
|
}
|
||||||
|
flushIfLarge();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Recorder::updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||||
|
std::uint32_t count)
|
||||||
|
{
|
||||||
|
const std::size_t size = std::size_t{elementSize} * count;
|
||||||
|
if (std::byte* payload = add(protocol::Op::UpdateBuffer, protocol::UpdateBuffer{id, elementSize, count}, size);
|
||||||
|
payload != nullptr && data != nullptr) {
|
||||||
|
pack(payload, data, stride, elementSize, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Recorder::destroyBuffer(std::uint32_t id) { add(protocol::Op::DestroyBuffer, protocol::DestroyBuffer{id}); }
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
// Records what the client draws as commands (include/t5de/protocol/commands.hpp) and sends them to the renderer
|
||||||
|
// process. Shared by an engine and the resources it creates (which can outlive it); once the renderer process is
|
||||||
|
// gone, it records nothing.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "renderer_process.hpp"
|
||||||
|
#include "t5de/imvu/types.hpp"
|
||||||
|
#include "t5de/protocol/commands.hpp"
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
class Recorder {
|
||||||
|
public:
|
||||||
|
explicit Recorder(std::unique_ptr<RendererProcess> process);
|
||||||
|
|
||||||
|
[[nodiscard]] bool active() const { return process_ && process_->alive(); }
|
||||||
|
|
||||||
|
// Records a command (if active); returns its payload to fill, or null.
|
||||||
|
template <class Body>
|
||||||
|
std::byte* add(protocol::Op op, const Body& body, std::size_t payloadSize = 0)
|
||||||
|
{
|
||||||
|
if (!active()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::byte* payload = writer_.add(op, body, payloadSize);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends what's recorded: at the end of each frame, and between frames when a lot is waiting (resources).
|
||||||
|
void flush(bool endsFrame);
|
||||||
|
void flushIfLarge();
|
||||||
|
|
||||||
|
std::uint32_t createTexture(const imvu::Pixmap& pixmap, std::uint32_t flags);
|
||||||
|
void updateTexture(std::uint32_t id, const imvu::Pixmap& pixmap);
|
||||||
|
void destroyTexture(std::uint32_t id);
|
||||||
|
|
||||||
|
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), from data (null: none yet).
|
||||||
|
std::uint32_t createBuffer(protocol::BufferKind kind, const void* data, std::uint32_t stride,
|
||||||
|
std::uint32_t elementSize, std::uint32_t count);
|
||||||
|
void updateBuffer(std::uint32_t id, const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||||
|
std::uint32_t count);
|
||||||
|
void destroyBuffer(std::uint32_t id);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags);
|
||||||
|
|
||||||
|
std::unique_ptr<RendererProcess> process_;
|
||||||
|
protocol::Writer writer_;
|
||||||
|
std::uint32_t nextId_ = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), into destination, packed.
|
||||||
|
void pack(std::byte* destination, const void* source, std::uint32_t stride, std::uint32_t elementSize,
|
||||||
|
std::uint32_t count);
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "renderer_process.hpp"
|
#include "renderer_process.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -11,6 +13,9 @@ namespace t5de {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// How long to wait for the renderer process to take a chunk before giving up on it.
|
||||||
|
constexpr DWORD consumeTimeout = 5000;
|
||||||
|
|
||||||
// The folder this DLL is in: the client's program folder.
|
// The folder this DLL is in: the client's program folder.
|
||||||
std::filesystem::path module_folder()
|
std::filesystem::path module_folder()
|
||||||
{
|
{
|
||||||
@@ -22,31 +27,39 @@ std::filesystem::path module_folder()
|
|||||||
return std::filesystem::path(path).parent_path();
|
return std::filesystem::path(path).parent_path();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string narrow(const std::wstring& text) { return std::filesystem::path(text).string(); }
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
|
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window, protocol::View view)
|
||||||
{
|
{
|
||||||
static std::atomic<int> engines = 0;
|
static std::atomic<int> engines = 0;
|
||||||
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++);
|
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++);
|
||||||
|
constexpr DWORD size = sizeof(protocol::Channel) + protocol::channelCapacity;
|
||||||
|
|
||||||
std::unique_ptr<RendererProcess> renderer(new RendererProcess());
|
std::unique_ptr<RendererProcess> renderer(new RendererProcess());
|
||||||
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, size, name.c_str());
|
||||||
sizeof(protocol::Channel), name.c_str());
|
|
||||||
if (renderer->mapping_ == nullptr) {
|
if (renderer->mapping_ == nullptr) {
|
||||||
log::info("renderer process: can't create the channel: error {}", GetLastError());
|
log::info("renderer process: can't create the channel: error {}", GetLastError());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
renderer->channel_ = static_cast<protocol::Channel*>(
|
renderer->channel_ =
|
||||||
MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(protocol::Channel)));
|
static_cast<protocol::Channel*>(MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size));
|
||||||
renderer->frameEvent_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::frameEventSuffix).c_str());
|
renderer->ready_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::readyEventSuffix).c_str());
|
||||||
if (renderer->channel_ == nullptr || renderer->frameEvent_ == nullptr) {
|
renderer->consumed_ = CreateEventW(nullptr, FALSE, TRUE, (name + protocol::consumedEventSuffix).c_str());
|
||||||
|
if (renderer->channel_ == nullptr || renderer->ready_ == nullptr || renderer->consumed_ == nullptr) {
|
||||||
log::info("renderer process: can't set up the channel: error {}", GetLastError());
|
log::info("renderer process: can't set up the channel: error {}", GetLastError());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
renderer->data_ = reinterpret_cast<std::byte*>(renderer->channel_ + 1);
|
||||||
*renderer->channel_ = {.version = protocol::version,
|
*renderer->channel_ = {.version = protocol::version,
|
||||||
.closing = 0,
|
.closing = 0,
|
||||||
.window = reinterpret_cast<std::uintptr_t>(window),
|
.window = reinterpret_cast<std::uintptr_t>(window),
|
||||||
.frames = 0};
|
.frames = 0,
|
||||||
|
.view = view,
|
||||||
|
.capacity = protocol::channelCapacity,
|
||||||
|
.chunkSize = 0,
|
||||||
|
.chunkFlags = 0};
|
||||||
|
|
||||||
// The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it.
|
// The renderer's window goes on top of the client's 3D view: keep the client's drawing out of it.
|
||||||
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
|
SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_CLIPCHILDREN);
|
||||||
@@ -62,8 +75,7 @@ std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
|
|||||||
}
|
}
|
||||||
CloseHandle(process.hThread);
|
CloseHandle(process.hThread);
|
||||||
renderer->process_ = process.hProcess;
|
renderer->process_ = process.hProcess;
|
||||||
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId,
|
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId, narrow(name));
|
||||||
std::filesystem::path(name).string());
|
|
||||||
return renderer;
|
return renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,22 +83,42 @@ RendererProcess::~RendererProcess()
|
|||||||
{
|
{
|
||||||
if (channel_ != nullptr) {
|
if (channel_ != nullptr) {
|
||||||
channel_->closing = 1;
|
channel_->closing = 1;
|
||||||
if (frameEvent_ != nullptr) {
|
if (ready_ != nullptr) {
|
||||||
SetEvent(frameEvent_);
|
SetEvent(ready_);
|
||||||
}
|
}
|
||||||
UnmapViewOfFile(channel_);
|
UnmapViewOfFile(channel_);
|
||||||
}
|
}
|
||||||
for (HANDLE handle : {frameEvent_, mapping_, process_}) {
|
for (HANDLE handle : {ready_, consumed_, mapping_, process_}) {
|
||||||
if (handle != nullptr) {
|
if (handle != nullptr) {
|
||||||
CloseHandle(handle);
|
CloseHandle(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RendererProcess::frameDrawn()
|
bool RendererProcess::send(std::span<const std::byte> commands, bool endsFrame)
|
||||||
{
|
{
|
||||||
++channel_->frames;
|
if (!alive_) {
|
||||||
SetEvent(frameEvent_);
|
return false;
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
const HANDLE waits[] = {consumed_, process_};
|
||||||
|
const DWORD result = WaitForMultipleObjects(2, waits, FALSE, consumeTimeout);
|
||||||
|
if (result != WAIT_OBJECT_0) {
|
||||||
|
log::info("renderer process: {}; giving up on it", result == WAIT_OBJECT_0 + 1 ? "exited" : "not responding");
|
||||||
|
alive_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::size_t size = std::min<std::size_t>(commands.size(), channel_->capacity);
|
||||||
|
std::memcpy(data_, commands.data(), size);
|
||||||
|
commands = commands.subspan(size);
|
||||||
|
channel_->chunkSize = static_cast<std::uint32_t>(size);
|
||||||
|
channel_->chunkFlags = endsFrame && commands.empty() ? protocol::chunkEndsFrame : 0;
|
||||||
|
if (endsFrame && commands.empty()) {
|
||||||
|
++channel_->frames;
|
||||||
|
}
|
||||||
|
SetEvent(ready_);
|
||||||
|
} while (!commands.empty());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace t5de
|
} // namespace t5de
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, told about
|
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, sent the
|
||||||
// each frame through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
|
// command stream through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstddef>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
#include "t5de/protocol/channel.hpp"
|
#include "t5de/protocol/channel.hpp"
|
||||||
|
|
||||||
@@ -13,23 +14,29 @@ namespace t5de {
|
|||||||
|
|
||||||
class RendererProcess {
|
class RendererProcess {
|
||||||
public:
|
public:
|
||||||
// Starts t5de-renderer.exe from this DLL's folder, drawing into window; null if it can't.
|
// Starts t5de-renderer.exe from this DLL's folder, drawing into part of window; null if it can't.
|
||||||
static std::unique_ptr<RendererProcess> start(HWND window);
|
static std::unique_ptr<RendererProcess> start(HWND window, protocol::View view);
|
||||||
|
|
||||||
RendererProcess(const RendererProcess&) = delete;
|
RendererProcess(const RendererProcess&) = delete;
|
||||||
RendererProcess& operator=(const RendererProcess&) = delete;
|
RendererProcess& operator=(const RendererProcess&) = delete;
|
||||||
~RendererProcess();
|
~RendererProcess();
|
||||||
|
|
||||||
// The client drew a frame.
|
// Sends commands, in as many chunks as they take; endsFrame marks the end of a frame. False if the renderer
|
||||||
void frameDrawn();
|
// process is gone or stopped taking them (it's then given up on).
|
||||||
|
bool send(std::span<const std::byte> commands, bool endsFrame);
|
||||||
|
|
||||||
|
[[nodiscard]] bool alive() const { return alive_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RendererProcess() = default;
|
RendererProcess() = default;
|
||||||
|
|
||||||
HANDLE mapping_ = nullptr;
|
HANDLE mapping_ = nullptr;
|
||||||
HANDLE frameEvent_ = nullptr;
|
HANDLE ready_ = nullptr;
|
||||||
|
HANDLE consumed_ = nullptr;
|
||||||
HANDLE process_ = nullptr;
|
HANDLE process_ = nullptr;
|
||||||
protocol::Channel* channel_ = nullptr;
|
protocol::Channel* channel_ = nullptr;
|
||||||
|
std::byte* data_ = nullptr;
|
||||||
|
bool alive_ = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace t5de
|
} // namespace t5de
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include "resources.hpp"
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
Texture::Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||||
|
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture::~Texture()
|
||||||
|
{
|
||||||
|
delete inner_; // through its virtual destructor: IMVU's code frees it
|
||||||
|
recorder_->destroyTexture(id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
imvu::Vec2i Texture::size() const { return inner_->size(); }
|
||||||
|
|
||||||
|
void Texture::updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions)
|
||||||
|
{
|
||||||
|
inner_->updateRegions(pixmap, regions);
|
||||||
|
recorder_->updateTexture(id_, pixmap); // all of it: the regions' layout isn't known
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::update(const imvu::Pixmap& pixmap)
|
||||||
|
{
|
||||||
|
inner_->update(pixmap);
|
||||||
|
recorder_->updateTexture(id_, pixmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
imvu::Pixmap* Texture::read(imvu::Pixmap* result) const { return inner_->read(result); }
|
||||||
|
|
||||||
|
IndexBuffer::IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||||
|
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexBuffer::~IndexBuffer()
|
||||||
|
{
|
||||||
|
delete inner_;
|
||||||
|
recorder_->destroyBuffer(id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
VertexBuffer::VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id)
|
||||||
|
: inner_(inner), recorder_(std::move(recorder)), id_(id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
VertexBuffer::~VertexBuffer()
|
||||||
|
{
|
||||||
|
delete inner_;
|
||||||
|
recorder_->destroyBuffer(id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VertexBuffer::update(const void* data, std::uint32_t stride, std::uint32_t elementSize, std::uint32_t elementCount)
|
||||||
|
{
|
||||||
|
inner_->update(data, stride, elementSize, elementCount);
|
||||||
|
recorder_->updateBuffer(id_, data, stride, elementSize, elementCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VertexBuffer::unknown2() { inner_->unknown2(); }
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
// The textures and buffers the adapter's engine hands to the client: each wraps the client renderer's own (which the
|
||||||
|
// client renderer still draws with) and mirrors it, by id, to the renderer process. The client deletes them through
|
||||||
|
// their virtual destructors.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "recorder.hpp"
|
||||||
|
#include "t5de/imvu/resources.hpp"
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
class Texture final : public imvu::PiRenderTexture {
|
||||||
|
public:
|
||||||
|
Texture(imvu::PiRenderTexture* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
|
||||||
|
~Texture() override;
|
||||||
|
|
||||||
|
imvu::Vec2i size() const override;
|
||||||
|
void updateRegions(const imvu::Pixmap& pixmap, const imvu::CompoundRegion& regions) override;
|
||||||
|
void update(const imvu::Pixmap& pixmap) override;
|
||||||
|
imvu::Pixmap* read(imvu::Pixmap* result) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] imvu::PiRenderTexture* inner() const { return inner_; }
|
||||||
|
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
imvu::PiRenderTexture* inner_;
|
||||||
|
std::shared_ptr<Recorder> recorder_;
|
||||||
|
std::uint32_t id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IndexBuffer final : public imvu::PiInternalIndexBuffer {
|
||||||
|
public:
|
||||||
|
IndexBuffer(imvu::PiInternalIndexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
|
||||||
|
~IndexBuffer() override;
|
||||||
|
|
||||||
|
[[nodiscard]] imvu::PiInternalIndexBuffer* inner() const { return inner_; }
|
||||||
|
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
imvu::PiInternalIndexBuffer* inner_;
|
||||||
|
std::shared_ptr<Recorder> recorder_;
|
||||||
|
std::uint32_t id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VertexBuffer final : public imvu::PiInternalVertexBuffer {
|
||||||
|
public:
|
||||||
|
VertexBuffer(imvu::PiInternalVertexBuffer* inner, std::shared_ptr<Recorder> recorder, std::uint32_t id);
|
||||||
|
~VertexBuffer() override;
|
||||||
|
|
||||||
|
void update(const void* data, std::uint32_t stride, std::uint32_t elementSize,
|
||||||
|
std::uint32_t elementCount) override;
|
||||||
|
void unknown2() override;
|
||||||
|
|
||||||
|
[[nodiscard]] imvu::PiInternalVertexBuffer* inner() const { return inner_; }
|
||||||
|
[[nodiscard]] std::uint32_t id() const { return id_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
imvu::PiInternalVertexBuffer* inner_;
|
||||||
|
std::shared_ptr<Recorder> recorder_;
|
||||||
|
std::uint32_t id_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The client renderer's object and our id for something the engine made; null and 0 for null.
|
||||||
|
inline imvu::PiRenderTexture* inner(imvu::PiRenderTexture* texture)
|
||||||
|
{
|
||||||
|
return texture != nullptr ? static_cast<Texture*>(texture)->inner() : nullptr;
|
||||||
|
}
|
||||||
|
inline std::uint32_t id(imvu::PiRenderTexture* texture)
|
||||||
|
{
|
||||||
|
return texture != nullptr ? static_cast<Texture*>(texture)->id() : 0;
|
||||||
|
}
|
||||||
|
inline imvu::PiInternalIndexBuffer* inner(imvu::PiInternalIndexBuffer* buffer)
|
||||||
|
{
|
||||||
|
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->inner() : nullptr;
|
||||||
|
}
|
||||||
|
inline std::uint32_t id(imvu::PiInternalIndexBuffer* buffer)
|
||||||
|
{
|
||||||
|
return buffer != nullptr ? static_cast<IndexBuffer*>(buffer)->id() : 0;
|
||||||
|
}
|
||||||
|
inline imvu::PiInternalVertexBuffer* inner(imvu::PiInternalVertexBuffer* buffer)
|
||||||
|
{
|
||||||
|
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->inner() : nullptr;
|
||||||
|
}
|
||||||
|
inline std::uint32_t id(imvu::PiInternalVertexBuffer* buffer)
|
||||||
|
{
|
||||||
|
return buffer != nullptr ? static_cast<VertexBuffer*>(buffer)->id() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
+16
-8
@@ -35,15 +35,23 @@ client exits.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
A spike: the adapter still has IMVU's Direct3D 9 engine draw the scene (the pass-through), and the renderer process
|
The renderer process draws the client's whole scene, over the right half of the 3D view, beside the client's own
|
||||||
only draws a box in the 3D view's corner, pulsing with the client's frames. Checked in the client (Direct3D 11 in
|
Direct3D renderer on the left (the adapter still forwards everything to it): `T5DE_RENDERER_VIEW=full` or `off`
|
||||||
software, on a machine without a graphics driver): the box draws over the scene from the 64-bit process, keeps in
|
changes that. Checked in rooms: the halves match, particles included.
|
||||||
step with the client's frames, clicks on it reach the scene, and each renderer process exits with its engine (room
|
|
||||||
changes) and with the client. The client keeps two engines alive in a room, so two processes (about 15 MB each);
|
|
||||||
one process serving all of a client's engines would be leaner.
|
|
||||||
|
|
||||||
Next: the protocol carrying each frame's calls and the resources (buffers, textures), and the renderer process
|
- The command stream (`include/t5de/protocol/commands.hpp`) carries state, transforms, lights, fog, textures,
|
||||||
drawing the scene itself, with IMVU's Direct3D engine left as the fallback.
|
buffers (copied when created or updated) and draws, in chunks through the channel; the adapter waits for each to
|
||||||
|
be taken, and gives up on a renderer process that stops taking them (the client's renderer carries on).
|
||||||
|
- The renderer process reproduces IMVU's Direct3D 9 setup (`renderer/shaders/fixed_function.hlsl`), particles
|
||||||
|
included (point sprites, drawn as instanced squares), with a pipeline per combination of states.
|
||||||
|
- Speed: on the development VM (software rendering) a room frame takes 8 to 60 ms to draw; the time from the
|
||||||
|
client finishing a frame to ours being presented averages about 60 ms. The renderer process draws only the
|
||||||
|
newest frame when it falls behind, and keeps at most one frame queued. Over Remote Desktop, the half drawn by the
|
||||||
|
renderer process (another process's window, composited by Windows) visibly trails the client's half by more
|
||||||
|
than that; that delay comes after presenting, and is to be checked on a real graphics card and display.
|
||||||
|
|
||||||
|
Next: render targets (offscreen frames are skipped), the renderer process drawing the whole view in place of the
|
||||||
|
client's renderer, and one renderer process for all of a client's views.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -71,7 +71,12 @@ virtual methods, IMVU's code only uses an engine through helpers that call its v
|
|||||||
vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations,
|
vertical field of view, Direct3D's 0 to 1 depth) and an orthographic one for 2D, orthonormal view rotations,
|
||||||
opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture
|
opaque clear colors, ambient light with zero alpha, linear fog (1 to 50), scrolling texture transforms. Furniture
|
||||||
vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter.
|
vertices are interleaved: positions and normals are 12-byte elements 32 bytes apart, so strides matter.
|
||||||
- Still unknown: point sprites (`drawPointSprites`, particles), render targets, `CompoundRegion` (texture update
|
- Particles (`drawPointSprites`): a Visual C++ 2010 `std::vector` of 16-byte points (position, color), drawn as
|
||||||
|
Direct3D 9 point sprites: squares viewport height / distance pixels across (Direct3D 9's defaults clamp them to
|
||||||
|
1 to 64; IMVU sets only the size, 1, and the scale factors 0, 0, 1), textured over the whole square.
|
||||||
|
- Fog: linear table fog. Direct3D 9 fogs by eye distance only for projections whose (3, 4) element is +1; IMVU's are
|
||||||
|
right-handed (-1), so it fogs by depth buffer value (0 to 1) against start 1 and end 50: in effect no fog.
|
||||||
|
- Still unknown: render targets, `CompoundRegion` (texture update
|
||||||
regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded
|
regions), `newRenderTexture`'s flags, and what `beginRendering` does with a null target. MSVC orders overloaded
|
||||||
virtual methods unlike their declaration, so our declarations never overload virtual methods.
|
virtual methods unlike their declaration, so our declarations never overload virtual methods.
|
||||||
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its
|
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its
|
||||||
|
|||||||
@@ -90,8 +90,21 @@ static_assert(sizeof(VertexAttributeArray<Vec3f>) == 12);
|
|||||||
// (width, height, pitch, rowBytes, pixels, ...).
|
// (width, height, pitch, rowBytes, pixels, ...).
|
||||||
class Pixmap;
|
class Pixmap;
|
||||||
|
|
||||||
// Unknown so far: used only by drawPointSprites (particles), as a Visual C++ 2010 std::vector<PointSprite>.
|
// A particle (drawPointSprites): a point drawn as a camera-facing square, textured, about viewport height / distance
|
||||||
struct PointSpriteVector;
|
// pixels across (1 to 64), as the Direct3D engine's point sprites are.
|
||||||
|
struct PointSprite {
|
||||||
|
Vec3f position;
|
||||||
|
Color color;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(PointSprite) == 16);
|
||||||
|
|
||||||
|
// std::vector<PointSprite> as Visual C++ 2010 lays it out (release builds): its elements are [first, last).
|
||||||
|
struct PointSpriteVector {
|
||||||
|
const PointSprite* first;
|
||||||
|
const PointSprite* last;
|
||||||
|
const PointSprite* end; // of the allocation
|
||||||
|
};
|
||||||
|
|
||||||
// Region lists IMVU passes to texture updates; unknown so far.
|
// Region lists IMVU passes to texture updates; unknown so far.
|
||||||
class CompoundRegion;
|
class CompoundRegion;
|
||||||
|
|||||||
@@ -7,19 +7,36 @@
|
|||||||
|
|
||||||
namespace t5de::protocol {
|
namespace t5de::protocol {
|
||||||
|
|
||||||
inline constexpr std::uint32_t version = 1;
|
inline constexpr std::uint32_t version = 2;
|
||||||
|
|
||||||
// A shared memory section the adapter creates for each engine. Its name is passed to the renderer process as
|
// Which part of the client's 3D view the renderer process draws (the client's own renderer draws all of it, under).
|
||||||
// `--channel <name>`; the event "<name>-frame" (auto-reset) is set after each frame the client draws.
|
enum class View : std::uint32_t {
|
||||||
|
Off, // none
|
||||||
|
Split, // the right half, to compare with the client's renderer on the left
|
||||||
|
Full,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A shared memory section the adapter creates for each engine, named by the renderer process's `--channel <name>`
|
||||||
|
// argument: this header, then `capacity` bytes of data. The adapter sends the command stream (commands.hpp) through
|
||||||
|
// it in chunks: it waits for the event "<name>-consumed", writes a chunk and sets "<name>-ready"; the renderer
|
||||||
|
// process copies the chunk out and sets "<name>-consumed" (both auto-reset; consumed starts set).
|
||||||
struct Channel {
|
struct Channel {
|
||||||
std::uint32_t version;
|
std::uint32_t version;
|
||||||
std::uint32_t closing; // nonzero: the engine is gone, the renderer process should exit
|
std::uint32_t closing; // nonzero: the engine is gone, the renderer process should exit
|
||||||
std::uint64_t window; // IMVU's 3D window (an HWND; window handles are valid in every process)
|
std::uint64_t window; // IMVU's 3D window (an HWND; window handles are valid in every process)
|
||||||
std::uint64_t frames; // frames the client has drawn
|
std::uint64_t frames; // frames the client has drawn
|
||||||
|
View view;
|
||||||
|
std::uint32_t capacity;
|
||||||
|
std::uint32_t chunkSize;
|
||||||
|
std::uint32_t chunkFlags; // chunkEndsFrame
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(Channel) == 24, "same layout in 32-bit and 64-bit builds");
|
static_assert(sizeof(Channel) == 40, "same layout in 32-bit and 64-bit builds");
|
||||||
|
|
||||||
inline constexpr wchar_t frameEventSuffix[] = L"-frame";
|
inline constexpr std::uint32_t chunkEndsFrame = 1;
|
||||||
|
inline constexpr std::uint32_t channelCapacity = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
inline constexpr wchar_t readyEventSuffix[] = L"-ready";
|
||||||
|
inline constexpr wchar_t consumedEventSuffix[] = L"-consumed";
|
||||||
|
|
||||||
} // namespace t5de::protocol
|
} // namespace t5de::protocol
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
#pragma once
|
||||||
|
// The command stream: what the adapter records from IMVU's renderer interface, and the renderer process carries out.
|
||||||
|
// Each command is a Header, its fixed-size body, then for some a variable-size payload (pixels, vertex data),
|
||||||
|
// padded to 4 bytes. Built for 32-bit and 64-bit: fixed-size fields only.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace t5de::protocol {
|
||||||
|
|
||||||
|
enum class Op : std::uint16_t {
|
||||||
|
BeginFrame = 1,
|
||||||
|
EndFrame,
|
||||||
|
Clear,
|
||||||
|
SetProjection,
|
||||||
|
SetModel,
|
||||||
|
SetView,
|
||||||
|
SetTextures,
|
||||||
|
SetState,
|
||||||
|
SetLight,
|
||||||
|
SetFog,
|
||||||
|
SetTextureTransform,
|
||||||
|
CreateTexture,
|
||||||
|
UpdateTexture,
|
||||||
|
DestroyTexture,
|
||||||
|
CreateBuffer,
|
||||||
|
UpdateBuffer,
|
||||||
|
DestroyBuffer,
|
||||||
|
DrawTriangles,
|
||||||
|
DrawArrays,
|
||||||
|
DrawPointSprites,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
Op op;
|
||||||
|
std::uint16_t reserved;
|
||||||
|
std::uint32_t size; // of the body and payload, padded to 4 bytes
|
||||||
|
};
|
||||||
|
|
||||||
|
// Colors are 0xAARRGGBB. Resource ids are the adapter's, per engine; 0 means none.
|
||||||
|
|
||||||
|
struct BeginFrame {
|
||||||
|
std::uint32_t offscreen; // drawn into a render target, not the window: nothing to show
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EndFrame {
|
||||||
|
std::uint64_t sentAt; // the adapter's QueryPerformanceCounter (the same clock in every process)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Clear {
|
||||||
|
std::uint32_t flags; // 1: color, 2: depth (to 1)
|
||||||
|
std::uint32_t color;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The viewport's size, and a Direct3D projection matrix (row-major, row vectors).
|
||||||
|
struct SetProjection {
|
||||||
|
std::int32_t width;
|
||||||
|
std::int32_t height;
|
||||||
|
float m[16];
|
||||||
|
};
|
||||||
|
|
||||||
|
// An affine transform (SetModel, SetView): a 3x3 matrix, row-major, then the translation.
|
||||||
|
struct SetTransform {
|
||||||
|
float m[12];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SetTextures {
|
||||||
|
std::uint32_t texture0;
|
||||||
|
std::uint32_t texture1;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class State : std::uint32_t {
|
||||||
|
DepthTest,
|
||||||
|
DepthWrite,
|
||||||
|
Blending,
|
||||||
|
BlendMode, // 1: alpha, 2: additive, 3: premultiplied, 4: additive alpha
|
||||||
|
AlphaTest,
|
||||||
|
AlphaReference, // 0-255: pixels pass if their alpha is greater
|
||||||
|
Culling,
|
||||||
|
NormalInverting, // flips which winding faces front
|
||||||
|
Lighting,
|
||||||
|
Fog,
|
||||||
|
Ambient, // a color
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SetState {
|
||||||
|
State state;
|
||||||
|
std::uint32_t value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SetLight {
|
||||||
|
std::uint32_t index;
|
||||||
|
std::uint32_t enabled;
|
||||||
|
std::uint32_t color;
|
||||||
|
std::uint32_t directional; // 0: a point light at vector; 1: light coming from direction vector
|
||||||
|
float vector[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SetFog {
|
||||||
|
float start;
|
||||||
|
float end;
|
||||||
|
std::uint32_t color;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Texture coordinates for a stage: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5].
|
||||||
|
struct SetTextureTransform {
|
||||||
|
std::uint32_t stage;
|
||||||
|
std::uint32_t identity;
|
||||||
|
float m[6];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Payload: width * height pixels (0xAARRGGBB), rows packed.
|
||||||
|
struct CreateTexture {
|
||||||
|
std::uint32_t id;
|
||||||
|
std::uint32_t width;
|
||||||
|
std::uint32_t height;
|
||||||
|
std::uint32_t flags; // 2: wrap u, 4: wrap v (else clamp)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UpdateTexture {
|
||||||
|
std::uint32_t id;
|
||||||
|
std::uint32_t width;
|
||||||
|
std::uint32_t height;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DestroyTexture {
|
||||||
|
std::uint32_t id;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BufferKind : std::uint32_t {
|
||||||
|
Index, // 16-bit indices
|
||||||
|
Vertex, // positions and normals (3 floats), texture coordinates (2 floats) or colors (0xAARRGGBB)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Payload: count elements of elementSize bytes, packed; empty for a buffer filled later (UpdateBuffer).
|
||||||
|
struct CreateBuffer {
|
||||||
|
std::uint32_t id;
|
||||||
|
BufferKind kind;
|
||||||
|
std::uint32_t elementSize;
|
||||||
|
std::uint32_t count;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UpdateBuffer {
|
||||||
|
std::uint32_t id;
|
||||||
|
std::uint32_t elementSize;
|
||||||
|
std::uint32_t count;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DestroyBuffer {
|
||||||
|
std::uint32_t id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triangles from buffers: indices and positions are required, the others optional.
|
||||||
|
struct DrawTriangles {
|
||||||
|
std::uint32_t indices;
|
||||||
|
std::uint32_t positions;
|
||||||
|
std::uint32_t normals;
|
||||||
|
std::uint32_t texCoords;
|
||||||
|
std::uint32_t colors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triangles from data in the payload, packed: indexCount 16-bit indices (padded to 4 bytes), then vertexCount
|
||||||
|
// positions, and normals, texture coordinates and colors if present.
|
||||||
|
struct DrawArrays {
|
||||||
|
std::uint32_t vertexCount;
|
||||||
|
std::uint32_t indexCount;
|
||||||
|
std::uint32_t hasNormals;
|
||||||
|
std::uint32_t hasTexCoords;
|
||||||
|
std::uint32_t hasColors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Particles: payload count sprites of 16 bytes, a position (3 floats) and a color. Each is a camera-facing square,
|
||||||
|
// viewport height / distance pixels across (1 to 64), textured with the first texture over the whole square.
|
||||||
|
struct DrawPointSprites {
|
||||||
|
std::uint32_t count;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(Header) == 8 && sizeof(SetProjection) == 72 && sizeof(SetLight) == 28
|
||||||
|
&& sizeof(DrawArrays) == 20);
|
||||||
|
|
||||||
|
inline constexpr std::size_t padded(std::size_t size) { return (size + 3) & ~std::size_t{3}; }
|
||||||
|
|
||||||
|
// Appends commands to a byte buffer.
|
||||||
|
class Writer {
|
||||||
|
public:
|
||||||
|
// Appends a command and returns its payload, payloadSize bytes for the caller to fill.
|
||||||
|
template <class Body>
|
||||||
|
std::byte* add(Op op, const Body& body, std::size_t payloadSize = 0)
|
||||||
|
{
|
||||||
|
static_assert(sizeof(Body) % 4 == 0);
|
||||||
|
const std::size_t size = sizeof(Body) + padded(payloadSize);
|
||||||
|
const std::size_t start = bytes_.size();
|
||||||
|
bytes_.resize(start + sizeof(Header) + size);
|
||||||
|
const Header header{op, 0, static_cast<std::uint32_t>(size)};
|
||||||
|
std::memcpy(bytes_.data() + start, &header, sizeof(Header));
|
||||||
|
std::memcpy(bytes_.data() + start + sizeof(Header), &body, sizeof(Body));
|
||||||
|
return bytes_.data() + start + sizeof(Header) + sizeof(Body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const std::byte> bytes() const { return bytes_; }
|
||||||
|
[[nodiscard]] std::size_t size() const { return bytes_.size(); }
|
||||||
|
void clear() { bytes_.clear(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::byte> bytes_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One command, as read back.
|
||||||
|
class Command {
|
||||||
|
public:
|
||||||
|
Command(Op op, std::span<const std::byte> data) : op_(op), data_(data) {}
|
||||||
|
|
||||||
|
[[nodiscard]] Op op() const { return op_; }
|
||||||
|
|
||||||
|
// The body, if the command is long enough to hold one.
|
||||||
|
template <class Body>
|
||||||
|
[[nodiscard]] std::optional<Body> body() const
|
||||||
|
{
|
||||||
|
if (data_.size() < sizeof(Body)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
Body body;
|
||||||
|
std::memcpy(&body, data_.data(), sizeof(Body));
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Body>
|
||||||
|
[[nodiscard]] std::span<const std::byte> payload() const
|
||||||
|
{
|
||||||
|
return data_.size() < sizeof(Body) ? std::span<const std::byte>{} : data_.subspan(sizeof(Body));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Op op_;
|
||||||
|
std::span<const std::byte> data_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reads commands back from a byte buffer; stops at the end, or at a truncated command.
|
||||||
|
class Reader {
|
||||||
|
public:
|
||||||
|
explicit Reader(std::span<const std::byte> bytes) : bytes_(bytes) {}
|
||||||
|
|
||||||
|
std::optional<Command> next()
|
||||||
|
{
|
||||||
|
if (bytes_.size() < sizeof(Header)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
Header header;
|
||||||
|
std::memcpy(&header, bytes_.data(), sizeof(Header));
|
||||||
|
if (bytes_.size() - sizeof(Header) < header.size) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
Command command(header.op, bytes_.subspan(sizeof(Header), header.size));
|
||||||
|
bytes_ = bytes_.subspan(sizeof(Header) + header.size);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::span<const std::byte> bytes_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace t5de::protocol
|
||||||
+14
-1
@@ -20,8 +20,21 @@ FetchContent_Declare(DiligentCore
|
|||||||
SYSTEM)
|
SYSTEM)
|
||||||
FetchContent_MakeAvailable(DiligentCore)
|
FetchContent_MakeAvailable(DiligentCore)
|
||||||
|
|
||||||
add_executable(t5de_renderer WIN32 main.cpp)
|
# Shader sources, embedded in the program (Diligent compiles them at run time): each shaders/<name>.hlsl becomes
|
||||||
|
# <name>_hlsl.hpp, defining <camelCaseName>Hlsl.
|
||||||
|
function(t5de_embed_shader file name)
|
||||||
|
set(T5DE_SHADER_FILE ${file})
|
||||||
|
set(T5DE_SHADER_NAME ${name})
|
||||||
|
file(READ ${CMAKE_CURRENT_SOURCE_DIR}/shaders/${file} T5DE_SHADER_SOURCE)
|
||||||
|
get_filename_component(stem ${file} NAME_WE)
|
||||||
|
configure_file(shaders/embed.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/generated/${stem}_hlsl.hpp @ONLY)
|
||||||
|
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS shaders/${file})
|
||||||
|
endfunction()
|
||||||
|
t5de_embed_shader(fixed_function.hlsl fixedFunctionHlsl)
|
||||||
|
|
||||||
|
add_executable(t5de_renderer WIN32 log.cpp main.cpp scene.cpp)
|
||||||
set_target_properties(t5de_renderer PROPERTIES OUTPUT_NAME t5de-renderer)
|
set_target_properties(t5de_renderer PROPERTIES OUTPUT_NAME t5de-renderer)
|
||||||
|
target_include_directories(t5de_renderer PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
||||||
target_link_libraries(t5de_renderer PRIVATE t5de::protocol t5de::options Diligent-GraphicsEngineD3D11-static Diligent-Common)
|
target_link_libraries(t5de_renderer PRIVATE t5de::protocol t5de::options Diligent-GraphicsEngineD3D11-static Diligent-Common)
|
||||||
|
|
||||||
install(TARGETS t5de_renderer RUNTIME DESTINATION .)
|
install(TARGETS t5de_renderer RUNTIME DESTINATION .)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace t5de::log {
|
||||||
|
|
||||||
|
// Each line is one appending write, so lines from several processes don't mix.
|
||||||
|
void write(std::string_view line)
|
||||||
|
{
|
||||||
|
static const HANDLE file = [] {
|
||||||
|
wchar_t temp[MAX_PATH + 1];
|
||||||
|
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||||
|
if (length == 0 || length > MAX_PATH) {
|
||||||
|
return INVALID_HANDLE_VALUE;
|
||||||
|
}
|
||||||
|
return CreateFileW((std::filesystem::path(temp) / L"t5de-renderer-process.log").c_str(), FILE_APPEND_DATA,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS,
|
||||||
|
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
}();
|
||||||
|
if (file != INVALID_HANDLE_VALUE) {
|
||||||
|
const std::string text = std::format("{}: {}\n", GetCurrentProcessId(), line);
|
||||||
|
DWORD written = 0;
|
||||||
|
WriteFile(file, text.data(), static_cast<DWORD>(text.size()), &written, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace t5de::log
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
// The renderer processes' log: %TEMP%\t5de-renderer-process.log, shared by all of them (lines start with the pid).
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace t5de::log {
|
||||||
|
|
||||||
|
void write(std::string_view line);
|
||||||
|
|
||||||
|
template <class... Args>
|
||||||
|
void info(std::format_string<Args...> format, Args&&... args)
|
||||||
|
{
|
||||||
|
write(std::format(format, std::forward<Args>(args)...));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace t5de::log
|
||||||
+127
-25
@@ -1,31 +1,45 @@
|
|||||||
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll) for each of the client's
|
// t5de-renderer.exe: the renderer process. Started by the adapter (t5deRenderEngine.dll) for each of the client's
|
||||||
// 3D views; draws into a child window it places in the client's window, frame by frame as the client draws.
|
// 3D views: receives the client's drawing as commands through a shared memory channel, and draws it with Diligent
|
||||||
//
|
// into a child window it places in the client's window (over the right half, for now, to compare with the client's
|
||||||
// For now a spike proving the architecture: a small box in the 3D view's corner, its color changing with each frame
|
// own renderer on the left).
|
||||||
// the client draws, drawn with Diligent (Direct3D 11) from this 64-bit process.
|
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "EngineFactoryD3D11.h"
|
#include "EngineFactoryD3D11.h"
|
||||||
#include "RefCntAutoPtr.hpp"
|
#include "RefCntAutoPtr.hpp"
|
||||||
|
#include "RenderDeviceD3D11.h"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "scene.hpp"
|
||||||
#include "t5de/protocol/channel.hpp"
|
#include "t5de/protocol/channel.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using namespace Diligent;
|
using namespace Diligent;
|
||||||
|
using t5de::protocol::Channel;
|
||||||
|
using t5de::protocol::View;
|
||||||
|
|
||||||
// The box: where in the client's 3D view, and how big.
|
using t5de::log::info;
|
||||||
constexpr int boxX = 16;
|
|
||||||
constexpr int boxY = 16;
|
// Diligent's messages (shader compilation errors, invalid states, ...), into the log.
|
||||||
constexpr int boxWidth = 240;
|
void DILIGENT_CALL_TYPE diligent_message(DEBUG_MESSAGE_SEVERITY severity, const Char* message, const Char* function,
|
||||||
constexpr int boxHeight = 80;
|
const Char*, int)
|
||||||
|
{
|
||||||
|
static constexpr const char* severities[] = {"info", "warning", "error", "fatal error"};
|
||||||
|
info("Diligent {}: {}{}{}", severities[severity < 4 ? severity : 3], function != nullptr ? function : "",
|
||||||
|
function != nullptr ? ": " : "", message != nullptr ? message : "");
|
||||||
|
}
|
||||||
|
|
||||||
struct Arguments {
|
struct Arguments {
|
||||||
std::wstring channel;
|
std::wstring channel;
|
||||||
@@ -67,7 +81,18 @@ HWND create_child_window(HWND parent)
|
|||||||
.lpszClassName = L"T5DERenderer"};
|
.lpszClassName = L"T5DERenderer"};
|
||||||
RegisterClassExW(&windowClass);
|
RegisterClassExW(&windowClass);
|
||||||
return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS,
|
return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS,
|
||||||
boxX, boxY, boxWidth, boxHeight, parent, nullptr, windowClass.hInstance, nullptr);
|
0, 0, 1, 1, parent, nullptr, windowClass.hInstance, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where the child window goes in the client's 3D view.
|
||||||
|
RECT placement(HWND parent, View view)
|
||||||
|
{
|
||||||
|
RECT client{};
|
||||||
|
GetClientRect(parent, &client);
|
||||||
|
if (view == View::Split) {
|
||||||
|
client.left = client.right / 2;
|
||||||
|
}
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -80,37 +105,74 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|||||||
}
|
}
|
||||||
HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, arguments.channel.c_str());
|
HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, arguments.channel.c_str());
|
||||||
auto* channel = mapping == nullptr ? nullptr
|
auto* channel = mapping == nullptr ? nullptr
|
||||||
: static_cast<t5de::protocol::Channel*>(MapViewOfFile(
|
: static_cast<Channel*>(MapViewOfFile(mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0));
|
||||||
mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(t5de::protocol::Channel)));
|
HANDLE ready = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::readyEventSuffix).c_str());
|
||||||
HANDLE frameEvent = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::frameEventSuffix).c_str());
|
HANDLE consumed =
|
||||||
|
OpenEventW(EVENT_MODIFY_STATE, FALSE, (arguments.channel + t5de::protocol::consumedEventSuffix).c_str());
|
||||||
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
|
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
|
||||||
if (channel == nullptr || frameEvent == nullptr || parent == nullptr || channel->version != t5de::protocol::version) {
|
if (channel == nullptr || ready == nullptr || consumed == nullptr || parent == nullptr
|
||||||
|
|| channel->version != t5de::protocol::version) {
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
const auto* data = reinterpret_cast<const std::byte*>(channel + 1);
|
||||||
|
|
||||||
HWND window = create_child_window(reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window)));
|
const HWND clientWindow = reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window));
|
||||||
|
const HWND window = create_child_window(clientWindow);
|
||||||
if (window == nullptr) {
|
if (window == nullptr) {
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info("started for window {:#x}, view {}", channel->window, static_cast<int>(channel->view));
|
||||||
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
|
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
|
||||||
|
factory->SetMessageCallback(diligent_message);
|
||||||
RefCntAutoPtr<IRenderDevice> device;
|
RefCntAutoPtr<IRenderDevice> device;
|
||||||
RefCntAutoPtr<IDeviceContext> context;
|
RefCntAutoPtr<IDeviceContext> context;
|
||||||
EngineD3D11CreateInfo engineInfo;
|
EngineD3D11CreateInfo engineInfo;
|
||||||
factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context);
|
factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
|
info("can't create a Direct3D 11 device");
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
// Show each frame as soon as it's drawn: Direct3D 11 otherwise lets 3 frames queue, which the view would trail by.
|
||||||
|
if (RefCntAutoPtr<IRenderDeviceD3D11> d3d11{device, IID_RenderDeviceD3D11}) {
|
||||||
|
IDXGIDevice1* dxgi = nullptr;
|
||||||
|
if (SUCCEEDED(d3d11->GetD3D11Device()->QueryInterface(__uuidof(IDXGIDevice1), reinterpret_cast<void**>(&dxgi)))) {
|
||||||
|
dxgi->SetMaximumFrameLatency(1);
|
||||||
|
dxgi->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
RefCntAutoPtr<ISwapChain> swapChain;
|
RefCntAutoPtr<ISwapChain> swapChain;
|
||||||
SwapChainDesc swapChainDesc;
|
SwapChainDesc swapChainDesc;
|
||||||
|
swapChainDesc.ColorBufferFormat = TEX_FORMAT_RGBA8_UNORM; // no sRGB conversion, like the client's renderer
|
||||||
|
swapChainDesc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT;
|
||||||
factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window},
|
factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window},
|
||||||
&swapChain);
|
&swapChain);
|
||||||
if (!swapChain) {
|
if (!swapChain) {
|
||||||
|
info("can't create a swap chain");
|
||||||
return 6;
|
return 6;
|
||||||
}
|
}
|
||||||
|
t5de::Scene scene(device, context, swapChain);
|
||||||
|
|
||||||
|
// Takes the chunk waiting in the channel (after "ready"), and lets the adapter send the next: appends it to
|
||||||
|
// frame, and returns whether it ends a frame.
|
||||||
|
std::vector<std::byte> frame;
|
||||||
|
const auto take_chunk = [&] {
|
||||||
|
const std::uint32_t size = channel->chunkSize <= channel->capacity ? channel->chunkSize : 0;
|
||||||
|
const bool endsFrame = (channel->chunkFlags & t5de::protocol::chunkEndsFrame) != 0;
|
||||||
|
frame.insert(frame.end(), data, data + size);
|
||||||
|
SetEvent(consumed);
|
||||||
|
return endsFrame;
|
||||||
|
};
|
||||||
|
|
||||||
|
LARGE_INTEGER frequency;
|
||||||
|
QueryPerformanceFrequency(&frequency);
|
||||||
|
double drawTime = 0;
|
||||||
|
double latency = 0;
|
||||||
|
int drawn = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
RECT placed{};
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const HANDLE handles[] = {frameEvent, parent};
|
const HANDLE handles[] = {ready, parent};
|
||||||
const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT);
|
const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT);
|
||||||
MSG message;
|
MSG message;
|
||||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||||
@@ -119,19 +181,59 @@ int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
|||||||
if (result == WAIT_OBJECT_0 + 1 || channel->closing != 0) {
|
if (result == WAIT_OBJECT_0 + 1 || channel->closing != 0) {
|
||||||
break; // the client exited, or the engine is gone
|
break; // the client exited, or the engine is gone
|
||||||
}
|
}
|
||||||
if (result != WAIT_OBJECT_0) {
|
if (result != WAIT_OBJECT_0 || !take_chunk()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// T5DE gold, pulsing with the client's frames.
|
// If the client has sent more frames meanwhile (drawing fell behind), catch up: carry out the older ones'
|
||||||
const float pulse = 0.5f + 0.5f * std::sin(static_cast<float>(channel->frames) * 0.1f);
|
// changes without drawing them, and draw only the newest. (An incomplete frame stays for the next round.)
|
||||||
const float color[] = {1.0f, 0.82f * pulse + 0.1f, 0.0f, 1.0f};
|
std::size_t complete = frame.size();
|
||||||
ITextureView* backBuffer = swapChain->GetCurrentBackBufferRTV();
|
while (WaitForSingleObject(ready, 0) == WAIT_OBJECT_0) {
|
||||||
context->SetRenderTargets(1, &backBuffer, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
bool ended = take_chunk();
|
||||||
context->ClearRenderTarget(backBuffer, color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
while (!ended && WaitForSingleObject(ready, 1000) == WAIT_OBJECT_0) {
|
||||||
swapChain->Present(0);
|
ended = take_chunk();
|
||||||
|
}
|
||||||
|
if (!ended) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
scene.execute(std::span(frame).first(complete), 0, false);
|
||||||
|
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
|
||||||
|
complete = frame.size();
|
||||||
|
++skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow the client's 3D view's size.
|
||||||
|
const RECT place = placement(clientWindow, channel->view);
|
||||||
|
const int width = place.right - place.left;
|
||||||
|
const int height = place.bottom - place.top;
|
||||||
|
if (std::memcmp(&place, &placed, sizeof(RECT)) != 0 && width > 0 && height > 0) {
|
||||||
|
SetWindowPos(window, nullptr, place.left, place.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
swapChain->Resize(static_cast<Uint32>(width), static_cast<Uint32>(height));
|
||||||
|
placed = place;
|
||||||
|
info("drawing {}x{} at {} in the client's 3D view", width, height, place.left);
|
||||||
|
}
|
||||||
|
LARGE_INTEGER start, end;
|
||||||
|
QueryPerformanceCounter(&start);
|
||||||
|
if (scene.execute(std::span(frame).first(complete), place.left, width > 0 && height > 0)) {
|
||||||
|
swapChain->Present(0);
|
||||||
|
QueryPerformanceCounter(&end);
|
||||||
|
drawTime += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
|
||||||
|
latency += static_cast<double>(end.QuadPart - static_cast<LONGLONG>(scene.lastSentAt())) /
|
||||||
|
static_cast<double>(frequency.QuadPart);
|
||||||
|
if (++drawn == 300) {
|
||||||
|
info("last 300 frames: {:.1f} ms each to draw and present, {:.1f} ms from the client's end of frame to "
|
||||||
|
"presented, {} skipped to catch up",
|
||||||
|
drawTime * 1000 / drawn, latency * 1000 / drawn, skipped);
|
||||||
|
drawTime = 0;
|
||||||
|
latency = 0;
|
||||||
|
drawn = 0;
|
||||||
|
skipped = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame.erase(frame.begin(), frame.begin() + static_cast<std::ptrdiff_t>(complete));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info("exiting");
|
||||||
DestroyWindow(window);
|
DestroyWindow(window);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,686 @@
|
|||||||
|
#include "scene.hpp"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "fixed_function_hlsl.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
using namespace Diligent;
|
||||||
|
using protocol::Op;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void set_identity(float (&m)[16])
|
||||||
|
{
|
||||||
|
std::memset(m, 0, sizeof(m));
|
||||||
|
m[0] = m[5] = m[10] = m[15] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An affine transform (3x3 row-major, then translation) as a 4x4 matrix for row vectors.
|
||||||
|
void set_transform(float (&m)[16], const float (&t)[12])
|
||||||
|
{
|
||||||
|
for (int row = 0; row < 3; ++row) {
|
||||||
|
m[row * 4 + 0] = t[row * 3 + 0];
|
||||||
|
m[row * 4 + 1] = t[row * 3 + 1];
|
||||||
|
m[row * 4 + 2] = t[row * 3 + 2];
|
||||||
|
m[row * 4 + 3] = 0;
|
||||||
|
}
|
||||||
|
m[12] = t[9];
|
||||||
|
m[13] = t[10];
|
||||||
|
m[14] = t[11];
|
||||||
|
m[15] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5], as two rows dotted with (u, v, 1).
|
||||||
|
void set_texture_transform(float (&rows)[8], const float (&m)[6])
|
||||||
|
{
|
||||||
|
const float values[8] = {m[0], m[1], m[4], 0, m[2], m[3], m[5], 0};
|
||||||
|
std::memcpy(rows, values, sizeof(rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_color(float (&rgba)[4], std::uint32_t color)
|
||||||
|
{
|
||||||
|
rgba[0] = static_cast<float>((color >> 16) & 0xff) / 255.0f;
|
||||||
|
rgba[1] = static_cast<float>((color >> 8) & 0xff) / 255.0f;
|
||||||
|
rgba[2] = static_cast<float>(color & 0xff) / 255.0f;
|
||||||
|
rgba[3] = static_cast<float>(color >> 24) / 255.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::uint32_t hasNormals = 1;
|
||||||
|
constexpr std::uint32_t hasTexCoords = 2;
|
||||||
|
constexpr std::uint32_t hasColors = 4;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain)
|
||||||
|
: device_(device), context_(context), swapChain_(swapChain)
|
||||||
|
{
|
||||||
|
BufferDesc constants;
|
||||||
|
constants.Name = "Constants";
|
||||||
|
constants.Size = sizeof(Constants);
|
||||||
|
constants.Usage = USAGE_DYNAMIC;
|
||||||
|
constants.BindFlags = BIND_UNIFORM_BUFFER;
|
||||||
|
constants.CPUAccessFlags = CPU_ACCESS_WRITE;
|
||||||
|
device_->CreateBuffer(constants, nullptr, &constantBuffer_);
|
||||||
|
|
||||||
|
// IMVU's textures: bilinear, no mipmaps, wrapped or clamped per texture.
|
||||||
|
for (int wrap = 0; wrap < 4; ++wrap) {
|
||||||
|
SamplerDesc sampler;
|
||||||
|
sampler.MinFilter = FILTER_TYPE_LINEAR;
|
||||||
|
sampler.MagFilter = FILTER_TYPE_LINEAR;
|
||||||
|
sampler.MipFilter = FILTER_TYPE_POINT;
|
||||||
|
sampler.AddressU = (wrap & 1) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
|
||||||
|
sampler.AddressV = (wrap & 2) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
|
||||||
|
device_->CreateSampler(sampler, &samplers_[wrap]);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_identity(constants_.world);
|
||||||
|
set_identity(constants_.view);
|
||||||
|
set_identity(constants_.projection);
|
||||||
|
const float identity[6] = {1, 0, 0, 1, 0, 0};
|
||||||
|
set_texture_transform(constants_.texTransform0, identity);
|
||||||
|
set_texture_transform(constants_.texTransform1, identity);
|
||||||
|
constants_.flags[2] = 127; // the client's alpha reference
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
|
||||||
|
{
|
||||||
|
offsetX_ = offsetX;
|
||||||
|
draw_ = draw;
|
||||||
|
bool drew = false;
|
||||||
|
protocol::Reader reader(commands);
|
||||||
|
while (const auto command = reader.next()) {
|
||||||
|
switch (command->op()) {
|
||||||
|
case Op::BeginFrame:
|
||||||
|
if (const auto begin = command->body<protocol::BeginFrame>()) {
|
||||||
|
onscreen_ = begin->offscreen == 0 && draw_;
|
||||||
|
if (onscreen_) {
|
||||||
|
ITextureView* target = swapChain_->GetCurrentBackBufferRTV();
|
||||||
|
context_->SetRenderTargets(1, &target, swapChain_->GetDepthBufferDSV(),
|
||||||
|
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::EndFrame:
|
||||||
|
if (const auto end = command->body<protocol::EndFrame>()) {
|
||||||
|
lastSentAt_ = end->sentAt;
|
||||||
|
}
|
||||||
|
drew = drew || onscreen_;
|
||||||
|
onscreen_ = false;
|
||||||
|
break;
|
||||||
|
case Op::Clear:
|
||||||
|
if (const auto clear = command->body<protocol::Clear>(); clear && onscreen_) {
|
||||||
|
if ((clear->flags & 1) != 0) {
|
||||||
|
float color[4];
|
||||||
|
set_color(color, clear->color);
|
||||||
|
context_->ClearRenderTarget(swapChain_->GetCurrentBackBufferRTV(), color,
|
||||||
|
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||||
|
}
|
||||||
|
if ((clear->flags & 2) != 0) {
|
||||||
|
context_->ClearDepthStencil(swapChain_->GetDepthBufferDSV(), CLEAR_DEPTH_FLAG, 1.0f, 0,
|
||||||
|
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetProjection:
|
||||||
|
if (const auto projection = command->body<protocol::SetProjection>()) {
|
||||||
|
setProjection(*projection);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetModel:
|
||||||
|
if (const auto model = command->body<protocol::SetTransform>()) {
|
||||||
|
set_transform(constants_.world, model->m);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetView:
|
||||||
|
if (const auto view = command->body<protocol::SetTransform>()) {
|
||||||
|
set_transform(constants_.view, view->m);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetTextures:
|
||||||
|
if (const auto textures = command->body<protocol::SetTextures>()) {
|
||||||
|
state_.texture0 = textures->texture0;
|
||||||
|
state_.texture1 = textures->texture1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetState:
|
||||||
|
if (const auto state = command->body<protocol::SetState>()) {
|
||||||
|
setState(*state);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetLight:
|
||||||
|
if (const auto light = command->body<protocol::SetLight>(); light && light->index < 8) {
|
||||||
|
set_color(constants_.lightColor[light->index], light->color);
|
||||||
|
constants_.lightColor[light->index][3] = light->enabled != 0 ? 1.0f : 0.0f;
|
||||||
|
std::memcpy(constants_.lightVector[light->index], light->vector, sizeof(light->vector));
|
||||||
|
constants_.lightVector[light->index][3] = light->directional != 0 ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetFog:
|
||||||
|
if (const auto fog = command->body<protocol::SetFog>()) {
|
||||||
|
constants_.fog[0] = fog->start;
|
||||||
|
constants_.fog[1] = fog->end;
|
||||||
|
set_color(constants_.fogColor, fog->color);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::SetTextureTransform:
|
||||||
|
if (const auto transform = command->body<protocol::SetTextureTransform>(); transform && transform->stage < 2) {
|
||||||
|
set_texture_transform(transform->stage == 0 ? constants_.texTransform0 : constants_.texTransform1,
|
||||||
|
transform->m);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::CreateTexture:
|
||||||
|
createTexture(*command);
|
||||||
|
break;
|
||||||
|
case Op::UpdateTexture:
|
||||||
|
updateTexture(*command);
|
||||||
|
break;
|
||||||
|
case Op::DestroyTexture:
|
||||||
|
if (const auto destroy = command->body<protocol::DestroyTexture>()) {
|
||||||
|
textures_.erase(destroy->id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::CreateBuffer:
|
||||||
|
createBuffer(*command);
|
||||||
|
break;
|
||||||
|
case Op::UpdateBuffer:
|
||||||
|
updateBuffer(*command);
|
||||||
|
break;
|
||||||
|
case Op::DestroyBuffer:
|
||||||
|
if (const auto destroy = command->body<protocol::DestroyBuffer>()) {
|
||||||
|
buffers_.erase(destroy->id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::DrawTriangles:
|
||||||
|
if (const auto triangles = command->body<protocol::DrawTriangles>(); triangles && onscreen_) {
|
||||||
|
drawTriangles(*triangles);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::DrawArrays:
|
||||||
|
if (onscreen_) {
|
||||||
|
drawArrays(*command);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::DrawPointSprites:
|
||||||
|
if (onscreen_) {
|
||||||
|
drawPointSprites(*command);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return drew;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::setProjection(const protocol::SetProjection& projection)
|
||||||
|
{
|
||||||
|
std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection));
|
||||||
|
constants_.viewport[0] = static_cast<float>(projection.width);
|
||||||
|
constants_.viewport[1] = static_cast<float>(projection.height);
|
||||||
|
if (!onscreen_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The client's projection covers its whole 3D view; the window shows part of it, from offsetX.
|
||||||
|
const SwapChainDesc& window = swapChain_->GetDesc();
|
||||||
|
Viewport viewport;
|
||||||
|
viewport.TopLeftX = static_cast<float>(-offsetX_);
|
||||||
|
viewport.TopLeftY = 0;
|
||||||
|
viewport.Width = static_cast<float>(projection.width);
|
||||||
|
viewport.Height = static_cast<float>(projection.height);
|
||||||
|
viewport.MinDepth = 0;
|
||||||
|
viewport.MaxDepth = 1;
|
||||||
|
context_->SetViewports(1, &viewport, window.Width, window.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::setState(const protocol::SetState& state)
|
||||||
|
{
|
||||||
|
const bool on = state.value != 0;
|
||||||
|
switch (state.state) {
|
||||||
|
case protocol::State::DepthTest:
|
||||||
|
state_.depthTest = on;
|
||||||
|
break;
|
||||||
|
case protocol::State::DepthWrite:
|
||||||
|
state_.depthWrite = on;
|
||||||
|
break;
|
||||||
|
case protocol::State::Blending:
|
||||||
|
state_.blending = on;
|
||||||
|
break;
|
||||||
|
case protocol::State::BlendMode:
|
||||||
|
if (state.value >= 1 && state.value <= 4) {
|
||||||
|
state_.blendMode = state.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case protocol::State::AlphaTest:
|
||||||
|
constants_.flags[1] = on ? 1.0f : 0.0f;
|
||||||
|
break;
|
||||||
|
case protocol::State::AlphaReference:
|
||||||
|
constants_.flags[2] = static_cast<float>(state.value);
|
||||||
|
break;
|
||||||
|
case protocol::State::Culling:
|
||||||
|
state_.culling = on;
|
||||||
|
break;
|
||||||
|
case protocol::State::NormalInverting:
|
||||||
|
state_.normalInverting = on;
|
||||||
|
break;
|
||||||
|
case protocol::State::Lighting:
|
||||||
|
constants_.flags[0] = on ? 1.0f : 0.0f;
|
||||||
|
break;
|
||||||
|
case protocol::State::Fog:
|
||||||
|
constants_.fog[2] = on ? 1.0f : 0.0f;
|
||||||
|
break;
|
||||||
|
case protocol::State::Ambient:
|
||||||
|
set_color(constants_.ambient, state.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::createTexture(const protocol::Command& command)
|
||||||
|
{
|
||||||
|
const auto create = command.body<protocol::CreateTexture>();
|
||||||
|
const auto pixels = command.payload<protocol::CreateTexture>();
|
||||||
|
if (!create || create->width == 0 || create->height == 0
|
||||||
|
|| pixels.size() < std::size_t{create->width} * create->height * 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TextureDesc desc;
|
||||||
|
desc.Name = "Client texture";
|
||||||
|
desc.Type = RESOURCE_DIM_TEX_2D;
|
||||||
|
desc.Width = create->width;
|
||||||
|
desc.Height = create->height;
|
||||||
|
desc.MipLevels = 1;
|
||||||
|
desc.Format = TEX_FORMAT_BGRA8_UNORM; // 0xAARRGGBB in memory
|
||||||
|
desc.BindFlags = BIND_SHADER_RESOURCE;
|
||||||
|
desc.Usage = USAGE_DEFAULT;
|
||||||
|
TextureSubResData level;
|
||||||
|
level.pData = pixels.data();
|
||||||
|
level.Stride = std::uint64_t{create->width} * 4;
|
||||||
|
TextureData data;
|
||||||
|
data.pSubResources = &level;
|
||||||
|
data.NumSubresources = 1;
|
||||||
|
Texture texture{.width = create->width, .height = create->height, .flags = create->flags};
|
||||||
|
device_->CreateTexture(desc, &data, &texture.texture);
|
||||||
|
if (!texture.texture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
texture.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE)
|
||||||
|
->SetSampler(samplers_[((create->flags & 2) != 0 ? 1 : 0) | ((create->flags & 4) != 0 ? 2 : 0)]);
|
||||||
|
textures_[create->id] = std::move(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::updateTexture(const protocol::Command& command)
|
||||||
|
{
|
||||||
|
const auto update = command.body<protocol::UpdateTexture>();
|
||||||
|
const auto pixels = command.payload<protocol::UpdateTexture>();
|
||||||
|
if (!update || pixels.size() < std::size_t{update->width} * update->height * 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto found = textures_.find(update->id);
|
||||||
|
if (found == textures_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Texture& texture = found->second;
|
||||||
|
if (texture.width != update->width || texture.height != update->height) {
|
||||||
|
// A different size: make it again, keeping its sampling.
|
||||||
|
protocol::Writer writer;
|
||||||
|
std::memcpy(writer.add(Op::CreateTexture,
|
||||||
|
protocol::CreateTexture{update->id, update->width, update->height, texture.flags},
|
||||||
|
pixels.size()),
|
||||||
|
pixels.data(), pixels.size());
|
||||||
|
protocol::Reader reader(writer.bytes());
|
||||||
|
createTexture(*reader.next());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Box box;
|
||||||
|
box.MaxX = update->width;
|
||||||
|
box.MaxY = update->height;
|
||||||
|
TextureSubResData level;
|
||||||
|
level.pData = pixels.data();
|
||||||
|
level.Stride = std::uint64_t{update->width} * 4;
|
||||||
|
context_->UpdateTexture(texture.texture, 0, 0, box, level, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
||||||
|
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::createBuffer(const protocol::Command& command)
|
||||||
|
{
|
||||||
|
const auto create = command.body<protocol::CreateBuffer>();
|
||||||
|
if (!create || create->count == 0 || create->elementSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t size = std::size_t{create->elementSize} * create->count;
|
||||||
|
const auto contents = command.payload<protocol::CreateBuffer>();
|
||||||
|
const bool filled = contents.size() >= size;
|
||||||
|
BufferDesc desc;
|
||||||
|
desc.Name = create->kind == protocol::BufferKind::Index ? "Client indices" : "Client vertices";
|
||||||
|
desc.Size = size;
|
||||||
|
desc.BindFlags = create->kind == protocol::BufferKind::Index ? BIND_INDEX_BUFFER : BIND_VERTEX_BUFFER;
|
||||||
|
desc.Usage = filled ? USAGE_IMMUTABLE : USAGE_DEFAULT; // dynamic buffers are filled by updates
|
||||||
|
BufferData data;
|
||||||
|
data.pData = contents.data();
|
||||||
|
data.DataSize = size;
|
||||||
|
Buffer buffer{.kind = create->kind, .elementSize = create->elementSize, .count = create->count};
|
||||||
|
device_->CreateBuffer(desc, filled ? &data : nullptr, &buffer.buffer);
|
||||||
|
if (buffer.buffer) {
|
||||||
|
buffers_[create->id] = std::move(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::updateBuffer(const protocol::Command& command)
|
||||||
|
{
|
||||||
|
const auto update = command.body<protocol::UpdateBuffer>();
|
||||||
|
const auto contents = command.payload<protocol::UpdateBuffer>();
|
||||||
|
if (!update) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto found = buffers_.find(update->id);
|
||||||
|
const std::size_t size = std::size_t{update->elementSize} * update->count;
|
||||||
|
if (found == buffers_.end() || contents.size() < size || found->second.elementSize != update->elementSize
|
||||||
|
|| found->second.count != update->count || found->second.buffer->GetDesc().Usage != USAGE_DEFAULT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context_->UpdateBuffer(found->second.buffer, 0, size, contents.data(), RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||||
|
}
|
||||||
|
|
||||||
|
ITextureView* Scene::textureView(std::uint32_t id)
|
||||||
|
{
|
||||||
|
const auto found = textures_.find(id);
|
||||||
|
return found == textures_.end() ? nullptr : found->second.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::drawTriangles(const protocol::DrawTriangles& draw)
|
||||||
|
{
|
||||||
|
const auto buffer = [this](std::uint32_t id, protocol::BufferKind kind, std::uint32_t elementSize) -> Buffer* {
|
||||||
|
const auto found = buffers_.find(id);
|
||||||
|
if (id == 0 || found == buffers_.end() || found->second.kind != kind || found->second.elementSize != elementSize) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &found->second;
|
||||||
|
};
|
||||||
|
Buffer* indices = buffer(draw.indices, protocol::BufferKind::Index, 2);
|
||||||
|
Buffer* positions = buffer(draw.positions, protocol::BufferKind::Vertex, 12);
|
||||||
|
if (indices == nullptr || positions == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Optional streams count only if they cover every vertex.
|
||||||
|
const auto optional = [&](std::uint32_t id, std::uint32_t elementSize) -> IBuffer* {
|
||||||
|
Buffer* found = buffer(id, protocol::BufferKind::Vertex, elementSize);
|
||||||
|
return found != nullptr && found->count >= positions->count ? found->buffer.RawPtr() : nullptr;
|
||||||
|
};
|
||||||
|
this->draw(indices->buffer, indices->count, positions->buffer, optional(draw.normals, 12),
|
||||||
|
optional(draw.texCoords, 8), optional(draw.colors, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::drawArrays(const protocol::Command& command)
|
||||||
|
{
|
||||||
|
const auto arrays = command.body<protocol::DrawArrays>();
|
||||||
|
const auto payload = command.payload<protocol::DrawArrays>();
|
||||||
|
if (!arrays || arrays->vertexCount == 0 || arrays->indexCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t indexBytes = protocol::padded(std::size_t{arrays->indexCount} * 2);
|
||||||
|
const std::size_t vertices = arrays->vertexCount;
|
||||||
|
if (payload.size() < indexBytes + vertices * (12 + (arrays->hasNormals ? 12 : 0) + (arrays->hasTexCoords ? 8 : 0) +
|
||||||
|
(arrays->hasColors ? 4 : 0))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto make = [this](const std::byte* data, std::size_t size, BIND_FLAGS bind) {
|
||||||
|
BufferDesc desc;
|
||||||
|
desc.Name = "Client arrays";
|
||||||
|
desc.Size = size;
|
||||||
|
desc.BindFlags = bind;
|
||||||
|
desc.Usage = USAGE_IMMUTABLE;
|
||||||
|
BufferData contents;
|
||||||
|
contents.pData = data;
|
||||||
|
contents.DataSize = size;
|
||||||
|
RefCntAutoPtr<IBuffer> buffer;
|
||||||
|
device_->CreateBuffer(desc, &contents, &buffer);
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
const std::byte* next = payload.data();
|
||||||
|
const auto indices = make(next, std::size_t{arrays->indexCount} * 2, BIND_INDEX_BUFFER);
|
||||||
|
next += indexBytes;
|
||||||
|
const auto positions = make(next, vertices * 12, BIND_VERTEX_BUFFER);
|
||||||
|
next += vertices * 12;
|
||||||
|
RefCntAutoPtr<IBuffer> normals, texCoords, colors;
|
||||||
|
if (arrays->hasNormals) {
|
||||||
|
normals = make(next, vertices * 12, BIND_VERTEX_BUFFER);
|
||||||
|
next += vertices * 12;
|
||||||
|
}
|
||||||
|
if (arrays->hasTexCoords) {
|
||||||
|
texCoords = make(next, vertices * 8, BIND_VERTEX_BUFFER);
|
||||||
|
next += vertices * 8;
|
||||||
|
}
|
||||||
|
if (arrays->hasColors) {
|
||||||
|
colors = make(next, vertices * 4, BIND_VERTEX_BUFFER);
|
||||||
|
}
|
||||||
|
draw(indices, arrays->indexCount, positions, normals, texCoords, colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::draw(IBuffer* indices, std::uint32_t indexCount, IBuffer* positions, IBuffer* normals, IBuffer* texCoords,
|
||||||
|
IBuffer* colors)
|
||||||
|
{
|
||||||
|
if (indices == nullptr || positions == nullptr || indexCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::uint32_t streams =
|
||||||
|
(normals != nullptr ? hasNormals : 0) | (texCoords != nullptr ? hasTexCoords : 0) | (colors != nullptr ? hasColors : 0);
|
||||||
|
if (prepare(streams, false) == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IBuffer* vertexBuffers[4];
|
||||||
|
std::uint32_t count = 0;
|
||||||
|
for (IBuffer* stream : {positions, normals, texCoords, colors}) {
|
||||||
|
if (stream != nullptr) {
|
||||||
|
vertexBuffers[count++] = stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context_->SetVertexBuffers(0, count, vertexBuffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
||||||
|
SET_VERTEX_BUFFERS_FLAG_RESET);
|
||||||
|
context_->SetIndexBuffer(indices, 0, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||||
|
DrawIndexedAttribs attributes;
|
||||||
|
attributes.NumIndices = indexCount;
|
||||||
|
attributes.IndexType = VT_UINT16;
|
||||||
|
context_->DrawIndexed(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scene::Pipeline* Scene::prepare(std::uint32_t streams, bool sprites)
|
||||||
|
{
|
||||||
|
// Like the client: no textures, the first, or both (the second only with a first).
|
||||||
|
ITextureView* texture0 = textureView(state_.texture0);
|
||||||
|
ITextureView* texture1 = texture0 != nullptr ? textureView(state_.texture1) : nullptr;
|
||||||
|
const std::uint32_t textures = texture0 == nullptr ? 0 : texture1 == nullptr ? 1 : 2;
|
||||||
|
Pipeline* selected = pipeline(streams, textures, sprites);
|
||||||
|
if (selected == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* mapped = nullptr;
|
||||||
|
context_->MapBuffer(constantBuffer_, MAP_WRITE, MAP_FLAG_DISCARD, mapped);
|
||||||
|
if (mapped == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::memcpy(mapped, &constants_, sizeof(constants_));
|
||||||
|
context_->UnmapBuffer(constantBuffer_, MAP_WRITE);
|
||||||
|
|
||||||
|
if (selected->texture0 != nullptr) {
|
||||||
|
selected->texture0->Set(texture0);
|
||||||
|
}
|
||||||
|
if (selected->texture1 != nullptr) {
|
||||||
|
selected->texture1->Set(texture1);
|
||||||
|
}
|
||||||
|
context_->SetPipelineState(selected->state);
|
||||||
|
context_->CommitShaderResources(selected->bindings, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::drawPointSprites(const protocol::Command& command)
|
||||||
|
{
|
||||||
|
const auto sprites = command.body<protocol::DrawPointSprites>();
|
||||||
|
const auto payload = command.payload<protocol::DrawPointSprites>();
|
||||||
|
if (!sprites || sprites->count == 0 || payload.size() < std::size_t{sprites->count} * 16) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BufferDesc desc;
|
||||||
|
desc.Name = "Client particles";
|
||||||
|
desc.Size = std::size_t{sprites->count} * 16;
|
||||||
|
desc.BindFlags = BIND_VERTEX_BUFFER;
|
||||||
|
desc.Usage = USAGE_IMMUTABLE;
|
||||||
|
BufferData contents;
|
||||||
|
contents.pData = payload.data();
|
||||||
|
contents.DataSize = desc.Size;
|
||||||
|
RefCntAutoPtr<IBuffer> buffer;
|
||||||
|
device_->CreateBuffer(desc, &contents, &buffer);
|
||||||
|
if (!buffer || prepare(0, true) == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (spritesLogged_ < 5) {
|
||||||
|
++spritesLogged_;
|
||||||
|
const Texture* texture = textures_.contains(state_.texture0) ? &textures_[state_.texture0] : nullptr;
|
||||||
|
log::info("particles: {}, texture {} {}x{} flags {}, blending {} mode {}, depth test {} write {}, alpha test {} "
|
||||||
|
"reference {}, lighting {}, fog {}, ambient ({:.2f} {:.2f} {:.2f}), texture transform [{} {} {} {} {} {}]",
|
||||||
|
sprites->count, state_.texture0, texture ? texture->width : 0, texture ? texture->height : 0,
|
||||||
|
texture ? texture->flags : 0, state_.blending, state_.blendMode, state_.depthTest, state_.depthWrite,
|
||||||
|
constants_.flags[1], constants_.flags[2], constants_.flags[0], constants_.fog[2], constants_.ambient[0],
|
||||||
|
constants_.ambient[1], constants_.ambient[2], constants_.texTransform0[0], constants_.texTransform0[1],
|
||||||
|
constants_.texTransform0[2], constants_.texTransform0[4], constants_.texTransform0[5],
|
||||||
|
constants_.texTransform0[6]);
|
||||||
|
}
|
||||||
|
IBuffer* buffers[] = {buffer};
|
||||||
|
context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
|
||||||
|
SET_VERTEX_BUFFERS_FLAG_RESET);
|
||||||
|
DrawAttribs attributes;
|
||||||
|
attributes.NumVertices = 4;
|
||||||
|
attributes.NumInstances = sprites->count;
|
||||||
|
context_->Draw(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textures, bool sprites)
|
||||||
|
{
|
||||||
|
const std::uint32_t key = streams | (textures << 3) | (sprites ? 1u << 5 : 0);
|
||||||
|
if (const auto found = shaders_.find(key); found != shaders_.end()) {
|
||||||
|
return found->second;
|
||||||
|
}
|
||||||
|
const std::string source = std::string("#define HAS_NORMALS ") + ((streams & hasNormals) != 0 ? "1" : "0") +
|
||||||
|
"\n#define HAS_TEX_COORDS " + ((streams & hasTexCoords) != 0 ? "1" : "0") +
|
||||||
|
"\n#define HAS_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") +
|
||||||
|
"\n#define TEXTURES " + std::to_string(textures) + "\n" + fixedFunctionHlsl;
|
||||||
|
Shaders created;
|
||||||
|
ShaderCreateInfo info;
|
||||||
|
info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
|
||||||
|
info.Desc.UseCombinedTextureSamplers = true; // g_TextureN with g_TextureN_sampler, set from each texture's view
|
||||||
|
info.Source = source.c_str();
|
||||||
|
info.Desc.ShaderType = SHADER_TYPE_VERTEX;
|
||||||
|
info.Desc.Name = sprites ? "Fixed function particles (vertex)" : "Fixed function (vertex)";
|
||||||
|
info.EntryPoint = sprites ? "vs_sprite" : "vs_main";
|
||||||
|
device_->CreateShader(info, &created.vertex);
|
||||||
|
info.Desc.ShaderType = SHADER_TYPE_PIXEL;
|
||||||
|
info.Desc.Name = "Fixed function (pixel)";
|
||||||
|
info.EntryPoint = "ps_main";
|
||||||
|
device_->CreateShader(info, &created.pixel);
|
||||||
|
return shaders_[key] = std::move(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites)
|
||||||
|
{
|
||||||
|
const std::uint32_t blend = state_.blending ? state_.blendMode : 0;
|
||||||
|
const bool culling = state_.culling && !sprites; // point sprites aren't culled
|
||||||
|
const std::uint32_t key = streams | (textures << 3) | (blend << 5) | (state_.depthTest ? 1u << 8 : 0) |
|
||||||
|
(state_.depthWrite ? 1u << 9 : 0) | (culling ? 1u << 10 : 0) |
|
||||||
|
(state_.normalInverting ? 1u << 11 : 0) | (sprites ? 1u << 12 : 0);
|
||||||
|
if (const auto found = pipelines_.find(key); found != pipelines_.end()) {
|
||||||
|
return found->second.state ? &found->second : nullptr;
|
||||||
|
}
|
||||||
|
Pipeline& entry = pipelines_[key];
|
||||||
|
const Shaders& compiled = shaders(streams, textures, sprites);
|
||||||
|
if (!compiled.vertex || !compiled.pixel) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphicsPipelineStateCreateInfo info;
|
||||||
|
info.PSODesc.Name = "Fixed function";
|
||||||
|
info.PSODesc.PipelineType = PIPELINE_TYPE_GRAPHICS;
|
||||||
|
const SwapChainDesc& window = swapChain_->GetDesc();
|
||||||
|
GraphicsPipelineDesc& graphics = info.GraphicsPipeline;
|
||||||
|
graphics.NumRenderTargets = 1;
|
||||||
|
graphics.RTVFormats[0] = window.ColorBufferFormat;
|
||||||
|
graphics.DSVFormat = window.DepthBufferFormat;
|
||||||
|
graphics.PrimitiveTopology = sprites ? PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||||
|
// The client culls clockwise triangles, or counter-clockwise ones when normals are inverted (mirrored objects).
|
||||||
|
graphics.RasterizerDesc.CullMode = culling ? CULL_MODE_BACK : CULL_MODE_NONE;
|
||||||
|
graphics.RasterizerDesc.FrontCounterClockwise = !state_.normalInverting;
|
||||||
|
graphics.DepthStencilDesc.DepthEnable = state_.depthTest;
|
||||||
|
graphics.DepthStencilDesc.DepthWriteEnable = state_.depthWrite;
|
||||||
|
graphics.DepthStencilDesc.DepthFunc = COMPARISON_FUNC_LESS;
|
||||||
|
RenderTargetBlendDesc& blending = graphics.BlendDesc.RenderTargets[0];
|
||||||
|
blending.BlendEnable = blend != 0;
|
||||||
|
if (blend != 0) {
|
||||||
|
const BLEND_FACTOR source = blend == 1 || blend == 4 ? BLEND_FACTOR_SRC_ALPHA : BLEND_FACTOR_ONE;
|
||||||
|
const BLEND_FACTOR destination = blend == 2 || blend == 4 ? BLEND_FACTOR_ONE : BLEND_FACTOR_INV_SRC_ALPHA;
|
||||||
|
blending.SrcBlend = blending.SrcBlendAlpha = source;
|
||||||
|
blending.DestBlend = blending.DestBlendAlpha = destination;
|
||||||
|
blending.BlendOp = blending.BlendOpAlpha = BLEND_OPERATION_ADD;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutElement elements[4];
|
||||||
|
std::uint32_t count = 0;
|
||||||
|
if (sprites) {
|
||||||
|
// One buffer of sprites, each a position and a color, read once per square (instance).
|
||||||
|
elements[0] = LayoutElement{0, 0, 3, VT_FLOAT32, False};
|
||||||
|
elements[1] = LayoutElement{3, 0, 4, VT_UINT8, True};
|
||||||
|
elements[1].RelativeOffset = 12;
|
||||||
|
for (LayoutElement& element : std::span(elements).first(2)) {
|
||||||
|
element.Stride = 16;
|
||||||
|
element.Frequency = INPUT_ELEMENT_FREQUENCY_PER_INSTANCE;
|
||||||
|
element.InstanceDataStepRate = 1;
|
||||||
|
}
|
||||||
|
count = 2;
|
||||||
|
} else {
|
||||||
|
elements[count] = LayoutElement{0, count, 3, VT_FLOAT32, False};
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
if (!sprites && (streams & hasNormals) != 0) {
|
||||||
|
elements[count] = LayoutElement{1, count, 3, VT_FLOAT32, False};
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
if (!sprites && (streams & hasTexCoords) != 0) {
|
||||||
|
elements[count] = LayoutElement{2, count, 2, VT_FLOAT32, False};
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
if (!sprites && (streams & hasColors) != 0) {
|
||||||
|
elements[count] = LayoutElement{3, count, 4, VT_UINT8, True};
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
graphics.InputLayout.LayoutElements = elements;
|
||||||
|
graphics.InputLayout.NumElements = count;
|
||||||
|
|
||||||
|
info.pVS = compiled.vertex;
|
||||||
|
info.pPS = compiled.pixel;
|
||||||
|
const ShaderResourceVariableDesc variables[] = {
|
||||||
|
{SHADER_TYPE_PIXEL, "g_Texture0", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
|
||||||
|
{SHADER_TYPE_PIXEL, "g_Texture1", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
|
||||||
|
};
|
||||||
|
info.PSODesc.ResourceLayout.DefaultVariableType = SHADER_RESOURCE_VARIABLE_TYPE_STATIC;
|
||||||
|
info.PSODesc.ResourceLayout.Variables = variables;
|
||||||
|
info.PSODesc.ResourceLayout.NumVariables = textures;
|
||||||
|
device_->CreateGraphicsPipelineState(info, &entry.state);
|
||||||
|
if (!entry.state) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (const SHADER_TYPE stage : {SHADER_TYPE_VERTEX, SHADER_TYPE_PIXEL}) {
|
||||||
|
if (IShaderResourceVariable* constants = entry.state->GetStaticVariableByName(stage, "Constants")) {
|
||||||
|
constants->Set(constantBuffer_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.state->CreateShaderResourceBinding(&entry.bindings, true);
|
||||||
|
if (textures >= 1) {
|
||||||
|
entry.texture0 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture0");
|
||||||
|
}
|
||||||
|
if (textures >= 2) {
|
||||||
|
entry.texture1 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture1");
|
||||||
|
}
|
||||||
|
return &entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#pragma once
|
||||||
|
// Carries out the command stream (include/t5de/protocol/commands.hpp) with Diligent, reproducing IMVU's Direct3D 9
|
||||||
|
// look (shaders/fixed_function.hlsl): the client's textures and buffers by id, its state as it sets it, a pipeline
|
||||||
|
// for each combination of states it draws with.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "EngineFactoryD3D11.h"
|
||||||
|
#include "RefCntAutoPtr.hpp"
|
||||||
|
#include "t5de/protocol/commands.hpp"
|
||||||
|
|
||||||
|
namespace t5de {
|
||||||
|
|
||||||
|
class Scene {
|
||||||
|
public:
|
||||||
|
Scene(Diligent::IRenderDevice* device, Diligent::IDeviceContext* context, Diligent::ISwapChain* swapChain);
|
||||||
|
|
||||||
|
// Carries out one frame's commands. offsetX is where the window starts within the client's 3D view; without draw,
|
||||||
|
// only the frame's changes are made (textures, buffers, state). True if the frame was drawn into the window (to
|
||||||
|
// present).
|
||||||
|
bool execute(std::span<const std::byte> commands, int offsetX, bool draw);
|
||||||
|
|
||||||
|
// When the adapter sent the last frame carried out (QueryPerformanceCounter).
|
||||||
|
[[nodiscard]] std::uint64_t lastSentAt() const { return lastSentAt_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Texture {
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::ITexture> texture;
|
||||||
|
std::uint32_t width = 0;
|
||||||
|
std::uint32_t height = 0;
|
||||||
|
std::uint32_t flags = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Buffer {
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IBuffer> buffer;
|
||||||
|
protocol::BufferKind kind = protocol::BufferKind::Vertex;
|
||||||
|
std::uint32_t elementSize = 0;
|
||||||
|
std::uint32_t count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Pipeline {
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IPipelineState> state;
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IShaderResourceBinding> bindings;
|
||||||
|
Diligent::IShaderResourceVariable* texture0 = nullptr;
|
||||||
|
Diligent::IShaderResourceVariable* texture1 = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Shaders {
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IShader> vertex;
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IShader> pixel;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mirrors the shader's constant buffer.
|
||||||
|
struct Constants {
|
||||||
|
float world[16];
|
||||||
|
float view[16];
|
||||||
|
float projection[16];
|
||||||
|
float texTransform0[8];
|
||||||
|
float texTransform1[8];
|
||||||
|
float ambient[4];
|
||||||
|
float lightColor[8][4];
|
||||||
|
float lightVector[8][4];
|
||||||
|
float fogColor[4];
|
||||||
|
float fog[4];
|
||||||
|
float flags[4];
|
||||||
|
float viewport[4];
|
||||||
|
};
|
||||||
|
|
||||||
|
// What a draw needs from the state, as set by the client.
|
||||||
|
struct State {
|
||||||
|
bool depthTest = true;
|
||||||
|
bool depthWrite = true;
|
||||||
|
bool blending = false;
|
||||||
|
std::uint32_t blendMode = 1;
|
||||||
|
bool culling = true;
|
||||||
|
bool normalInverting = false;
|
||||||
|
std::uint32_t texture0 = 0;
|
||||||
|
std::uint32_t texture1 = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void createTexture(const protocol::Command& command);
|
||||||
|
void updateTexture(const protocol::Command& command);
|
||||||
|
void createBuffer(const protocol::Command& command);
|
||||||
|
void updateBuffer(const protocol::Command& command);
|
||||||
|
void setProjection(const protocol::SetProjection& projection);
|
||||||
|
void setState(const protocol::SetState& state);
|
||||||
|
void drawTriangles(const protocol::DrawTriangles& draw);
|
||||||
|
void drawArrays(const protocol::Command& command);
|
||||||
|
void drawPointSprites(const protocol::Command& command);
|
||||||
|
|
||||||
|
// Draws indexCount indices from index, with vertex streams (positions required, the others may be null).
|
||||||
|
void draw(Diligent::IBuffer* indices, std::uint32_t indexCount, Diligent::IBuffer* positions,
|
||||||
|
Diligent::IBuffer* normals, Diligent::IBuffer* texCoords, Diligent::IBuffer* colors);
|
||||||
|
// The pipeline for the current state, with these vertex streams and textures, for triangles or particles.
|
||||||
|
Pipeline* pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites);
|
||||||
|
const Shaders& shaders(std::uint32_t streams, std::uint32_t textures, bool sprites);
|
||||||
|
// Picks the pipeline for the current state and sets it up for a draw: constants, textures. Null if it can't.
|
||||||
|
Pipeline* prepare(std::uint32_t streams, bool sprites);
|
||||||
|
Diligent::ITextureView* textureView(std::uint32_t id);
|
||||||
|
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IRenderDevice> device_;
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IDeviceContext> context_;
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::ISwapChain> swapChain_;
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::IBuffer> constantBuffer_;
|
||||||
|
Diligent::RefCntAutoPtr<Diligent::ISampler> samplers_[4]; // by wrap u (1) and wrap v (2)
|
||||||
|
|
||||||
|
std::unordered_map<std::uint32_t, Texture> textures_;
|
||||||
|
std::unordered_map<std::uint32_t, Buffer> buffers_;
|
||||||
|
std::unordered_map<std::uint32_t, Shaders> shaders_;
|
||||||
|
std::unordered_map<std::uint32_t, Pipeline> pipelines_;
|
||||||
|
|
||||||
|
Constants constants_{};
|
||||||
|
State state_;
|
||||||
|
bool onscreen_ = false; // in a frame drawn into the window
|
||||||
|
bool draw_ = true;
|
||||||
|
std::uint64_t lastSentAt_ = 0;
|
||||||
|
int spritesLogged_ = 0;
|
||||||
|
int offsetX_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace t5de
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
// Generated by CMake from renderer/shaders/@T5DE_SHADER_FILE@: the shader source, compiled at run time by Diligent.
|
||||||
|
|
||||||
|
inline constexpr const char* @T5DE_SHADER_NAME@ = R"hlsl(@T5DE_SHADER_SOURCE@)hlsl";
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// IMVU's Direct3D 9 fixed-function look, as its renderer sets it up (docs/interface.md):
|
||||||
|
// - per-vertex (Gouraud) lighting, with the vertex color as diffuse and ambient material (white without one): the
|
||||||
|
// environment's ambient light, plus each light's color times max(0, N.L); no specular or emissive light;
|
||||||
|
// - textures: none (the lit color), one (times the lit color), or two (the second blended over the first by its
|
||||||
|
// alpha, then times the lit color), both on the vertex's texture coordinates with their own transforms;
|
||||||
|
// - an alpha test passing pixels whose alpha is greater than the reference, then linear fog. Direct3D 9 fogs by
|
||||||
|
// distance from the eye only for projections with +1 as their (3, 4) element; IMVU's (right-handed) have -1, so
|
||||||
|
// it fogs by the depth buffer's value (0 to 1) against the fog's start and end (in scene units, 1 to 50 in
|
||||||
|
// rooms): in effect, no fog. Reproduced as is.
|
||||||
|
//
|
||||||
|
// Particles (vs_sprite) are Direct3D 9 point sprites there: camera-facing squares, viewport height / distance pixels
|
||||||
|
// across (1 to 64), textured over the whole square; here each is drawn as an instance of a 4-vertex strip.
|
||||||
|
//
|
||||||
|
// Compiled for each combination of the vertex streams present (HAS_NORMALS, HAS_TEX_COORDS, HAS_COLORS) and the
|
||||||
|
// number of textures (TEXTURES).
|
||||||
|
|
||||||
|
cbuffer Constants
|
||||||
|
{
|
||||||
|
row_major float4x4 g_World;
|
||||||
|
row_major float4x4 g_View;
|
||||||
|
row_major float4x4 g_Projection;
|
||||||
|
float4 g_TexTransform0[2]; // u' = dot((u, v, 1), [0].xyz), v' = dot((u, v, 1), [1].xyz)
|
||||||
|
float4 g_TexTransform1[2];
|
||||||
|
float4 g_Ambient; // rgb
|
||||||
|
float4 g_LightColor[8]; // rgb, a: enabled
|
||||||
|
float4 g_LightVector[8]; // xyz: position, or direction toward the light; w: 1 if directional
|
||||||
|
float4 g_FogColor; // rgb
|
||||||
|
float4 g_Fog; // x: start, y: end, z: on
|
||||||
|
float4 g_Flags; // x: lighting on, y: alpha test on, z: alpha reference (0-255)
|
||||||
|
float4 g_Viewport; // xy: size in pixels
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VSInput
|
||||||
|
{
|
||||||
|
float3 Position : ATTRIB0;
|
||||||
|
#if HAS_NORMALS
|
||||||
|
float3 Normal : ATTRIB1;
|
||||||
|
#endif
|
||||||
|
#if HAS_TEX_COORDS
|
||||||
|
float2 TexCoord : ATTRIB2;
|
||||||
|
#endif
|
||||||
|
#if HAS_COLORS
|
||||||
|
float4 Color : ATTRIB3; // bytes B, G, R, A
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PSInput
|
||||||
|
{
|
||||||
|
float4 Position : SV_POSITION;
|
||||||
|
float4 Color : COLOR0;
|
||||||
|
float2 TexCoord0 : TEXCOORD0;
|
||||||
|
float2 TexCoord1 : TEXCOORD1;
|
||||||
|
};
|
||||||
|
|
||||||
|
void vs_main(in VSInput input, out PSInput output)
|
||||||
|
{
|
||||||
|
float4 world = mul(float4(input.Position, 1.0), g_World);
|
||||||
|
float4 view = mul(world, g_View);
|
||||||
|
output.Position = mul(view, g_Projection);
|
||||||
|
|
||||||
|
float4 material = float4(1.0, 1.0, 1.0, 1.0);
|
||||||
|
#if HAS_COLORS
|
||||||
|
material = input.Color.zyxw;
|
||||||
|
#endif
|
||||||
|
float4 color = material;
|
||||||
|
if (g_Flags.x > 0.5)
|
||||||
|
{
|
||||||
|
float3 normal = float3(0.0, 0.0, 0.0);
|
||||||
|
#if HAS_NORMALS
|
||||||
|
normal = normalize(mul(input.Normal, (float3x3)g_World));
|
||||||
|
#endif
|
||||||
|
float3 lit = g_Ambient.rgb * material.rgb;
|
||||||
|
for (int i = 0; i < 8; ++i)
|
||||||
|
{
|
||||||
|
if (g_LightColor[i].a > 0.5)
|
||||||
|
{
|
||||||
|
float3 toLight = g_LightVector[i].w > 0.5 ? g_LightVector[i].xyz : g_LightVector[i].xyz - world.xyz;
|
||||||
|
lit += g_LightColor[i].rgb * material.rgb * max(0.0, dot(normal, normalize(toLight)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color = float4(saturate(lit), material.a);
|
||||||
|
}
|
||||||
|
output.Color = color;
|
||||||
|
|
||||||
|
float3 uv = float3(0.0, 0.0, 1.0);
|
||||||
|
#if HAS_TEX_COORDS
|
||||||
|
uv.xy = input.TexCoord;
|
||||||
|
#endif
|
||||||
|
output.TexCoord0 = float2(dot(uv, g_TexTransform0[0].xyz), dot(uv, g_TexTransform0[1].xyz));
|
||||||
|
output.TexCoord1 = float2(dot(uv, g_TexTransform1[0].xyz), dot(uv, g_TexTransform1[1].xyz));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpriteInput
|
||||||
|
{
|
||||||
|
float3 Position : ATTRIB0;
|
||||||
|
float4 Color : ATTRIB3; // bytes B, G, R, A
|
||||||
|
uint Corner : SV_VertexID;
|
||||||
|
};
|
||||||
|
|
||||||
|
void vs_sprite(in SpriteInput input, out PSInput output)
|
||||||
|
{
|
||||||
|
float4 view = mul(mul(float4(input.Position, 1.0), g_World), g_View);
|
||||||
|
float4 clip = mul(view, g_Projection);
|
||||||
|
float size = clamp(g_Viewport.y / max(length(view.xyz), 1e-4), 1.0, 64.0);
|
||||||
|
float2 corner = float2((input.Corner & 1) != 0 ? 1.0 : -1.0, (input.Corner & 2) != 0 ? -1.0 : 1.0);
|
||||||
|
clip.xy += corner * size / g_Viewport.xy * clip.w; // half the size each way, in clip space
|
||||||
|
output.Position = clip;
|
||||||
|
|
||||||
|
// No normals: lit by the ambient light alone.
|
||||||
|
float4 material = input.Color.zyxw;
|
||||||
|
output.Color = g_Flags.x > 0.5 ? float4(saturate(g_Ambient.rgb * material.rgb), material.a) : material;
|
||||||
|
output.TexCoord0 = float2(corner.x * 0.5 + 0.5, 0.5 - corner.y * 0.5);
|
||||||
|
output.TexCoord1 = output.TexCoord0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TEXTURES >= 1
|
||||||
|
Texture2D g_Texture0;
|
||||||
|
SamplerState g_Texture0_sampler;
|
||||||
|
#endif
|
||||||
|
#if TEXTURES >= 2
|
||||||
|
Texture2D g_Texture1;
|
||||||
|
SamplerState g_Texture1_sampler;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
float4 ps_main(in PSInput input) : SV_Target
|
||||||
|
{
|
||||||
|
float4 color = input.Color;
|
||||||
|
#if TEXTURES == 1
|
||||||
|
color *= g_Texture0.Sample(g_Texture0_sampler, input.TexCoord0);
|
||||||
|
#elif TEXTURES == 2
|
||||||
|
float4 first = g_Texture0.Sample(g_Texture0_sampler, input.TexCoord0);
|
||||||
|
float4 second = g_Texture1.Sample(g_Texture1_sampler, input.TexCoord1);
|
||||||
|
color *= lerp(first, second, second.a);
|
||||||
|
#endif
|
||||||
|
if (g_Flags.y > 0.5 && !(color.a * 255.0 > g_Flags.z))
|
||||||
|
{
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
if (g_Fog.z > 0.5)
|
||||||
|
{
|
||||||
|
float visible = saturate((g_Fog.y - input.Position.z) / max(g_Fog.y - g_Fog.x, 1e-6));
|
||||||
|
color.rgb = lerp(g_FogColor.rgb, color.rgb, visible);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
add_executable(shared_ptr_test shared_ptr_test.cpp)
|
add_executable(shared_ptr_test shared_ptr_test.cpp)
|
||||||
target_link_libraries(shared_ptr_test PRIVATE t5de::imvu t5de::options)
|
target_link_libraries(shared_ptr_test PRIVATE t5de::imvu t5de::options)
|
||||||
add_test(NAME shared_ptr COMMAND shared_ptr_test)
|
add_test(NAME shared_ptr COMMAND shared_ptr_test)
|
||||||
|
|
||||||
|
add_executable(protocol_test protocol_test.cpp)
|
||||||
|
target_link_libraries(protocol_test PRIVATE t5de::protocol t5de::options)
|
||||||
|
add_test(NAME protocol COMMAND protocol_test)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// The command stream: what the adapter writes, the renderer process reads back the same.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "t5de/protocol/commands.hpp"
|
||||||
|
|
||||||
|
using namespace t5de::protocol;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
void check(bool ok, const char* what)
|
||||||
|
{
|
||||||
|
if (!ok) {
|
||||||
|
std::printf("FAIL: %s\n", what);
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
Writer writer;
|
||||||
|
writer.add(Op::Clear, Clear{3, 0xff131313});
|
||||||
|
const unsigned char pixels[] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||||
|
std::memcpy(writer.add(Op::CreateTexture, CreateTexture{7, 2, 1, 6}, sizeof(pixels)), pixels, sizeof(pixels));
|
||||||
|
std::byte* odd = writer.add(Op::UpdateBuffer, UpdateBuffer{9, 1, 3}, 3); // padded to 4
|
||||||
|
std::memset(odd, 0xab, 3);
|
||||||
|
writer.add(Op::EndFrame, EndFrame{});
|
||||||
|
check(writer.size() % 4 == 0, "commands stay 4-byte aligned");
|
||||||
|
|
||||||
|
Reader reader(writer.bytes());
|
||||||
|
auto clear = reader.next();
|
||||||
|
check(clear && clear->op() == Op::Clear && clear->body<Clear>()->color == 0xff131313, "clear");
|
||||||
|
auto texture = reader.next();
|
||||||
|
check(texture && texture->op() == Op::CreateTexture && texture->body<CreateTexture>()->id == 7, "texture");
|
||||||
|
check(texture && texture->payload<CreateTexture>().size() == sizeof(pixels)
|
||||||
|
&& std::memcmp(texture->payload<CreateTexture>().data(), pixels, sizeof(pixels)) == 0,
|
||||||
|
"texture pixels");
|
||||||
|
auto update = reader.next();
|
||||||
|
check(update && update->payload<UpdateBuffer>().size() == 4, "payload padded");
|
||||||
|
auto end = reader.next();
|
||||||
|
check(end && end->op() == Op::EndFrame, "end of frame");
|
||||||
|
check(!reader.next(), "nothing after");
|
||||||
|
|
||||||
|
// A truncated stream (a chunk cut short) yields only whole commands.
|
||||||
|
Reader truncated(writer.bytes().first(writer.size() - 2));
|
||||||
|
int count = 0;
|
||||||
|
while (truncated.next()) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
check(count == 3, "truncated: whole commands only");
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
std::printf("ok\n");
|
||||||
|
}
|
||||||
|
return failures == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user