4 Commits
Author SHA1 Message Date
dhkatzandClaude Opus 5.5 5c35c8b63a Note the renderer process spike's results
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 05:22:28 -07:00
dhkatzandClaude Opus 5.5 9a7d7d2908 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]>
2026-10-28 05:17:03 -07:00
dhkatzandClaude Opus 5.5 0d126574bf Note the data types checked in the client
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 04:39:15 -07:00
dhkatzandClaude Opus 5.5 83e0665681 Declare the renderer interface's data types
Transforms, matrices, colors, lights, blend modes, fog, texture transforms, vertex arrays and the engine's buffer and texture objects, worked out from IMVU's Direct3D engine. The pass-through logs the first calls' arguments read through them, to check them in the client.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 04:36:37 -07:00
21 changed files with 795 additions and 131 deletions

No files matched your search

+10 -5
View File
@@ -1,7 +1,9 @@
# t5de-renderer
A renderer plugin for IMVU Classic (see README.md and docs/interface.md). Part of T5DE; the patches and the patcher
are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one).
A new renderer for IMVU Classic: a 32-bit adapter DLL the client loads as a renderer plugin, and a 64-bit renderer
process drawing with Diligent Engine (see README.md, docs/architecture.md, docs/interface.md). Part of T5DE; the
patches and the patcher are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside this one).
The user chose this architecture for ray tracing, mesh shaders and a possible browser client later.
## Rules
@@ -13,9 +15,12 @@ are in the `t5de` repository (git.t5de.dev/t5de/t5de, usually checked out beside
## Notes
- The plugin must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall`
virtual methods in IMVU's vtable order, Boost 1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's
code (a different C runtime); the CRT is linked statically.
- The adapter must match IMVU's ABI: 32-bit, MSVC (cl or clang-cl; MinGW's C++ ABI differs), `__thiscall`
virtual methods in IMVU's vtable order (never overloaded: MSVC orders overloads unlike their declaration), Boost
1.61's `shared_ptr` layout. Exceptions must not cross into IMVU's code (a different C runtime); the CRT is linked
statically. Code shared with the 64-bit renderer process (`include/t5de/protocol/`) uses fixed-size fields only.
- The development VM has no graphics driver (Microsoft Basic Render Driver): Direct3D 11 works in software, IMVU's
OpenGL renderer doesn't, and performance there means nothing.
- Ghidra 12.2 (Java 25) is at `%LOCALAPPDATA%\Programs\ghidra_12.2_PUBLIC`; projects in
`%LOCALAPPDATA%\t5de-dev\ghidra`, analysed headless (`support\analyzeHeadless.bat`). The client to analyse is the
`t5de` checkout's `.cache/<version>/install`.
+26 -23
View File
@@ -5,15 +5,18 @@ project(t5de-renderer
DESCRIPTION "A new 3D renderer for the IMVU Classic client"
LANGUAGES CXX)
# The plugin is loaded into IMVU Classic, a 32-bit program built with Visual C++: it must use the same ABI.
# Two programs, for two architectures (a preset for each, see CMakePresets.json):
# - 32-bit: the adapter, t5deRenderEngine.dll, a renderer plugin loaded into IMVU Classic (a 32-bit Visual C++
# program, so it must match its ABI).
# - 64-bit: the renderer process, t5de-renderer.exe, which draws.
if(NOT MSVC) # cl or clang-cl
message(FATAL_ERROR "Build with MSVC or clang-cl: the plugin must match IMVU Classic's Visual C++ ABI.")
endif()
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
message(FATAL_ERROR "Build for 32-bit x86 (IMVU Classic is 32-bit): use a CMakePresets.json preset.")
message(FATAL_ERROR "Build with MSVC or clang-cl: the adapter must match IMVU Classic's Visual C++ ABI.")
endif()
# The C runtime is linked statically, so the plugin needs nothing installed beside it.
# No C++ modules: skip scanning for them.
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
# The C runtime is linked statically, so the programs need nothing installed beside them.
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
# Warnings and conformance for our own targets.
@@ -23,25 +26,25 @@ target_compile_features(t5de_options INTERFACE cxx_std_20)
target_compile_options(t5de_options INTERFACE /W4 /permissive- /utf-8 /Zc:__cplusplus)
target_compile_definitions(t5de_options INTERFACE WIN32_LEAN_AND_MEAN NOMINMAX UNICODE _UNICODE)
# Our declarations of IMVU's interfaces.
add_library(t5de_imvu INTERFACE)
add_library(t5de::imvu ALIAS t5de_imvu)
target_include_directories(t5de_imvu INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
target_compile_features(t5de_imvu INTERFACE cxx_std_20)
# The renderer plugin, t5deRenderEngine.dll.
add_library(t5de_render_engine SHARED
src/log.cpp
src/passthrough_engine.cpp
src/plugin.cpp
src/plugin.def)
set_target_properties(t5de_render_engine PROPERTIES OUTPUT_NAME t5deRenderEngine)
target_link_libraries(t5de_render_engine PRIVATE t5de::imvu t5de::options)
install(TARGETS t5de_render_engine RUNTIME DESTINATION .)
# What the adapter and the renderer process share.
add_library(t5de_protocol INTERFACE)
add_library(t5de::protocol ALIAS t5de_protocol)
target_include_directories(t5de_protocol INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
target_compile_features(t5de_protocol INTERFACE cxx_std_20)
include(CTest)
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
# Our declarations of IMVU's interfaces (32-bit layouts).
add_library(t5de_imvu INTERFACE)
add_library(t5de::imvu ALIAS t5de_imvu)
target_include_directories(t5de_imvu INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
target_compile_features(t5de_imvu INTERFACE cxx_std_20)
add_subdirectory(adapter)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
else()
add_subdirectory(renderer)
endif()
+33 -12
View File
@@ -3,29 +3,43 @@
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
"configurePresets": [
{
"name": "windows-x86",
"displayName": "Windows, 32-bit (Visual Studio Build Tools)",
"description": "IMVU Classic is a 32-bit Visual C++ program: build for Win32 with MSVC.",
"name": "windows",
"hidden": true,
"generator": "Visual Studio 18 2026",
"architecture": { "value": "Win32", "strategy": "set" },
"binaryDir": "${sourceDir}/build/${presetName}",
"installDir": "${sourceDir}/build/${presetName}/install",
"installDir": "${sourceDir}/build/install",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
},
{
"name": "windows-x86",
"inherits": "windows",
"displayName": "Adapter (32-bit)",
"description": "t5deRenderEngine.dll, loaded into IMVU Classic, a 32-bit Visual C++ program.",
"architecture": { "value": "Win32", "strategy": "set" }
},
{
"name": "windows-x64",
"inherits": "windows",
"displayName": "Renderer process (64-bit)",
"description": "t5de-renderer.exe, with Diligent Engine (fetched and built on the first configure).",
"architecture": { "value": "x64", "strategy": "set" }
}
],
"buildPresets": [
{ "name": "debug", "configurePreset": "windows-x86", "configuration": "Debug" },
{ "name": "release", "configurePreset": "windows-x86", "configuration": "Release" }
{ "name": "adapter-debug", "configurePreset": "windows-x86", "configuration": "Debug" },
{ "name": "adapter-release", "configurePreset": "windows-x86", "configuration": "Release" },
{ "name": "renderer-debug", "configurePreset": "windows-x64", "configuration": "Debug" },
{ "name": "renderer-release", "configurePreset": "windows-x64", "configuration": "Release" }
],
"testPresets": [
{
"name": "debug",
"name": "adapter-debug",
"configurePreset": "windows-x86",
"configuration": "Debug",
"output": { "outputOnFailure": true }
},
{
"name": "release",
"name": "adapter-release",
"configurePreset": "windows-x86",
"configuration": "Release",
"output": { "outputOnFailure": true }
@@ -33,11 +47,18 @@
],
"workflowPresets": [
{
"name": "release",
"name": "adapter",
"steps": [
{ "type": "configure", "name": "windows-x86" },
{ "type": "build", "name": "release" },
{ "type": "test", "name": "release" }
{ "type": "build", "name": "adapter-release" },
{ "type": "test", "name": "adapter-release" }
]
},
{
"name": "renderer",
"steps": [
{ "type": "configure", "name": "windows-x64" },
{ "type": "build", "name": "renderer-release" }
]
}
]
+14 -11
View File
@@ -1,25 +1,28 @@
# t5de-renderer
A new 3D renderer for the IMVU Classic client, as a renderer plugin the client loads like its own OpenGL renderer.
Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not affiliated with IMVU.
A new 3D renderer for the IMVU Classic client. Part of [T5DE](https://git.t5de.dev/t5de/t5de); unofficial, not
affiliated with IMVU.
For now it's a pass-through: it wraps IMVU's Direct3D renderer and logs what each frame draws, to prove our
declarations of IMVU's interface (`docs/interface.md`).
Two programs (`docs/architecture.md`): an adapter, `t5deRenderEngine.dll`, which IMVU Classic loads as a renderer
plugin, and a 64-bit renderer process, `t5de-renderer.exe`, which draws with
[Diligent Engine](https://github.com/DiligentGraphics/DiligentCore). For now the client's own Direct3D renderer still
draws the scene, and the renderer process proves the setup by drawing a small box over it. `docs/interface.md` has
what we know of IMVU's renderer interface.
## Building
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. IMVU Classic is a 32-bit
Visual C++ program, so the plugin is built for 32-bit Windows (the `windows-x86` preset):
Needs Visual Studio Build Tools 2026 (the C++ workload) and CMake 3.28 or newer. One preset per program:
```sh
cmake --workflow --preset release
cmake --workflow --preset adapter # build/windows-x86/adapter/Release/t5deRenderEngine.dll, and the tests
cmake --workflow --preset renderer # build/windows-x64/renderer/Release/t5de-renderer.exe
```
That configures, builds `build/windows-x86/Release/t5deRenderEngine.dll` and runs the tests. Or step by step:
`cmake --preset windows-x86`, `cmake --build --preset debug`, `ctest --preset debug`.
The renderer's first configure fetches Diligent Engine (pinned in `renderer/CMakeLists.txt`) and builds it, which
takes a while.
## Trying it
Copy `t5deRenderEngine.dll` into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL
setting load it in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The log is
Copy both programs into a T5DE client folder built with the `t5de-renderer` patch, which has the OpenGL setting load
the adapter in place of `glRenderEngine.dll`, then choose OpenGL in Settings > Advanced. The adapter's log is
`%TEMP%\t5de-renderer.log`.
+12
View File
@@ -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 .)
View File
File renamed without changes.
View File
File renamed without changes.
@@ -1,5 +1,8 @@
#include "passthrough_engine.hpp"
#include <windows.h>
#include <atomic>
#include <string>
#include "log.hpp"
@@ -49,9 +52,50 @@ constexpr std::array<const char*, PassthroughEngine::slots> names = {
// 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) : inner_(std::move(inner))
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()));
}
@@ -73,26 +117,42 @@ void PassthroughEngine::enable2dFrameBuffer()
inner_->enable2dFrameBuffer();
}
void PassthroughEngine::clear(std::uint32_t flags, std::uint32_t color)
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);
}
@@ -117,6 +177,9 @@ void PassthroughEngine::setDepthWritingOn(bool on)
void PassthroughEngine::setBlendMode(imvu::RenderBlendMode mode)
{
if (sampled(10)) {
log::info("setBlendMode({})", static_cast<int>(mode));
}
count(10);
inner_->setBlendMode(mode);
}
@@ -129,6 +192,9 @@ void PassthroughEngine::setAlphaTestingOn(bool on)
void PassthroughEngine::setAlphaTestValue(float value)
{
if (sampled(12)) {
log::info("setAlphaTestValue({:.3f})", value);
}
count(12);
inner_->setAlphaTestValue(value);
}
@@ -159,6 +225,10 @@ void PassthroughEngine::setLightingOn(bool 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);
}
@@ -169,8 +239,11 @@ void PassthroughEngine::setFogOn(bool on)
inner_->setFogOn(on);
}
void PassthroughEngine::setFogAttributes(float start, float end, std::uint32_t color)
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);
}
@@ -183,39 +256,61 @@ void PassthroughEngine::setTextureTransformMatrixIdentity(std::uint32_t 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(std::uint32_t color)
void PassthroughEngine::setEnvironmentAmbient(imvu::Color color)
{
if (sampled(22)) {
log::info("setEnvironmentAmbient({:08x})", color);
}
count(22);
inner_->setEnvironmentAmbient(color);
}
imvu::PiRenderTexture* PassthroughEngine::newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags)
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 a,
std::uint32_t b, std::uint32_t c)
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, a, b, c);
return inner_->newStaticVertexBuffer(data, stride, elementSize, elementCount);
}
imvu::PiInternalVertexBuffer* PassthroughEngine::newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b)
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(a, b);
return inner_->newDynamicVertexBuffer(elementSize, elementCount);
}
imvu::SharedPtr<imvu::PiPixmapRenderTarget> PassthroughEngine::newPixmapRenderTarget(imvu::Vec2i size)
@@ -242,6 +337,9 @@ 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) {
@@ -257,15 +355,18 @@ void PassthroughEngine::endRendering()
}
void PassthroughEngine::drawTriangles(imvu::PiInternalIndexBuffer* indices, imvu::PiInternalVertexBuffer* positions,
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords0,
imvu::PiInternalVertexBuffer* texCoords1)
imvu::PiInternalVertexBuffer* normals, imvu::PiInternalVertexBuffer* texCoords,
imvu::PiInternalVertexBuffer* colors)
{
count(31);
inner_->drawTriangles(indices, positions, normals, texCoords0, texCoords1);
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);
}
@@ -281,8 +382,12 @@ void PassthroughEngine::drawTrianglesFromMemory(std::uint32_t vertexCount, std::
const imvu::VertexAttributeArray<imvu::Vec3f>& positions,
const imvu::VertexAttributeArray<imvu::Vec3f>& normals,
const imvu::VertexAttributeArray<imvu::Vec2f>& texCoords,
const imvu::VertexAttributeArray<std::uint32_t>& colors)
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);
}
@@ -4,19 +4,22 @@
#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:
explicit PassthroughEngine(imvu::SharedPtr<imvu::PiRenderEngine> inner);
// 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, std::uint32_t color) 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;
@@ -32,29 +35,31 @@ public:
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, std::uint32_t color) 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(std::uint32_t color) override;
imvu::PiRenderTexture* newRenderTexture(imvu::Pixmap* pixmap, std::uint32_t flags) 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 a, std::uint32_t b,
std::uint32_t c) override;
imvu::PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) 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* texCoords0,
imvu::PiInternalVertexBuffer* texCoords1) override;
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<std::uint32_t>& 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;
@@ -63,6 +68,7 @@ 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;
};
+5 -1
View File
@@ -7,6 +7,7 @@
#include "log.hpp"
#include "passthrough_engine.hpp"
#include "renderer_process.hpp"
#include "t5de/imvu/render_engine.hpp"
namespace {
@@ -39,11 +40,14 @@ extern "C" imvu::SharedPtr<imvu::PiRenderEngine> __cdecl createRenderEngine(imvu
{
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 {};
}
return imvu::SharedPtr<imvu::PiRenderEngine>(std::make_unique<PassthroughEngine>(std::move(inner)));
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());
File renamed without changes.
+92
View File
@@ -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
+35
View File
@@ -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
+52
View File
@@ -0,0 +1,52 @@
# Architecture
The renderer is two programs:
- **The adapter**, `t5deRenderEngine.dll` (32-bit, `adapter/`): a renderer plugin IMVU Classic loads in place of its
OpenGL renderer (selected by the `t5de-renderer` patch in the `t5de` repository). It implements IMVU's
`PiRenderEngine` interface (`include/t5de/imvu/`, `docs/interface.md`) and passes what the client draws to the
renderer process. It stays thin: IMVU's process is 32-bit (4 GB of address space shared with Python, Gecko and
IMVU's own code) and tied to Visual C++ 2010's ABI.
- **The renderer process**, `t5de-renderer.exe` (64-bit, `renderer/`): draws, with
[Diligent Engine](https://github.com/DiligentGraphics/DiligentCore) (Apache-2.0). Being 64-bit and separate, it can
use any modern API and library, isn't limited by the client's address space, and can crash without taking the
client down.
Why Diligent: one API over Direct3D 11 (every Windows PC, and software rendering on machines without a graphics
driver), Direct3D 12 and Vulkan (ray tracing, mesh shaders), and WebGPU (browsers, for a possible future client).
It's built with CMake and fetched by `FetchContent`, pinned to a commit (`renderer/CMakeLists.txt`); only the
Direct3D 11 backend is built for now.
## How they talk
`include/t5de/protocol/` is shared by both builds, so it uses fixed-size fields only. For each engine the client
creates (each 3D view, and again on room changes), the adapter:
1. creates a shared memory channel (`Local\t5de-renderer-<pid>-<n>`) holding IMVU's window handle and a frame
count, and an event set after each frame;
2. starts `t5de-renderer.exe --channel <name> --parent <pid>` from its own folder;
3. sets the event at each `endRendering`, and sets `closing` when the engine is destroyed.
The renderer process draws into a child window it creates inside the client's window: window handles are valid in
every process, and Windows allows child windows across processes (Chrome's GPU process works this way). The child
is disabled so mouse input goes to the client's window under it, and the adapter gives the client's window
`WS_CLIPCHILDREN` so the client's own drawing stays out of it. The process exits when the engine is destroyed or the
client exits.
## Status
A spike: the adapter still has IMVU's Direct3D 9 engine draw the scene (the pass-through), and the renderer process
only draws a box in the 3D view's corner, pulsing with the client's frames. Checked in the client (Direct3D 11 in
software, on a machine without a graphics driver): the box draws over the scene from the 64-bit process, keeps in
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
drawing the scene itself, with IMVU's Direct3D engine left as the fallback.
## Building
Two presets, one per architecture: `cmake --workflow --preset adapter` (32-bit, with the tests) and
`cmake --workflow --preset renderer` (64-bit; the first configure fetches and builds Diligent). Both programs go in
the client's folder.
+15 -3
View File
@@ -59,9 +59,21 @@ virtual methods, IMVU's code only uses an engine through helpers that call its v
`destroy`, `get_deleter`, `get_untyped_deleter`), the use count and the weak count, changed atomically.
`include/t5de/imvu/shared_ptr.hpp` matches it. With non-trivial copy and destruction, it's returned through a
hidden pointer and passed on the stack, as IMVU's code expects.
- Still to work out, for a renderer of our own: `Mat4f`, `Transform`, `LightProperties`, `TextureTransform`,
`Pixmap` (`pixmap.dll`), the buffers' contents, `VertexAttributeArray<T>`, `std::vector<PointSprite>` as
Visual C++ 2010 lays it out, and `RenderBlendMode`'s values.
- The data types are in `include/t5de/imvu/types.hpp`, worked out from the Direct3D engine: matrices and
transforms in Direct3D's row-vector convention, colors as `0xAARRGGBB`, lights (point or directional), blend
modes, linear fog, texture coordinate transforms, vertex attribute arrays (data, count, stride). `Pixmap` is used
through `pixmap.dll`'s exported methods (`width`, `height`, `pitch`, `pixels`, ...), not its layout.
- `drawTriangles`' vertex buffers are positions (`Vec3f`), normals (`Vec3f`), texture coordinates (`Vec2f`) and
colors; vertex buffers are made with a stride, element size and count, dynamic ones filled later through the
buffer's own `update`. The objects an engine creates (buffers, textures, render targets) are declared in
`include/t5de/imvu/resources.hpp`; IMVU's code deletes them through their virtual destructors.
- Checked in the client with the pass-through's logged arguments: a right-handed perspective projection (45 degree
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
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
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.
- Memory crosses the boundary only through virtual methods (destructors, control blocks), so each side can use its
own C runtime. Exceptions must not cross it.
+17 -51
View File
@@ -1,54 +1,16 @@
#pragma once
// IMVU's renderer interface, PiRenderEngine (SceneWindow.dll), declared to match its vtable: the order of the methods
// and their parameter types are what matter. The names are IMVU's, from SceneWindow.dll's exported RenderEngineDx
// methods; parameter names are ours, and guesses until the calls are traced. Types used only through pointers or
// references are left incomplete until we need their layout. See docs/interface.md.
// methods; parameter names are ours, from what IMVU's Direct3D engine does with them. See docs/interface.md.
#include <cstdint>
#include "t5de/imvu/resources.hpp"
#include "t5de/imvu/shared_ptr.hpp"
#include "t5de/imvu/types.hpp"
namespace t5de::imvu {
template <class T>
struct Vec2 {
T x;
T y;
};
template <class T>
struct Vec3 {
T x;
T y;
T z;
};
using Vec2i = Vec2<int>;
using Vec2f = Vec2<float>;
using Vec3f = Vec3<float>;
struct Mat4f;
struct Transform;
struct LightProperties;
struct TextureTransform;
struct PointSprite;
class Pixmap;
class INativeWindow;
class PiRenderTexture;
class PiInternalIndexBuffer;
class PiInternalVertexBuffer;
class PiRenderTarget;
class PiPixmapRenderTarget;
class PiTextureRenderTarget;
template <class T>
class VertexAttributeArray;
// std::vector<PointSprite> as Visual C++ 2010 lays it out.
struct PointSpriteVector;
// Values not yet known.
enum class RenderBlendMode : int {};
class PiRenderEngine {
public:
PiRenderEngine() = default;
@@ -59,7 +21,7 @@ public:
virtual void resizeFrameBuffer(Vec2i size) = 0;
virtual void enable2dFrameBuffer() = 0;
virtual void clear(std::uint32_t flags, std::uint32_t color) = 0;
virtual void clear(std::uint32_t flags, Color color) = 0; // ClearFlags
virtual void setProjection(Vec2i viewport, const Mat4f& projection) = 0;
virtual void setModelSpace(const Transform& model) = 0; // slot 5
virtual void setViewSpace(const Transform& view) = 0;
@@ -75,29 +37,33 @@ public:
virtual void setLightingOn(bool on) = 0;
virtual void setLightProperties(std::uint32_t index, const LightProperties& light) = 0;
virtual void setFogOn(bool on) = 0;
virtual void setFogAttributes(float start, float end, std::uint32_t color) = 0;
virtual void setFogAttributes(float start, float end, Color color) = 0;
virtual void setTextureTransformMatrixIdentity(std::uint32_t stage) = 0; // slot 20
virtual void setTextureTransformMatrix(std::uint32_t stage, const TextureTransform& transform) = 0;
virtual void setEnvironmentAmbient(std::uint32_t color) = 0;
virtual PiRenderTexture* newRenderTexture(Pixmap* pixmap, std::uint32_t flags) = 0;
virtual void setEnvironmentAmbient(Color color) = 0; // zero alpha
virtual PiRenderTexture* newRenderTexture(const Pixmap& pixmap, std::uint32_t flags) = 0;
virtual PiInternalIndexBuffer* newIndexBuffer(std::uint32_t count, const std::uint16_t* indices) = 0;
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t a, std::uint32_t b,
std::uint32_t c) = 0; // slot 25
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t a, std::uint32_t b) = 0;
virtual PiInternalVertexBuffer* newStaticVertexBuffer(const void* data, std::uint32_t stride,
std::uint32_t elementSize,
std::uint32_t elementCount) = 0; // slot 25
// Filled later with PiInternalVertexBuffer::update.
virtual PiInternalVertexBuffer* newDynamicVertexBuffer(std::uint32_t elementSize, std::uint32_t elementCount) = 0;
virtual SharedPtr<PiPixmapRenderTarget> newPixmapRenderTarget(Vec2i size) = 0;
virtual SharedPtr<PiTextureRenderTarget> newTextureRenderTarget(Vec2i size) = 0;
virtual void beginRendering(PiRenderTarget* target) = 0;
virtual void endRendering() = 0; // slot 30
// Vertex buffers: positions (Vec3f), normals (Vec3f), texture coordinates (Vec2f), colors (Color); all but
// positions may be null.
virtual void drawTriangles(PiInternalIndexBuffer* indices, PiInternalVertexBuffer* positions,
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords0,
PiInternalVertexBuffer* texCoords1) = 0;
PiInternalVertexBuffer* normals, PiInternalVertexBuffer* texCoords,
PiInternalVertexBuffer* colors) = 0;
virtual void drawLine(Vec3f from, Vec3f to, std::uint32_t color) = 0;
virtual void drawPointSprites(const PointSpriteVector& sprites) = 0;
virtual void drawTrianglesFromMemory(std::uint32_t vertexCount, std::uint32_t indexCount,
const std::uint16_t* indices, const VertexAttributeArray<Vec3f>& positions,
const VertexAttributeArray<Vec3f>& normals,
const VertexAttributeArray<Vec2f>& texCoords,
const VertexAttributeArray<std::uint32_t>& colors) = 0; // slot 34
const VertexAttributeArray<Color>& colors) = 0; // slot 34
private:
// PiRenderEngine's own fields, after the vtable pointer: a magic number IMVU's destructor checks, and a size
+46
View File
@@ -0,0 +1,46 @@
#pragma once
// The objects a PiRenderEngine creates and hands to IMVU's code: buffers, textures, render targets. IMVU's code keeps
// them, calls their virtual methods, and deletes them through their virtual destructors. Declared from the vtables in
// SceneWindow.dll; method names are ours, from what IMVU's implementations do. Not yet verified by a renderer of our
// own.
#include <cstdint>
#include "t5de/imvu/types.hpp"
namespace t5de::imvu {
class PiInternalIndexBuffer {
public:
virtual ~PiInternalIndexBuffer() = default;
};
class PiInternalVertexBuffer {
public:
virtual ~PiInternalVertexBuffer() = default;
// Replaces a dynamic buffer's contents: elementCount elements of elementSize bytes, stride bytes apart (a multiple
// of 4, at least elementSize). Element size and count are the ones the buffer was created with.
virtual void update(const void* data, std::uint32_t stride, std::uint32_t elementSize,
std::uint32_t elementCount) = 0;
// Unknown: IMVU's software buffer forgets its data; its Direct3D buffer asserts, so it isn't called there.
virtual void unknown2() = 0;
};
class PiRenderTexture {
public:
virtual ~PiRenderTexture() = default;
virtual Vec2i size() const = 0;
// Copies the parts of pixmap in regions into the texture. (Not overloaded with update: MSVC orders overloaded
// virtual methods unlike their declaration.)
virtual void updateRegions(const Pixmap& pixmap, const CompoundRegion& regions) = 0;
// Copies all of pixmap into the texture.
virtual void update(const Pixmap& pixmap) = 0;
// Reads the texture into a new Pixmap, constructed in result (a Pixmap returned by value).
virtual Pixmap* read(Pixmap* result) const = 0;
};
class PiRenderTarget;
class PiPixmapRenderTarget;
class PiTextureRenderTarget;
} // namespace t5de::imvu
+113
View File
@@ -0,0 +1,113 @@
#pragma once
// The data types IMVU's renderer interface passes, as SceneWindow.dll's Direct3D 9 engine reads them. Most are passed
// by reference: where IMVU's type might hold more than we read, its size isn't asserted. See docs/interface.md.
#include <cstddef>
#include <cstdint>
struct HWND__; // windows.h's window handle type (HWND is HWND__*)
namespace t5de::imvu {
template <class T>
struct Vec2 {
T x;
T y;
};
template <class T>
struct Vec3 {
T x;
T y;
T z;
};
using Vec2i = Vec2<int>;
using Vec2f = Vec2<float>;
using Vec3f = Vec3<float>;
// Colors are 0xAARRGGBB (Direct3D's D3DCOLOR). Light and ambient colors must have zero alpha.
using Color = std::uint32_t;
// A 4x4 matrix in Direct3D's convention: row-major, transforming row vectors (p' = p * M).
struct Mat4f {
float m[4][4];
};
// An affine transform: a 3x3 matrix (row-major), then a translation; p' = p * linear + translation, as in Direct3D.
// The Direct3D engine expands it to [linear 0; translation 1]. (What it reads; IMVU's type may be larger.)
struct Transform {
float linear[3][3];
Vec3f translation;
};
// A texture coordinate transform: u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5].
struct TextureTransform {
float m[6];
};
enum class LightType : int {
Point = 0, // at `vector`; the Direct3D engine sets no attenuation and a fixed range
Directional = 1, // shining along -`vector` (a zero vector means along -x)
};
// What the Direct3D engine reads of a light. (IMVU's type may be larger.)
struct LightProperties {
bool enabled;
Color diffuse; // zero alpha
Vec3f vector;
LightType type;
};
static_assert(offsetof(LightProperties, diffuse) == 4 && offsetof(LightProperties, vector) == 8
&& offsetof(LightProperties, type) == 0x14);
// Blend functions, as source and destination factors.
enum class RenderBlendMode : int {
Alpha = 1, // source alpha, inverse source alpha
Additive = 2, // one, one
Premultiplied = 3, // one, inverse source alpha
AdditiveAlpha = 4, // source alpha, one
};
// clear()'s flags.
enum ClearFlags : std::uint32_t {
ClearColor = 1,
ClearDepth = 2, // to 1.0
};
// Vertex attributes in memory: count elements, stride bytes apart (0: packed).
template <class T>
struct VertexAttributeArray {
const T* data;
std::uint32_t count;
std::uint32_t stride;
};
static_assert(sizeof(VertexAttributeArray<Vec3f>) == 12);
// IMVU's image class (pixmap.dll): 32-bit pixels (Color). Its layout isn't needed: pixmap.dll exports its methods
// (width, height, pitch, rowBytes, pixels, ...).
class Pixmap;
// Unknown so far: used only by drawPointSprites (particles), as a Visual C++ 2010 std::vector<PointSprite>.
struct PointSpriteVector;
// Region lists IMVU passes to texture updates; unknown so far.
class CompoundRegion;
struct UiEvent;
using NativeWindowHandle = ::HWND__*; // an HWND
// IMVU's window (SceneWindow.dll's NativeWindow), as far as a renderer needs it: the first methods of its vtable.
// Its later methods include overloads, which MSVC lays out unlike their declaration: declare them from the vtable
// if they're ever needed.
class INativeWindow {
public:
virtual ~INativeWindow() = default;
virtual void handleMessage(UiEvent* event) = 0;
virtual int onSetCursor(Vec2i position) = 0;
virtual NativeWindowHandle getPlatformWindow() = 0;
};
} // namespace t5de::imvu
+25
View File
@@ -0,0 +1,25 @@
#pragma once
// What the adapter (t5deRenderEngine.dll, in IMVU's 32-bit process) and the renderer process (64-bit) share. Both
// are built from this header, for different architectures: fixed-size fields only, no pointers or handles' native
// sizes.
#include <cstdint>
namespace t5de::protocol {
inline constexpr std::uint32_t version = 1;
// A shared memory section the adapter creates for each engine. Its name is passed to the renderer process as
// `--channel <name>`; the event "<name>-frame" (auto-reset) is set after each frame the client draws.
struct Channel {
std::uint32_t version;
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 frames; // frames the client has drawn
};
static_assert(sizeof(Channel) == 24, "same layout in 32-bit and 64-bit builds");
inline constexpr wchar_t frameEventSuffix[] = L"-frame";
} // namespace t5de::protocol
+27
View File
@@ -0,0 +1,27 @@
# t5de-renderer.exe, the 64-bit renderer process, drawing with Diligent Engine.
include(FetchContent)
# Diligent Engine's core, pinned to an exact commit. Only the Direct3D 11 backend for now; the others (Direct3D 12,
# Vulkan for ray tracing and mesh shaders, WebGPU for browsers) are switched on as they're needed.
set(DILIGENT_NO_DIRECT3D12 ON CACHE BOOL "" FORCE)
set(DILIGENT_NO_VULKAN ON CACHE BOOL "" FORCE)
set(DILIGENT_NO_OPENGL ON CACHE BOOL "" FORCE)
set(DILIGENT_NO_WEBGPU ON CACHE BOOL "" FORCE)
set(DILIGENT_NO_ARCHIVER ON CACHE BOOL "" FORCE)
set(DILIGENT_NO_SUPER_RESOLUTION ON CACHE BOOL "" FORCE)
set(DILIGENT_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(DILIGENT_INSTALL_CORE OFF CACHE BOOL "" FORCE)
FetchContent_Declare(DiligentCore
GIT_REPOSITORY https://github.com/DiligentGraphics/DiligentCore.git
GIT_TAG c555e4d26de3f414eaf792fa16c0c24827619b17 # master, 2026-10-27
GIT_SUBMODULES ThirdParty/xxHash
EXCLUDE_FROM_ALL
SYSTEM)
FetchContent_MakeAvailable(DiligentCore)
add_executable(t5de_renderer WIN32 main.cpp)
set_target_properties(t5de_renderer PROPERTIES OUTPUT_NAME t5de-renderer)
target_link_libraries(t5de_renderer PRIVATE t5de::protocol t5de::options Diligent-GraphicsEngineD3D11-static Diligent-Common)
install(TARGETS t5de_renderer RUNTIME DESTINATION .)
+137
View File
@@ -0,0 +1,137 @@
// 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.
//
// For now a spike proving the architecture: a small box in the 3D view's corner, its color changing with each frame
// the client draws, drawn with Diligent (Direct3D 11) from this 64-bit process.
#include <windows.h>
#include <shellapi.h>
#include <cmath>
#include <cstdint>
#include <string>
#include <string_view>
#include "EngineFactoryD3D11.h"
#include "RefCntAutoPtr.hpp"
#include "t5de/protocol/channel.hpp"
namespace {
using namespace Diligent;
// The box: where in the client's 3D view, and how big.
constexpr int boxX = 16;
constexpr int boxY = 16;
constexpr int boxWidth = 240;
constexpr int boxHeight = 80;
struct Arguments {
std::wstring channel;
DWORD parent = 0;
};
Arguments parse_arguments()
{
Arguments arguments;
int count = 0;
wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &count);
for (int i = 1; i + 1 < count; i += 2) {
const std::wstring_view name = argv[i];
if (name == L"--channel") {
arguments.channel = argv[i + 1];
} else if (name == L"--parent") {
arguments.parent = static_cast<DWORD>(std::wcstoul(argv[i + 1], nullptr, 10));
}
}
LocalFree(argv);
return arguments;
}
LRESULT CALLBACK window_procedure(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
if (message == WM_ERASEBKGND) {
return 1; // we draw all of it
}
return DefWindowProcW(window, message, wparam, lparam);
}
// A child of the client's window. Disabled, so mouse input goes to the client's window under it.
HWND create_child_window(HWND parent)
{
WNDCLASSEXW windowClass{.cbSize = sizeof(WNDCLASSEXW),
.lpfnWndProc = window_procedure,
.hInstance = GetModuleHandleW(nullptr),
.hCursor = LoadCursorW(nullptr, IDC_ARROW),
.lpszClassName = L"T5DERenderer"};
RegisterClassExW(&windowClass);
return CreateWindowExW(0, windowClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_DISABLED | WS_CLIPSIBLINGS,
boxX, boxY, boxWidth, boxHeight, parent, nullptr, windowClass.hInstance, nullptr);
}
} // namespace
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
const Arguments arguments = parse_arguments();
if (arguments.channel.empty()) {
return 2;
}
HANDLE mapping = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, arguments.channel.c_str());
auto* channel = mapping == nullptr ? nullptr
: static_cast<t5de::protocol::Channel*>(MapViewOfFile(
mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(t5de::protocol::Channel)));
HANDLE frameEvent = OpenEventW(SYNCHRONIZE, FALSE, (arguments.channel + t5de::protocol::frameEventSuffix).c_str());
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, arguments.parent);
if (channel == nullptr || frameEvent == nullptr || parent == nullptr || channel->version != t5de::protocol::version) {
return 3;
}
HWND window = create_child_window(reinterpret_cast<HWND>(static_cast<std::uintptr_t>(channel->window)));
if (window == nullptr) {
return 4;
}
IEngineFactoryD3D11* factory = GetEngineFactoryD3D11();
RefCntAutoPtr<IRenderDevice> device;
RefCntAutoPtr<IDeviceContext> context;
EngineD3D11CreateInfo engineInfo;
factory->CreateDeviceAndContextsD3D11(engineInfo, &device, &context);
if (!device) {
return 5;
}
RefCntAutoPtr<ISwapChain> swapChain;
SwapChainDesc swapChainDesc;
factory->CreateSwapChainD3D11(device, context, swapChainDesc, FullScreenModeDesc{}, Win32NativeWindow{window},
&swapChain);
if (!swapChain) {
return 6;
}
for (;;) {
const HANDLE handles[] = {frameEvent, parent};
const DWORD result = MsgWaitForMultipleObjects(2, handles, FALSE, 1000, QS_ALLINPUT);
MSG message;
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
DispatchMessageW(&message);
}
if (result == WAIT_OBJECT_0 + 1 || channel->closing != 0) {
break; // the client exited, or the engine is gone
}
if (result != WAIT_OBJECT_0) {
continue;
}
// T5DE gold, pulsing with the client's frames.
const float pulse = 0.5f + 0.5f * std::sin(static_cast<float>(channel->frames) * 0.1f);
const float color[] = {1.0f, 0.82f * pulse + 0.1f, 0.0f, 1.0f};
ITextureView* backBuffer = swapChain->GetCurrentBackBufferRTV();
context->SetRenderTargets(1, &backBuffer, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
context->ClearRenderTarget(backBuffer, color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
swapChain->Present(0);
}
DestroyWindow(window);
return 0;
}