Add the renderer process
The renderer becomes two programs: the adapter (t5deRenderEngine.dll, 32-bit, in IMVU's process) and a 64-bit renderer process (t5de-renderer.exe) drawing with Diligent Engine, fetched with FetchContent and pinned. For now a spike: the adapter starts the renderer process for each engine and signals each frame through a shared memory channel, and the renderer process draws a box into a child window in the client's 3D view. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0d126574bf
commit
9a7d7d2908
18 files changed
+491
-57
No files matched your search
@@ -0,0 +1,12 @@
|
||||
# t5deRenderEngine.dll, the adapter: a renderer plugin for IMVU Classic (32-bit).
|
||||
|
||||
add_library(t5de_render_engine SHARED
|
||||
log.cpp
|
||||
passthrough_engine.cpp
|
||||
plugin.cpp
|
||||
plugin.def
|
||||
renderer_process.cpp)
|
||||
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
|
||||
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::protocol t5de::options)
|
||||
|
||||
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "log.hpp"
|
||||
|
||||
#include <share.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
|
||||
namespace t5de::log {
|
||||
|
||||
namespace {
|
||||
|
||||
std::FILE* open()
|
||||
{
|
||||
wchar_t temp[MAX_PATH + 1];
|
||||
const DWORD length = GetTempPathW(MAX_PATH + 1, temp);
|
||||
if (length == 0 || length > MAX_PATH) {
|
||||
return nullptr;
|
||||
}
|
||||
// Shared, so it can be read while the client runs.
|
||||
return _wfsopen((std::filesystem::path(temp) / L"t5de-renderer.log").c_str(), L"w", _SH_DENYNO);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void write(std::string_view line)
|
||||
{
|
||||
static std::mutex mutex;
|
||||
static std::FILE* file = open();
|
||||
const std::scoped_lock lock(mutex);
|
||||
if (file != nullptr) {
|
||||
std::fwrite(line.data(), 1, line.size(), file);
|
||||
std::fputc('\n', file);
|
||||
std::fflush(file);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace t5de::log
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
// The renderer's log: %TEMP%\t5de-renderer.log, replaced each time the client starts.
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,395 @@
|
||||
#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,76 @@
|
||||
#pragma once
|
||||
// A renderer that forwards every call to another one (IMVU's Direct3D engine) and logs what a frame is made of: proof
|
||||
// that our declaration of the interface matches IMVU's, and a way to see how the client draws.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "renderer_process.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class PassthroughEngine final : public imvu::PiRenderEngine {
|
||||
public:
|
||||
// renderer may be null.
|
||||
PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner, std::unique_ptr<RendererProcess> renderer);
|
||||
~PassthroughEngine() override;
|
||||
|
||||
void resizeFrameBuffer(imvu::Vec2i size) override;
|
||||
void enable2dFrameBuffer() override;
|
||||
void clear(std::uint32_t flags, imvu::Color color) override;
|
||||
void setProjection(imvu::Vec2i viewport, const imvu::Mat4f& projection) override;
|
||||
void setModelSpace(const imvu::Transform& model) override;
|
||||
void setViewSpace(const imvu::Transform& view) override;
|
||||
void setTextures(imvu::PiRenderTexture* texture0, imvu::PiRenderTexture* texture1) override;
|
||||
void setDepthTestingOn(bool on) override;
|
||||
void setDepthWritingOn(bool on) override;
|
||||
void setBlendMode(imvu::RenderBlendMode mode) override;
|
||||
void setAlphaTestingOn(bool on) override;
|
||||
void setAlphaTestValue(float value) override;
|
||||
void setBlendingOn(bool on) override;
|
||||
void setBackFaceCullingOn(bool on) override;
|
||||
void setNormalInvertingOn(bool on) override;
|
||||
void setLightingOn(bool on) override;
|
||||
void setLightProperties(std::uint32_t index, const imvu::LightProperties& light) override;
|
||||
void setFogOn(bool on) override;
|
||||
void setFogAttributes(float start, float end, imvu::Color color) override;
|
||||
void setTextureTransformMatrixIdentity(std::uint32_t stage) override;
|
||||
void setTextureTransformMatrix(std::uint32_t stage, const imvu::TextureTransform& transform) override;
|
||||
void setEnvironmentAmbient(imvu::Color color) override;
|
||||
imvu::PiRenderTexture* newRenderTexture(const imvu::Pixmap& pixmap, std::uint32_t flags) override;
|
||||
imvu::PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) override;
|
||||
imvu::PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
|
||||
std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) override;
|
||||
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize,
|
||||
std::uint32_t elementCount) override;
|
||||
imvu::SharedPtr<imvu::PiPixmapRenderTarget> newPixmapRenderTarget(imvu::Vec2i size) override;
|
||||
imvu::SharedPtr<imvu::PiTextureRenderTarget> newTextureRenderTarget(imvu::Vec2i size) override;
|
||||
void beginRendering(imvu::PiRenderTarget* target) override;
|
||||
void endRendering() override;
|
||||
void drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
|
||||
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
|
||||
imvu::PiInternalVertexBuffer* colors) override;
|
||||
void drawLine(imvu::Vec3f from, imvu::Vec3f to, std::uint32_t color) override;
|
||||
void drawPointSprites(const imvu::PointSpriteVector& sprites) override;
|
||||
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>& normals,
|
||||
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
|
||||
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:
|
||||
void count(std::size_t slot);
|
||||
|
||||
imvu::SharedPtr<imvu::PiRenderEngine> inner_;
|
||||
std::unique_ptr<RendererProcess> renderer_;
|
||||
std::array<std::uint32_t, slots> calls_{};
|
||||
std::uint64_t frames_ = 0;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <exception>
|
||||
|
||||
#include "log.hpp"
|
||||
#include "passthrough_engine.hpp"
|
||||
#include "renderer_process.hpp"
|
||||
#include "t5de/imvu/render_engine.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace t5de;
|
||||
|
||||
// IMVU's Direct3D 9 engine, made by SceneWindow.dll (already loaded: it's what loads this plugin). Direct3D
|
||||
// rather than IMVU's OpenGL engine, which needs a real graphics driver: Direct3D also works on Windows' basic
|
||||
// display driver (virtual machines), and it's what most players use.
|
||||
imvu::SharedPtr<imvu::PiRenderEngine> create_imvu_engine(imvu::SharedPtr<imvu::INativeWindow> window)
|
||||
{
|
||||
HMODULE scene_window = GetModuleHandleW(L"SceneWindow.dll");
|
||||
auto create = scene_window == nullptr
|
||||
? nullptr
|
||||
: reinterpret_cast<imvu::CreateRenderer>(GetProcAddress(scene_window, imvu::createRendererName));
|
||||
if (create == nullptr) {
|
||||
log::info("SceneWindow.dll's createRenderer not found");
|
||||
return {};
|
||||
}
|
||||
return create(std::move(window), imvu::RendererType::Direct3D);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int __cdecl isRenderEngineSupported() { return 1; }
|
||||
|
||||
// Returns a C++ object from a C-linkage function: fine here, the caller is C++ built for the same ABI.
|
||||
#pragma warning(suppress : 4190)
|
||||
extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu::SharedPtr<imvu::INativeWindow> window)
|
||||
{
|
||||
try {
|
||||
log::info("createRenderEngine: window {}", static_cast<void*>(window.get()));
|
||||
HWND hwnd = window ? window->getPlatformWindow() : nullptr;
|
||||
auto inner = create_imvu_engine(std::move(window));
|
||||
if (!inner) {
|
||||
return {};
|
||||
}
|
||||
auto renderer = hwnd != nullptr ? RendererProcess::start(hwnd) : nullptr;
|
||||
return imvu::SharedPtr<imvu::PiRenderEngine>(
|
||||
std::make_unique<PassthroughEngine>(std::move(inner), std::move(renderer)));
|
||||
} catch (const std::exception& e) {
|
||||
// Exceptions mustn't cross into IMVU's code (a different C++ runtime).
|
||||
log::info("createRenderEngine failed: {}", e.what());
|
||||
return {};
|
||||
} catch (...) {
|
||||
log::info("createRenderEngine failed");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
; The names SceneWindow.dll's createDLLRenderer looks up, without C++ or __cdecl decoration.
|
||||
LIBRARY t5deRenderEngine
|
||||
EXPORTS
|
||||
isRenderEngineSupported
|
||||
createRenderEngine
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "renderer_process.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
#include "log.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
namespace {
|
||||
|
||||
// The folder this DLL is in: the client's program folder.
|
||||
std::filesystem::path module_folder()
|
||||
{
|
||||
HMODULE self = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&module_folder), &self);
|
||||
wchar_t path[MAX_PATH];
|
||||
GetModuleFileNameW(self, path, MAX_PATH);
|
||||
return std::filesystem::path(path).parent_path();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<RendererProcess> RendererProcess::start(HWND window)
|
||||
{
|
||||
static std::atomic<int> engines = 0;
|
||||
const std::wstring name = std::format(L"Local\\t5de-renderer-{}-{}", GetCurrentProcessId(), engines++);
|
||||
|
||||
std::unique_ptr<RendererProcess> renderer(new RendererProcess());
|
||||
renderer->mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
||||
sizeof(protocol::Channel), name.c_str());
|
||||
if (renderer->mapping_ == nullptr) {
|
||||
log::info("renderer process: can't create the channel: error {}", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
renderer->channel_ = static_cast<protocol::Channel*>(
|
||||
MapViewOfFile(renderer->mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(protocol::Channel)));
|
||||
renderer->frameEvent_ = CreateEventW(nullptr, FALSE, FALSE, (name + protocol::frameEventSuffix).c_str());
|
||||
if (renderer->channel_ == nullptr || renderer->frameEvent_ == nullptr) {
|
||||
log::info("renderer process: can't set up the channel: error {}", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
*renderer->channel_ = {.version = protocol::version,
|
||||
.closing = 0,
|
||||
.window = reinterpret_cast<std::uintptr_t>(window),
|
||||
.frames = 0};
|
||||
|
||||
// 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);
|
||||
|
||||
const auto exe = module_folder() / L"t5de-renderer.exe";
|
||||
std::wstring command = std::format(L"\"{}\" --channel {} --parent {}", exe.wstring(), name, GetCurrentProcessId());
|
||||
STARTUPINFOW startup{.cb = sizeof(STARTUPINFOW)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(exe.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup,
|
||||
&process)) {
|
||||
log::info("renderer process: can't start {}: error {}", exe.string(), GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
renderer->process_ = process.hProcess;
|
||||
log::info("renderer process: started {} (pid {}) on channel {}", exe.string(), process.dwProcessId,
|
||||
std::filesystem::path(name).string());
|
||||
return renderer;
|
||||
}
|
||||
|
||||
RendererProcess::~RendererProcess()
|
||||
{
|
||||
if (channel_ != nullptr) {
|
||||
channel_->closing = 1;
|
||||
if (frameEvent_ != nullptr) {
|
||||
SetEvent(frameEvent_);
|
||||
}
|
||||
UnmapViewOfFile(channel_);
|
||||
}
|
||||
for (HANDLE handle : {frameEvent_, mapping_, process_}) {
|
||||
if (handle != nullptr) {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RendererProcess::frameDrawn()
|
||||
{
|
||||
++channel_->frames;
|
||||
SetEvent(frameEvent_);
|
||||
}
|
||||
|
||||
} // namespace t5de
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
// The renderer process (t5de-renderer.exe, 64-bit) an engine draws through: started with the engine, told about
|
||||
// each frame through a shared memory channel (include/t5de/protocol/channel.hpp), and told to exit with it.
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "t5de/protocol/channel.hpp"
|
||||
|
||||
namespace t5de {
|
||||
|
||||
class RendererProcess {
|
||||
public:
|
||||
// Starts t5de-renderer.exe from this DLL's folder, drawing into window; null if it can't.
|
||||
static std::unique_ptr<RendererProcess> start(HWND window);
|
||||
|
||||
RendererProcess(const RendererProcess&) = delete;
|
||||
RendererProcess& operator=(const RendererProcess&) = delete;
|
||||
~RendererProcess();
|
||||
|
||||
// The client drew a frame.
|
||||
void frameDrawn();
|
||||
|
||||
private:
|
||||
RendererProcess() = default;
|
||||
|
||||
HANDLE mapping_ = nullptr;
|
||||
HANDLE frameEvent_ = nullptr;
|
||||
HANDLE process_ = nullptr;
|
||||
protocol::Channel* channel_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace t5de
|
||||
Reference in new issue
Block a user